diff --git a/supabase/catalog/build_startbestand_test.ts b/supabase/catalog/build_startbestand_test.ts index 60ccebd..f096067 100644 --- a/supabase/catalog/build_startbestand_test.ts +++ b/supabase/catalog/build_startbestand_test.ts @@ -305,6 +305,7 @@ Deno.test("alt_titles: Fehler anderer Art nennen die Zeilennummer", async () => Deno.test("approxNormalize: Marken- und Schutzzeichen, Akzente, ß, Æ, Sonderzeichen", () => { 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("Æon’s End"), "aeon s end"); assert.equal(approxNormalize("7 Wonders: Duel"), "7 wonders duel"); diff --git a/supabase/migrations/20261103235947_catalog_search.sql b/supabase/migrations/20261103235947_catalog_search.sql index 0124840..70776a2 100644 --- a/supabase/migrations/20261103235947_catalog_search.sql +++ b/supabase/migrations/20261103235947_catalog_search.sql @@ -152,7 +152,8 @@ declare v_offset integer := coalesce(p_offset, 0); v_q 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_genres uuid[]; v_mechanics uuid[]; @@ -189,6 +190,7 @@ begin v_qc := replace(v_q, ' ', ''); v_words := coalesce( 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; @@ -215,10 +217,10 @@ begin 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_age is null or c.min_age <= p_age) - and (v_genre_n = 0 or (select count(*) from public.game_genres gg - where gg.game_id = c.id and gg.genre_id = any (v_genres)) = v_genre_n) - and (v_mech_n = 0 or (select count(*) from public.game_mechanics gm - where gm.game_id = c.id and gm.mechanic_id = any (v_mechanics)) = v_mech_n) + 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) + group by gg.game_id having count(*) = v_genre_n)) + 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) + group by gm.game_id having count(*) = v_mech_n)) order by g.title_search collate "C", c.id limit v_limit offset v_offset ) s @@ -235,10 +237,10 @@ begin 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_age is null or c.min_age <= p_age) - and (v_genre_n = 0 or (select count(*) from public.game_genres gg - where gg.game_id = c.id and gg.genre_id = any (v_genres)) = v_genre_n) - and (v_mech_n = 0 or (select count(*) from public.game_mechanics gm - where gm.game_id = c.id and gm.mechanic_id = any (v_mechanics)) = v_mech_n) + 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) + group by gg.game_id having count(*) = v_genre_n)) + 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) + group by gm.game_id having count(*) = v_mech_n)) ), -- Kandidaten-Titel: Haupttitel (alt_title null) und alle alternativen Titel cand as ( @@ -247,50 +249,72 @@ begin select b.id, a.title_search, a.title 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 ( select t.id, t.ts, t.alt_title, 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 2 when replace(t.ts, ' ', '') like '%' || v_qc || '%' - or (cardinality(v_words) > 0 - and not exists (select 1 from unnest(v_words) as w where t.ts not like '%' || w || '%')) + or (cardinality(v_patterns) > 0 and t.ts like all (v_patterns)) then 3 - end as rk, - extensions.similarity(t.ts, v_q) as sim + end as rk from cand t ), agg as ( select r.id, 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 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 + min(r.rk) filter (where r.alt_title is not null) as alt_rk from ranked r 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 ( - 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 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 -- Rang 4: jedes Wort kommt in einem Titel oder einem Verlagsnamen des Spiels vor - or (cardinality(v_words) > 0 - and not exists ( - select 1 from unnest(v_words) as w - where not (exists (select 1 from ranked r2 where r2.id = b.id and r2.ts like '%' || w || '%') - or exists (select 1 from public.game_publishers gp - join public.publishers p on p.id = gp.publisher_id - where gp.game_id = b.id and p.name_search like '%' || w || '%')))) + or (cardinality(v_words) > 0 and wc.n = cardinality(v_words)) + ), + -- Ähnlichkeit (zweites Sortierkriterium): die höchste über Haupttitel und alle alternativen Titel + sims as ( + select r.id, max(extensions.similarity(r.ts, v_q)) as sim + from ranked r + join hit h on h.id = r.id + group by r.id ), 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 - 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 ) select s.id, s.title, s.year, s.min_players, s.max_players, s.min_minutes, s.max_minutes, s.min_age, diff --git a/supabase/tests/database/09_catalog_search.test.sql b/supabase/tests/database/09_catalog_search.test.sql index 80f595b..bb24238 100644 --- a/supabase/tests/database/09_catalog_search.test.sql +++ b/supabase/tests/database/09_catalog_search.test.sql @@ -6,7 +6,7 @@ -- 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. begin; -select plan(201); +select plan(211); 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"}'), @@ -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 ('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-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 ('f9000000-0000-0000-0000-0000000000d1', 'Qxz Mechanik A', 'Qxz mechanic A'), ('f9000000-0000-0000-0000-0000000000d2', 'Qxz Mechanik B', 'Qxz mechanic B'); insert into public.publishers (id, name) values - ('f9000000-0000-0000-0000-0000000000b1', 'Qxz Hersteller Eins'), - ('f9000000-0000-0000-0000-0000000000b2', 'Qxz Hersteller Zwei'), + ('f9000000-0000-0000-0000-0000000000b2', 'Qxz Hersteller Eins'), + ('f9000000-0000-0000-0000-0000000000b1', 'Qxz Hersteller Zwei'), ('f9000000-0000-0000-0000-0000000000b3', 'Qxz Zorb Verlag'); 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-000000000040', 'Qxz Doppelspiel', '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 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-000000000025', 'f9000000-0000-0000-0000-0000000000e2'), ('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 ('f9000000-0000-0000-0000-000000000020', '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" insert into public.game_publishers (game_id, publisher_id) values - ('f9000000-0000-0000-0000-000000000001', 'f9000000-0000-0000-0000-0000000000b1'), - ('f9000000-0000-0000-0000-000000000040', 'f9000000-0000-0000-0000-0000000000b2'), + ('f9000000-0000-0000-0000-000000000001', 'f9000000-0000-0000-0000-0000000000b2'), ('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'); -- 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-000000000033', 'QXZ ZORB', 'private', '22222222-2222-2222-2222-222222222222'); insert into public.game_publishers (game_id, publisher_id) values - ('f9000000-0000-0000-0000-000000000030', 'f9000000-0000-0000-0000-0000000000b1'), - ('f9000000-0000-0000-0000-000000000033', 'f9000000-0000-0000-0000-0000000000b1'); + ('f9000000-0000-0000-0000-000000000030', 'f9000000-0000-0000-0000-0000000000b2'), + ('f9000000-0000-0000-0000-000000000033', 'f9000000-0000-0000-0000-0000000000b2'); -- 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 @@ -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-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-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', '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', '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'); -update public.publishers set name = 'Qxz Hersteller Zwei & Söhne' where id = 'f9000000-0000-0000-0000-0000000000b2'; -select is((select name_search from public.publishers where id = 'f9000000-0000-0000-0000-0000000000b2'), 'qxz hersteller zwei sohne', +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-0000000000b1'), 'qxz hersteller zwei sohne', '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) 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')$$, $$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)'); +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 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'); @@ -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$$, $$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'); +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')$$, $$values ('f9000000-0000-0000-0000-000000000001'::uuid, 4)$$, '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$$, $$values ('f9000000-0000-0000-0000-000000000021'::uuid), ('f9000000-0000-0000-0000-000000000025')$$, '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$$, $$values ('f9000000-0000-0000-0000-000000000021'::uuid)$$, '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), ('f9000000-0000-0000-0000-000000000005', null, null, 2)$$, '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 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)');