mirror of
https://github.com/philomena-dev/philomena.git
synced 2025-01-19 22:27:59 +01:00
proper post search
This commit is contained in:
parent
8b79f5808f
commit
4635bd1c65
4 changed files with 268 additions and 124 deletions
128
lib/philomena/posts/query.ex
Normal file
128
lib/philomena/posts/query.ex
Normal file
|
@ -0,0 +1,128 @@
|
|||
defmodule Philomena.Posts.Query do
|
||||
alias Search.Parser
|
||||
|
||||
def user_id_transform(_ctx, data) do
|
||||
case Integer.parse(data) do
|
||||
{int, _rest} ->
|
||||
{
|
||||
:ok,
|
||||
%{
|
||||
bool: %{
|
||||
must: [
|
||||
%{term: %{anonymous: false}},
|
||||
%{term: %{user_id: int}}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_err ->
|
||||
{:error, "Unknown `user_id' value."}
|
||||
end
|
||||
end
|
||||
|
||||
def author_transform(_ctx, data) do
|
||||
{
|
||||
:ok,
|
||||
%{
|
||||
bool: %{
|
||||
must: [
|
||||
%{term: %{anonymous: false}},
|
||||
%{wildcard: %{author: data}}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def user_my_transform(%{user: %{id: id}}, "posts"),
|
||||
do: {:ok, %{term: %{user_id: id}}}
|
||||
|
||||
def user_my_transform(_ctx, _value),
|
||||
do: {:error, "Unknown `my' value."}
|
||||
|
||||
int_fields = ~W(id)
|
||||
date_fields = ~W(created_at updated_at)
|
||||
literal_fields = ~W(image_id)
|
||||
ngram_fields = ~W(body subject)
|
||||
custom_fields = ~W(author user_id forum_id topic_id)
|
||||
default_field = "body"
|
||||
transforms = %{
|
||||
"user_id" => &Philomena.Posts.Query.user_id_transform/2,
|
||||
"author" => &Philomena.Posts.Query.author_transform/2
|
||||
}
|
||||
|
||||
user_custom = custom_fields ++ ~W(my)
|
||||
user_transforms = Map.merge(transforms, %{
|
||||
"my" => &Philomena.Posts.Query.user_my_transform/2
|
||||
})
|
||||
|
||||
mod_literal_fields = literal_fields ++ ~W(fingerprint)
|
||||
mod_ip_fields = ~W(ip)
|
||||
mod_bool_fields = ~W(anonymous deleted)
|
||||
mod_aliases = %{
|
||||
"deleted" => "hidden_from_users"
|
||||
}
|
||||
|
||||
|
||||
@anonymous_parser Parser.parser(
|
||||
int_fields: int_fields,
|
||||
date_fields: date_fields,
|
||||
literal_fields: literal_fields,
|
||||
ngram_fields: ngram_fields,
|
||||
custom_fields: custom_fields,
|
||||
default_field: default_field
|
||||
)
|
||||
|
||||
@user_parser Parser.parser(
|
||||
int_fields: int_fields,
|
||||
date_fields: date_fields,
|
||||
literal_fields: literal_fields,
|
||||
ngram_fields: ngram_fields,
|
||||
custom_fields: user_custom,
|
||||
transforms: user_transforms,
|
||||
default_field: default_field
|
||||
)
|
||||
|
||||
@moderator_parser Parser.parser(
|
||||
int_fields: int_fields,
|
||||
date_fields: date_fields,
|
||||
literal_fields: mod_literal_fields,
|
||||
ip_fields: mod_ip_fields,
|
||||
ngram_fields: ngram_fields,
|
||||
bool_fields: mod_bool_fields,
|
||||
custom_fields: user_custom,
|
||||
transforms: user_transforms,
|
||||
default_field: default_field
|
||||
)
|
||||
|
||||
def parse_anonymous(context, query_string) do
|
||||
Parser.parse(@anonymous_parser, query_string, context)
|
||||
end
|
||||
|
||||
def parse_user(context, query_string) do
|
||||
Parser.parse(@user_parser, query_string, context)
|
||||
end
|
||||
|
||||
def parse_moderator(context, query_string) do
|
||||
Parser.parse(@moderator_parser, query_string, context)
|
||||
end
|
||||
|
||||
def compile(user, query_string) do
|
||||
query_string = query_string || ""
|
||||
|
||||
case user do
|
||||
nil ->
|
||||
parse_anonymous(%{user: nil}, query_string)
|
||||
|
||||
%{role: role} when role in ~W(user assistant) ->
|
||||
parse_user(%{user: user}, query_string)
|
||||
|
||||
%{role: role} when role in ~W(moderator admin) ->
|
||||
parse_moderator(%{user: user}, query_string)
|
||||
|
||||
_ ->
|
||||
raise ArgumentError, "Unknown user role."
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,22 +1,30 @@
|
|||
defmodule PhilomenaWeb.PostController do
|
||||
use PhilomenaWeb, :controller
|
||||
|
||||
alias Philomena.{Forums.Forum, Posts.Post, Textile.Renderer}
|
||||
alias Philomena.Repo
|
||||
alias Philomena.{Posts.Query, Posts.Post, Textile.Renderer}
|
||||
import Ecto.Query
|
||||
|
||||
def index(conn, params) do
|
||||
user = conn.assigns.current_user
|
||||
cq = params["pq"] || "created_at.gte:1 week ago"
|
||||
|
||||
{:ok, query} = Query.compile(conn.assigns.current_user, cq)
|
||||
|
||||
posts =
|
||||
Post.search_records(
|
||||
%{
|
||||
query: %{
|
||||
bool: %{
|
||||
must: parse_search(conn, params) ++ [%{term: %{deleted: false}}]
|
||||
must: [
|
||||
query,
|
||||
%{term: %{access_level: "normal"}},
|
||||
],
|
||||
must_not: [
|
||||
%{terms: %{image_tag_ids: conn.assigns.current_filter.hidden_tag_ids}},
|
||||
%{term: %{hidden_from_users: true}}
|
||||
]
|
||||
}
|
||||
},
|
||||
sort: parse_sort(params)
|
||||
sort: %{created_at: :desc}
|
||||
},
|
||||
conn.assigns.pagination,
|
||||
Post |> preload([topic: :forum, user: [awards: :badge]])
|
||||
|
@ -27,79 +35,8 @@ defmodule PhilomenaWeb.PostController do
|
|||
|> Renderer.render_collection()
|
||||
|
||||
posts =
|
||||
%{posts | entries: Enum.zip(posts.entries, rendered)}
|
||||
%{posts | entries: Enum.zip(rendered, posts.entries)}
|
||||
|
||||
forums =
|
||||
Forum
|
||||
|> order_by(asc: :name)
|
||||
|> Repo.all()
|
||||
|> Enum.filter(&Canada.Can.can?(user, :show, &1))
|
||||
|> Enum.map(&{&1.name, &1.id})
|
||||
|
||||
forums = [{"-", ""} | forums]
|
||||
|
||||
render(conn, "index.html", posts: posts, forums: forums, layout_class: "layout--wide")
|
||||
render(conn, "index.html", posts: posts)
|
||||
end
|
||||
|
||||
defp parse_search(conn, %{"post" => post_params}) do
|
||||
parse_author(post_params) ++
|
||||
parse_subject(post_params) ++
|
||||
parse_forum_id(conn, post_params) ++
|
||||
parse_body(post_params)
|
||||
end
|
||||
defp parse_search(_conn, _params), do: [%{match_all: %{}}]
|
||||
|
||||
defp parse_author(%{"author" => author}) when is_binary(author) and author not in [nil, ""] do
|
||||
case String.contains?(author, ["*", "?"]) do
|
||||
true ->
|
||||
[
|
||||
%{wildcard: %{author: String.downcase(author)}},
|
||||
%{term: %{anonymous: false}}
|
||||
]
|
||||
|
||||
false ->
|
||||
[
|
||||
%{term: %{author: String.downcase(author)}},
|
||||
%{term: %{anonymous: false}}
|
||||
]
|
||||
end
|
||||
end
|
||||
defp parse_author(_params), do: []
|
||||
|
||||
defp parse_subject(%{"subject" => subject}) when is_binary(subject) and subject not in [nil, ""] do
|
||||
[%{match: %{subject: %{query: subject, operator: "and"}}}]
|
||||
end
|
||||
defp parse_subject(_params), do: []
|
||||
|
||||
defp parse_forum_id(conn, %{"forum_id" => forum_id}) when is_binary(forum_id) and forum_id not in [nil, ""] do
|
||||
with {forum_id, _rest} <- Integer.parse(forum_id),
|
||||
true <- valid_forum?(conn.assigns.current_user, forum_id)
|
||||
do
|
||||
[%{term: %{forum_id: forum_id}}]
|
||||
else
|
||||
_error ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
defp parse_forum_id(_conn, _params), do: []
|
||||
|
||||
defp parse_body(%{"body" => body}) when is_binary(body) and body not in [nil, ""],
|
||||
do: [%{match: %{body: body}}]
|
||||
defp parse_body(_params), do: []
|
||||
|
||||
defp parse_sort(%{"post" => %{"sf" => sf, "sd" => sd}}) when sf in ["created_at", "_score"] and sd in ["desc", "asc"] do
|
||||
%{sf => sd}
|
||||
end
|
||||
defp parse_sort(_params) do
|
||||
%{created_at: :desc}
|
||||
end
|
||||
|
||||
defp valid_forum?(user, forum_id) do
|
||||
forum =
|
||||
Forum
|
||||
|> where(id: ^forum_id)
|
||||
|> Repo.one()
|
||||
|
||||
Canada.Can.can?(user, :show, forum)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -69,7 +69,7 @@ header.header
|
|||
a.header__link href='/comments?cq=my:comments'
|
||||
i.fa.fa-fw.fa-comments>
|
||||
| Comments
|
||||
a.header__link href="/posts/posted"
|
||||
a.header__link href="/posts/?pq=my:posts"
|
||||
i.fa.fa-fw.fa-pencil>
|
||||
| Posts
|
||||
a.header__link href='/user_links'
|
||||
|
|
|
@ -1,49 +1,128 @@
|
|||
elixir:
|
||||
route = fn p -> Routes.post_path(@conn, :index, p) end
|
||||
pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @posts, route: route
|
||||
h1 Posts
|
||||
|
||||
= form_for :posts, Routes.post_path(@conn, :index), [method: "get", class: "hform", enforce_utf8: false], fn f ->
|
||||
.field
|
||||
= text_input f, :pq, name: :pq, value: @conn.params["pq"] || "created_at.gte:1 week ago", class: "input hform__text", placeholder: "Search posts", autocapitalize: "none"
|
||||
= submit "Search", class: "hform__button button", data: [disable_with: false]
|
||||
|
||||
.fieldlabel
|
||||
' For more information, see the
|
||||
a href="/pages/search_syntax" search syntax documentation
|
||||
' . Search results are sorted by creation date.
|
||||
|
||||
h2 Search Results
|
||||
|
||||
= cond do
|
||||
- Enum.any?(@posts) ->
|
||||
- route = fn p -> Routes.post_path(@conn, :index, p) end
|
||||
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @posts, route: route, params: [cq: @conn.params["pq"]], conn: @conn
|
||||
|
||||
= for {body, post} <- @posts, post.topic.hidden_from_users == false do
|
||||
= render PhilomenaWeb.PostView, "_post.html", body: body, post: post, conn: @conn
|
||||
|
||||
.column-layout
|
||||
.column-layout__left
|
||||
.block
|
||||
.block__content
|
||||
h3 Search Posts
|
||||
|
||||
= form_for @conn, Routes.post_path(@conn, :index), [as: :post, method: "get", class: "hform"], fn f ->
|
||||
.field = label f, :author, "Author"
|
||||
.field = text_input f, :author, class: "input hform__text", placeholder: "Author (* is wildcard)"
|
||||
|
||||
.field = label f, :forum_id, "Forum"
|
||||
.field = select f, :forum_id, @forums, class: "input input--wide"
|
||||
|
||||
.field = label f, :body, "Body"
|
||||
.field = textarea f, :body, class: "input input--wide", placeholder: "Body"
|
||||
|
||||
.field = label f, :sf, "Sort by"
|
||||
.field
|
||||
=> select f, :sf, ["Creation Date": "created_at", "Relevance": "_score"], class: "input"
|
||||
=> select f, :sd, ["Descending": "desc", "Ascending": "asc"], class: "input"
|
||||
|
||||
.field
|
||||
= submit "Search", class: "button button--state-primary"
|
||||
|
||||
.column-layout__main
|
||||
.block
|
||||
.block__header
|
||||
.block__header.block__header--light.flex
|
||||
= pagination
|
||||
span.block__header__title
|
||||
= render PhilomenaWeb.PaginationView, "_pagination_info.html", page: @posts, conn: @conn
|
||||
|
||||
.post-search-results
|
||||
= for {post, body} <- @posts do
|
||||
.post-entry-wrapper
|
||||
h3
|
||||
=> link post.topic.forum.name, to: Routes.forum_path(@conn, :show, post.topic.forum)
|
||||
' »
|
||||
=> link post.topic.title, to: Routes.forum_topic_path(@conn, :show, post.topic.forum, post.topic)
|
||||
' »
|
||||
a href=(Routes.forum_topic_path(@conn, :show, post.topic.forum, post.topic, post_id: post.id) <> "#post_#{post.id}")
|
||||
= if post.topic_position == 0 do
|
||||
' Topic Opener
|
||||
- else
|
||||
' Post
|
||||
= post.topic_position + 1
|
||||
- assigns[:error] ->
|
||||
p
|
||||
' Oops, there was an error evaluating your query:
|
||||
pre = assigns[:error]
|
||||
|
||||
- true ->
|
||||
p
|
||||
' No posts found!
|
||||
|
||||
= render PhilomenaWeb.PostView, "_post.html", post: post, body: body, conn: @conn
|
||||
h3 Default search
|
||||
p
|
||||
' If you do not specify a field to search over, the search engine will
|
||||
' search for posts with a body that is similar to the query's
|
||||
em word stems
|
||||
' . For example, posts containing the words
|
||||
code winged humanization
|
||||
' ,
|
||||
code wings
|
||||
' , and
|
||||
code> spread wings
|
||||
' would all be found by a search for
|
||||
code wing
|
||||
' , but
|
||||
code> sewing
|
||||
' would not be.
|
||||
|
||||
h3 Allowed fields
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
th Field Selector
|
||||
th Type
|
||||
th Description
|
||||
th Example
|
||||
tbody
|
||||
tr
|
||||
td
|
||||
code author
|
||||
td Literal
|
||||
td Matches the author of this post. Anonymous authors will never match this term.
|
||||
td
|
||||
code = link "author:Joey", to: Routes.post_path(@conn, :index, cq: "author:Joey")
|
||||
tr
|
||||
td
|
||||
code body
|
||||
td Full Text
|
||||
td Matches the body of this post. This is the default field.
|
||||
td
|
||||
code = link "body:test", to: Routes.post_path(@conn, :index, cq: "body:test")
|
||||
tr
|
||||
td
|
||||
code created_at
|
||||
td Date/Time Range
|
||||
td Matches the creation time of this post.
|
||||
td
|
||||
code = link "created_at:2015", to: Routes.post_path(@conn, :index, cq: "created_at:2015")
|
||||
tr
|
||||
td
|
||||
code id
|
||||
td Numeric Range
|
||||
td Matches the numeric surrogate key for this post.
|
||||
td
|
||||
code = link "id:1000000", to: Routes.post_path(@conn, :index, cq: "id:1000000")
|
||||
tr
|
||||
td
|
||||
code my
|
||||
td Meta
|
||||
td
|
||||
code> my:posts
|
||||
' matches posts you have posted if you are signed in.
|
||||
td
|
||||
code = link "my:posts", to: Routes.post_path(@conn, :index, cq: "my:posts")
|
||||
tr
|
||||
td
|
||||
code subject
|
||||
td Full Text
|
||||
td Matches the title of the topic.
|
||||
td
|
||||
code = link "subject:time wasting thread", to: Routes.post_path(@conn, :index, cq: "subject:time wasting thread")
|
||||
tr
|
||||
td
|
||||
code topic_id
|
||||
td Literal
|
||||
td Matches the numeric surrogate key for the topic this post belongs to.
|
||||
td
|
||||
code = link "topic_id:7000", to: Routes.post_path(@conn, :index, cq: "topic_id:7000")
|
||||
tr
|
||||
td
|
||||
code updated_at
|
||||
td Date/Time Range
|
||||
td Matches the creation or last edit time of this post.
|
||||
td
|
||||
code = link "updated_at.gte:2 weeks ago", to: Routes.post_path(@conn, :index, cq: "updated_at.gte:2 weeks ago")
|
||||
tr
|
||||
td
|
||||
code user_id
|
||||
td Literal
|
||||
td Matches posts with the specified user_id. Anonymous users will never match this term.
|
||||
td
|
||||
code = link "user_id:211190", to: Routes.post_path(@conn, :index, cq: "user_id:211190")
|
Loading…
Reference in a new issue