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
|
||||
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,
|
||||
domain: Mv.Membership,
|
||||
|
|
@ -141,28 +143,16 @@ defmodule Mv.Membership.Member do
|
|||
q2 = String.trim(q)
|
||||
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
|
||||
|> Ash.Query.filter(
|
||||
expr(
|
||||
# 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)
|
||||
)
|
||||
expr(^fts_match or ^substring_match or ^custom_field_match or ^fuzzy_match)
|
||||
)
|
||||
else
|
||||
query
|
||||
|
|
@ -507,6 +497,67 @@ defmodule Mv.Membership.Member do
|
|||
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
|
||||
# user_email: may be nil/empty when creating new user, or populated when editing
|
||||
# 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
|
||||
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
|
||||
|> Ash.Query.filter(
|
||||
expr(
|
||||
# Email match candidate (for filter_by_email_match priority)
|
||||
# If email is "", this is always false and fuzzy search takes over
|
||||
# Fuzzy search candidates
|
||||
# Email exact match has highest priority (for filter_by_email_match)
|
||||
# If email is "", this is always false and search filters take over
|
||||
email == ^trimmed_email or
|
||||
fragment("search_vector @@ websearch_to_tsquery('simple', ?)", ^trimmed_search) or
|
||||
fragment("search_vector @@ plainto_tsquery('simple', ?)", ^trimmed_search) or
|
||||
fragment("? % first_name", ^trimmed_search) or
|
||||
fragment("? % last_name", ^trimmed_search) or
|
||||
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)
|
||||
^fts_match or
|
||||
^custom_field_match or
|
||||
^fuzzy_match or
|
||||
^email_substring_match
|
||||
)
|
||||
)
|
||||
else
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue