Shuffle/docs/specs/3-katalog-suche-umsetzung.md
cschlaefke 81be119f4e test(catalog): Latenzmessung der Katalogsuche (Projekt perf, vorlaeufig) und Doku
Playwright-Projekt perf (nur mit PERF_WEB_URL, nur lesend, nie in der CI):
CDP-Drosselung 100 ms / 10 Mbit/s, mind. 100 Suchen, Messung im Browser vom
letzten Tastendruck bis zur ersten Kachel. Ladeskript fuer synthetisches
Volumen unter supabase/perf/. Messung und Aussagegrenzen in der Umsetzungsdoku.

Refs #3

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 19:50:03 +01:00

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

5. PR 3c (E2E und vorläufige Latenzmessung), Stand 2026-11-04

Branch feature/3c-katalog-suche-e2e, Refs #3. Ergänzt § 7.2/7.3 der Spec; die Spec selbst wird nicht geändert.

5.1 Was gebaut ist

  • Web-E2E e2e/tests/web/kat01-suche.spec.ts (17 Tests, ohne Anmeldung): Einstieg über die Schaltfläche „Search games“ auf der Startseite, Titel in anderer Schreibweise (Krit. 1), Relevanzreihenfolge, ausgeblendete/zusammengeführte Spiele (KAT-05 Krit. 4), alternative Titel de/en (Krit. 2), Verlage inkl. zweitem Verlag (Krit. 3), Filter Genre/Spielerzahl/Dauer/Alter (Krit. 4), nur Filter und UND-Verknüpfung (Krit. 8), Filter-Sheet im mittleren Layout, Nachladen über Scrollen mit Prüfung der Anfrage p_offset 30 (Krit. 5), Leerzustände (Krit. 6, Krit. 8), URL-Zustand, Neuladen, Zurück in einem Schritt, Deep Link mit Filtern, ungültige URL-Parameter, Offline mit „Try again“.
  • API-E2E e2e/tests/api/kat01-suche.spec.ts (18 Tests): search_catalog_games als anon über Kong (Vertrag, Normalisierung, Relevanzstufen 0 bis 4, alternative Titel, Verlage, Filter, Paging, total_count), nie sichtbar: ausgeblendete, zusammengeführte und private Spiele (auch nicht für den Besitzer), Fehlerfälle search_query_too_long, search_filter_invalid, search_paging_invalid (P0001, HTTP 400), 22003 bei smallint-Überlauf, 22P02 bei falscher UUID, service_role ohne Ausführungsrecht (42501), kein Schreiben auf game_alt_titles.
  • Testdaten e2e/support/catalog.ts: je Spec-Datei ein eigenes Datenset mit eigenem UUID-Präfix und Kunstwort (e2ec0001-/zqvk web, e2ec0002-/wlpd api), Anlage als postgres im beforeAll, Aufräumen im afterAll (der API-Test prüft, dass nichts zurückbleibt); seed löscht zuerst und ist nach einem Abbruch idempotent. Die Dateien laufen intern seriell und untereinander parallel. Eigene Genres/Mechaniken statt des Vokabulars, damit „nur Filter“-Tests unabhängig vom Startbestand sind.
  • Latenzmessung (Projekt perf, nur mit PERF_WEB_URL, nie in e2e-run.sh/CI): e2e/tests/perf/kat01-suche-latenz.spec.ts, Ladeskript supabase/perf/catalog_search_perf_load.sql (committet, mit Präfix 9e4f0000-) und …_unload.sql. Bedienung: e2e/README.md.
  • playwright.config.ts: perf bedingt registriert; mit PERF_ONLY=1 kein Web-Server und kein Import der lokalen Auth-Konstanten. Ohne die Variablen verhält sich die Konfiguration wie vorher. Die allgemeinen Flutter-Helfer liegen jetzt in support/flutter.ts (support/web.ts exportiert sie unverändert weiter).

5.2 Messung KAT-01 Krit. 7 (vorläufig, Option A)

Lauf am 2026-11-04, lokaler Stack (Supabase CLI auf Docker/macOS), Web-Build --release, Chrome (Playwright-Chromium, Desktop), CDP-Drosselung 100 ms Latenz, 10 Mbit/s je Richtung. Daten: Startbestand-Stand der lokalen DB (1 Spiel) plus synthetisch 10 000 Katalogspiele, 20 000 alternative Titel, 2 000 Verlage, 12 951 Verlagszuordnungen, je Spiel 2 Genres und 3 Mechaniken (wie catalog_search_perf.sql).

Wert
Suchen gemessen 116 (1 Erststart mit neuer Verbindung, 110 inhaltliche Suchen, 5 ohne Treffer)
p50 441 ms
p95 461 ms
Maximum 637 ms (der Erststart; ohne ihn 535 ms)
Minimum 404 ms
Grenze Krit. 7 p95 ≤ 2000 ms: erfüllt

