feat: add member fuzzy search for linking (#168)
This commit is contained in:
parent
2781f77a72
commit
ce21ebad9b
1 changed files with 76 additions and 1 deletions
|
|
@ -152,7 +152,8 @@ defmodule Mv.Membership.Member do
|
||||||
prepare fn query, _ctx ->
|
prepare fn query, _ctx ->
|
||||||
q = Ash.Query.get_argument(query, :query) || ""
|
q = Ash.Query.get_argument(query, :query) || ""
|
||||||
|
|
||||||
# 0.2 as similarity threshold (recommended) - lower value can lead to more results but also to more unspecific results
|
# 0.2 as similarity threshold (recommended)
|
||||||
|
# Lower value can lead to more results but also to more unspecific results
|
||||||
threshold = Ash.Query.get_argument(query, :similarity_threshold) || 0.2
|
threshold = Ash.Query.get_argument(query, :similarity_threshold) || 0.2
|
||||||
|
|
||||||
if is_binary(q) and String.trim(q) != "" do
|
if is_binary(q) and String.trim(q) != "" do
|
||||||
|
|
@ -187,7 +188,81 @@ defmodule Mv.Membership.Member do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Action to find members available for linking to a user account
|
||||||
|
# Returns only unlinked members (user_id == nil), limited to 10 results
|
||||||
|
#
|
||||||
|
# Special behavior for email matching:
|
||||||
|
# - When user_email AND search_query are both provided: filter by email (email takes precedence)
|
||||||
|
# - When only user_email provided: return all unlinked members (caller should use filter_by_email_match helper)
|
||||||
|
# - When only search_query provided: filter by search terms
|
||||||
|
read :available_for_linking do
|
||||||
|
argument :user_email, :string, allow_nil?: true
|
||||||
|
argument :search_query, :string, allow_nil?: true
|
||||||
|
|
||||||
|
prepare fn query, _ctx ->
|
||||||
|
user_email = Ash.Query.get_argument(query, :user_email)
|
||||||
|
search_query = Ash.Query.get_argument(query, :search_query)
|
||||||
|
|
||||||
|
# Start with base filter: only unlinked members
|
||||||
|
base_query = Ash.Query.filter(query, is_nil(user))
|
||||||
|
|
||||||
|
# Determine filtering strategy
|
||||||
|
# Priority: search_query (if present) > no filters
|
||||||
|
# user_email is used for POST-filtering via filter_by_email_match helper
|
||||||
|
if not is_nil(search_query) and String.trim(search_query) != "" do
|
||||||
|
# Search query present: Use fuzzy search (regardless of user_email)
|
||||||
|
trimmed = String.trim(search_query)
|
||||||
|
|
||||||
|
# Use same fuzzy search as :search action (PostgreSQL Trigram + FTS)
|
||||||
|
base_query
|
||||||
|
|> Ash.Query.filter(
|
||||||
|
expr(
|
||||||
|
# Full-text search
|
||||||
|
# Trigram similarity for names
|
||||||
|
# Exact substring match for email
|
||||||
|
fragment("search_vector @@ websearch_to_tsquery('simple', ?)", ^trimmed) or
|
||||||
|
fragment("search_vector @@ plainto_tsquery('simple', ?)", ^trimmed) or
|
||||||
|
fragment("? % first_name", ^trimmed) or
|
||||||
|
fragment("? % last_name", ^trimmed) or
|
||||||
|
fragment("word_similarity(?, first_name) > 0.2", ^trimmed) or
|
||||||
|
fragment("word_similarity(?, last_name) > 0.2", ^trimmed) or
|
||||||
|
fragment("similarity(first_name, ?) > 0.2", ^trimmed) or
|
||||||
|
fragment("similarity(last_name, ?) > 0.2", ^trimmed) or
|
||||||
|
contains(email, ^trimmed)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|> Ash.Query.limit(10)
|
||||||
|
else
|
||||||
|
# No search query: return all unlinked members
|
||||||
|
# Caller should use filter_by_email_match helper for email match logic
|
||||||
|
base_query
|
||||||
|
|> Ash.Query.limit(10)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Public helper function to apply email match logic after query execution
|
||||||
|
# This should be called after using :available_for_linking with user_email argument
|
||||||
|
#
|
||||||
|
# If a member with matching email exists, returns only that member
|
||||||
|
# Otherwise returns all members (no filtering)
|
||||||
|
def filter_by_email_match(members, user_email)
|
||||||
|
when is_list(members) and is_binary(user_email) do
|
||||||
|
# Check if any member matches the email
|
||||||
|
email_match = Enum.find(members, &(&1.email == user_email))
|
||||||
|
|
||||||
|
if email_match do
|
||||||
|
# Return only the email-matched member
|
||||||
|
[email_match]
|
||||||
|
else
|
||||||
|
# No email match, return all members
|
||||||
|
members
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_by_email_match(members, _user_email), do: members
|
||||||
|
|
||||||
validations do
|
validations do
|
||||||
# Required fields are covered by allow_nil? false
|
# Required fields are covered by allow_nil? false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue