Ctrl K
ComunidadGuías…

tuning-roadmap.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Search Performance Tuning Roadmap

## Ziel

Diese Roadmap fokussiert auf die Suchlatenz fuer:

- Ragfind UI
- MCP `search_rag_context`
- MCP `smart_search`
- die gemeinsame Retrieval-Pipeline in der API

Ziel ist nicht nur ein einzelner Fix, sondern eine gestufte Optimierung mit messbaren Zwischenzielen. Die groessten Hebel liegen aktuell in der gemeinsamen Query-Pipeline, nicht im Frontend.

## Kurzfazit

Die Suche ist heute vor allem deshalb langsam, weil pro Anfrage mehrere teure Schritte kombiniert werden:

- externer Embedding-Roundtrip
- Elasticsearch-Kandidatensuche
- grosse PostgreSQL-CTE mit Volltext-, Fuzzy- und Dokumentsignal-Berechnung
- zusaetzliche Refinement-Queries auf Dokument- oder Section-Ebene
- Small-to-big-Kontexterweiterung
- Dateianreicherung und Ragfind-Nachbearbeitung

Der wichtigste Architekturpunkt fuer die Beschleunigung ist:

1. Elasticsearch oder ein anderer Vorfilter muss die Kandidatenmenge wirklich hart begrenzen.
2. PostgreSQL darf danach nur noch auf einem kleinen Kandidatenset reranken.
3. Interaktive Suche braucht einen Fast Path ohne alle teuren Verfeinerungen.

## Aktuelle Hotspots im Code

### 1. Gemeinsame Retrieval-Pipeline

Datei: `src/routes/api.ts`

Kritischer Einstieg:

- `executeSimilarityQuery(...)`

Hauptprobleme dort:

- pro Query wird immer ein Embedding erzeugt
- Elasticsearch-Kandidaten werden zwar geholt, aber nicht hart als SQL-Vorfilter verwendet
- `document_signals` berechnet pro Anfrage dynamische `lower(regexp_replace(...))`, `LIKE`, `similarity(...)`, `word_similarity(...)`
- `left(extracted_text, 16000)` wird pro Query neu normalisiert
- anschliessend kommen weitere Nachstufen wie `refineItemsWithinDocument`, `refineNarrativeSectionsWithinDocument`, `applySmallToBigWithWindow` und `attachOriginalFiles`

Relevante Unterfunktionen in derselben Datei:

- `refineNarrativeSectionsWithinDocument(...)`
- `refineItemsWithinDocument(...)`
- `applySmallToBigWithWindow(...)`
- `attachOriginalFiles(...)`

### 2. Ragfind-spezifischer Suchpfad

Datei: `src/ragfind/server.ts`

Kritische Punkte:

- `buildSearchResults(...)` ruft `executeSmartSearchQuery(...)` auf
- `retrievalTopK` wird aggressiv hochgesetzt: `topK * 6`, Minimum `36`, Maximum `120`
- danach folgen Gruppierung, Snippet-Bildung, Dateianreicherung und bei Bedarf `findSupplementalDocuments(...)`
- `findSupplementalDocuments(...)` nutzt breite `LIKE`-Pruefungen auf `documents.extracted_text`

### 3. Elasticsearch-Nutzung

Datei: `src/services/searchIndexService.ts`

Status heute:

- Elasticsearch wird nur fuer Chunk- und Dokumentkandidaten genutzt
- beide ES-Abfragen laufen bereits parallel
- ES dient aktuell als Zusatzsignal, nicht als echter Gatekeeper
- Mapping ist funktional, aber noch relativ generisch

### 4. Index- und DB-Grundlage

Dateien:

- `migrations/001_init.sql`
- `migrations/002_hybrid_search_indexes.sql`
- `migrations/003_fuzzy_search_indexes.sql`

Status heute:

- pgvector `ivfflat` vorhanden
- GIN-TSV-Index fuer Chunk-Content vorhanden
- GIN-TSV-Index fuer Dokumenttitel plus `source_ref` vorhanden
- Trigram-Index fuer Dokument-Lookup vorhanden

Was fehlt:

- materialisierte Normalisierungsfelder
- materialisierte TSVECTOR-Spalten fuer genau die Queryformen im Code
- klarer Kandidaten-Cut vor aufwendigen Dokumentsignal-Berechnungen
- systematische Messung mit `EXPLAIN ANALYZE`

## Vermutete Hauptursachen fuer hohe Latenz

### A. Kandidatenmenge wird zu spaet begrenzt

