perf(db): set-based rank 4 and filters in search_catalog_games, harden tests (KAT-01, 3a)

Rank 4 and the genre/mechanic filters ran a correlated subplan per game
(13 s at 10 000 games). Now: one set-based word coverage, filters as one hashed
IN set, 'like all' patterns, similarity only for hits. Adds pgTAP cases found
by mutation testing (paging by relevance, alt title choice, duplicate words,
mechanic duplicates) and a Deno case for mark characters.

Refs #3

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
cschlaefke 2026-11-04 01:40:43 +01:00
parent 976fbe9fe1
commit 683b5c830e
3 changed files with 123 additions and 45 deletions

View file

@ -305,6 +305,7 @@ Deno.test("alt_titles: Fehler anderer Art nennen die Zeilennummer", async () =>
Deno.test("approxNormalize: Marken- und Schutzzeichen, Akzente, ß, Æ, Sonderzeichen", () => { Deno.test("approxNormalize: Marken- und Schutzzeichen, Akzente, ß, Æ, Sonderzeichen", () => {
assert.equal(approxNormalize("CATAN®"), "catan"); assert.equal(approxNormalize("CATAN®"), "catan");
assert.equal(approxNormalize("Spiel™Ding"), "spielding"); // Schutzzeichen werden entfernt, nicht zum Trenner (wie search_normalize)
assert.equal(approxNormalize("Café Größe"), "cafe grosse"); assert.equal(approxNormalize("Café Größe"), "cafe grosse");
assert.equal(approxNormalize("Æon’s End"), "aeon s end"); assert.equal(approxNormalize("Æon’s End"), "aeon s end");
assert.equal(approxNormalize("7 Wonders: Duel"), "7 wonders duel"); assert.equal(approxNormalize("7 Wonders: Duel"), "7 wonders duel");

View file

@ -152,7 +152,8 @@ declare
v_offset integer := coalesce(p_offset, 0); v_offset integer := coalesce(p_offset, 0);
v_q text; v_q text;
v_qc text; v_qc text;
v_words text[] := '{}'; v_words text[] := '{}'::text[];
v_patterns text[] := '{}'::text[]; -- '%wort%' je Suchwort, für like all
v_has_query boolean := false; v_has_query boolean := false;
v_genres uuid[]; v_genres uuid[];
v_mechanics uuid[]; v_mechanics uuid[];
@ -189,6 +190,7 @@ begin
v_qc := replace(v_q, ' ', ''); v_qc := replace(v_q, ' ', '');
v_words := coalesce( v_words := coalesce(
array(select distinct w from unnest(string_to_array(v_q, ' ')) as w where char_length(w) >= 2), '{}'); array(select distinct w from unnest(string_to_array(v_q, ' ')) as w where char_length(w) >= 2), '{}');
v_patterns := array(select '%' || w || '%' from unnest(v_words) as w);
end if; end if;
end if; end if;
@ -215,10 +217,10 @@ begin
where (p_players is null or (c.min_players <= p_players and p_players <= c.max_players)) where (p_players is null or (c.min_players <= p_players and p_players <= c.max_players))
and (p_max_minutes is null or coalesce(c.max_minutes, c.min_minutes) <= p_max_minutes) and (p_max_minutes is null or coalesce(c.max_minutes, c.min_minutes) <= p_max_minutes)
and (p_age is null or c.min_age <= p_age) and (p_age is null or c.min_age <= p_age)
and (v_genre_n = 0 or (select count(*) from public.game_genres gg and (v_genre_n = 0 or c.id in (select gg.game_id from public.game_genres gg where gg.genre_id = any (v_genres)
where gg.game_id = c.id and gg.genre_id = any (v_genres)) = v_genre_n) group by gg.game_id having count(*) = v_genre_n))
and (v_mech_n = 0 or (select count(*) from public.game_mechanics gm and (v_mech_n = 0 or c.id in (select gm.game_id from public.game_mechanics gm where gm.mechanic_id = any (v_mechanics)
where gm.game_id = c.id and gm.mechanic_id = any (v_mechanics)) = v_mech_n) group by gm.game_id having count(*) = v_mech_n))
order by g.title_search collate "C", c.id order by g.title_search collate "C", c.id
limit v_limit offset v_offset limit v_limit offset v_offset
) s ) s
@ -235,10 +237,10 @@ begin
where (p_players is null or (c.min_players <= p_players and p_players <= c.max_players)) where (p_players is null or (c.min_players <= p_players and p_players <= c.max_players))
and (p_max_minutes is null or coalesce(c.max_minutes, c.min_minutes) <= p_max_minutes) and (p_max_minutes is null or coalesce(c.max_minutes, c.min_minutes) <= p_max_minutes)
and (p_age is null or c.min_age <= p_age) and (p_age is null or c.min_age <= p_age)
and (v_genre_n = 0 or (select count(*) from public.game_genres gg and (v_genre_n = 0 or c.id in (select gg.game_id from public.game_genres gg where gg.genre_id = any (v_genres)
where gg.game_id = c.id and gg.genre_id = any (v_genres)) = v_genre_n) group by gg.game_id having count(*) = v_genre_n))
and (v_mech_n = 0 or (select count(*) from public.game_mechanics gm and (v_mech_n = 0 or c.id in (select gm.game_id from public.game_mechanics gm where gm.mechanic_id = any (v_mechanics)
where gm.game_id = c.id and gm.mechanic_id = any (v_mechanics)) = v_mech_n) group by gm.game_id having count(*) = v_mech_n))
), ),
-- Kandidaten-Titel: Haupttitel (alt_title null) und alle alternativen Titel -- Kandidaten-Titel: Haupttitel (alt_title null) und alle alternativen Titel
cand as ( cand as (
@ -247,50 +249,72 @@ begin
select b.id, a.title_search, a.title select b.id, a.title_search, a.title
from base b join public.game_alt_titles a on a.game_id = b.id from base b join public.game_alt_titles a on a.game_id = b.id
), ),
-- Relevanzstufe je Kandidaten-Titel (null = kein Titeltreffer) -- Relevanzstufe je Kandidaten-Titel (null = kein Titeltreffer). Nur günstige Vergleiche; die teure
-- Trigramm-Ähnlichkeit wird erst für die Treffer berechnet (sims, alt_pick).
ranked as ( ranked as (
select t.id, t.ts, t.alt_title, select t.id, t.ts, t.alt_title,
case case
when t.ts = v_q or replace(t.ts, ' ', '') = v_qc then 0 -- Stufe 0: gleich dem Suchbegriff (auch ohne Leerzeichen gleich; das schließt ts = v_q ein)
when replace(t.ts, ' ', '') = v_qc then 0
when t.ts like v_q || '%' then 1 when t.ts like v_q || '%' then 1
when t.ts like '% ' || v_q || '%' then 2 when t.ts like '% ' || v_q || '%' then 2
when replace(t.ts, ' ', '') like '%' || v_qc || '%' when replace(t.ts, ' ', '') like '%' || v_qc || '%'
or (cardinality(v_words) > 0 or (cardinality(v_patterns) > 0 and t.ts like all (v_patterns))
and not exists (select 1 from unnest(v_words) as w where t.ts not like '%' || w || '%'))
then 3 then 3
end as rk, end as rk
extensions.similarity(t.ts, v_q) as sim
from cand t from cand t
), ),
agg as ( agg as (
select r.id, select r.id,
min(r.rk) as best_rk, min(r.rk) as best_rk,
min(r.rk) filter (where r.alt_title is null) as main_rk, min(r.rk) filter (where r.alt_title is null) as main_rk,
min(r.rk) filter (where r.alt_title is not null) as alt_rk, min(r.rk) filter (where r.alt_title is not null) as alt_rk
(array_agg(r.alt_title order by r.rk, r.sim desc nulls last, r.ts, r.alt_title)
filter (where r.alt_title is not null and r.rk is not null))[1] as alt_title,
max(r.sim) as sim
from ranked r from ranked r
group by r.id group by r.id
), ),
-- der alternative Titel mit der besten Stufe (bei Gleichstand: größte Ähnlichkeit, dann Schreibweise)
alt_pick as (
select distinct on (r.id) r.id, r.alt_title
from ranked r
where r.alt_title is not null and r.rk is not null
order by r.id, r.rk, extensions.similarity(r.ts, v_q) desc, r.ts, r.alt_title
),
-- Rang 4: welches Suchwort kommt in welchem Spiel in einem Titel oder einem Verlagsnamen vor?
-- (als Mengenoperation statt korrelierter Unterabfrage je Spiel: sonst wächst die Zeit quadratisch)
word_hits as (
select r.id, w.w from ranked r cross join unnest(v_words) as w(w) where r.ts like '%' || w.w || '%'
union
select gp.game_id, w.w
from public.publishers p
join public.game_publishers gp on gp.publisher_id = p.id
cross join unnest(v_words) as w(w)
where p.name_search like '%' || w.w || '%'
),
word_cover as (
select wh.id, count(*) as n from word_hits wh group by wh.id
),
hit as ( hit as (
select b.*, a.best_rk, a.main_rk, a.alt_rk, a.alt_title, a.sim select b.*, a.best_rk, a.main_rk, a.alt_rk, ap.alt_title
from base b from base b
join agg a on a.id = b.id join agg a on a.id = b.id
left join alt_pick ap on ap.id = b.id
left join word_cover wc on wc.id = b.id
where a.best_rk is not null where a.best_rk is not null
-- Rang 4: jedes Wort kommt in einem Titel oder einem Verlagsnamen des Spiels vor -- Rang 4: jedes Wort kommt in einem Titel oder einem Verlagsnamen des Spiels vor
or (cardinality(v_words) > 0 or (cardinality(v_words) > 0 and wc.n = cardinality(v_words))
and not exists ( ),
select 1 from unnest(v_words) as w -- Ähnlichkeit (zweites Sortierkriterium): die höchste über Haupttitel und alle alternativen Titel
where not (exists (select 1 from ranked r2 where r2.id = b.id and r2.ts like '%' || w || '%') sims as (
or exists (select 1 from public.game_publishers gp select r.id, max(extensions.similarity(r.ts, v_q)) as sim
join public.publishers p on p.id = gp.publisher_id from ranked r
where gp.game_id = b.id and p.name_search like '%' || w || '%')))) join hit h on h.id = r.id
group by r.id
), ),
paged as ( paged as (
select h.*, coalesce(h.best_rk, 4) as rk, count(*) over () as total select h.*, sm.sim, coalesce(h.best_rk, 4) as rk, count(*) over () as total
from hit h from hit h
order by coalesce(h.best_rk, 4), h.sim desc nulls last, h.title_search collate "C", h.id join sims sm on sm.id = h.id
order by coalesce(h.best_rk, 4), sm.sim desc nulls last, h.title_search collate "C", h.id
limit v_limit offset v_offset limit v_limit offset v_offset
) )
select s.id, s.title, s.year, s.min_players, s.max_players, s.min_minutes, s.max_minutes, s.min_age, select s.id, s.title, s.year, s.min_players, s.max_players, s.min_minutes, s.max_minutes, s.min_age,

View file

@ -6,7 +6,7 @@
-- Ohne Suchbegriff wird nur auf Fixture-IDs geprüft (Hilfsfunktion pg_temp.hits blättert durch). -- Ohne Suchbegriff wird nur auf Fixture-IDs geprüft (Hilfsfunktion pg_temp.hits blättert durch).
-- Die Include-Dateien in ../support/ (\ir) erzeugt supabase/catalog/build_startbestand.ts. -- Die Include-Dateien in ../support/ (\ir) erzeugt supabase/catalog/build_startbestand.ts.
begin; begin;
select plan(201); select plan(211);
insert into auth.users (id, email, raw_user_meta_data) values insert into auth.users (id, email, raw_user_meta_data) values
('11111111-1111-1111-1111-111111111111', 'anna@example.com', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"Anna"}'), ('11111111-1111-1111-1111-111111111111', 'anna@example.com', '{"age_confirmed":"true","terms_accepted":"true","terms_version":"test","display_name":"Anna"}'),
@ -116,13 +116,14 @@ select results_eq($$select polcmd::text, array(select rolname from pg_roles wher
insert into public.genres (id, name_de, name_en) values insert into public.genres (id, name_de, name_en) values
('f9000000-0000-0000-0000-0000000000e1', 'Qxz Genre A', 'Qxz genre A'), ('f9000000-0000-0000-0000-0000000000e1', 'Qxz Genre A', 'Qxz genre A'),
('f9000000-0000-0000-0000-0000000000e2', 'Qxz Genre B', 'Qxz genre B'), ('f9000000-0000-0000-0000-0000000000e2', 'Qxz Genre B', 'Qxz genre B'),
('f9000000-0000-0000-0000-0000000000e3', 'Qxz Genre Q', 'Qxz genre Q'); ('f9000000-0000-0000-0000-0000000000e3', 'Qxz Genre Q', 'Qxz genre Q'),
('f9000000-0000-0000-0000-0000000000e4', 'Qxz Genre R', 'Qxz genre R');
insert into public.mechanics (id, name_de, name_en) values insert into public.mechanics (id, name_de, name_en) values
('f9000000-0000-0000-0000-0000000000d1', 'Qxz Mechanik A', 'Qxz mechanic A'), ('f9000000-0000-0000-0000-0000000000d1', 'Qxz Mechanik A', 'Qxz mechanic A'),
('f9000000-0000-0000-0000-0000000000d2', 'Qxz Mechanik B', 'Qxz mechanic B'); ('f9000000-0000-0000-0000-0000000000d2', 'Qxz Mechanik B', 'Qxz mechanic B');
insert into public.publishers (id, name) values insert into public.publishers (id, name) values
('f9000000-0000-0000-0000-0000000000b1', 'Qxz Hersteller Eins'), ('f9000000-0000-0000-0000-0000000000b2', 'Qxz Hersteller Eins'),
('f9000000-0000-0000-0000-0000000000b2', 'Qxz Hersteller Zwei'), ('f9000000-0000-0000-0000-0000000000b1', 'Qxz Hersteller Zwei'),
('f9000000-0000-0000-0000-0000000000b3', 'Qxz Zorb Verlag'); ('f9000000-0000-0000-0000-0000000000b3', 'Qxz Zorb Verlag');
insert into public.games (id, title, visibility) values insert into public.games (id, title, visibility) values
@ -144,7 +145,18 @@ insert into public.games (id, title, visibility) values
('f9000000-0000-0000-0000-000000000016', 'QXZ CATAN®', 'catalog'), ('f9000000-0000-0000-0000-000000000016', 'QXZ CATAN®', 'catalog'),
('f9000000-0000-0000-0000-000000000040', 'Qxz Doppelspiel', 'catalog'), ('f9000000-0000-0000-0000-000000000040', 'Qxz Doppelspiel', 'catalog'),
('f9000000-0000-0000-0000-000000000041', 'Qxz Einzelspiel', 'catalog'), ('f9000000-0000-0000-0000-000000000041', 'Qxz Einzelspiel', 'catalog'),
('f9000000-0000-0000-0000-000000000060', 'Qxz Cabin', 'catalog'); ('f9000000-0000-0000-0000-000000000060', 'Qxz Cabin', 'catalog'),
-- Sortierung ist binär (collate "C"): "qxz b" kommt vor "qxzab". Hinweis: Mit ICU-Sortierung (wie lokal) gilt das auch
-- ohne collate "C"; der Test ist dort nicht mutationsempfindlich und schützt vor libc-Sortierungen, die Leerzeichen ignorieren.
('f9000000-0000-0000-0000-000000000081', 'Qxzab', 'catalog'),
('f9000000-0000-0000-0000-000000000095', 'Qxz Zwilling', 'catalog'),
('f9000000-0000-0000-0000-000000000093', 'Qxz Zwilling', 'catalog'),
('f9000000-0000-0000-0000-000000000091', 'Qxz Zwilling', 'catalog'),
('f9000000-0000-0000-0000-000000000094', 'Qxz Zwilling', 'catalog'),
('f9000000-0000-0000-0000-000000000092', 'Qxz Zwilling', 'catalog'),
('f9000000-0000-0000-0000-000000000090', 'Qxz Zwilling', 'catalog'),
('f9000000-0000-0000-0000-000000000017', 'Qxz Wanderer', 'catalog'),
('f9000000-0000-0000-0000-000000000080', 'Qxz B', 'catalog');
-- Filter-Fixtures (20-25); 22 ohne jede Angabe, 23 nur max_minutes, 24 nur min_minutes -- Filter-Fixtures (20-25); 22 ohne jede Angabe, 23 nur max_minutes, 24 nur min_minutes
insert into public.games (id, title, visibility, min_players, max_players, min_minutes, max_minutes, min_age) values insert into public.games (id, title, visibility, min_players, max_players, min_minutes, max_minutes, min_age) values
@ -161,7 +173,9 @@ insert into public.game_genres (game_id, genre_id) values
('f9000000-0000-0000-0000-000000000022', 'f9000000-0000-0000-0000-0000000000e1'), ('f9000000-0000-0000-0000-000000000022', 'f9000000-0000-0000-0000-0000000000e1'),
('f9000000-0000-0000-0000-000000000025', 'f9000000-0000-0000-0000-0000000000e2'), ('f9000000-0000-0000-0000-000000000025', 'f9000000-0000-0000-0000-0000000000e2'),
('f9000000-0000-0000-0000-000000000005', 'f9000000-0000-0000-0000-0000000000e3'), ('f9000000-0000-0000-0000-000000000005', 'f9000000-0000-0000-0000-0000000000e3'),
('f9000000-0000-0000-0000-000000000060', 'f9000000-0000-0000-0000-0000000000e3'); ('f9000000-0000-0000-0000-000000000060', 'f9000000-0000-0000-0000-0000000000e3'),
('f9000000-0000-0000-0000-000000000080', 'f9000000-0000-0000-0000-0000000000e4'),
('f9000000-0000-0000-0000-000000000081', 'f9000000-0000-0000-0000-0000000000e4');
insert into public.game_mechanics (game_id, mechanic_id) values insert into public.game_mechanics (game_id, mechanic_id) values
('f9000000-0000-0000-0000-000000000020', 'f9000000-0000-0000-0000-0000000000d1'), ('f9000000-0000-0000-0000-000000000020', 'f9000000-0000-0000-0000-0000000000d1'),
('f9000000-0000-0000-0000-000000000021', 'f9000000-0000-0000-0000-0000000000d1'), ('f9000000-0000-0000-0000-000000000021', 'f9000000-0000-0000-0000-0000000000d1'),
@ -170,10 +184,10 @@ insert into public.game_mechanics (game_id, mechanic_id) values
-- Verlage: 01 und 30 bei Eins; 40 bei Eins und Zwei; 41 bei Zwei; 06 bei "Qxz Zorb Verlag" -- Verlage: 01 und 30 bei Eins; 40 bei Eins und Zwei; 41 bei Zwei; 06 bei "Qxz Zorb Verlag"
insert into public.game_publishers (game_id, publisher_id) values insert into public.game_publishers (game_id, publisher_id) values
('f9000000-0000-0000-0000-000000000001', 'f9000000-0000-0000-0000-0000000000b1'), ('f9000000-0000-0000-0000-000000000001', 'f9000000-0000-0000-0000-0000000000b2'),
('f9000000-0000-0000-0000-000000000040', 'f9000000-0000-0000-0000-0000000000b2'),
('f9000000-0000-0000-0000-000000000040', 'f9000000-0000-0000-0000-0000000000b1'), ('f9000000-0000-0000-0000-000000000040', 'f9000000-0000-0000-0000-0000000000b1'),
('f9000000-0000-0000-0000-000000000041', 'f9000000-0000-0000-0000-0000000000b2'), ('f9000000-0000-0000-0000-000000000040', 'f9000000-0000-0000-0000-0000000000b2'),
('f9000000-0000-0000-0000-000000000041', 'f9000000-0000-0000-0000-0000000000b1'),
('f9000000-0000-0000-0000-000000000006', 'f9000000-0000-0000-0000-0000000000b3'); ('f9000000-0000-0000-0000-000000000006', 'f9000000-0000-0000-0000-0000000000b3');
-- Ausgeblendet, zusammengeführt, privat -- Ausgeblendet, zusammengeführt, privat
@ -185,8 +199,8 @@ insert into public.games (id, title, visibility, owner_id) values
('f9000000-0000-0000-0000-000000000032', 'Qxz Privat', 'private', '11111111-1111-1111-1111-111111111111'), ('f9000000-0000-0000-0000-000000000032', 'Qxz Privat', 'private', '11111111-1111-1111-1111-111111111111'),
('f9000000-0000-0000-0000-000000000033', 'QXZ ZORB', 'private', '22222222-2222-2222-2222-222222222222'); ('f9000000-0000-0000-0000-000000000033', 'QXZ ZORB', 'private', '22222222-2222-2222-2222-222222222222');
insert into public.game_publishers (game_id, publisher_id) values insert into public.game_publishers (game_id, publisher_id) values
('f9000000-0000-0000-0000-000000000030', 'f9000000-0000-0000-0000-0000000000b1'), ('f9000000-0000-0000-0000-000000000030', 'f9000000-0000-0000-0000-0000000000b2'),
('f9000000-0000-0000-0000-000000000033', 'f9000000-0000-0000-0000-0000000000b1'); ('f9000000-0000-0000-0000-000000000033', 'f9000000-0000-0000-0000-0000000000b2');
-- Bilder: 50 mit Quelle ohne Erlaubnis (maskiert), 51 manuell gepflegt, 52 ohne Quelle -- Bilder: 50 mit Quelle ohne Erlaubnis (maskiert), 51 manuell gepflegt, 52 ohne Quelle
insert into public.games (id, title, visibility, image_path, catalog_source_id, source_ref) values insert into public.games (id, title, visibility, image_path, catalog_source_id, source_ref) values
@ -211,18 +225,22 @@ insert into public.game_alt_titles (id, game_id, locale, title) values
('f9000000-0000-0000-0000-0000000000a6', 'f9000000-0000-0000-0000-000000000030', 'de', 'Qxz Verborgen Alt'), ('f9000000-0000-0000-0000-0000000000a6', 'f9000000-0000-0000-0000-000000000030', 'de', 'Qxz Verborgen Alt'),
('f9000000-0000-0000-0000-0000000000a7', 'f9000000-0000-0000-0000-000000000031', 'de', 'Qxz Verschmolzen Alt'), ('f9000000-0000-0000-0000-0000000000a7', 'f9000000-0000-0000-0000-000000000031', 'de', 'Qxz Verschmolzen Alt'),
('f9000000-0000-0000-0000-0000000000a8', 'f9000000-0000-0000-0000-000000000032', 'de', 'Qxz Privat Alt'), ('f9000000-0000-0000-0000-0000000000a8', 'f9000000-0000-0000-0000-000000000032', 'de', 'Qxz Privat Alt'),
('f9000000-0000-0000-0000-0000000000a9', 'f9000000-0000-0000-0000-000000000033', 'en', 'Qxz Bens Alt'); ('f9000000-0000-0000-0000-0000000000a9', 'f9000000-0000-0000-0000-000000000033', 'en', 'Qxz Bens Alt'),
-- drei Treffer-Titel für "qxz nomad": Stufe 2, Stufe 1 (länger, weniger ähnlich), Stufe 1 (kürzer, ähnlicher)
('f9000000-0000-0000-0000-0000000000c1', 'f9000000-0000-0000-0000-000000000017', 'de', 'Stern Qxz Nomad'),
('f9000000-0000-0000-0000-0000000000c2', 'f9000000-0000-0000-0000-000000000017', 'en', 'Qxz Nomad Sternenwanderer'),
('f9000000-0000-0000-0000-0000000000c3', 'f9000000-0000-0000-0000-000000000017', 'de', 'Qxz Nomadisch');
select is((select title_search from public.games where id = 'f9000000-0000-0000-0000-000000000014'), 'qxz aeon s end', select is((select title_search from public.games where id = 'f9000000-0000-0000-0000-000000000014'), 'qxz aeon s end',
'KAT-01 Kriterium 1: games.title_search wird beim Anlegen gefüllt'); 'KAT-01 Kriterium 1: games.title_search wird beim Anlegen gefüllt');
select is((select title_search from public.game_alt_titles where id = 'f9000000-0000-0000-0000-0000000000a5'), 'qxz einzigartig alt', select is((select title_search from public.game_alt_titles where id = 'f9000000-0000-0000-0000-0000000000a5'), 'qxz einzigartig alt',
'KAT-01 Kriterium 2: game_alt_titles.title_search wird gefüllt'); 'KAT-01 Kriterium 2: game_alt_titles.title_search wird gefüllt');
select is((select name_search from public.publishers where id = 'f9000000-0000-0000-0000-0000000000b2'), 'qxz hersteller zwei', select is((select name_search from public.publishers where id = 'f9000000-0000-0000-0000-0000000000b1'), 'qxz hersteller zwei',
'KAT-01 Kriterium 3: publishers.name_search wird gefüllt'); 'KAT-01 Kriterium 3: publishers.name_search wird gefüllt');
update public.publishers set name = 'Qxz Hersteller Zwei & Söhne' where id = 'f9000000-0000-0000-0000-0000000000b2'; update public.publishers set name = 'Qxz Hersteller Zwei & Söhne' where id = 'f9000000-0000-0000-0000-0000000000b1';
select is((select name_search from public.publishers where id = 'f9000000-0000-0000-0000-0000000000b2'), 'qxz hersteller zwei sohne', select is((select name_search from public.publishers where id = 'f9000000-0000-0000-0000-0000000000b1'), 'qxz hersteller zwei sohne',
'KAT-01: publishers.name_search folgt einer Namensänderung'); 'KAT-01: publishers.name_search folgt einer Namensänderung');
update public.publishers set name = 'Qxz Hersteller Zwei' where id = 'f9000000-0000-0000-0000-0000000000b2'; update public.publishers set name = 'Qxz Hersteller Zwei' where id = 'f9000000-0000-0000-0000-0000000000b1';
-- Hilfsfunktion: alle Fixture-Treffer durchblättern (nur als postgres, für Abfragen ohne Suchbegriff) -- Hilfsfunktion: alle Fixture-Treffer durchblättern (nur als postgres, für Abfragen ohne Suchbegriff)
create function pg_temp.hits(p_query text default null, p_players smallint default null, p_max_minutes smallint default null, create function pg_temp.hits(p_query text default null, p_players smallint default null, p_max_minutes smallint default null,
@ -257,6 +275,28 @@ select results_eq($$select id, match_rank::int from public.search_catalog_games(
select results_eq($$select id from public.search_catalog_games('qxz twin')$$, select results_eq($$select id from public.search_catalog_games('qxz twin')$$,
$$values ('f9000000-0000-0000-0000-000000000007'::uuid), ('f9000000-0000-0000-0000-000000000008')$$, $$values ('f9000000-0000-0000-0000-000000000007'::uuid), ('f9000000-0000-0000-0000-000000000008')$$,
'KAT-01 Kriterium 5: bei völligem Gleichstand entscheidet die ID (stabile Reihenfolge)'); 'KAT-01 Kriterium 5: bei völligem Gleichstand entscheidet die ID (stabile Reihenfolge)');
select results_eq($$select id from public.search_catalog_games('qxz zorb', null, null, null, null, null, 4, 0)$$,
$$values ('f9000000-0000-0000-0000-000000000001'::uuid), ('f9000000-0000-0000-0000-000000000002'), ('f9000000-0000-0000-0000-000000000003'), ('f9000000-0000-0000-0000-000000000004')$$,
'KAT-01 Kriterium 5: die erste Seite enthält die relevantesten Treffer (Stufe, dann Ähnlichkeit), nicht die ersten nach Titel');
select results_eq($$select id from public.search_catalog_games('qxz zorb', null, null, null, null, null, 2, 0)
union all select id from public.search_catalog_games('qxz zorb', null, null, null, null, null, 2, 2)
union all select id from public.search_catalog_games('qxz zorb', null, null, null, null, null, 2, 4)$$,
$$select id from public.search_catalog_games('qxz zorb')$$,
'KAT-01 Kriterium 5: Seiten zu je zwei ergeben dieselbe Reihenfolge wie die ganze Liste');
select results_eq($$select id from public.search_catalog_games('qxz twin', null, null, null, null, null, 1, 0)
union all select id from public.search_catalog_games('qxz twin', null, null, null, null, null, 1, 1)$$,
$$values ('f9000000-0000-0000-0000-000000000007'::uuid), ('f9000000-0000-0000-0000-000000000008')$$,
'KAT-01 Kriterium 5: bei völligem Gleichstand entscheidet die ID auch über die Seitengrenze hinweg');
select results_eq($$select id from public.search_catalog_games('qxz zwilling', null, null, null, null, null, 3, 0)
union all select id from public.search_catalog_games('qxz zwilling', null, null, null, null, null, 3, 3)$$,
$$select id from public.games where title = 'Qxz Zwilling' order by id$$,
'KAT-01 Kriterium 5: sechs gleich gute Treffer erscheinen seitenübergreifend in ID-Reihenfolge');
select results_eq($$select id, match_rank::int, matched_alt_title from public.search_catalog_games('qxz nomad')$$,
$$values ('f9000000-0000-0000-0000-000000000017'::uuid, 1, 'Qxz Nomadisch'::text)$$,
'KAT-01 Kriterium 2: von mehreren treffenden alternativen Titeln gewinnt die beste Stufe, dann die größte Ähnlichkeit');
select results_eq($$select id, match_rank::int, matched_alt_title from public.search_catalog_games('qxz einzig')$$,
$$values ('f9000000-0000-0000-0000-000000000012'::uuid, 1, null::text)$$,
'KAT-01 Kriterium 2: bei gleicher Stufe von Haupttitel und alternativem Titel gilt der Haupttitel (matched_alt_title null)');
select is((select count(*) from public.search_catalog_games('qxz käse')), 1::bigint, 'KAT-01 Kriterium 1: Umlaut im Suchbegriff findet den Umlaut-Titel'); select is((select count(*) from public.search_catalog_games('qxz käse')), 1::bigint, 'KAT-01 Kriterium 1: Umlaut im Suchbegriff findet den Umlaut-Titel');
select is((select count(*) from public.search_catalog_games('QXZ KASE GROSSE')), 1::bigint, 'KAT-01 Kriterium 1: ohne Umlaut und ß wird derselbe Titel gefunden'); select is((select count(*) from public.search_catalog_games('QXZ KASE GROSSE')), 1::bigint, 'KAT-01 Kriterium 1: ohne Umlaut und ß wird derselbe Titel gefunden');
select is((select count(*) from public.search_catalog_games('qxz grösse')), 1::bigint, 'KAT-01 Kriterium 1: ö und ß in der Eingabe'); select is((select count(*) from public.search_catalog_games('qxz grösse')), 1::bigint, 'KAT-01 Kriterium 1: ö und ß in der Eingabe');
@ -324,6 +364,9 @@ select results_eq($$select id, match_rank::int, publisher_names from public.sear
select results_eq($$select id from public.search_catalog_games('qxz hersteller eins') order by id$$, select results_eq($$select id from public.search_catalog_games('qxz hersteller eins') order by id$$,
$$values ('f9000000-0000-0000-0000-000000000001'::uuid), ('f9000000-0000-0000-0000-000000000040')$$, $$values ('f9000000-0000-0000-0000-000000000001'::uuid), ('f9000000-0000-0000-0000-000000000040')$$,
'KAT-01 Kriterium 3: der andere Verlag des Spiels findet es ebenfalls; ausgeblendete Spiele des Verlags fehlen'); 'KAT-01 Kriterium 3: der andere Verlag des Spiels findet es ebenfalls; ausgeblendete Spiele des Verlags fehlen');
select results_eq($$select id, match_rank::int from public.search_catalog_games('qxz zorb zorb eins')$$,
$$values ('f9000000-0000-0000-0000-000000000001'::uuid, 4)$$,
'KAT-01 Kriterium 3: ein doppelt eingegebenes Wort zählt einfach (Verlagstreffer bleibt auffindbar)');
select results_eq($$select id, match_rank::int from public.search_catalog_games('qxz zorb eins')$$, select results_eq($$select id, match_rank::int from public.search_catalog_games('qxz zorb eins')$$,
$$values ('f9000000-0000-0000-0000-000000000001'::uuid, 4)$$, $$values ('f9000000-0000-0000-0000-000000000001'::uuid, 4)$$,
'KAT-01 Kriterium 3: ein Wort aus dem Titel plus ein Wort aus dem Verlag (UND)'); 'KAT-01 Kriterium 3: ein Wort aus dem Titel plus ein Wort aus dem Verlag (UND)');
@ -367,6 +410,9 @@ select results_eq($$select h from pg_temp.hits(p_genre_ids := array['f9000000-00
select results_eq($$select h from pg_temp.hits(p_mechanic_ids := array['f9000000-0000-0000-0000-0000000000d2']::uuid[]) h order by 1$$, select results_eq($$select h from pg_temp.hits(p_mechanic_ids := array['f9000000-0000-0000-0000-0000000000d2']::uuid[]) h order by 1$$,
$$values ('f9000000-0000-0000-0000-000000000021'::uuid), ('f9000000-0000-0000-0000-000000000025')$$, $$values ('f9000000-0000-0000-0000-000000000021'::uuid), ('f9000000-0000-0000-0000-000000000025')$$,
'KAT-01 Kriterium 4: Mechanik allein'); 'KAT-01 Kriterium 4: Mechanik allein');
select results_eq($$select h from pg_temp.hits(p_mechanic_ids := array['f9000000-0000-0000-0000-0000000000d2', 'f9000000-0000-0000-0000-0000000000d2']::uuid[]) h order by 1$$,
$$select h from pg_temp.hits(p_mechanic_ids := array['f9000000-0000-0000-0000-0000000000d2']::uuid[]) h order by 1$$,
'KAT-01 Kriterium 4: Duplikate im Mechanik-Array zählen einfach');
select results_eq($$select h from pg_temp.hits(p_mechanic_ids := array['f9000000-0000-0000-0000-0000000000d1', 'f9000000-0000-0000-0000-0000000000d2']::uuid[]) h$$, select results_eq($$select h from pg_temp.hits(p_mechanic_ids := array['f9000000-0000-0000-0000-0000000000d1', 'f9000000-0000-0000-0000-0000000000d2']::uuid[]) h$$,
$$values ('f9000000-0000-0000-0000-000000000021'::uuid)$$, $$values ('f9000000-0000-0000-0000-000000000021'::uuid)$$,
'KAT-01 Spec-F1: zwei Mechaniken: nur Spiele mit beiden'); 'KAT-01 Spec-F1: zwei Mechaniken: nur Spiele mit beiden');
@ -386,6 +432,13 @@ select results_eq($$select id, match_rank::int, matched_alt_title, total_count::
$$values ('f9000000-0000-0000-0000-000000000060'::uuid, null::int, null::text, 2), $$values ('f9000000-0000-0000-0000-000000000060'::uuid, null::int, null::text, 2),
('f9000000-0000-0000-0000-000000000005', null, null, 2)$$, ('f9000000-0000-0000-0000-000000000005', null, null, 2)$$,
'KAT-01 Kriterium 8: ohne Suchbegriff nach Titel sortiert, match_rank und matched_alt_title null'); 'KAT-01 Kriterium 8: ohne Suchbegriff nach Titel sortiert, match_rank und matched_alt_title null');
select results_eq($$select id from public.search_catalog_games(null, null, null, null, array['f9000000-0000-0000-0000-0000000000e3']::uuid[], null, 1, 0)
union all select id from public.search_catalog_games(null, null, null, null, array['f9000000-0000-0000-0000-0000000000e3']::uuid[], null, 1, 1)$$,
$$values ('f9000000-0000-0000-0000-000000000060'::uuid), ('f9000000-0000-0000-0000-000000000005')$$,
'KAT-01 Kriterium 5: ohne Suchbegriff bestimmt der Titel (nicht die ID) auch die Seitengrenze');
select results_eq($$select id from public.search_catalog_games(null, null, null, null, array['f9000000-0000-0000-0000-0000000000e4']::uuid[])$$,
$$values ('f9000000-0000-0000-0000-000000000080'::uuid), ('f9000000-0000-0000-0000-000000000081')$$,
'KAT-01 Kriterium 8: ohne Suchbegriff wird binär nach dem normalisierten Titel sortiert (Leerzeichen vor Buchstaben)');
select results_eq($$select h from pg_temp.hits() h order by 1$$, select results_eq($$select h from pg_temp.hits() h order by 1$$,
$$select id from public.games where id::text like 'f9000000%' and visibility = 'catalog' and hidden_at is null and merged_into_id is null order by id$$, $$select id from public.games where id::text like 'f9000000%' and visibility = 'catalog' and hidden_at is null and merged_into_id is null order by id$$,
'KAT-01 Kriterium 8: ohne Suchbegriff und ohne Filter erscheinen alle sichtbaren Fixture-Spiele (ohne ausgeblendete, zusammengeführte, private)'); 'KAT-01 Kriterium 8: ohne Suchbegriff und ohne Filter erscheinen alle sichtbaren Fixture-Spiele (ohne ausgeblendete, zusammengeführte, private)');