Implement suggestions from the Meilisearch MR
[akkoma] / lib / pleroma / search / meilisearch.ex
1 defmodule Pleroma.Search.Meilisearch do
2 require Logger
3 require Pleroma.Constants
4
5 alias Pleroma.Activity
6
7 import Pleroma.Search.DatabaseSearch
8 import Ecto.Query
9
10 defp meili_headers do
11 private_key = Pleroma.Config.get([Pleroma.Search.Meilisearch, :private_key])
12
13 [{"Content-Type", "application/json"}] ++
14 if is_nil(private_key), do: [], else: [{"X-Meili-API-Key", private_key}]
15 end
16
17 def meili_get(path) do
18 endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
19
20 result =
21 Pleroma.HTTP.get(
22 Path.join(endpoint, path),
23 meili_headers()
24 )
25
26 with {:ok, res} <- result do
27 {:ok, Jason.decode!(res.body)}
28 end
29 end
30
31 def meili_post(path, params) do
32 endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
33
34 result =
35 Pleroma.HTTP.post(
36 Path.join(endpoint, path),
37 Jason.encode!(params),
38 meili_headers()
39 )
40
41 with {:ok, res} <- result do
42 {:ok, Jason.decode!(res.body)}
43 end
44 end
45
46 def meili_put(path, params) do
47 endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
48
49 result =
50 Pleroma.HTTP.request(
51 :put,
52 Path.join(endpoint, path),
53 Jason.encode!(params),
54 meili_headers(),
55 []
56 )
57
58 with {:ok, res} <- result do
59 {:ok, Jason.decode!(res.body)}
60 end
61 end
62
63 def meili_delete!(path) do
64 endpoint = Pleroma.Config.get([Pleroma.Search.Meilisearch, :url])
65
66 {:ok, _} =
67 Pleroma.HTTP.request(
68 :delete,
69 Path.join(endpoint, path),
70 "",
71 meili_headers(),
72 []
73 )
74 end
75
76 def search(user, query, options \\ []) do
77 limit = Enum.min([Keyword.get(options, :limit), 40])
78 offset = Keyword.get(options, :offset, 0)
79 author = Keyword.get(options, :author)
80
81 res =
82 meili_post(
83 "/indexes/objects/search",
84 %{q: query, offset: offset, limit: limit}
85 )
86
87 with {:ok, result} <- res do
88 hits = result["hits"] |> Enum.map(& &1["ap"])
89
90 try do
91 hits
92 |> Activity.create_by_object_ap_id()
93 |> Activity.with_preloaded_object()
94 |> Activity.with_preloaded_object()
95 |> Activity.restrict_deactivated_users()
96 |> maybe_restrict_local(user)
97 |> maybe_restrict_author(author)
98 |> maybe_restrict_blocked(user)
99 |> maybe_fetch(user, query)
100 |> order_by([object: obj], desc: obj.data["published"])
101 |> Pleroma.Repo.all()
102 rescue
103 _ -> maybe_fetch([], user, query)
104 end
105 end
106 end
107
108 def object_to_search_data(object) do
109 # Only index public or unlisted Notes
110 if not is_nil(object) and object.data["type"] == "Note" and
111 not is_nil(object.data["content"]) and
112 (Pleroma.Constants.as_public() in object.data["to"] or
113 Pleroma.Constants.as_public() in object.data["cc"]) and
114 String.length(object.data["content"]) > 1 do
115 data = object.data
116
117 content_str =
118 case data["content"] do
119 [nil | rest] -> to_string(rest)
120 str -> str
121 end
122
123 content =
124 with {:ok, scrubbed} <- FastSanitize.strip_tags(content_str),
125 trimmed <- String.trim(scrubbed) do
126 trimmed
127 end
128
129 if String.length(content) > 1 do
130 {:ok, published, _} = DateTime.from_iso8601(data["published"])
131
132 %{
133 id: object.id,
134 content: content,
135 ap: data["id"],
136 published: published |> DateTime.to_unix()
137 }
138 end
139 end
140 end
141
142 def add_to_index(activity) do
143 maybe_search_data = object_to_search_data(activity.object)
144
145 if activity.data["type"] == "Create" and maybe_search_data do
146 result =
147 meili_put(
148 "/indexes/objects/documents",
149 [maybe_search_data]
150 )
151
152 with {:ok, res} <- result,
153 true <- Map.has_key?(res, "updateId") do
154 # Do nothing
155 else
156 _ ->
157 Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}")
158 end
159 end
160 end
161
162 def remove_from_index(object) do
163 meili_delete!("/indexes/objects/documents/#{object.id}")
164 end
165 end