Der groesste Architekturfehler ist derzeit, dass PostgreSQL zu viel Arbeit fuer zu viele Dokumente macht.

Elasticsearch liefert zwar Kandidaten, aber die SQL-Pipeline in `executeSimilarityQuery(...)` rechnet trotzdem ueber eine viel zu breite Datenbasis weiter.

### B. Teure String-Normalisierung pro Query

Mehrfach verwendete Muster wie:

- `lower(regexp_replace(...))`
- `LIKE '%' || ... || '%'`
- `similarity(...)`
- `word_similarity(...)`

werden zur Laufzeit auf grossen Textmengen gerechnet. Das ist teuer und teilweise schlecht indexierbar.

### C. Zu viele zweite und dritte Suchstufen im Standardpfad

Fuer viele Anfragen werden aktuell automatisch ausgelost:

- Dokumentfokus-Refinement
- Narrativ-Section-Refinement
- Small-to-big
- Dateianreicherung

Das verbessert Qualitaet, kostet aber fuer interaktive Nutzung viel Zeit.

### D. Ragfind holt fuer Top-K zu viele Rohkandidaten

Fuer UI-Suche ist `topK * 6` mit Mindestwert `36` oft zu teuer. Die meisten Treffer werden spaeter sowieso verworfen.

### E. Embedding-Lookup ist ungecached

Jede Suchanfrage geht fuer das Query-Embedding erneut an den Embedding-Endpunkt. Das ist ein externer Netz- und Modell-Roundtrip.

### F. Fehlende Stufenmessung

Es gibt heute keine saubere Aufteilung von Suchzeit in:

- embedding
- elasticsearch chunk candidates
- elasticsearch document candidates
- sql retrieval
- rerank
- refinement
- small-to-big
- file enrichment

Ohne diese Sicht ist Tuning unnoetig riskant.

## Tuning-Ziele

### Ziel-Latenzen

Interaktive Ziele fuer typische Queries:

- Ragfind einfache Suche: p50 < 400 ms, p95 < 900 ms ohne Embedding-Cache-Miss
- MCP `smart_search`: p50 < 700 ms, p95 < 1500 ms
- MCP `search_rag_context`: p50 < 500 ms, p95 < 1200 ms

Mit Cache-Miss beim Query-Embedding duerfen die Zahlen hoeher sein, sollten aber trotzdem kontrolliert bleiben.

### Zielarchitektur fuer Suchpfade

Fast Path:

- Embedding aus Cache oder schneller Modell-Call
- Elasticsearch oder schmaler PG-Vorfilter liefert kleine Kandidatenmenge
- PostgreSQL rerankt nur diese Kandidaten
- kein Small-to-big, kein Dokumentfokus, keine narrativen Zusatzstufen

Deep Path:

- fuer schwierige oder hochwertige Fragen
- Fokus-Refinement, Small-to-big, Dokumentfokus, weitere Kontextstufen
- explizit oder heuristisch zugeschaltet

## Phase 0: Messbarkeit herstellen

### 0.1 Stufen-Timing einbauen

Einbau in `src/routes/api.ts` und `src/ragfind/server.ts`:

- Zeit fuer `vectorService.embedOne(...)`
- Zeit fuer `searchChunkCandidates(...)`
- Zeit fuer `searchDocumentCandidates(...)`
- Zeit fuer die grosse SQL-Query in `executeSimilarityQuery(...)`
- Zeit fuer `refineItemsWithinDocument(...)`
- Zeit fuer `refineNarrativeSectionsWithinDocument(...)`
- Zeit fuer `applySmallToBigWithWindow(...)`
- Zeit fuer `attachOriginalFiles(...)`
- Zeit fuer Ragfind-Gruppierung plus Supplemental-Search

Ausgabe:

- API-Log je Query
- optional Debug-Response fuer Admin oder lokales Benchmarking

### 0.2 Baseline-Messungen festschreiben

Vor jeder inhaltlichen Optimierung messen:

- p50, p95, max
- Query-Typen: kurz, lang, typo-lastig, dokumentfokussiert, inventory-artig
- Ragfind und MCP getrennt
- mit und ohne Elasticsearch erreichbar

### 0.3 EXPLAIN ANALYZE fuer Schluesselqueries

Explizit messen fuer:

- Hauptquery aus `executeSimilarityQuery(...)`
- `findSupplementalDocuments(...)`
- `refineItemsWithinDocument(...)`
- `refineNarrativeSectionsWithinDocument(...)`

