Shuffle/docs/specs/3-katalog-suche-umsetzung.md
cschlaefke 9fa91eb684
All checks were successful
CI / flutter (pull_request) Successful in 1m19s
CI / web-build (pull_request) Successful in 57s
CI / supabase (pull_request) Successful in 11s
CI / supabase-tests (pull_request) Successful in 38s
CI / e2e (pull_request) Successful in 2m59s
fix(db): single filter set, filter tests with query, perf guards, evidence docs (KAT-01, 3a review)
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>
2026-11-04 10:39:02 +01:00

7.5 KiB
Raw Blame History

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

  • null für p_limit/p_offset gilt 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"), dann id.
  • Rang 0 prüft nur replace(ts,' ','') = qc (schließt ts = q ein).
  • 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_title null.
  • 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/ statt supabase/tests/perf/: supabase-db-tests.sh und supabase test db laufen rekursiv über tests/ und würden es mitlaufen lassen.
  • Testplan: 00_schema 22 Tabellen; 07/08 tragen game_alt_titles.game_id als "bewusst ausgelassen" ein; 06 bekam die Prüfabfrage-Zeile Info-Alt (statt einer zweiten Info) und Grants für die Test-Rolle catalog_importer.
  • Für SAM-08: Die Import-Rolle braucht execute auf public.search_normalize(text), usage auf das Schema extensions und Schreibrechte auf game_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 Fehler search_query_too_long, search_paging_invalid) steht weder im Datenbank- noch im PostgREST-Protokoll. Fehler stehen mit dem Platzhalter $1 im Protokoll (STATEMENT: WITH pgrst_source AS (SELECT $1 AS json_data) …), weil log_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=markergetxyz liefert 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 keine auto_explain-Ausgabe erzeugen. log_parameter_max_length_on_error, auto_explain.log_min_duration und die Rollen-Zeitlimits müssen zusammen geprüft werden (pgTAP prüft die lokalen Annahmen; Runbook docs/katalog-pflege.md 8.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.