X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fuser%2Fsearch.ex;h=ed06c2ab92f2f367a90c7176279b12bb408b8b42;hb=9ef912aecfbeef670db0614ec2014321c37f3b7a;hp=add6a0bbf63d33b4beffde0c694079ffe75b1dbc;hpb=5f1b6f5acadb512fc98fff3a177e1ea417bf4b95;p=akkoma diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index add6a0bbf..6b55df483 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -3,81 +3,156 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.User.Search do - alias Pleroma.Repo + alias Pleroma.Pagination alias Pleroma.User import Ecto.Query - def search(query, opts \\ []) do + @limit 20 + + def search(query_string, opts \\ []) do resolve = Keyword.get(opts, :resolve, false) - for_user = Keyword.get(opts, :for_user) + following = Keyword.get(opts, :following, false) + result_limit = Keyword.get(opts, :limit, @limit) + offset = Keyword.get(opts, :offset, 0) - # Strip the beginning @ off if there is a query - query = String.trim_leading(query, "@") + for_user = Keyword.get(opts, :for_user) - maybe_resolve(resolve, for_user, query) + query_string = format_query(query_string) - {:ok, results} = - Repo.transaction(fn -> - Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", []) + maybe_resolve(resolve, for_user, query_string) - query - |> search_query(for_user) - |> Repo.all() - end) + results = + query_string + |> search_query(for_user, following) + |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset) results end - defp maybe_resolve(true, %User{}, query) do - User.get_or_fetch(query) - end + defp format_query(query_string) do + # Strip the beginning @ off if there is a query + query_string = String.trim_leading(query_string, "@") - defp maybe_resolve(true, _, query) do - unless restrict_local?(), do: User.get_or_fetch(query) + with [name, domain] <- String.split(query_string, "@"), + formatted_domain <- String.replace(domain, ~r/[!-\-|@|[-`|{-~|\/|:|\s]+/, "") do + name <> "@" <> to_string(:idna.encode(formatted_domain)) + else + _ -> query_string + end end - defp maybe_resolve(_, _, _), do: :noop - - defp search_query(query, for_user) do - query - |> union_query() - |> distinct_query() - |> boost_search_rank_query(for_user) + defp search_query(query_string, for_user, following) do + for_user + |> base_query(following) + |> filter_blocked_user(for_user) + |> filter_invisible_users() + |> filter_blocked_domains(for_user) + |> fts_search(query_string) + |> trigram_rank(query_string) + |> boost_search_rank(for_user) |> subquery() |> order_by(desc: :search_rank) - |> limit(20) |> maybe_restrict_local(for_user) end - defp restrict_local? do - Pleroma.Config.get([:instance, :limit_unauthenticated_to_local_content], true) + defp fts_search(query, query_string) do + query_string = to_tsquery(query_string) + + from( + u in query, + where: + fragment( + """ + (to_tsvector('simple', ?) || to_tsvector('simple', ?)) @@ to_tsquery('simple', ?) + """, + u.name, + u.nickname, + ^query_string + ) + ) + end + + defp to_tsquery(query_string) do + String.trim_trailing(query_string, "@" <> local_domain()) + |> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ") + |> String.trim() + |> String.split() + |> Enum.map(&(&1 <> ":*")) + |> Enum.join(" | ") end - defp union_query(query) do - fts_subquery = fts_search_subquery(query) - trigram_subquery = trigram_search_subquery(query) + defp trigram_rank(query, query_string) do + from( + u in query, + select_merge: %{ + search_rank: + fragment( + "similarity(?, trim(? || ' ' || coalesce(?, '')))", + ^query_string, + u.nickname, + u.name + ) + } + ) + end + + defp base_query(_user, false), do: User + defp base_query(user, true), do: User.get_followers_query(user) - from(s in trigram_subquery, union_all: ^fts_subquery) + defp filter_invisible_users(query) do + from(q in query, where: q.invisible == false) end - defp distinct_query(q) do - from(s in subquery(q), order_by: s.search_type, distinct: s.id) + defp filter_blocked_user(query, %User{} = blocker) do + query + |> join(:left, [u], b in Pleroma.UserRelationship, + as: :blocks, + on: b.relationship_type == ^:block and b.source_id == ^blocker.id and u.id == b.target_id + ) + |> where([blocks: b], is_nil(b.target_id)) end - # unauthenticated users can only search local activities - defp maybe_restrict_local(q, %User{}), do: q + defp filter_blocked_user(query, _), do: query - defp maybe_restrict_local(q, _) do - if restrict_local?() do - where(q, [u], u.local == true) - else - q + defp filter_blocked_domains(query, %User{domain_blocks: domain_blocks}) + when length(domain_blocks) > 0 do + domains = Enum.join(domain_blocks, ",") + + from( + q in query, + where: fragment("substring(ap_id from '.*://([^/]*)') NOT IN (?)", ^domains) + ) + end + + defp filter_blocked_domains(query, _), do: query + + defp maybe_resolve(true, user, query) do + case {limit(), user} do + {:all, _} -> :noop + {:unauthenticated, %User{}} -> User.get_or_fetch(query) + {:unauthenticated, _} -> :noop + {false, _} -> User.get_or_fetch(query) end end - defp boost_search_rank_query(query, nil), do: query + defp maybe_resolve(_, _, _), do: :noop + + defp maybe_restrict_local(q, user) do + case {limit(), user} do + {:all, _} -> restrict_local(q) + {:unauthenticated, %User{}} -> q + {:unauthenticated, _} -> restrict_local(q) + {false, _} -> q + end + end + + defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated) + + defp restrict_local(q), do: where(q, [u], u.local == true) + + defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) - defp boost_search_rank_query(query, for_user) do + defp boost_search_rank(query, %User{} = for_user) do friends_ids = User.get_friends_ids(for_user) followers_ids = User.get_followers_ids(for_user) @@ -86,8 +161,8 @@ defmodule Pleroma.User.Search do search_rank: fragment( """ - CASE WHEN (?) THEN 0.5 + (?) * 1.3 - WHEN (?) THEN 0.5 + (?) * 1.2 + CASE WHEN (?) THEN (?) * 1.5 + WHEN (?) THEN (?) * 1.3 WHEN (?) THEN (?) * 1.1 ELSE (?) END """, @@ -103,64 +178,5 @@ defmodule Pleroma.User.Search do ) end - defp fts_search_subquery(term, query \\ User) do - processed_query = - term - |> String.replace(~r/\W+/, " ") - |> String.trim() - |> String.split() - |> Enum.map(&(&1 <> ":*")) - |> Enum.join(" | ") - - from( - u in query, - select_merge: %{ - search_type: ^0, - search_rank: - fragment( - """ - ts_rank_cd( - setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') || - setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'), - to_tsquery('simple', ?), - 32 - ) - """, - u.nickname, - u.name, - ^processed_query - ) - }, - where: - fragment( - """ - (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') || - setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?) - """, - u.nickname, - u.name, - ^processed_query - ) - ) - |> User.restrict_deactivated() - end - - defp trigram_search_subquery(term) do - from( - u in User, - select_merge: %{ - # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason - search_type: fragment("?", 1), - search_rank: - fragment( - "similarity(?, trim(? || ' ' || coalesce(?, '')))", - ^term, - u.nickname, - u.name - ) - }, - where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term) - ) - |> User.restrict_deactivated() - end + defp boost_search_rank(query, _for_user), do: query end