Ergebnis soll dokumentiert werden mit:

- Seq Scan vs Index Scan
- Rows before filter
- Sort- und Hash-Kosten
- Anteil von Trigram- und Textsearch-Teilen

## Phase 1: Hohe Hebel mit geringem Risiko

### 1.1 Elasticsearch-Kandidaten als harter Vorfilter

Prioritaet: sehr hoch

Heute:

- ES-Kandidaten werden geholt
- PostgreSQL arbeitet trotzdem noch zu breit

Ziel:

- `elasticChunkCandidateIds` und/oder `elasticDocumentCandidates` muessen die SQL-Menge begrenzen
- SQL soll dann nur noch auf diesen Chunk- oder Dokument-IDs laufen

Konkrete Umsetzung:

- in `executeSimilarityQuery(...)` eine CTE fuer `candidate_documents` oder `candidate_chunks`
- wenn ES Kandidaten liefert, nur auf diese IDs joinen
- Fallback auf bisherigen PG-Pfad nur dann, wenn ES nichts liefert oder deaktiviert ist

Erwarteter Effekt:

- deutlich weniger Arbeit in `document_signals`
- schnellere Keyword- und Vector-CTEs
- weniger Sortier- und Gruppierungskosten

### 1.2 Fast Path fuer Ragfind und MCP

Prioritaet: sehr hoch

Ein neuer Suchmodus fuer interaktive Nutzung sollte standardmaessig deaktivieren:

- `preferDocumentFocus`
- `preferAdjacentSections`
- `enableSmallToBig`
- narrative section refinement

Nur bei Bedarf oder explizitem Deep-Modus zuschalten.

Betroffene Stellen:

- `executeSmartSearchQuery(...)`
- `buildSearchResults(...)`
- MCP `smart_search`

### 1.3 Ragfind-Rohkandidatenmenge reduzieren

Prioritaet: hoch

Aktuell:

- `retrievalTopK = Math.min(Math.max(topK * 6, 36), 120)`

Vorschlag:

- konservativer Fast-Mode: `topK * 3`
- kleineres Minimum, z. B. `12` oder `18`
- tieferes Maximum, z. B. `48`

Nur bei schwachen Queries oder wenigen Treffern eine zweite erweiterte Runde starten.

### 1.4 Query-Embedding-Cache

Prioritaet: hoch

Fuer normalisierte Query-Strings:

- In-Memory-LRU fuer Single-Instance
- optional Redis fuer mehrere App-Instanzen

Cache-Key:

- normalisierte Query
- Modellname

Effekt:

- MCP- und Ragfind-Wiederholungsqueries werden deutlich billiger

### 1.5 `findSupplementalDocuments(...)` abspecken

Prioritaet: hoch

Heute wird dort breit auf `documents.extracted_text` mit `LIKE` gearbeitet.

Vorschlaege:

- zuerst nur Titel plus `source_ref`
- `extracted_text` nur als spaeteres Fallback
- langfristig auf Elasticsearch umstellen

## Phase 2: PostgreSQL- und pgvector-Tuning

### 2.1 Materialisierte Normalisierungsfelder einfuehren

Prioritaet: hoch

Neue persistierte Felder oder generierte Spalten fuer:

- normalisierte Dokumentreferenz (`title + source_ref`)
- normalisierte Textvorschau des Dokuments
- normalisierte Chunk-Inhalte, wenn sinnvoll

Damit entfaellt ein Teil der wiederholten `lower(regexp_replace(...))`-Arbeit pro Query.

### 2.2 TSVECTOR-Spalten materialisieren

Prioritaet: hoch

Heute wird `to_tsvector(...)` in mehreren Queryteilen inline gebaut.

Vorschlag:

- `documents.lookup_tsv`
- `document_chunks.content_tsv`

mit Trigger oder Update-Logik beim Ingest.

Dann koennen die bestehenden GIN-Indizes sauberer greifen.

### 2.3 `document_signals` nur auf Kandidatenset rechnen

Prioritaet: sehr hoch

Die teuren Dokumentsignale duerfen nicht mehr ueber die gesamte Dokumentbasis laufen.

Ziel:

- Candidate Set aus ES oder schmalem PG-Filter
- `document_signals` nur fuer diese Dokument-IDs

### 2.4 Fuzzy-Anteile gezielter einsetzen

Prioritaet: mittel bis hoch

`similarity(...)` und `word_similarity(...)` sind teuer.

Vorschlag:

