diff --git a/lib/membership/member.ex b/lib/membership/member.ex index 56549fc..c62c47d 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -3,6 +3,11 @@ defmodule Mv.Membership.Member do domain: Mv.Membership, 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 table "members" repo Mv.Repo @@ -108,6 +113,47 @@ defmodule Mv.Membership.Member do where [changing(:user)] 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 + + 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( + 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 + end + end end validations do @@ -281,4 +327,21 @@ defmodule Mv.Membership.Member do identities do identity :unique_email, [:email] 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