Filters are evaluated once into an id set used by both search branches. Adds tests for age/genre/mechanic filters with a search term, rank 2 vs 3 and null p_limit. approxNormalize uses NFKD. Perf script checks confirm_local value and unix socket and re-analyzes after rollback. Adds docs/specs/3-katalog-suche-umsetzung.md (measurements, log check incl. GET finding), release checklist section 2a and the CSV-only note for alt titles. Refs #3 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
7.5 KiB
Spec #3 – KAT-01 Spiele suchen: Umsetzungs-Nachtrag PR 3a (Backend)
Ergänzt docs/specs/3-katalog-suche.md (die Spec selbst wird hier nicht geändert). Stand: 2026-11-04, Branch feature/3a-katalog-suche-backend, Migration 20261103235947_catalog_search.sql, Tests supabase/tests/database/09_catalog_search.test.sql.
1. Abweichungen und Präzisierungen zum Vertrag
nullfürp_limit/p_offsetgilt als Standardwert (30/0). Nur Leerraum (auch Tab, Zeilenumbruch) gilt als kein Suchbegriff.- Ein
smallint-Parameter außerhalb des Typbereichs scheitert beim Umwandeln mit 22003, nicht mit P0001. - Die Ähnlichkeit (zweites Sortierkriterium) ist das Maximum über Haupttitel und alle alternativen Titel. Sortierung binär (
collate "C"), dannid. - Rang 0 prüft nur
replace(ts,' ','') = qc(schließtts = qein). - Von mehreren treffenden alternativen Titeln gewinnt die beste Stufe, dann die größte Ähnlichkeit, dann die Schreibweise. Bei gleicher Stufe von Haupt- und alternativem Titel ist
matched_alt_titlenull. - Stufe 4 trifft auch ohne Verlag: Jedes Suchwort muss in einem Titel (Haupt- oder alternativer Titel) oder einem Verlagsnamen des Spiels vorkommen. Verteilen sich die Wörter auf Haupt- und alternativen Titel (kein einzelner Titel enthält alle), ist der Treffer ebenfalls Stufe 4, obwohl kein Verlag beteiligt ist. Die Spec nennt als Stufe-4-Fall nur den Verlagstreffer.
- Doppelt eingegebene Wörter zählen einfach.
- Filter einmal berechnet: Spielerzahl, Dauer, Alter, Genre und Mechanik werden zu einer ID-Menge ausgewertet, die beide Zweige (mit und ohne Suchbegriff) nutzen.
- Messskript unter
supabase/perf/stattsupabase/tests/perf/:supabase-db-tests.shundsupabase test dblaufen rekursiv übertests/und würden es mitlaufen lassen. - Testplan:
00_schema22 Tabellen;07/08tragengame_alt_titles.game_idals "bewusst ausgelassen" ein;06bekam die Prüfabfrage-ZeileInfo-Alt(statt einer zweitenInfo) und Grants für die Test-Rollecatalog_importer. - Für SAM-08: Die Import-Rolle braucht
executeaufpublic.search_normalize(text),usageauf das Schemaextensionsund Schreibrechte aufgame_alt_titles. - Performance-Fehler der ersten Fassung: korrelierte Unterabfragen je Spiel (Rang 4, Filter) brauchten 13 s bei 10 000 Spielen. Behoben durch Mengenoperationen (
word_hits/word_cover, gehashte ID-Menge,like all, Ähnlichkeit nur für Treffer). Stufen 1 und 2 (Kandidaten-CTE,title_compact) sind nicht gebaut.
2. Messung B5 (lokal, Docker auf macOS, Postgres 17.11)
Skript supabase/perf/catalog_search_perf.sql: 10 000 Katalogspiele, 20 002 alternative Titel, 2 000 Verlage, 12 919 Verlagszuordnungen, je Spiel 2 Genres und 3 Mechaniken; eine Transaktion mit Rollback (danach vacuum (analyze), weil ANALYZE nicht zurückrollbar ist); Messung als anon mit RLS, set local role anon, Generic Plans (ab der 6. Ausführung, wie bei PostgREST-Verbindungen). Das Rollen-statement_timeout (3 s) gilt bei set local role nicht und wird nicht angewandt; Aufrufe über 3 s wurden gezählt (keiner).
2.1 Zeiten je Fall (Endstand, n = 100 je Fall, 1400 Aufrufe, Host-Last 1,7 bis 2,2)
| Fall | p50 ms | p95 ms | p99 ms | max ms |
|---|---|---|---|---|
| 01 Titelteil (4 Zeichen) | 70,4 | 77,4 | 84,8 | 115,8 |
| 02 zwei Zeichen | 77,8 | 90,7 | 112,6 | 116,6 |
| 03 ganzes Wort | 70,4 | 75,8 | 78,6 | 82,8 |
| 04 zwei Wörter | 73,5 | 77,9 | 89,0 | 105,2 |
| 05 ohne Leerzeichen | 69,4 | 73,4 | 85,2 | 98,4 |
| 06 Verlag (ein Wort) | 69,8 | 73,1 | 81,9 | 91,8 |
| 06b Verlag + häufiges Wort | 76,6 | 81,3 | 86,6 | 147,7 |
| 07 alternativer Titel | 70,1 | 77,9 | 87,2 | 112,4 |
| 08 Begriff + Filter | 18,6 | 20,5 | 22,6 | 23,4 |
| 09 nur Filter (Spielerzahl, Genre) | 8,5 | 8,7 | 8,8 | 8,9 |
| 10 nur alle Filter | 10,7 | 11,3 | 12,8 | 12,8 |
| 11 ohne alles, erste Seite | 8,9 | 9,4 | 10,4 | 11,4 |
| 12 ohne alles, Versatz 5000 | 10,7 | 12,3 | 13,3 | 13,3 |
| 13 ohne Treffer | 66,3 | 72,3 | 93,7 | 101,3 |
| alle | 78,6 | 147,7 |
Budget p95 ≤ 150 ms: erfüllt. Frühere Läufe derselben Fassung (vor dem einmaligen Filter-Set): ruhiger Host gesamt 110 ms (je Fall 77 bis 131 ms); auf einem stark belasteten Host (Last 8 bis 15) je Textfall 190 bis 325 ms, gesamt 215 ms, also über Budget. Das Ergebnis hängt von der CPU-Verfügbarkeit ab. Die Fixkosten sind der Volltext-Scan über rund 30 000 Titel (Suche ohne Treffer: 66 bis 72 ms). Hetzner/Staging ist nicht gemessen.
2.2 Pläne (auto_explain, analyze, buffers, als anon mit RLS)
Das Skript gibt für alle 13 Fälle den Plan mit Zeilenzahlen und Buffers als NOTICE aus (log_timing aus, weil die Zeitmessung selbst die Zeiten stark verfälscht). Gesamtdauer der Anweisung und Buffers, sechs Fälle:
| Fall | Dauer der Anweisung | Treffer (Zeilen) | Buffers (shared hit) | Plan |
|---|---|---|---|---|
| 01 Titelteil | 47,7 ms | 30 | 10 102 | Custom Plan |
| 02 zwei Zeichen | 75,8 ms | 30 | 9 795 | Custom Plan |
| 06 Verlag | 75,1 ms | 11 | 177 098 | Generic Plan |
| 08 Begriff + Filter | 21,5 ms | 2 | 26 236 | Generic Plan |
| 10 alle Filter | 13,3 ms | 30 | 14 437 | Generic Plan |
| 13 ohne Treffer | 72,2 ms | 0 | 176 884 | Generic Plan |
Die Pläne zeigen Seq Scan auf games (View und Join) und auf game_alt_titles; die Trigramm-Indizes werden nicht genutzt (Zweizeichen-Begriffe, Regel ohne Leerzeichen, RLS-Prüfungen für anon), wie in Spec 3.2 Nr. 3 vorhergesagt. Die Generic Plans lesen mehr Buffers (RLS-Probe auf games je Verlagszeile), bleiben aber in der Zeit gleich.
3. Protokollprüfung (QUE-04)
Lokale Werte: log_min_duration_statement = -1, log_statement = ddl, log_parameter_max_length = -1, log_parameter_max_length_on_error = 0, pgaudit.log = none, pgaudit.log_parameter = off, auto_explain.log_min_duration = 10 s mit auto_explain.log_parameter_max_length = -1.
- POST über Kong: Ein Aufruf mit dem Marker
geheimerbegriff9z(Erfolg und Fehlersearch_query_too_long,search_paging_invalid) steht weder im Datenbank- noch im PostgREST-Protokoll. Fehler stehen mit dem Platzhalter$1im Protokoll (STATEMENT: WITH pgrst_source AS (SELECT $1 AS json_data) …), weillog_parameter_max_length_on_error = 0. - GET über Kong (Befund der Review, am 2026-11-04 bestätigt):
GET /rest/v1/rpc/search_catalog_games?p_query=markergetxyzliefert 200 und steht im Zugriffsprotokoll des Gateways (Kong); das POST-Gegenstück (markerpostxyz) nicht. Datenbank-Log in beiden Fällen ohne Marker. Die App nutzt POST; der GET-Weg muss im Proxy gesperrt werden (Release-Checkliste 2a). - Abhängigkeit: Suchbegriffe bleiben nur deshalb aus dem
auto_explain-Log, weil die Rollen-Zeitlimits (3 s, 8 s) kürzer als 10 s sind und abgebrochene Anweisungen keineauto_explain-Ausgabe erzeugen.log_parameter_max_length_on_error,auto_explain.log_min_durationund die Rollen-Zeitlimits müssen zusammen geprüft werden (pgTAP prüft die lokalen Annahmen; Runbookdocs/katalog-pflege.md8.2). - Hetzner/Staging nicht geprüft.
4. Mutationsprüfung
Mutationen in einer Kopie der Testdatei (Rollback), nicht am echten Schema: 103 von 107 SQL-Mutationen (RPC, search_normalize, Rechte, Tabelle, Check, Indizes) erkannt; Überlebende: ID-Tie-Break innen und außen (der Upstream-Plan liefert bereits ID-Reihenfolge), beide collate "C"-Mutationen (lokal ICU-Sortierung, für [a-z0-9 ] gleich). Fixtures: 4 von 5 (überlebt: Rohtitel-Vergleich, gleicher Endzustand durch die Semantik datenändernder CTEs). Deno-Werkzeug: 17 von 17.