- nur fuer kurze Queries oder Typo-Verdacht
- nur auf Titel und `source_ref`
- nicht breit ueber `normalized_text`
- nur fuer wenige Dokumentkandidaten

### 2.5 pgvector-Tuning messen

Prioritaet: mittel

Vorhanden:

- `ivfflat` mit `lists = 100`

Pruefen:

- ob der Index wirklich genutzt wird
- ob `lists = 100` zur Datenmenge passt
- ob `ivfflat.probes` queryseitig gesetzt werden sollte
- ob HNSW in eurer pgvector-Version verfuegbar und schneller ist

Empfohlene Arbeitsschritte:

- `EXPLAIN ANALYZE` auf der Chunk-Vektorsuche
- Vergleich `ivfflat` vs HNSW
- Messung verschiedener `lists`/`probes`-Werte

### 2.6 Batch-Queries fuer Small-to-big

Prioritaet: mittel

Heute werden Kontextfenster itemweise geholt.

Ziel:

- mehrere Chunk-Fenster in einer Batch-Query laden
- zugehoerige Section-Metadaten ebenfalls batchen

### 2.7 DB-seitige Limits und Schutzmassnahmen

Prioritaet: mittel

Einbau von:

- `statement_timeout` fuer teure Deep-Queries
- Monitoring fuer langsame Queries
- Trennung von Fast Path und Deep Path mit verschiedenen Querybudgets

## Phase 3: Elasticsearch-Tuning

### 3.1 ES vom Zusatzsignal zum echten Candidate Retriever machen

Prioritaet: sehr hoch

Das ist der wichtigste ES-Schritt.

Heute:

- ES liefert Zusatzsignale

Ziel:

- ES liefert die Kandidatenmenge fuer Chunk- und Dokument-IDs
- PG macht nur das finale Reranking und Kontextladen

### 3.2 Mappings fuer Suchqualitaet und Geschwindigkeit ueberarbeiten

Prioritaet: hoch

Empfehlungen:

- eigene Felder fuer `title`, `source_ref`, `content`
- `keyword`-Subfields fuer Exact-Match-Faelle
- `copy_to` fuer ein gemeinsames Suchfeld
- analyzers fuer deutsch/mehrsprachig, `asciifolding`, lowercase
- optional `edge_ngram` oder `search_as_you_type` fuer UI-nahe Eingaben

### 3.3 Nicht den kompletten Dokumentvolltext als Standard-Suchfeld verwenden

Prioritaet: hoch

In `searchDocumentCandidates(...)` wird `extracted_text` voll befragt.

Vorschlag:

- stattdessen reduziertes Dokument-Suchfeld
- etwa Titel, `source_ref`, Exzerpt, strukturierte Felder
- Volltext primär auf Chunk-Ebene lassen

### 3.4 ES-Query-Kosten senken

Prioritaet: mittel

Moegliche Optimierungen:

- `track_total_hits: false`
- `terminate_after` fuer Fast Path
- kleinere `size` fuer Kandidatensuche
- `_source` minimal halten, was bereits teilweise geschieht
- `msearch` statt zwei separater ES-Requests pruefen

### 3.5 Index-Betriebsparameter tunen

Prioritaet: mittel

Fuer die Laufzeit:

- `refresh_interval` erhoehen, wenn Near-Real-Time nicht kritisch ist
- in Single-Node-Setups `number_of_replicas = 0`
- Heap-Groesse und Circuit Breaker beobachten
- Shard-Zahl explizit passend setzen statt Defaults zu akzeptieren

### 3.6 ES-Aliasse oder Filter je Knowledge Base

Prioritaet: mittel

Wenn KBs wachsen:

- filtered aliases oder Routing nach KB
- Kandidatensuche pro KB billiger machen

## Phase 4: Ragfind-spezifische Optimierungen

### 4.1 Zweistufige UI-Suche

Fast Path fuer erste Treffer:

- kleine Kandidatenmenge
- keine Deep-Refinements
- schnelle Antwort mit `tookMs`

Optionaler Follow-up:

- Deep Search nachladen
- mehr Snippets oder mehr Kontext erst danach

### 4.2 Debounce und Anfragekoaleszenz im Frontend

Wenn Ragfind bei jeder Eingabe sofort sucht:

- 200 bis 300 ms debounce
- inflight-request cancellation
- gleiche Querys deduplizieren

### 4.3 Snippet-Erzeugung nicht auf zu grossen Kontexten

`highlightSnippet(...)` sollte moeglichst nicht schon auf stark expandierten Kontexten laufen.

