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

72 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.