mirror of
https://github.com/ash-project/ash_postgres.git
synced 2024-09-19 13:03:14 +12:00
feat: add trigram search
This commit is contained in:
parent
9765d58277
commit
e4d6381428
4 changed files with 231 additions and 1 deletions
|
@ -1 +1,9 @@
|
|||
# AshPostgres
|
||||
|
||||
# TODO
|
||||
|
||||
## Configuration
|
||||
|
||||
- Need to figure out how to only fetch config one time in the configuration of the repo.
|
||||
Right now, we are calling the `installed_extensions()` function in both `supervisor` and
|
||||
`runtime` but that could mean checking the system environment variables every time (is that bad?)
|
||||
|
|
|
@ -17,6 +17,7 @@ defmodule AshPostgres do
|
|||
)
|
||||
|
||||
alias Ash.Filter.{And, Eq, In, NotEq, NotIn, Or}
|
||||
alias AshPostgres.Predicates.Trigram
|
||||
|
||||
@moduledoc """
|
||||
A postgres data layer that levereges Ecto's postgres tools.
|
||||
|
@ -67,6 +68,21 @@ defmodule AshPostgres do
|
|||
resource.repo()
|
||||
end
|
||||
|
||||
@impl true
|
||||
def custom_filters(resource) do
|
||||
config = repo(resource).config()
|
||||
|
||||
add_pg_trgm_search(%{}, config)
|
||||
end
|
||||
|
||||
defp add_pg_trgm_search(filters, config) do
|
||||
if "pg_trgm" in config[:installed_extensions] do
|
||||
Map.update(filters, :string, [{:trigram, AshPostgres.Predicates.Trigram}], fn filters ->
|
||||
[{:trigram, AshPostgres.Predicates.Trigram} | filters]
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
import Ecto.Query, only: [from: 2]
|
||||
|
||||
@impl true
|
||||
|
@ -82,6 +98,7 @@ defmodule AshPostgres do
|
|||
def can?({:filter, :and}), do: true
|
||||
def can?({:filter, :or}), do: true
|
||||
def can?({:filter, :not}), do: true
|
||||
def can?({:filter, :trigram}), do: true
|
||||
def can?({:filter_related, _}), do: true
|
||||
def can?(_), do: false
|
||||
|
||||
|
@ -443,7 +460,7 @@ defmodule AshPostgres do
|
|||
not_filter ->
|
||||
{params, new_expr} = filter_to_expr(not_filter, bindings, params, current_binding, path)
|
||||
|
||||
{params, join_exprs(expr, {:not, new_expr}, :and)}
|
||||
{params, join_exprs(expr, {:not, [], [new_expr]}, :and)}
|
||||
end
|
||||
|
||||
{params, expr} =
|
||||
|
@ -505,6 +522,72 @@ defmodule AshPostgres do
|
|||
]}}}
|
||||
end
|
||||
|
||||
defp filter_value_to_expr(
|
||||
attribute,
|
||||
%Trigram{} = trigram,
|
||||
current_binding,
|
||||
params
|
||||
) do
|
||||
param_count = Enum.count(params)
|
||||
|
||||
case trigram do
|
||||
%{equals: equals, greater_than: nil, less_than: nil, text: text} ->
|
||||
{params ++ [{text, {current_binding, attribute}}, {equals, :float}],
|
||||
{:fragment, [],
|
||||
[
|
||||
raw: "similarity(",
|
||||
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
|
||||
raw: ", ",
|
||||
expr: {:^, [], [param_count]},
|
||||
raw: ") = ",
|
||||
expr: {:^, [], [param_count + 1]},
|
||||
raw: ""
|
||||
]}}
|
||||
|
||||
%{equals: nil, greater_than: greater_than, less_than: nil, text: text} ->
|
||||
{params ++ [{text, {current_binding, attribute}}, {greater_than, :float}],
|
||||
{:fragment, [],
|
||||
[
|
||||
raw: "similarity(",
|
||||
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
|
||||
raw: ", ",
|
||||
expr: {:^, [], [param_count]},
|
||||
raw: ") > ",
|
||||
expr: {:^, [], [param_count + 1]},
|
||||
raw: ""
|
||||
]}}
|
||||
|
||||
%{equals: nil, greater_than: nil, less_than: less_than, text: text} ->
|
||||
{params ++ [{text, {current_binding, attribute}}, {less_than, :float}],
|
||||
{:fragment, [],
|
||||
[
|
||||
raw: "similarity(",
|
||||
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
|
||||
raw: ", ",
|
||||
expr: {:^, [], [param_count]},
|
||||
raw: ") < ",
|
||||
expr: {:^, [], [param_count + 1]},
|
||||
raw: ""
|
||||
]}}
|
||||
|
||||
%{equals: nil, greater_than: greater_than, less_than: less_than, text: text} ->
|
||||
{params ++
|
||||
[{text, {current_binding, attribute}}, {less_than, :float}, {greater_than, :float}],
|
||||
{:fragment, [],
|
||||
[
|
||||
raw: "similarity(",
|
||||
expr: {{:., [], [{:&, [], [current_binding]}, attribute]}, [], []},
|
||||
raw: ", ",
|
||||
expr: {:^, [], [param_count]},
|
||||
raw: ") BETWEEN ",
|
||||
expr: {:^, [], [param_count + 1]},
|
||||
raw: " AND ",
|
||||
expr: {:^, [], [param_count + 2]},
|
||||
raw: ""
|
||||
]}}
|
||||
end
|
||||
end
|
||||
|
||||
defp filter_value_to_expr(
|
||||
attribute,
|
||||
%And{left: left, right: right},
|
||||
|
|
110
lib/predicates/trigram.ex
Normal file
110
lib/predicates/trigram.ex
Normal file
|
@ -0,0 +1,110 @@
|
|||
defmodule AshPostgres.Predicates.Trigram do
|
||||
defstruct [:text, :greater_than, :less_than, :equals]
|
||||
|
||||
def new(_resource, attr_name, attr_type, opts) do
|
||||
with :ok <- required_options_provided(opts),
|
||||
{:ok, value} <- Ash.Type.cast_input(attr_type, opts[:text]),
|
||||
{:ok, less_than} <- validate_similarity(opts[:less_than]),
|
||||
{:ok, greater_than} <- validate_similarity(opts[:greater_than]),
|
||||
{:ok, equals} <- validate_similarity(opts[:equals]) do
|
||||
{:ok,
|
||||
%__MODULE__{text: value, greater_than: greater_than, less_than: less_than, equals: equals}}
|
||||
else
|
||||
_ ->
|
||||
{:error,
|
||||
Ash.Error.Filter.InvalidFilterValue.exception(
|
||||
filter: %__MODULE__{
|
||||
text: opts[:text],
|
||||
less_than: opts[:less_than],
|
||||
greater_than: opts[:greater_than],
|
||||
equals: opts[:equals]
|
||||
},
|
||||
value: opts,
|
||||
field: attr_name
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_similarity(nil), do: {:ok, nil}
|
||||
defp validate_similarity(1), do: {:ok, 1}
|
||||
defp validate_similarity(0), do: {:ok, 0}
|
||||
|
||||
defp validate_similarity(similarity)
|
||||
when is_float(similarity) and similarity <= 1.0 and similarity >= 0.0 do
|
||||
{:ok, similarity}
|
||||
end
|
||||
|
||||
defp validate_similarity(similarity) when is_binary(similarity) do
|
||||
sanitized =
|
||||
case similarity do
|
||||
"." <> decimal_part -> "0." <> decimal_part
|
||||
other -> other
|
||||
end
|
||||
|
||||
case Float.parse(sanitized) do
|
||||
{float, ""} -> {:ok, float}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp required_options_provided(opts) do
|
||||
if Keyword.has_key?(opts, :text) do
|
||||
case {opts[:greater_than], opts[:less_than], opts[:equals]} do
|
||||
{nil, nil, nil} -> :error
|
||||
{nil, nil, _equals} -> :ok
|
||||
{_greater_than, nil, nil} -> :ok
|
||||
{nil, _less_than, nil} -> :ok
|
||||
{_greater_than, _less_than, nil} -> :ok
|
||||
end
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: AshPostgres.Predicates.Trigram do
|
||||
import Inspect.Algebra
|
||||
import Ash.Filter.InspectHelpers
|
||||
|
||||
def inspect(%{text: text, equals: nil, less_than: nil, greater_than: greater_than}, opts) do
|
||||
concat([
|
||||
attr(opts),
|
||||
" trigram similarity to ",
|
||||
to_doc(text, opts),
|
||||
" is greater than ",
|
||||
to_doc(greater_than, opts)
|
||||
])
|
||||
end
|
||||
|
||||
def inspect(%{text: text, equals: nil, less_than: less_than, greater_than: nil}, opts) do
|
||||
concat([
|
||||
attr(opts),
|
||||
" trigram similarity to ",
|
||||
to_doc(text, opts),
|
||||
" is less than ",
|
||||
to_doc(less_than, opts)
|
||||
])
|
||||
end
|
||||
|
||||
def inspect(%{text: text, equals: nil, less_than: less_than, greater_than: greater_than}, opts) do
|
||||
concat([
|
||||
attr(opts),
|
||||
" trigram similarity to ",
|
||||
to_doc(text, opts),
|
||||
" is between ",
|
||||
to_doc(less_than, opts),
|
||||
" and ",
|
||||
to_doc(greater_than, opts)
|
||||
])
|
||||
end
|
||||
|
||||
def inspect(%{text: text, equals: equals, less_than: nil, greater_than: nil}, opts) do
|
||||
concat([
|
||||
attr(opts),
|
||||
" trigram similarity to ",
|
||||
to_doc(text, opts),
|
||||
" == ",
|
||||
to_doc(equals, opts)
|
||||
])
|
||||
end
|
||||
end
|
29
lib/repo.ex
Normal file
29
lib/repo.ex
Normal file
|
@ -0,0 +1,29 @@
|
|||
defmodule AshPostgres.Repo do
|
||||
@callback installed_extensions() :: [String.t()]
|
||||
|
||||
defmacro __using__(opts) do
|
||||
quote bind_quoted: [opts: opts] do
|
||||
otp_app = opts[:otp_app] || raise("Must configure OTP app")
|
||||
|
||||
use Ecto.Repo,
|
||||
adapter: Ecto.Adapters.Postgres,
|
||||
otp_app: otp_app
|
||||
|
||||
def installed_extensions() do
|
||||
[]
|
||||
end
|
||||
|
||||
def init(:supervisor, config) do
|
||||
new_config = Keyword.put(config, :installed_extensions, installed_extensions())
|
||||
|
||||
{:ok, new_config}
|
||||
end
|
||||
|
||||
def init(:runtime, config) do
|
||||
init(:supervisor, config)
|
||||
end
|
||||
|
||||
defoverridable installed_extensions: 0
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue