feat: updates query in member ressource
This commit is contained in:
parent
b025be5932
commit
142f1858e6
1 changed files with 86 additions and 45 deletions
|
|
@ -29,7 +29,9 @@ defmodule Mv.Membership.Member do
|
||||||
|
|
||||||
## Full-Text Search
|
## Full-Text Search
|
||||||
Members have a `search_vector` attribute (tsvector) that is automatically
|
Members have a `search_vector` attribute (tsvector) that is automatically
|
||||||
updated via database trigger. Search includes name, email, notes, and contact fields.
|
updated via database trigger. Search includes name, email, notes, contact fields,
|
||||||
|
and all custom field values. Custom field values are automatically included in
|
||||||
|
the search vector with weight 'C' (same as phone_number, city, etc.).
|
||||||
"""
|
"""
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
domain: Mv.Membership,
|
domain: Mv.Membership,
|
||||||
|
|
@ -141,28 +143,16 @@ defmodule Mv.Membership.Member do
|
||||||
q2 = String.trim(q)
|
q2 = String.trim(q)
|
||||||
pat = "%" <> q2 <> "%"
|
pat = "%" <> q2 <> "%"
|
||||||
|
|
||||||
# FTS as main filter and fuzzy search just for first name, last name and strees
|
# Build search filters grouped by search type for maintainability
|
||||||
|
# Priority: FTS > Substring > Custom Fields > Fuzzy Matching
|
||||||
|
fts_match = build_fts_filter(q2)
|
||||||
|
substring_match = build_substring_filter(q2, pat)
|
||||||
|
custom_field_match = build_custom_field_filter(pat)
|
||||||
|
fuzzy_match = build_fuzzy_filter(q2, threshold)
|
||||||
|
|
||||||
query
|
query
|
||||||
|> Ash.Query.filter(
|
|> Ash.Query.filter(
|
||||||
expr(
|
expr(^fts_match or ^substring_match or ^custom_field_match or ^fuzzy_match)
|
||||||
# Substring on numeric-like fields (best effort, supports middle substrings)
|
|
||||||
fragment("search_vector @@ websearch_to_tsquery('simple', ?)", ^q2) or
|
|
||||||
fragment("search_vector @@ plainto_tsquery('simple', ?)", ^q2) or
|
|
||||||
contains(postal_code, ^q2) or
|
|
||||||
contains(house_number, ^q2) or
|
|
||||||
contains(phone_number, ^q2) or
|
|
||||||
contains(email, ^q2) or
|
|
||||||
contains(city, ^q2) or ilike(city, ^pat) 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
|
else
|
||||||
query
|
query
|
||||||
|
|
@ -507,6 +497,67 @@ defmodule Mv.Membership.Member do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Search Filter Builders
|
||||||
|
# ============================================================================
|
||||||
|
# These functions build search filters grouped by search type for maintainability.
|
||||||
|
# Priority order: FTS > Substring > Custom Fields > Fuzzy Matching
|
||||||
|
|
||||||
|
# Builds full-text search filter using tsvector (highest priority, fastest)
|
||||||
|
# Uses GIN index on search_vector for optimal performance
|
||||||
|
defp build_fts_filter(query) do
|
||||||
|
expr(
|
||||||
|
fragment("search_vector @@ websearch_to_tsquery('simple', ?)", ^query) or
|
||||||
|
fragment("search_vector @@ plainto_tsquery('simple', ?)", ^query)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Builds substring search filter for structured fields
|
||||||
|
# Note: contains/2 uses ILIKE '%value%' which is not index-optimized
|
||||||
|
# Performance: Good for small datasets, may be slow on large tables
|
||||||
|
defp build_substring_filter(query, pattern) do
|
||||||
|
expr(
|
||||||
|
contains(postal_code, ^query) or
|
||||||
|
contains(house_number, ^query) or
|
||||||
|
contains(phone_number, ^query) or
|
||||||
|
contains(email, ^query) or
|
||||||
|
contains(city, ^query) or
|
||||||
|
ilike(city, ^pattern)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Builds search filter for custom field values using LIKE on JSONB
|
||||||
|
# Note: LIKE on JSONB is not index-optimized, may be slow with many custom fields
|
||||||
|
# This is a fallback for substring matching in custom fields (e.g., phone numbers)
|
||||||
|
defp build_custom_field_filter(pattern) do
|
||||||
|
expr(
|
||||||
|
fragment(
|
||||||
|
"EXISTS (SELECT 1 FROM custom_field_values WHERE member_id = id AND (value->>'_union_value' LIKE ? OR value->>'value' LIKE ? OR (value->'_union_value')::text LIKE ? OR (value->'value')::text LIKE ?))",
|
||||||
|
^pattern,
|
||||||
|
^pattern,
|
||||||
|
^pattern,
|
||||||
|
^pattern
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Builds fuzzy/trigram matching filter for name and street fields
|
||||||
|
# Uses pg_trgm extension with GIN indexes for performance
|
||||||
|
# Note: Requires trigram indexes on first_name, last_name, street
|
||||||
|
defp build_fuzzy_filter(query, threshold) do
|
||||||
|
expr(
|
||||||
|
fragment("? % first_name", ^query) or
|
||||||
|
fragment("? % last_name", ^query) or
|
||||||
|
fragment("? % street", ^query) or
|
||||||
|
fragment("word_similarity(?, first_name) > ?", ^query, ^threshold) or
|
||||||
|
fragment("word_similarity(?, last_name) > ?", ^query, ^threshold) or
|
||||||
|
fragment("word_similarity(?, street) > ?", ^query, ^threshold) or
|
||||||
|
fragment("similarity(first_name, ?) > ?", ^query, ^threshold) or
|
||||||
|
fragment("similarity(last_name, ?) > ?", ^query, ^threshold) or
|
||||||
|
fragment("similarity(street, ?) > ?", ^query, ^threshold)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
# Private helper to apply filters for :available_for_linking action
|
# Private helper to apply filters for :available_for_linking action
|
||||||
# user_email: may be nil/empty when creating new user, or populated when editing
|
# user_email: may be nil/empty when creating new user, or populated when editing
|
||||||
# search_query: optional search term for fuzzy matching
|
# search_query: optional search term for fuzzy matching
|
||||||
|
|
@ -527,34 +578,24 @@ defmodule Mv.Membership.Member do
|
||||||
# Search query provided: return email-match OR fuzzy-search candidates
|
# Search query provided: return email-match OR fuzzy-search candidates
|
||||||
trimmed_search = String.trim(search_query)
|
trimmed_search = String.trim(search_query)
|
||||||
|
|
||||||
|
pat = "%" <> trimmed_search <> "%"
|
||||||
|
|
||||||
|
# Build search filters using modular functions for maintainability
|
||||||
|
fts_match = build_fts_filter(trimmed_search)
|
||||||
|
custom_field_match = build_custom_field_filter(pat)
|
||||||
|
fuzzy_match = build_fuzzy_filter(trimmed_search, @default_similarity_threshold)
|
||||||
|
email_substring_match = expr(contains(email, ^trimmed_search))
|
||||||
|
|
||||||
query
|
query
|
||||||
|> Ash.Query.filter(
|
|> Ash.Query.filter(
|
||||||
expr(
|
expr(
|
||||||
# Email match candidate (for filter_by_email_match priority)
|
# Email exact match has highest priority (for filter_by_email_match)
|
||||||
# If email is "", this is always false and fuzzy search takes over
|
# If email is "", this is always false and search filters take over
|
||||||
# Fuzzy search candidates
|
|
||||||
email == ^trimmed_email or
|
email == ^trimmed_email or
|
||||||
fragment("search_vector @@ websearch_to_tsquery('simple', ?)", ^trimmed_search) or
|
^fts_match or
|
||||||
fragment("search_vector @@ plainto_tsquery('simple', ?)", ^trimmed_search) or
|
^custom_field_match or
|
||||||
fragment("? % first_name", ^trimmed_search) or
|
^fuzzy_match or
|
||||||
fragment("? % last_name", ^trimmed_search) or
|
^email_substring_match
|
||||||
fragment("word_similarity(?, first_name) > 0.2", ^trimmed_search) or
|
|
||||||
fragment(
|
|
||||||
"word_similarity(?, last_name) > ?",
|
|
||||||
^trimmed_search,
|
|
||||||
^@default_similarity_threshold
|
|
||||||
) or
|
|
||||||
fragment(
|
|
||||||
"similarity(first_name, ?) > ?",
|
|
||||||
^trimmed_search,
|
|
||||||
^@default_similarity_threshold
|
|
||||||
) or
|
|
||||||
fragment(
|
|
||||||
"similarity(last_name, ?) > ?",
|
|
||||||
^trimmed_search,
|
|
||||||
^@default_similarity_threshold
|
|
||||||
) or
|
|
||||||
contains(email, ^trimmed_search)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue