From 4635bd1c650a3b87b2920a858d8c12f00ab3de7d Mon Sep 17 00:00:00 2001 From: "byte[]" Date: Sat, 30 Nov 2019 20:44:57 -0500 Subject: [PATCH] proper post search --- lib/philomena/posts/query.ex | 128 ++++++++++++++ .../controllers/post_controller.ex | 95 ++-------- .../templates/layout/_header.html.slime | 2 +- .../templates/post/index.html.slime | 167 +++++++++++++----- 4 files changed, 268 insertions(+), 124 deletions(-) create mode 100644 lib/philomena/posts/query.ex diff --git a/lib/philomena/posts/query.ex b/lib/philomena/posts/query.ex new file mode 100644 index 00000000..c180263f --- /dev/null +++ b/lib/philomena/posts/query.ex @@ -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 diff --git a/lib/philomena_web/controllers/post_controller.ex b/lib/philomena_web/controllers/post_controller.ex index b023e9cb..76f1d88c 100644 --- a/lib/philomena_web/controllers/post_controller.ex +++ b/lib/philomena_web/controllers/post_controller.ex @@ -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 \ No newline at end of file +end diff --git a/lib/philomena_web/templates/layout/_header.html.slime b/lib/philomena_web/templates/layout/_header.html.slime index 5977e9f2..86846306 100644 --- a/lib/philomena_web/templates/layout/_header.html.slime +++ b/lib/philomena_web/templates/layout/_header.html.slime @@ -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' diff --git a/lib/philomena_web/templates/post/index.html.slime b/lib/philomena_web/templates/post/index.html.slime index a628852e..ff7d00c6 100644 --- a/lib/philomena_web/templates/post/index.html.slime +++ b/lib/philomena_web/templates/post/index.html.slime @@ -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 \ No newline at end of file +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") \ No newline at end of file