Merge branch 'bugfix/377-stuck-follow-request' into 'develop'
[akkoma] / lib / pleroma / user / search.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.User.Search do
6 alias Pleroma.Repo
7 alias Pleroma.User
8 import Ecto.Query
9
10 def search(query, opts \\ []) do
11 resolve = Keyword.get(opts, :resolve, false)
12 for_user = Keyword.get(opts, :for_user)
13
14 # Strip the beginning @ off if there is a query
15 query = String.trim_leading(query, "@")
16
17 if match?(%User{}, for_user) and resolve, do: User.get_or_fetch(query)
18
19 {:ok, results} =
20 Repo.transaction(fn ->
21 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
22
23 query
24 |> search_query(for_user)
25 |> Repo.all()
26 end)
27
28 results
29 end
30
31 defp search_query(query, for_user) do
32 query
33 |> union_query()
34 |> distinct_query()
35 |> boost_search_rank_query(for_user)
36 |> subquery()
37 |> order_by(desc: :search_rank)
38 |> limit(20)
39 |> maybe_restrict_local(for_user)
40 end
41
42 defp union_query(query) do
43 fts_subquery = fts_search_subquery(query)
44 trigram_subquery = trigram_search_subquery(query)
45
46 from(s in trigram_subquery, union_all: ^fts_subquery)
47 end
48
49 defp distinct_query(q) do
50 from(s in subquery(q), order_by: s.search_type, distinct: s.id)
51 end
52
53 # unauthenticated users can only search local activities
54 defp maybe_restrict_local(q, %User{}), do: q
55 defp maybe_restrict_local(q, _), do: where(q, [u], u.local == true)
56
57 defp boost_search_rank_query(query, nil), do: query
58
59 defp boost_search_rank_query(query, for_user) do
60 friends_ids = User.get_friends_ids(for_user)
61 followers_ids = User.get_followers_ids(for_user)
62
63 from(u in subquery(query),
64 select_merge: %{
65 search_rank:
66 fragment(
67 """
68 CASE WHEN (?) THEN (?) * 1.3
69 WHEN (?) THEN (?) * 1.2
70 WHEN (?) THEN (?) * 1.1
71 ELSE (?) END
72 """,
73 u.id in ^friends_ids and u.id in ^followers_ids,
74 u.search_rank,
75 u.id in ^friends_ids,
76 u.search_rank,
77 u.id in ^followers_ids,
78 u.search_rank,
79 u.search_rank
80 )
81 }
82 )
83 end
84
85 defp fts_search_subquery(term, query \\ User) do
86 processed_query =
87 term
88 |> String.replace(~r/\W+/, " ")
89 |> String.trim()
90 |> String.split()
91 |> Enum.map(&(&1 <> ":*"))
92 |> Enum.join(" | ")
93
94 from(
95 u in query,
96 select_merge: %{
97 search_type: ^0,
98 search_rank:
99 fragment(
100 """
101 ts_rank_cd(
102 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
103 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
104 to_tsquery('simple', ?),
105 32
106 )
107 """,
108 u.nickname,
109 u.name,
110 ^processed_query
111 )
112 },
113 where:
114 fragment(
115 """
116 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
117 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
118 """,
119 u.nickname,
120 u.name,
121 ^processed_query
122 )
123 )
124 |> User.restrict_deactivated()
125 end
126
127 defp trigram_search_subquery(term) do
128 from(
129 u in User,
130 select_merge: %{
131 # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
132 search_type: fragment("?", 1),
133 search_rank:
134 fragment(
135 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
136 ^term,
137 u.nickname,
138 u.name
139 )
140 },
141 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
142 )
143 |> User.restrict_deactivated()
144 end
145 end