Je Art (p50): Titel 436, Titelanfang 455, Wort 451, Wortkombination 437, Verlag 454, GROSSBUCHSTABEN 441, alternativer Titel 437, ohne Treffer 447 ms. Der Wert besteht zu rund 300 ms aus der Entprellung der App; der Rest ist eine Round-Trip-Zeit (100 ms), Verarbeitung und Rendern. Gegenprobe des Gates: mit PERF_LATENCY_MS=1800 steigt der gemessene p95 auf 2157 ms (über der Grenze), das Gate reagiert also.

Aussagegrenzen (wichtig):

  • Kein Staging, keine Produktion. Gemessen ist der lokale Stack auf demselben Rechner wie der Browser (Docker unter macOS); Rechenzeit von Datenbank, Gateway und Browser konkurriert miteinander und mit anderen Prozessen (auf dem Rechner lief gleichzeitig Backend-Arbeit mit eigenen Datenbank-Läufen). Die Staging-Messung bleibt Freigabebedingung in docs/release-checkliste.md (Abschnitt 2a).
  • Die Drosselung ist eine Browser-Emulation (Chrome DevTools): sie verzögert Antworten und begrenzt den Durchsatz im Browser, bildet aber keine Funkstrecke, kein TLS-Handshake über echte Latenz, keine Paketverluste und keinen Wechsel zwischen Funkzellen ab. Die Latenz gilt je Anfrage; ob ein Verbindungsaufbau (TCP/TLS) mit 100 ms Round-Trip in der Emulation vollständig abgebildet ist, ist nicht geprüft.
  • Nur Web (Desktop-Chrome, ungedrosselte CPU). Keine Stichprobe auf Android/iOS (Spec 7.3, Aufgabe C1, Chris), keine CPU-Drosselung für ein schwächeres Mobilgerät.
  • Synthetische Daten: Wörter mit ungleicher Häufigkeit, aber kein echter Titelbestand; die Relevanz mit dem echten Startbestand bleibt C1. Der Verlag „zusammengesetzt“ entspricht nicht echten Verlagsnamen.
  • Suchliste: feste Schrittweite durch die ersten 600 Spiele in alphabetischer Reihenfolge, nicht durch einen echten Nutzungsverlauf. Ein Treffer-Erwartungswert je Suche kommt aus derselben RPC (selbstprüfend, aber ohne unabhängiges Orakel).
  • Kein Ratenlimit im lokalen Kong für die RPC (Entscheidung D1 vor dem Go-live): die Messung enthält keine Drosselung durch ein Gateway-Limit.
  • Gemessen ist bis zur ersten sichtbaren Kachel (Textknoten im Semantics-Baum), nicht bis zum gezeichneten Frame; die Differenz ist nicht gemessen.
  • Eine einzelne Messreihe (kein Vergleich über mehrere Läufe/Tageszeiten); Streuung zwischen zwei früheren Läufen mit 110 Suchen: p95 456 und 461 ms.

5.3 Befunde und offene Punkte

  • Trefferkacheln im Semantics-Baum: Alle Kacheln liegen als Kinder eines einzigen Listen-Containers (role=group); die Texte stehen in span-Knoten. Der Container hat keinen eigenen Namen. Für Bildschirmleser ist das Verhalten nicht geprüft (NVDA/VoiceOver-Durchgang, manuell). Die Tests suchen die Kacheln als Textknoten.
  • Semantics-Eingabefeld: Ein direkt geladenes /catalog nimmt per Playwright getippten Text nicht verlässlich an (Feld bleibt leer; Autofokus ist mal gesetzt, mal nicht). Über die Startseite funktioniert es. Nicht untersucht, ob echte Nutzer (Mausklick) betroffen sind. Der Wert des Eingabefelds ist nach einem Auswahlmenü oder Neuladen im DOM leer, obwohl die Suche den Begriff behält (Beleg: URL und Ergebnis); die Tests prüfen deshalb die URL.
  • Ausgeblendete Spiele: Die Tabelle game_alt_titles ist für anon auch für alternative Titel ausgeblendeter Katalogspiele lesbar (die RLS-Regel folgt der Sichtbarkeit in games, nicht der View catalog_games). Über die Suche erscheinen sie nicht. Ob das gewollt ist, ist fachlich zu entscheiden (nicht geraten).
  • Gateway ohne apikey: Die lokale CLI beantwortet die RPC auch ohne apikey-Header als anon (HTTP 200). Auf dem Server ist das eine Frage der Kong-Konfiguration (Release-Checkliste).
  • Krit. 6: „Spiel melden“ ist erst mit #13 sichtbar; der Test prüft den Hinweis, nicht die Aktion (Spec-F4).
  • Nicht automatisiert: „Weitere laden“ per Tastatur (das automatische Nachladen kommt der Schaltfläche zuvor; Widget-Tests in 3b), Ansage der Trefferzahl für Bildschirmleser, Textskalierung 200 %, Tab-Reihenfolge im Browser.