screenText sammelt Ansagen per MutationObserver und vergleicht sie; wartet auf leere Region mit registrierter Ansage bzw. 400 ms Ruhe. Unload-Skript mit TCP-Schutz, Doku-Praezisierungen. Refs #3 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
19 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.
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 Anfragep_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_gamesalsanonü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ällesearch_query_too_long,search_filter_invalid,search_paging_invalid(P0001, HTTP 400), 22003 bei smallint-Überlauf, 22P02 bei falscher UUID,service_roleohne Ausführungsrecht (42501), kein Schreiben aufgame_alt_titles. - Testdaten
e2e/support/catalog.ts: je Spec-Datei ein eigenes Datenset mit eigenem UUID-Präfix und Kunstwort (e2ec0001-/zqvkweb,e2ec0002-/wlpdapi), Anlage alspostgresimbeforeAll, Aufräumen imafterAll(der API-Test prüft, dass Spiele, Verlage, Genres und Mechaniken nicht zurückbleiben);seedlöscht zuerst und ist nach einem Abbruch idempotent. Die Dateien laufen intern seriell und untereinander parallel. Die Trennung gilt nur, wenn jede Suche das Kunstwort des eigenen Datensets enthält (z. B.zqvk dunkelforst): die Unterscheidungswörter (Dunkelforst, Quarzfeld, Duo, …) sind in beiden Sets gleich. Mit Kunstwort treffen Suchen weder das andere Set noch den (auch wachsenden) Startbestand noch die pgTAP-Fixtures (qxz), solange kein echter Titel das Kunstwort enthält. Eigene Genres/Mechaniken statt des Vokabulars, damit „nur Filter“-Tests unabhängig vom Startbestand sind. - Latenzmessung (Projekt
perf, nur mitPERF_WEB_URL, nie ine2e-run.sh/CI):e2e/tests/perf/kat01-suche-latenz.spec.ts, Ladeskriptsupabase/perf/catalog_search_perf_load.sql(committet, mit Präfix9e4f0000-) und…_unload.sql. Bedienung:e2e/README.md. playwright.config.ts:perfbedingt registriert; mitPERF_ONLY=1kein 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 insupport/flutter.ts(support/web.tsexportiert 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, 103 Textsuchen, 5 ohne Treffer, 6 nur Filter [Genre + Mechanik]; 7 Kandidaten übersprungen) |
| p50 | 446 ms |
| p95 | 458 ms |
| Maximum | 626 ms (der Erststart; Textsuchen ohne ihn höchstens ca. 540 ms) |
| Minimum | 172 ms (Filterklick ohne Entprellung; getippte Suchen mindestens ca. 420 ms) |
| Grenze Krit. 7 | p95 ≤ 2000 ms: erfüllt |
Je Art (p50) im ersten Lauf dieser Fassung: Textsuchen 436 bis 455 ms (Titel, Titelanfang, Wort, Wortkombination, Verlag, GROSSBUCHSTABEN, alternativer Titel, ohne Treffer), nur Filter 176 ms. Der Wert besteht bei Textsuchen 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 misst der Test p95 2165 ms und schlägt fehl (Error: p95 2165 ms, Expected: <= 2000, Lauf 9,2 min). Schutz gegen vorzeitiges Ende der Messung: Die Messung endet bei der ersten Kachel mit dem erwarteten Titel. Damit nicht schon sichtbare Kacheln sie beenden, wird vor jeder Suche das Feld geleert und der Katalogbeginn abgewartet; Suchen, deren erster Treffer unter den ersten 30 Katalogtiteln (bzw. bei Filtern unter den ersten 30 des Zwischenergebnisses) liegt, werden übersprungen. Zusätzlich prüft der Test, dass kein getippter Messwert unter 300 ms und kein Filtermesswert unter 90 % der Round-Trip-Zeit liegt. Filtersuchen werden ab einem Zeitpunkt kurz vor dem Klick gemessen (Playwright-Aufruf, wenige ms).
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.
- 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 mit diesen Daten (kein Vergleich über mehrere Läufe/Tageszeiten). Ein früherer Lauf mit fehlerhafter Verlagsverteilung der Testdaten (alle Spiele demselben Verlag zugeordnet; behoben) ergab p95 456 ms.
Abweichungen von Spec 7.3 / offen:
- Die Suchart „nur Filter“ ist enthalten, aber als Kombination Genre-Chip + Mechanik-Chip (6 Läufe), nicht als einzelner Filter (der erste Treffer ohne Suchbegriff liegt alphabetisch fast immer im schon sichtbaren Katalogbeginn). Spielerzahl, Dauer und Alter sind nicht Teil der Messung.
- Die „Wortkombinationen“ sind die ersten zwei Wörter eines Titels; Mehrwortsuchen über Titel und Verlag gemischt fehlen.
- C1 (mobile Stichprobe von 20 Suchen auf Android/iOS, Chris) ist offen. D1 (
workflow_dispatch-Job fürperf, Staging-Messung als Release-Freigabebedingung) ist offen;docs/release-checkliste.md§ 2a nennt die Messung, die Bedienung steht ine2e/README.md.
5.3 Befunde und offene Punkte
- Trefferkacheln im Semantics-Baum: Alle Kacheln liegen als Kinder eines einzigen Listen-Containers (
role=group); die Texte stehen inspan-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. - Direkt geladenes
/catalogund Playwright: Text, der über den eingeschalteten Semantics-Baum getippt wird, geht bei einem direkt geladenen/catalogverloren (der Autofokus des Suchfelds ist beim Einschalten der Semantics schon gesetzt). Gegenprobe ohne Semantics: Mausklick auf das Feld und Tastatureingabe auf einem direkt geladenen/catalogsetzen den Suchbegriff in die URL (3 von 3 Läufen). Das ist ein Effekt des Testwerkzeugs, kein Produktfehler; die Tests öffnen die Suche deshalb über die Startseite. Der Wert des Semantics-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 und RLS (Frage an Chris, Issue #69):
games_selectlässt jede Zeile mitvisibility = 'catalog'zu, auch ausgeblendete und zusammengeführte (Spec #2 E3 versteckt sie nur in der Viewcatalog_games; Spec #2 Spec-F2 und R4 bewerten die Lesbarkeit als unkritisch). R4 unterschätzt die Lage: über die Tabellen ist alles auflistbar und durchsuchbar, nicht nur per ID abrufbar, einschließlich der alternativen Titel (game_alt_titlesfolgtgames) ausgeblendeter Spiele. Übersearch_catalog_gameserscheinen sie nie. Das wurde mit #3 nicht eingeführt; obgames_selectausgeblendete Katalogspiele füranonverbergen soll, entscheidet Chris. Hier keine Änderung. - Gateway ohne
apikey: Die lokale CLI beantwortet die RPC auch ohneapikey-Header alsanon(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.
5.4 Laufzeit und Stabilität der Suite
- Lokal,
--workers=2(CI-Wert), Projekteapiundweb: ohne die neuen Tests (87 Tests) 43,6 bis 47,8 s (4 Läufe), mit ihnen (122 Tests) rund 78 s (ein Lauf, lokal; CI nicht gemessen); die Mehrlaufzeit beträgt lokal also etwa 30 s. Die Web-Dateikat01-suche.spec.tsist ein serieller Block von etwa 45 s (ein Worker); ein CI-Retry (retries: 1) wiederholt die ganze Datei. Die API-Datei braucht etwa 2 s. Der Jobe2eunde2e-run.shsind unverändert (api,webundthrottleziehen die neuen Dateien automatisch mit). - Die bestehenden Web-Tests der Anmeldung sind lastabhängig flaky (zeitversetzte Live-Region, zu knappe Wartezeiten): Bei höherer Rechnerlast scheitert in einzelnen Läufen ein Test (mal
krit04,krit07,krit01,krit49). Das trat in Läufen mit und ohne die neuen Tests auf (ohne: 1 von 4 Läufen, mit: 5 von 9; alle Läufe auf einem gemeinsam genutzten Rechner mit wechselnder Last, daher nicht belastbar).screenTextinkrit04-06-07-anmeldung.spec.tssammelt jetzt über einenMutationObserver(Init-Skript) jede nichtleere Ansage der Live-Regionen (flt-announcement-polite/-assertive, getrimmt) und gibt sie zusammen mit dem Baum zurück; der Test vergleicht sie zwischen neuer und vorhandener Adresse bzw. falscher E-Mail und falschem Passwort. Der Schnappschuss entsteht erst, wenn die Region leer ist und entweder mindestens eine Ansage registriert wurde oder die Region 400 ms am Stück leer war (Flutter setzt die Ansage zeitversetzt und leert sie nach 300 ms). Damit ist die Prüfung gegenüber vorher nicht abgeschwächt, sondern um den Vergleich der Ansagen erweitert. Die übrigen Instabilitäten sind nicht untersucht (allgemeine Robustheit der Anmelde-Tests: Issue #68).