proper post search

This commit is contained in:
byte[] 2019-11-30 20:44:57 -05:00
parent 8b79f5808f
commit 4635bd1c65
4 changed files with 268 additions and 124 deletions

View 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

View file

@ -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

View file

@ -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'

View file

@ -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)
' &raquo;
=> link post.topic.title, to: Routes.forum_topic_path(@conn, :show, post.topic.forum, post.topic)
' &raquo;
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")