Erst kurze Treffer, dann optional erweiterten Kontext laden.

## Phase 5: MCP-spezifische Optimierungen

### 5.1 MCP standardmaessig auf Fast Path setzen

Interaktive Tool-Consumer profitieren mehr von Geschwindigkeit als von maximaler Kontextanreicherung.

Fuer MCP standardmaessig:

- geringere Kandidatenmengen
- kein Small-to-big
- nur leichte Dokumentfokuslogik

### 5.2 Wiederholte Queries cachen

Viele MCP-Clients senden sehr aehnliche oder identische Follow-up-Queries.

Cachebar sind:

- Query-Embedding
- ES-Kandidatenlisten
- finaler QueryResponse fuer kurze TTLs

### 5.3 Optionaler Deep-Mode als separates Tool

Moegliche Trennung:

- `smart_search_fast`
- `smart_search_deep`

Dann bleibt der Standard schnell, und hochwertige tiefe Suchen sind explizit.

## Phase 6: Infrastruktur und Betriebsoptimierung

### 6.1 Embedding-Service entlasten

Pruefen:

- Antwortzeit des Embedding-Modells
- Warm-start-Verhalten
- Keep-alive / Connection Reuse
- moeglicherweise kleineres, schnelleres Embedding-Modell testen

### 6.2 API-Instanzen horizontal skalieren

Wenn Last steigt:

- mehrere `ingestor-app`-Instanzen
- Redis-basierter Query-Cache
- ggf. getrennte Worker fuer Ingest und Search-orientierte App-Instanzen

### 6.3 DB- und ES-Ressourcen sauber dimensionieren

Pruefen:

- Shared Buffers / Work Mem fuer PostgreSQL
- ES-Heap und CPU-Saettigung
- I/O fuer pgvector- und GIN-Last

## Empfohlene Umsetzungsreihenfolge

### Sprint 1

- Stufen-Timings einbauen
- Baseline messen
- Ragfind-Kandidatenmenge reduzieren
- Fast Path fuer Ragfind und MCP einfuehren
- Query-Embedding-Cache einfuehren

### Sprint 2

- Elasticsearch-Kandidaten als harten PG-Vorfilter nutzen
- `document_signals` nur noch auf Kandidatenset rechnen
- `findSupplementalDocuments(...)` abspecken oder auf ES umstellen

### Sprint 3

- materialisierte Normalisierungsfelder
- materialisierte TSVECTOR-Spalten
- Batch-Optimierung fuer Small-to-big und Zusatzqueries

### Sprint 4

- Elasticsearch-Mappings und Queryprofil ueberarbeiten
- `track_total_hits`, `msearch`, `copy_to`, analyzer-Tuning
- Dokumentindex auf reduzierte Suchfelder umbauen

### Sprint 5

- pgvector-Tuning mit `EXPLAIN ANALYZE`
- HNSW vs IVFFLAT evaluieren
- `lists`/`probes` justieren

## Konkrete Erfolgskriterien

Als abgeschlossen gilt die Tuning-Arbeit erst, wenn folgende Punkte messbar erreicht sind:

- deutliche Reduktion von p95 fuer Ragfind und MCP
- Timing-Logs zeigen, dass der SQL-Hauptteil nicht mehr dominiert
- PostgreSQL arbeitet im Normalfall auf kleiner Kandidatenmenge
- Deep-Refinements laufen nur noch bewusst oder heuristisch sparsam
- ES ist echter Candidate Retriever und nicht nur Zusatzsignal
- wiederholte Queries umgehen den Embedding-Roundtrip ueber Cache

## Risiko- und Nebenwirkungscheck

Bei allen Performance-Schritten mitpruefen:

- Recall-Verlust durch zu aggressive Kandidatenbegrenzung
- Qualitaetsverlust bei deaktiviertem Small-to-big
- Unterschiede zwischen Ragfind und MCP duerfen nicht verwirrend werden
- ES-Abhaengigkeit braucht sauberen Fallback, falls ES kurzzeitig nicht erreichbar ist

## Empfehlung fuer den ersten konkreten Umsetzungsschritt

Wenn nur ein einzelner Hebel zuerst umgesetzt wird, dann dieser:

- `executeSimilarityQuery(...)` so umbauen, dass Elasticsearch-Kandidaten die PostgreSQL-Menge hart begrenzen

Das ist der Punkt mit dem besten Verhaeltnis aus Risiko, technischem Aufwand und erwartetem Latenzgewinn.