Add fuzzy matching to ParadeDB search
Make ParadeDB search typo-tolerant via configurable fuzzy distance and document the new setting. Add coverage for fuzzy query SQL and id parsing, and keep integration test stability under connection load.
This commit is contained in:
parent
74da3e01f7
commit
7efd296afb
6 changed files with 79 additions and 7 deletions
|
|
@ -940,7 +940,8 @@ config :pleroma, Pleroma.Search.Meilisearch,
|
||||||
|
|
||||||
config :pleroma, Pleroma.Search.ParadeDB,
|
config :pleroma, Pleroma.Search.ParadeDB,
|
||||||
url: nil,
|
url: nil,
|
||||||
table: "pleroma_search_documents"
|
table: "pleroma_search_documents",
|
||||||
|
fuzzy_distance: 0
|
||||||
|
|
||||||
config :pleroma, Pleroma.Search.ParadeDB.Repo,
|
config :pleroma, Pleroma.Search.ParadeDB.Repo,
|
||||||
pool_size: 10,
|
pool_size: 10,
|
||||||
|
|
|
||||||
|
|
@ -3560,6 +3560,12 @@ config :pleroma, :config_description, [
|
||||||
type: :string,
|
type: :string,
|
||||||
description: "Table name used for search documents.",
|
description: "Table name used for search documents.",
|
||||||
suggestion: ["pleroma_search_documents"]
|
suggestion: ["pleroma_search_documents"]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :fuzzy_distance,
|
||||||
|
type: :integer,
|
||||||
|
description: "Edit-distance used for fuzzy token matching (0 disables fuzziness, max 2).",
|
||||||
|
suggestion: [0, 1, 2]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,8 @@ To use it, set the search module to `Pleroma.Search.ParadeDB` and configure the
|
||||||
>
|
>
|
||||||
> config :pleroma, Pleroma.Search.ParadeDB,
|
> config :pleroma, Pleroma.Search.ParadeDB,
|
||||||
> url: System.get_env("PARADEDB_DATABASE_URL") || "postgres://postgres:postgres@127.0.0.1:5432/paradedb",
|
> url: System.get_env("PARADEDB_DATABASE_URL") || "postgres://postgres:postgres@127.0.0.1:5432/paradedb",
|
||||||
> table: "pleroma_search_documents"
|
> table: "pleroma_search_documents",
|
||||||
|
> fuzzy_distance: 0
|
||||||
|
|
||||||
Then, create the table and BM25 index once:
|
Then, create the table and BM25 index once:
|
||||||
|
|
||||||
|
|
@ -178,4 +179,6 @@ After that, run an initial indexing pass to backfill existing posts:
|
||||||
|
|
||||||
Note: Like the other external search backends, only public/unlisted Notes are indexed.
|
Note: Like the other external search backends, only public/unlisted Notes are indexed.
|
||||||
|
|
||||||
|
Set `fuzzy_distance` to `1` or `2` to allow typo-tolerant matching (`2` is the maximum ParadeDB supports).
|
||||||
|
|
||||||
Indexing runs via the Oban `search_indexing` queue, so ensure that queue is enabled (not paused) in your Oban configuration.
|
Indexing runs via the Oban `search_indexing` queue, so ensure that queue is enabled (not paused) in your Oban configuration.
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,22 @@ defmodule Pleroma.Search.ParadeDB do
|
||||||
Config.get([Pleroma.Search.ParadeDB, :table], @default_table)
|
Config.get([Pleroma.Search.ParadeDB, :table], @default_table)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp fuzzy_distance do
|
||||||
|
case Config.get([Pleroma.Search.ParadeDB, :fuzzy_distance], 0) do
|
||||||
|
value when is_integer(value) and value in 0..2 ->
|
||||||
|
value
|
||||||
|
|
||||||
|
value when is_binary(value) ->
|
||||||
|
case Integer.parse(value) do
|
||||||
|
{parsed, ""} when parsed in 0..2 -> parsed
|
||||||
|
_ -> 0
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def create_index do
|
def create_index do
|
||||||
with {:ok, _} <- client_impl().query("CREATE EXTENSION IF NOT EXISTS pg_search", []),
|
with {:ok, _} <- client_impl().query("CREATE EXTENSION IF NOT EXISTS pg_search", []),
|
||||||
|
|
@ -190,7 +206,9 @@ defmodule Pleroma.Search.ParadeDB do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp search_ids(query, limit, offset, author) do
|
defp search_ids(query, limit, offset, author) do
|
||||||
base_sql = "SELECT id FROM #{table()} WHERE content &&& $1"
|
base_sql =
|
||||||
|
"SELECT id FROM #{table()} WHERE content &&& #{search_query_param_sql(fuzzy_distance())}"
|
||||||
|
|
||||||
params = [query]
|
params = [query]
|
||||||
|
|
||||||
{base_sql, params} =
|
{base_sql, params} =
|
||||||
|
|
@ -216,6 +234,9 @@ defmodule Pleroma.Search.ParadeDB do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp search_query_param_sql(0), do: "$1"
|
||||||
|
defp search_query_param_sql(distance), do: "$1::pdb.fuzzy(#{distance})"
|
||||||
|
|
||||||
defp create_table_sql(table) do
|
defp create_table_sql(table) do
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS #{table} (
|
CREATE TABLE IF NOT EXISTS #{table} (
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ defmodule Pleroma.Search.ParadeDBIntegrationTest do
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{:ok, _pid} = start_supervised({Pleroma.Search.ParadeDB.Repo, pool_size: 1})
|
{:ok, _pid} = start_supervised({Pleroma.Search.ParadeDB.Repo, pool_size: 5})
|
||||||
|
|
||||||
previous_table = Pleroma.Config.get([Pleroma.Search.ParadeDB, :table], nil)
|
previous_table = Pleroma.Config.get([Pleroma.Search.ParadeDB, :table], nil)
|
||||||
table = "pleroma_search_documents_it_#{System.unique_integer([:positive])}"
|
table = "pleroma_search_documents_it_#{System.unique_integer([:positive])}"
|
||||||
|
|
|
||||||
|
|
@ -161,12 +161,15 @@ defmodule Pleroma.Search.ParadeDBTest do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Config
|
Config
|
||||||
|> expect(:get, 2, fn
|
|> expect(:get, 3, fn
|
||||||
[Pleroma.Search.ParadeDB, :client_impl], nil ->
|
[Pleroma.Search.ParadeDB, :client_impl], nil ->
|
||||||
ClientMock
|
ClientMock
|
||||||
|
|
||||||
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
|
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
|
||||||
"pleroma_search_documents"
|
"pleroma_search_documents"
|
||||||
|
|
||||||
|
[Pleroma.Search.ParadeDB, :fuzzy_distance], 0 ->
|
||||||
|
0
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert [%{id: ^activity2_id}, %{id: ^activity1_id}] =
|
assert [%{id: ^activity2_id}, %{id: ^activity1_id}] =
|
||||||
|
|
@ -196,12 +199,15 @@ defmodule Pleroma.Search.ParadeDBTest do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Config
|
Config
|
||||||
|> expect(:get, 2, fn
|
|> expect(:get, 3, fn
|
||||||
[Pleroma.Search.ParadeDB, :client_impl], nil ->
|
[Pleroma.Search.ParadeDB, :client_impl], nil ->
|
||||||
ClientMock
|
ClientMock
|
||||||
|
|
||||||
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
|
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
|
||||||
"pleroma_search_documents"
|
"pleroma_search_documents"
|
||||||
|
|
||||||
|
[Pleroma.Search.ParadeDB, :fuzzy_distance], 0 ->
|
||||||
|
0
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert [%{id: ^public_activity_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0)
|
assert [%{id: ^public_activity_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0)
|
||||||
|
|
@ -225,15 +231,50 @@ defmodule Pleroma.Search.ParadeDBTest do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Config
|
Config
|
||||||
|> expect(:get, 2, fn
|
|> expect(:get, 3, fn
|
||||||
[Pleroma.Search.ParadeDB, :client_impl], nil ->
|
[Pleroma.Search.ParadeDB, :client_impl], nil ->
|
||||||
ClientMock
|
ClientMock
|
||||||
|
|
||||||
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
|
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
|
||||||
"pleroma_search_documents"
|
"pleroma_search_documents"
|
||||||
|
|
||||||
|
[Pleroma.Search.ParadeDB, :fuzzy_distance], 0 ->
|
||||||
|
0
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert [%{id: ^activity_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0)
|
assert [%{id: ^activity_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "search uses configured fuzzy distance" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{status: "runing shoes", visibility: "public"})
|
||||||
|
|
||||||
|
activity_id = activity.id
|
||||||
|
{:ok, dumped_id} = FlakeId.Ecto.CompatType.dump(activity_id)
|
||||||
|
|
||||||
|
ClientMock
|
||||||
|
|> expect(:query, fn sql, params ->
|
||||||
|
assert sql =~ "SELECT id FROM pleroma_search_documents"
|
||||||
|
assert sql =~ "$1::pdb.fuzzy(1)"
|
||||||
|
assert ["running shoes", _limit, _offset] = params
|
||||||
|
|
||||||
|
{:ok, %{rows: [[dumped_id]]}}
|
||||||
|
end)
|
||||||
|
|
||||||
|
Config
|
||||||
|
|> expect(:get, 3, fn
|
||||||
|
[Pleroma.Search.ParadeDB, :client_impl], nil ->
|
||||||
|
ClientMock
|
||||||
|
|
||||||
|
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
|
||||||
|
"pleroma_search_documents"
|
||||||
|
|
||||||
|
[Pleroma.Search.ParadeDB, :fuzzy_distance], 0 ->
|
||||||
|
1
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert [%{id: ^activity_id}] = ParadeDB.search(nil, "running shoes", limit: 40, offset: 0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue