This commit is contained in:
parent
f6575319f7
commit
63b8e70e62
4 changed files with 83 additions and 77 deletions
|
|
@ -233,9 +233,8 @@ Settings (1) → MembershipFeeType (0..1)
|
|||
## Full-Text Search
|
||||
|
||||
### Implementation
|
||||
- **Trigger:** `members_search_vector_trigger()` on `members` (INSERT/UPDATE)
|
||||
- **Trigger:** `update_member_search_vector_from_member_groups()` on `member_groups` (INSERT/UPDATE/DELETE)
|
||||
- **Function:** Automatically updates `search_vector` on member and member_groups changes
|
||||
- **Trigger** on `members` (INSERT/UPDATE): runs function `members_search_vector_trigger()`
|
||||
- **Trigger** on `member_groups` (INSERT/UPDATE/DELETE): `update_member_search_vector_on_member_groups_change` runs function `update_member_search_vector_from_member_groups()`
|
||||
- **Index Type:** GIN (Generalized Inverted Index)
|
||||
|
||||
### Weighted Fields
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ defmodule Mv.Repo.Migrations.AddGroupNamesToMemberSearchVector do
|
|||
FROM custom_field_values
|
||||
WHERE member_id = NEW.id AND value IS NOT NULL;
|
||||
|
||||
-- Aggregate group names for this member (weight B, same as city/notes)
|
||||
-- Aggregate group names for this member (weight B, same as notes/email)
|
||||
SELECT string_agg(g.name, ' ')
|
||||
INTO groups_text
|
||||
FROM member_groups mg
|
||||
|
|
@ -162,7 +162,8 @@ defmodule Mv.Repo.Migrations.AddGroupNamesToMemberSearchVector do
|
|||
$$ LANGUAGE plpgsql;
|
||||
""")
|
||||
|
||||
# 3. Trigger on member_groups: when associations change, refresh that member's search_vector
|
||||
# 3. Trigger on member_groups: when associations change, refresh affected member(s) search_vector.
|
||||
# On UPDATE with different member_id, refresh both OLD and NEW member so neither keeps a stale vector.
|
||||
execute("""
|
||||
CREATE FUNCTION update_member_search_vector_from_member_groups() RETURNS trigger AS $$
|
||||
DECLARE
|
||||
|
|
@ -180,66 +181,71 @@ defmodule Mv.Repo.Migrations.AddGroupNamesToMemberSearchVector do
|
|||
custom_values_text text;
|
||||
groups_text text;
|
||||
BEGIN
|
||||
member_id_val := COALESCE(NEW.member_id, OLD.member_id);
|
||||
FOR member_id_val IN
|
||||
SELECT COALESCE(NEW.member_id, OLD.member_id)
|
||||
UNION ALL
|
||||
SELECT OLD.member_id
|
||||
WHERE TG_OP = 'UPDATE' AND OLD.member_id IS DISTINCT FROM NEW.member_id
|
||||
LOOP
|
||||
SELECT
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
join_date,
|
||||
exit_date,
|
||||
notes,
|
||||
city,
|
||||
street,
|
||||
house_number,
|
||||
postal_code
|
||||
INTO
|
||||
member_first_name,
|
||||
member_last_name,
|
||||
member_email,
|
||||
member_join_date,
|
||||
member_exit_date,
|
||||
member_notes,
|
||||
member_city,
|
||||
member_street,
|
||||
member_house_number,
|
||||
member_postal_code
|
||||
FROM members
|
||||
WHERE id = member_id_val;
|
||||
|
||||
SELECT
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
join_date,
|
||||
exit_date,
|
||||
notes,
|
||||
city,
|
||||
street,
|
||||
house_number,
|
||||
postal_code
|
||||
INTO
|
||||
member_first_name,
|
||||
member_last_name,
|
||||
member_email,
|
||||
member_join_date,
|
||||
member_exit_date,
|
||||
member_notes,
|
||||
member_city,
|
||||
member_street,
|
||||
member_house_number,
|
||||
member_postal_code
|
||||
FROM members
|
||||
WHERE id = member_id_val;
|
||||
SELECT string_agg(
|
||||
CASE
|
||||
WHEN value ? '_union_value' THEN value->>'_union_value'
|
||||
WHEN value ? 'value' THEN value->>'value'
|
||||
ELSE ''
|
||||
END,
|
||||
' '
|
||||
)
|
||||
INTO custom_values_text
|
||||
FROM custom_field_values
|
||||
WHERE member_id = member_id_val AND value IS NOT NULL;
|
||||
|
||||
SELECT string_agg(
|
||||
CASE
|
||||
WHEN value ? '_union_value' THEN value->>'_union_value'
|
||||
WHEN value ? 'value' THEN value->>'value'
|
||||
ELSE ''
|
||||
END,
|
||||
' '
|
||||
)
|
||||
INTO custom_values_text
|
||||
FROM custom_field_values
|
||||
WHERE member_id = member_id_val AND value IS NOT NULL;
|
||||
SELECT string_agg(g.name, ' ')
|
||||
INTO groups_text
|
||||
FROM member_groups mg
|
||||
JOIN groups g ON g.id = mg.group_id
|
||||
WHERE mg.member_id = member_id_val;
|
||||
|
||||
SELECT string_agg(g.name, ' ')
|
||||
INTO groups_text
|
||||
FROM member_groups mg
|
||||
JOIN groups g ON g.id = mg.group_id
|
||||
WHERE mg.member_id = member_id_val;
|
||||
|
||||
UPDATE members
|
||||
SET search_vector =
|
||||
setweight(to_tsvector('simple', coalesce(member_first_name, '')), 'A') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_last_name, '')), 'A') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_email, '')), 'B') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_join_date::text, '')), 'D') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_exit_date::text, '')), 'D') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_notes, '')), 'B') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_city, '')), 'C') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_street, '')), 'C') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_house_number::text, '')), 'C') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_postal_code::text, '')), 'C') ||
|
||||
setweight(to_tsvector('simple', coalesce(custom_values_text, '')), 'C') ||
|
||||
setweight(to_tsvector('simple', coalesce(groups_text, '')), 'B')
|
||||
WHERE id = member_id_val;
|
||||
UPDATE members
|
||||
SET search_vector =
|
||||
setweight(to_tsvector('simple', coalesce(member_first_name, '')), 'A') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_last_name, '')), 'A') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_email, '')), 'B') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_join_date::text, '')), 'D') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_exit_date::text, '')), 'D') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_notes, '')), 'B') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_city, '')), 'C') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_street, '')), 'C') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_house_number::text, '')), 'C') ||
|
||||
setweight(to_tsvector('simple', coalesce(member_postal_code::text, '')), 'C') ||
|
||||
setweight(to_tsvector('simple', coalesce(custom_values_text, '')), 'C') ||
|
||||
setweight(to_tsvector('simple', coalesce(groups_text, '')), 'B')
|
||||
WHERE id = member_id_val;
|
||||
END LOOP;
|
||||
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END
|
||||
|
|
|
|||
|
|
@ -273,7 +273,9 @@ defmodule Mv.Membership.MemberSearchGroupsIntegrationTest do
|
|||
end
|
||||
|
||||
describe "edge cases" do
|
||||
test "partial group name matches via FTS", %{system_actor: actor} do
|
||||
test "token match: single word in group name matches (e.g. Board in Board Members)", %{
|
||||
system_actor: actor
|
||||
} do
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(
|
||||
%{first_name: "Partial", last_name: "Test", email: "partial@example.com"},
|
||||
|
|
@ -300,7 +302,9 @@ defmodule Mv.Membership.MemberSearchGroupsIntegrationTest do
|
|||
"Search for 'Board' should find member in group 'Board Members'"
|
||||
end
|
||||
|
||||
test "special characters in group name do not break search", %{system_actor: actor} do
|
||||
test "search with token from group name containing special characters does not crash", %{
|
||||
system_actor: actor
|
||||
} do
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(
|
||||
%{first_name: "Special", last_name: "Char", email: "special@example.com"},
|
||||
|
|
@ -317,7 +321,7 @@ defmodule Mv.Membership.MemberSearchGroupsIntegrationTest do
|
|||
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
|
||||
|> Ash.create(actor: actor)
|
||||
|
||||
# Search should not crash; at least exact or word match should work
|
||||
# Search for a token from the group name; proves tokenization does not crash on "A&B"
|
||||
results =
|
||||
Member
|
||||
|> Member.fuzzy_search(%{query: "Team"})
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
|
|||
import Phoenix.LiveViewTest
|
||||
require Ash.Query
|
||||
|
||||
alias Mv.Membership.{Group, MemberGroup, CustomField, CustomFieldValue}
|
||||
alias Mv.Helpers.SystemActor
|
||||
alias Mv.Membership.{CustomField, CustomFieldValue, Group, MemberGroup}
|
||||
alias Mv.MembershipFees.{MembershipFeeCycle, MembershipFeeType}
|
||||
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
|
||||
# Create test members
|
||||
{:ok, member1} =
|
||||
|
|
@ -81,15 +83,10 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Verify groups column is visible by default
|
||||
# Verify groups column is visible by default (header and content)
|
||||
assert html =~ group1.name
|
||||
assert html =~ member1.first_name
|
||||
|
||||
# Hide groups column via field visibility dropdown
|
||||
# (This tests integration with field visibility feature)
|
||||
# Note: Actual implementation depends on how field visibility works
|
||||
# For now, we verify the column exists and can be toggled
|
||||
assert html
|
||||
assert html =~ "Groups"
|
||||
end
|
||||
|
||||
test "groups filter works with custom field filters", %{
|
||||
|
|
@ -141,11 +138,11 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
|
|||
member1: member1,
|
||||
group1: group1
|
||||
} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
|
||||
# Create a membership fee type and cycle for member1
|
||||
{:ok, fee_type} =
|
||||
Mv.MembershipFees.MembershipFeeType
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test Fee",
|
||||
amount: Decimal.new("50.00"),
|
||||
|
|
@ -160,7 +157,7 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
|
|||
)
|
||||
|
||||
{:ok, _cycle} =
|
||||
Mv.MembershipFees.MembershipFeeCycle
|
||||
MembershipFeeCycle
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
membership_fee_type_id: fee_type.id,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue