Implement fuzzy search #187
1 changed files with 63 additions and 0 deletions
|
|
@ -3,6 +3,11 @@ defmodule Mv.Membership.Member do
|
||||||
domain: Mv.Membership,
|
domain: Mv.Membership,
|
||||||
data_layer: AshPostgres.DataLayer
|
data_layer: AshPostgres.DataLayer
|
||||||
|
|
||||||
|
require Ash.Query
|
||||||
|
import Ash.Expr
|
||||||
|
|
||||||
|
@default_fields [:first_name, :last_name, :email, :phone_number, :city, :street, :house_number, :postal_code]
|
||||||
|
|
||||||
postgres do
|
postgres do
|
||||||
table "members"
|
table "members"
|
||||||
repo Mv.Repo
|
repo Mv.Repo
|
||||||
|
|
@ -108,6 +113,47 @@ defmodule Mv.Membership.Member do
|
||||||
where [changing(:user)]
|
where [changing(:user)]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
read :search do
|
||||||
|
argument :query, :string, allow_nil?: true
|
||||||
|
argument :fields, {:array, :atom}, allow_nil?: true
|
||||||
|
argument :similarity_threshold, :float, allow_nil?: true
|
||||||
|
|
||||||
|
prepare fn query, _ctx ->
|
||||||
|
q = Ash.Query.get_argument(query, :query) || ""
|
||||||
|
fields = Ash.Query.get_argument(query, :fields) || @default_fields
|
||||||
|
threshold = Ash.Query.get_argument(query, :similarity_threshold) || 0.2
|
||||||
|
|
||||||
|
carla marked this conversation as resolved
|
|||||||
|
if is_binary(q) and String.trim(q) != "" do
|
||||||
|
q2 = String.trim(q)
|
||||||
|
pat = "%" <> q2 <> "%"
|
||||||
|
|
||||||
|
# FTS as main filter and fuzzy search just fo first name, last name and strees
|
||||||
|
query
|
||||||
|
|> Ash.Query.filter(
|
||||||
|
carla marked this conversation as resolved
moritz
commented
the the `fields` argument is never used in the `search` function.
|
|||||||
|
expr(
|
||||||
|
fragment("search_vector @@ websearch_to_tsquery('simple', ?)", ^q2) or
|
||||||
|
fragment("search_vector @@ plainto_tsquery('simple', ?)", ^q2) or
|
||||||
|
# Substring on numeric-like fields (best effort, supports middle substrings)
|
||||||
|
contains(postal_code, ^q2) or
|
||||||
|
contains(house_number, ^q2) or
|
||||||
|
contains(phone_number, ^q2) or
|
||||||
|
fragment("? % first_name", ^q2) or
|
||||||
|
fragment("? % last_name", ^q2) or
|
||||||
|
fragment("? % street", ^q2) or
|
||||||
|
fragment("word_similarity(?, first_name) > ?", ^q2, ^threshold) or
|
||||||
|
fragment("word_similarity(?, last_name) > ?", ^q2, ^threshold) or
|
||||||
|
fragment("word_similarity(?, street) > ?", ^q2, ^threshold) or
|
||||||
|
fragment("similarity(first_name, ?) > ?", ^q2, ^threshold) or
|
||||||
|
fragment("similarity(last_name, ?) > ?", ^q2, ^threshold) or
|
||||||
|
fragment("similarity(street, ?) > ?", ^q2, ^threshold)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
query
|
||||||
|
end
|
||||||
|
carla marked this conversation as resolved
moritz
commented
maybe the duplicate code for maybe the duplicate code for `first name` `last name` and `street` could be generated dynamically by the fields argument?
carla
commented
I decided to omit fields now...but maybe during refactoring we could think about a dynamic way I decided to omit fields now...but maybe during refactoring we could think about a dynamic way
|
|||||||
|
end
|
||||||
|
end
|
||||||
|
carla marked this conversation as resolved
moritz
commented
filter for email is missing
filter for email is missing
adding this worked for me:
```
fragment("? % email", ^q2) or
fragment("word_similarity(?, email) > ?", ^q2, ^threshold) or
fragment("similarity(email, ?) > ?", ^q2, ^threshold) or
```
carla
commented
You're totally right, i forgot the email field. But I would favor for a simple contains ilike on the email field, because I think fuzzy search on the name already is enough? or what do you think? You're totally right, i forgot the email field. But I would favor for a simple contains ilike on the email field, because I think fuzzy search on the name already is enough? or what do you think?
carla
commented
I added it as ilike and not as fuzzy search. But we can create another issue if we see we need it :) I added it as ilike and not as fuzzy search. But we can create another issue if we see we need it :)
|
|||||||
end
|
end
|
||||||
|
|
||||||
validations do
|
validations do
|
||||||
|
|
@ -281,4 +327,21 @@ defmodule Mv.Membership.Member do
|
||||||
identities do
|
identities do
|
||||||
identity :unique_email, [:email]
|
identity :unique_email, [:email]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Fuzzy Search function that can be called by live view and calls search action
|
||||||
|
def fuzzy_search(query, opts) do
|
||||||
|
q = (opts[:query] || opts["query"] || "") |> to_string()
|
||||||
|
|
||||||
|
if String.trim(q) == "" do
|
||||||
|
query
|
||||||
|
else
|
||||||
|
args =
|
||||||
|
case (opts[:fields] || opts["fields"]) do
|
||||||
|
nil -> %{query: q}
|
||||||
|
fields -> %{query: q, fields: fields}
|
||||||
|
end
|
||||||
|
|
||||||
|
Ash.Query.for_read(query, :search, args)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue
some moduledocs would be nice for the public
searchfunction.