logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

search.ex (7281B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.User.Search do
  5. alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType
  6. alias Pleroma.Pagination
  7. alias Pleroma.User
  8. import Ecto.Query
  9. @limit 20
  10. def search(query_string, opts \\ []) do
  11. resolve = Keyword.get(opts, :resolve, false)
  12. following = Keyword.get(opts, :following, false)
  13. result_limit = Keyword.get(opts, :limit, @limit)
  14. offset = Keyword.get(opts, :offset, 0)
  15. for_user = Keyword.get(opts, :for_user)
  16. query_string = format_query(query_string)
  17. # If this returns anything, it should bounce to the top
  18. maybe_resolved = maybe_resolve(resolve, for_user, query_string)
  19. top_user_ids =
  20. []
  21. |> maybe_add_resolved(maybe_resolved)
  22. |> maybe_add_ap_id_match(query_string)
  23. |> maybe_add_uri_match(query_string)
  24. results =
  25. query_string
  26. |> search_query(for_user, following, top_user_ids)
  27. |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
  28. results
  29. end
  30. defp maybe_add_resolved(list, {:ok, %User{} = user}) do
  31. [user.id | list]
  32. end
  33. defp maybe_add_resolved(list, _), do: list
  34. defp maybe_add_ap_id_match(list, query) do
  35. if user = User.get_cached_by_ap_id(query) do
  36. [user.id | list]
  37. else
  38. list
  39. end
  40. end
  41. defp maybe_add_uri_match(list, query) do
  42. with {:ok, query} <- UriType.cast(query),
  43. q = from(u in User, where: u.uri == ^query, select: u.id),
  44. users = Pleroma.Repo.all(q) do
  45. users ++ list
  46. else
  47. _ -> list
  48. end
  49. end
  50. defp format_query(query_string) do
  51. # Strip the beginning @ off if there is a query
  52. query_string = String.trim_leading(query_string, "@")
  53. with [name, domain] <- String.split(query_string, "@") do
  54. encoded_domain =
  55. domain
  56. |> String.replace(~r/[!-\-|@|[-`|{-~|\/|:|\s]+/, "")
  57. |> String.to_charlist()
  58. |> :idna.encode()
  59. |> to_string()
  60. name <> "@" <> encoded_domain
  61. else
  62. _ -> query_string
  63. end
  64. end
  65. defp search_query(query_string, for_user, following, top_user_ids) do
  66. for_user
  67. |> base_query(following)
  68. |> filter_blocked_user(for_user)
  69. |> filter_invisible_users()
  70. |> filter_internal_users()
  71. |> filter_blocked_domains(for_user)
  72. |> fts_search(query_string)
  73. |> select_top_users(top_user_ids)
  74. |> trigram_rank(query_string)
  75. |> boost_search_rank(for_user, top_user_ids)
  76. |> subquery()
  77. |> order_by(desc: :search_rank)
  78. |> maybe_restrict_local(for_user)
  79. |> filter_deactivated_users()
  80. end
  81. defp select_top_users(query, top_user_ids) do
  82. from(u in query,
  83. or_where: u.id in ^top_user_ids
  84. )
  85. end
  86. defp fts_search(query, query_string) do
  87. query_string = to_tsquery(query_string)
  88. from(
  89. u in query,
  90. where:
  91. fragment(
  92. # The fragment must _exactly_ match `users_fts_index`, otherwise the index won't work
  93. """
  94. (
  95. setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
  96. setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')
  97. ) @@ to_tsquery('simple', ?)
  98. """,
  99. u.nickname,
  100. u.name,
  101. ^query_string
  102. )
  103. )
  104. end
  105. defp to_tsquery(query_string) do
  106. String.trim_trailing(query_string, "@" <> local_domain())
  107. |> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ")
  108. |> String.trim()
  109. |> String.split()
  110. |> Enum.map(&(&1 <> ":*"))
  111. |> Enum.join(" | ")
  112. end
  113. # Considers nickname match, localized nickname match, name match; preferences nickname match
  114. defp trigram_rank(query, query_string) do
  115. from(
  116. u in query,
  117. select_merge: %{
  118. search_rank:
  119. fragment(
  120. """
  121. similarity(?, ?) +
  122. similarity(?, regexp_replace(?, '@.+', '')) +
  123. similarity(?, trim(coalesce(?, '')))
  124. """,
  125. ^query_string,
  126. u.nickname,
  127. ^query_string,
  128. u.nickname,
  129. ^query_string,
  130. u.name
  131. )
  132. }
  133. )
  134. end
  135. defp base_query(%User{} = user, true), do: User.get_friends_query(user)
  136. defp base_query(_user, _following), do: User
  137. defp filter_invisible_users(query) do
  138. from(q in query, where: q.invisible == false)
  139. end
  140. defp filter_internal_users(query) do
  141. from(q in query, where: q.actor_type != "Application")
  142. end
  143. defp filter_deactivated_users(query) do
  144. from(q in query, where: q.is_active == true)
  145. end
  146. defp filter_blocked_user(query, %User{} = blocker) do
  147. query
  148. |> join(:left, [u], b in Pleroma.UserRelationship,
  149. as: :blocks,
  150. on: b.relationship_type == ^:block and b.source_id == ^blocker.id and u.id == b.target_id
  151. )
  152. |> where([blocks: b], is_nil(b.target_id))
  153. end
  154. defp filter_blocked_user(query, _), do: query
  155. defp filter_blocked_domains(query, %User{domain_blocks: domain_blocks})
  156. when length(domain_blocks) > 0 do
  157. domains = Enum.join(domain_blocks, ",")
  158. from(
  159. q in query,
  160. where: fragment("substring(ap_id from '.*://([^/]*)') NOT IN (?)", ^domains)
  161. )
  162. end
  163. defp filter_blocked_domains(query, _), do: query
  164. defp maybe_resolve(true, user, query) do
  165. case {limit(), user} do
  166. {:all, _} -> :noop
  167. {:unauthenticated, %User{}} -> User.get_or_fetch(query)
  168. {:unauthenticated, _} -> :noop
  169. {false, _} -> User.get_or_fetch(query)
  170. end
  171. end
  172. defp maybe_resolve(_, _, _), do: :noop
  173. defp maybe_restrict_local(q, user) do
  174. case {limit(), user} do
  175. {:all, _} -> restrict_local(q)
  176. {:unauthenticated, %User{}} -> q
  177. {:unauthenticated, _} -> restrict_local(q)
  178. {false, _} -> q
  179. end
  180. end
  181. defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
  182. defp restrict_local(q), do: where(q, [u], u.local == true)
  183. defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
  184. defp boost_search_rank(query, %User{} = for_user, top_user_ids) do
  185. friends_ids = User.get_friends_ids(for_user)
  186. followers_ids = User.get_followers_ids(for_user)
  187. from(u in subquery(query),
  188. select_merge: %{
  189. search_rank:
  190. fragment(
  191. """
  192. CASE WHEN (?) THEN (?) * 1.5
  193. WHEN (?) THEN (?) * 1.3
  194. WHEN (?) THEN (?) * 1.1
  195. WHEN (?) THEN 9001
  196. ELSE (?) END
  197. """,
  198. u.id in ^friends_ids and u.id in ^followers_ids,
  199. u.search_rank,
  200. u.id in ^friends_ids,
  201. u.search_rank,
  202. u.id in ^followers_ids,
  203. u.search_rank,
  204. u.id in ^top_user_ids,
  205. u.search_rank
  206. )
  207. }
  208. )
  209. end
  210. defp boost_search_rank(query, _for_user, top_user_ids) do
  211. from(u in subquery(query),
  212. select_merge: %{
  213. search_rank:
  214. fragment(
  215. """
  216. CASE WHEN (?) THEN 9001
  217. ELSE (?) END
  218. """,
  219. u.id in ^top_user_ids,
  220. u.search_rank
  221. )
  222. }
  223. )
  224. end
  225. end