Zum Inhalt springen

Suche

Die Suche läuft über search-Blöcke in der Sammlungs-YAML plus Abfrage per q und qName. Suchbegriffe: Volltextsuche, Hybrid-Suche, Suchindex neu aufbauen.

content.yml definiert drei Indexe:

Index Schlüssel Zweck
content_text $text:$**, defaultLanguage: none Volltext über alle Felder
path_lang path + lang, unique Pfad je Sprache eindeutig
translation translationKey + lang, unique Übersetzung je Sprache eindeutig

navigation.yml und medialib.yml nutzen nur einen default-Index im Modus regex (kein Textindex nötig).

content.yml — search-Blöcke (gekürzt)
# content.yml — search (gekürzt)
search:
- name: combined # Hybrid aus semantic + fuzzy
mode: combined
rrf: { k: 60, topK: 100, weights: { semantic: 1.5, fuzzy: 1.0 } }
- name: fuzzy
mode: ngram
autoRegenerate: true
fields: [name, path, meta.title, meta.description, blocks.headline, blocks.subline,
blocks.text, blocks.items.title, blocks.items.text, blocks.items.question, blocks.items.answer]
- name: semantic
mode: vector
autoRegenerate: true
fields: [name, meta.title, meta.description, blocks.headline, blocks.subline,
blocks.text, blocks.items.title, blocks.items.text, blocks.items.question, blocks.items.answer]
vector: { provider: bge_m3 }
qName Sammlung Modus
combined content Hybrid (RRF aus semantic + fuzzy)
fuzzy content n-gram, fehlertolerant
semantic content Vektor (bge_m3)
default tag, navigation, comment, medialib regex

Grundlagen (Filter, Lookup, Facetten) stehen unter Lesen und suchen. Hier nur die Suche:

Suche per q und qName
GET …/content?q=kontakt&qName=combined
GET …/tag?q=hero
GET …/content?q=kontakt&qName=combined&qExplain=1
  • q ist der Suchtext, qName wählt den Block (ohne qName greift der Standard).
  • q und qName gelten auch für _count und _facet — Facetten zählen dann nur die Treffer (Lesen und suchen).
  • qExplain=1 zeigt die Gewichtung — nützlich beim Debuggen.
  • Suche mit und ohne lookup testen: ?q=…&qName=combined&lookup=blocks.heroImage.image:medialib,blocks.image:medialib löst Treffer-Bilder gleich auf.

combined mischt Vektor- und n-gram-Treffer per Reciprocal Rank Fusion (internal/rankfusion, tibi-server):

RRF-Formel (combined)
score(doc) += weight / (k + rank) # k = 60, weights: semantic 1.5, fuzzy 1.0
  • topK: 100 begrenzt die Kandidaten je Quelle und kappt das Fusionsergebnis (MergeRRF). Ohne Angabe im rrf-Block gilt topK: 500.
  • Das gekappte Fusionsergebnis ist die Treffermenge der Liste und das Standardfenster der Facetten (_facet?q=…).
  • Weitere Strategien (score_rrf, score_sum, rank_score, hybrid), Normierung (minmax, zscore), Gewichte (scoreWeight) und Quellen-Config (sourceConfig) existieren im Code und sind als rrf-Block in der Projekt-YAML einstellbar — aus Projektsicht reicht combined mit den Starter-Gewichten.
  • Alte Score-Beispiele aus Erzähl-Docs nicht übernehmen — nur diese Formel gilt.

Alle Modi aus collection.go:314 — fields gilt für regex und ngram, vector.provider muss auf einen Server-Provider zeigen (collection.go:377):

Modus Einsatzfall Braucht Index/Aufbau
text MongoDB-Volltext, eine Textstelle pro Sammlung Ja — Mongo-Textindex (hasTextIndex; nur ein Textindex je Sammlung)
regex Einfache Teilübereinstimmung (default-Block in tag, navigation, comment, medialib) Nein — läuft direkt auf den Feldern, kein Aufbau, keine Queue
eval Eigene JS-Bewertung pro Treffer (eval-Ausdruck) Nein — Ausdruck läuft zur Abfragezeit, optional score.expr
filter Feste MongoDB-Filtervorlage mit $subst Nein — Filter zur Abfragezeit, optional score.expr
ngram Fehlertolerante Teilwortsuche (Starter-fuzzy) Ja — _search-Anreicherung mit autoRegenerate, Queue bis zum Worker-Lauf
vector Semantische Suche (Starter-semantic, Provider bge_m3) Ja — Embeddings über Embedding-Provider, Queue bis zum Worker-Lauf
combined Hybrid aus mehreren Blöcken (Starter-combined = RRF aus semantic + fuzzy) Je beteiligtem Block (siehe oben); Fusion selbst braucht keinen eigenen Index
  1. Bearbeite Einträge normal — fuzzy und semantic haben autoRegenerate: true und bauen sich selbst nach.
  2. Beobachte im tibi-admin den Hinweis SearchIndexBuildNotice: Er erscheint im Projekt-Header (Summe offener _queue-Einträge aller ngram-/vector-Sammlungen) und im Sammlungs-Header der betroffenen Sammlung.
  3. Warte, bis der Hinweis nach dem nächsten Polling von selbst verschwindet (_queue = 0). Der regex-Modus (default) hat keine Queue und keinen Hinweis.
  4. Bleibt die Queue stehen oder hast du die Config geändert: POST /_/:project/_/admin/regenerate-search aufrufen — optional mit Body { collection, modes, names, ids, filter, force } (admin_regenerate_search.go:17-25). Mit ?async=1 nur einreihen und sofort antworten ({"message": "queued", …}), ohne filter auch synchron möglich. Ungültige IDs ohne einzige gültige Hex-ID brechen mit 400 no valid document IDs in request ab.
  5. Status prüfen: GET /_/:project/_/admin/search-worker/status zeigt pro Provider Aktiv-Flag, Queue-Tiefe, laufenden Job und Fortschrittszähler (admin_search_status.go:10-15). Auth jeweils: Scope project.regenerate-search plus Token-Flag AllowRegenerateSearch.

Füllwörter wie „zum”, „der” oder „für” treffen in einer n-gram-Suche jeden Text, in dem sie irgendwo vorkommen — auch Beschreibungen, in denen sie nichts aussagen. ngram.stopwords entfernt sie aus beiden Seiten: aus den gespeicherten N-Grammen und aus der Suchanfrage. Einseitig wäre falsch: stehen gebliebene Stoppwort-N-Gramme blähen die Ähnlichkeitswerte auf und drücken die Unterschiede zwischen relevanten und irrelevanten Treffern zusammen.

Projekt-YAML — Stoppwörter für fuzzy
search:
- name: fuzzy
mode: ngram
fields: [name, meta.title, meta.description]
ngram:
sizes: [2, 3]
stopwords:
presets: [de] # de, en, fr, es, it
words: [zum, zur] # optionale Ergänzungen, werden gemerged
  • Die Presets enthalten reine Funktionswörter: Artikel, Pronomen, Präpositionen, Konjunktionen, Hilfsverben und bedeutungsfreie Adverbien (internal/utils/ngram_stopwords.go). Negationswörter wie „nicht” oder „kein” bleiben bewusst draußen — „nicht wasserdicht” würde sonst zur Suche nach „wasserdicht”.
  • Einträge werden mit derselben Normalisiererkette gebildet wie Dokumente und Anfragen. Ohne normalize-Block laufen alle Zusatznormalisierer, aus „für” wird fuer; nur mit normalize.use: [] bleibt es beim Standard-Normalisierer und damit bei fur. Einträge, die nicht genau ein Wort ergeben, erzeugen einen Projekt-Fehler (internal/models/collection_setup.go).
  • stopwords gilt nur für mode: ngram. Ein Block mit anderem Modus und gesetzten Stoppwörtern erzeugt einen Projekt-Fehler; das Projekt lädt trotzdem, die Stoppwörter bleiben in diesem Fall aber aus.
  • Ein Set gilt für den ganzen Block, nicht je Sprache: Es wirkt auf jedes Wort in den konfigurierten Feldern. Bei gemischtsprachigen Inhalten lieber words statt Presets nutzen oder einen Block je Sprache anlegen — sonst verschwinden Wörter der falschen Sprache (etwa car/or aus deutschen Texten, wenn fr aktiv ist). words ergänzt das Set nur; ein Preset-Wort lässt sich damit nicht wieder freistellen.
  • Eine Anfrage, die nur aus Stoppwörtern besteht (oder nur aus Begriffen kürzer als jede konfigurierte Größe), liefert null Treffer statt der ganzen Sammlung (internal/utils/ngram.go).

ngram.stopwords ist Teil des Anreicherungs-Hashs. Hinzufügen ist unkritisch: Die Anfrage verwirft die Wörter, die Dokumente bleiben über ihre übrigen Begriffe auffindbar. Scores können bis zum Neuaufbau abweichen, weil Kreuz-Wort-N-Gramme ohne die Stoppwort-Lücke neu entstehen. Entfernen braucht den Neuaufbau zuerst — sonst liefern diese Wörter null Treffer, bis jedes Dokument neu angereichert ist:

  1. stopwords in der Projekt-YAML ändern und das Projekt neu laden.
  2. POST /_/:project/_/admin/regenerate-search mit { collection, modes: ["ngram"] } aufrufen — oder autoRegenerate: true setzen und den Reload abwarten.
  3. GET /_/:project/:collection/_queue beobachten, bis der Zähler 0 ist. Erst danach die alten Wörter aus der Suche nehmen.

Beide Optionen sind standardmäßig aus und ändern ohne Konfiguration nichts.

Wiederholte Combined-Queries (Katalog-Filter, Blättern durch dieselbe Treffermenge) können die gemergte Rangliste kurzzeitig wiederverwenden:

search:
- name: combined
mode: combined
rrf: { k: 60, topK: 100, weights: { semantic: 1.5, fuzzy: 1.0 } }
combinedCache:
enabled: true # Default: false
ttl: 45s # Default: 45s
maxEntries: 1000 # LRU-Cap, Default: 1000
  • Gecacht werden nur die gemergten IDs + Scores; die Dokumente der Seite werden immer frisch geladen.
  • Der Key enthält Query, Config, den effektiven Filter (inkl. Rollen-preFilter und Hook-Filter) und alle Fusionsparameter — Ergebnisse können nicht zwischen Rollen oder Sammlungen wandern.
  • Aktiv: X-Cache: HIT|MISS im Response-Header. Bei HIT bleiben X-Search-Sources/X-Search-Config leer (es lief keine Subsearch).
  • Schreibvorgänge, die einen In-Memory-Index (Vektor oder Ngram) verändern, invalidieren den Cache sofort (der Mutationszähler des Index steckt im Key); die ttl ist die Obergrenze für Änderungen, die noch nicht im Index stehen. Nach einem Restore wird der Cache verworfen.
  • Grenzen: ttl kurz halten (≤30s bei mehreren Server-Instanzen, die Invalidierung ist node-lokal).

Für große Sammlungen kann der ngram-Anteil einer Combined-Suche aus dem RAM beantwortet werden statt aus MongoDB:

# Server-config.yml — globaler Schalter, ohne ihn passiert nichts
search:
inMemory:
enabled: true
maxMemoryMB: 1024
# Projekt-YAML
search:
- name: fuzzy
mode: ngram
fields: [title, context]
ngram:
sizes: [3]
queryFilter: { minMatchRatio: 0.4 }
inMemory:
enabled: true # Default: false
maxDocs: 150000 # mehr Dokumente → MongoDB
maxMemoryMB: 512 # Budget pro Index
  • Gebaut wird der Index beim Projekt-(Re-)Load aus den gespeicherten _search-Ngrammen (nie aus den Quellfeldern), damit er nicht vom MongoDB-Pfad abweichen kann.
  • Genutzt wird er für ngram-Quellen von combined-Suchen und für Einzel-ngram-Suchen — dort ersetzt er nur das Ranking, die Seitendokumente werden anschließend per _id geladen (Projektionen/?select wirken weiter; ?lookup= und ?aggregate= erzwingen dagegen MongoDB). Bei combined zeigt der Debug-Header inMemory=ram|mongo(loading)|mongo(budget)|mongo(candidates)|mongo(no-terms); mongo(no-terms) erscheint nur, wenn für die Quelle überhaupt ein In-Memory-Index existiert.
  • Einzel-ngram-Suchen fallen mit X-Search-Debug: true im Kopf X-Search-Memory sichtbar auf MongoDB zurück, wenn der Index nicht bereit ist oder die Anfrage etwas braucht, das der Index nicht garantieren kann: mongo(disabled), mongo(loading), mongo(budget), mongo(count) (bei ?count=1), mongo(sort) (bei eigenem ?sort=), mongo(no-terms) — sowie mongo(lookup), mongo(aggregate), mongo(hook-pipeline) und mongo(hook-filter), wenn die Anfrage Schritte braucht, die in der MongoDB-Pipeline laufen. Der Read-Hook selbst läuft einmal pro Anfrage; nur seine pipelineMod-Funktion kann im MongoDB-Fallback zweimal laufen (Klassifikation + echte Pipeline). Ein Hook, der context.filter zurückgibt oder in-place ändert, ersetzt den effektiven Filter samt Suchbedingung und fällt auf mongo(hook-filter) zurück. Ein pipelineMod aus einem Read-Hook stört den RAM-Pfad nur, wenn er die Ergebnismenge ändern kann ($match, $sort, $group, $project, …), Stufen vor die bestehende Pipeline setzt oder _id/_search schreibt bzw. $_search liest. Reine angehängte Feld-Transformationen ($addFields, $set, $unset) laufen auf der geladenen Seite nach der Projektion und bleiben im RAM.
  • Rollen-preFilter, ?filter= und Hook-Filter gelten weiter: der Index liefert Kandidaten-IDs, die danach gegen den effektiven Filter geprüft werden.
  • Schreibvorgänge (Anlegen/Ändern/Löschen/Bulk/Worker/Regenerate) halten den Index aktuell; Snapshot- und Backup-Restore bauen alle In-Memory-Zustände neu auf (Indizes + Cache) — alte Treffer können einen Restore nicht überleben. Während des Neuaufbaus laufen Suchen auf MongoDB.
  • Budget-Abbruch (zu viele Dokumente, Speicher- oder RSS-Grenze) lässt den Index leer und dauerhaft auf MongoDB; ein Reload startet einen neuen Versuch.
  • Gemessen (91k Dokumente): Die ngram-Subsearch von „blauer pullover” fällt von ~1,3 s auf wenige Millisekunden; Einzelwort-Suchen von ~1,4–4,9 s auf 85–158 ms. Das kompakte Index-Layout kostet ≈155 MB statt ≈1,15 GB und lädt in ≈2,7 s.
  • Der 20000er-Kandidaten-Cap gilt nur für combined (dort auf Roh-Kandidaten). Einzel-ngram-Suchen prüfen nur so viele Kandidaten in Rangfolge, wie die Seite braucht (Blöcke à 500 _ids).

Der Vektor-Index wird beim Projekt-Start im Hintergrund aufgebaut; bis dahin liefern Suchen Teilergebnisse. Für 91k Dokumente mit 1024 Dimensionen liegt der Load bei ~14 s (count-balancierte _id-Bereiche, parallel dekodiert) und der Speicher bei ~690 MB pro Config und Sprache. Die Sparse-Suche ist mit dem kompakten Layout von ~190 ms auf ~4 ms gefallen, Dense bleibt bei ~5 ms (parallel über die Kerne).

Zwei Dense-Speicherformen sind im Umlauf: BSON-Binary (aktuell) und ein Legacy-Byte-Array (4× größer, ~5× langsamer beim Laden). Der Server liest beide; für Altbestände gibt es im Server-Repo tools/vectormigrate (erst -dry-run, danach Projekt neu laden). Ein MongoDB-Fallback existiert nicht: fehlt der Index, bricht die Vektor-Suche mit einem klaren Fehler ab.

  • Provider-Name: vector.provider (z. B. bge_m3) muss mit einem Servernamen aus der Embedding-Registry übereinstimmen (collection.go:377) — sonst löst die Config nicht auf. Unbekannte Provider-Typen brechen ab, defekte einzelne Provider werden übersprungen (registry.go:58-70).
  • Gesundheit prüfen: GET /_/:project/_/admin/embedding/status zeigt Gruppen-/Mitglieder-Gesundheit (an diesem Scope sind Fehlertexte redigiert, nur Zustände und Zähler; Volldetail nur am Server-Endpunkt GET /api/v1/admin/embedding/status, admin_embedding_status.go:49-72).
  • Feinheiten (optional): documentPrefix/queryPrefix, overflow: truncate (Standard) oder chunk mit chunkSize (Standard 512) und chunkOverlap (Standard 50) — collection.go:376-383.
  • Debug: Mit Header X-Search-Debug: true antwortet die Suche mit X-Search-Config, X-Search-Timing und X-Search-Sources (Treffer-/Score-Spannen je Quelle, search_debug.go:145-159).
Symptom Ursache Lösung
Leere Scores / keine Treffer trotz Daten Index fehlt oder _queue noch > 0 Hinweis im Admin abwarten, dann erneut suchen
combined findet nichts, fuzzy schon Vektor-Anteil ohne Embeddings semantic-Felder und Provider (bge_m3) prüfen
400 no valid document IDs in request Regenerate mit nur ungültigen ids Hex-IDs prüfen oder ids weglassen (Vollaufbau)
404 collection "…" not found Regenerate mit falschem collection-Namen Namen gegen api/collections/*.yml prüfen
Worker-Queue steht Provider defekt oder überlastet search-worker/status und embedding/status lesen, dann Regenerate mit force: true
Suche über tag/navigation unscharf default ist regex, kein Fuzzy Erwartung anpassen oder content-Indexe nutzen
inMemory=ram erscheint nie im Debug-Header Globaler Schalter aus, Config ohne inMemory, Index noch nicht fertig oder Budget-Abbruch search.inMemory.enabled, ngram.inMemory.enabled prüfen, Reload, Budget-Logs lesen
Suche mit einem einzelnen Füllwort liefert nichts ngram.stopwords verwirft das Wort aus der Anfrage Inhaltliches Wort suchen; soll das Wort suchbar bleiben, das Preset weglassen und das Set nur über words bauen
Ein entferntes Stoppwort liefert weiterhin nichts Neuaufbau nach dem Entfernen fehlt oder läuft noch regenerate-search anstoßen, _queue auf 0 warten
mongo(no-terms) in X-Search-Sources Combined-Quelle ohne verwertbare Terme (nur Stoppwörter oder zu kurze Begriffe) Kein Fehler — Anfrage präzisieren
X-Search-Memory: mongo(disabled) bei jeder Suche In-Memory-Index nicht aktiviert oder nicht geladen search.inMemory.enabled und ngram.inMemory.enabled prüfen, Projekt neu laden
X-Search-Memory: mongo(budget) Index-Load über dem Budget abgebrochen (Doc-Anzahl, Speicher oder RSS) Budget erhöhen oder Collection kleiner halten; Loader-Log lesen
X-Search-Memory: mongo(sort) / mongo(count) Eigenes ?sort= bzw. ?count=1 — der Index kann das nicht garantieren Erwartetes Verhalten; ohne sort/count messen
X-Search-Memory: mongo(lookup) / mongo(aggregate) / mongo(hook-pipeline) / mongo(hook-filter) Anfrage nutzt Schritte oder einen Hook-Filter, die in der MongoDB-Pipeline laufen Erwartetes Verhalten; ohne diese Parameter messen
Nach Restore kurz alte Treffer Restore erfolgt, Index-Neuaufbau läuft noch Warten bis inMemory=ram wieder erscheint (bis dahin MongoDB-Fallback)
Combined-Suche liefert kurz eine ältere Reihenfolge combinedCache aktiv, TTL noch nicht abgelaufen TTL senken oder Cache für kritische Daten abschalten
?facet= an der Liste wirkungslos Facetten sind eine eigene Route (…/_facet) q/filter dort mitgeben — Lesen und suchen
Facettenzahlen kleiner als erwartet Fenster ist rrf.topK (Default 500), nicht die ganze Sammlung ?qLimit=all an _facet setzen oder topK im rrf-Block erhöhen

Weiter: Lesen und suchen, Sammlungen anlegen, Felder und Widgets.

Verifiziert am e3f92eb 2026-10-14 (tibi-svelte-starter), am 68b4278 2026-10-15 (tibi-server, rankfusion), am 2b091d8 2026-10-13 (tibi-admin-nova, Test-Plan 41-search-index-status), am 63a2848 2026-10-19 (tibi-server: combinedCache, ngram.inMemory, _count-Fast-Path, Restore-Rebuild), am 76f1336 2026-10-19 (tibi-server: ngram.stopwords, Regenerate-Endpunkt, mongo(no-terms)); am 92c3ad74 2026-10-20 (tibi-server: kompakter ngramindex, collection_ngram_search.go, Hook-Pipeline-Klassifikation, X-Search-Memory), am 654b568 2026-10-19 (tibi-server: kompakter Sparse-Vektorindex, Legacy-Dense-Format, MemoryEstimate, vectormigrate, paralleler Range-Loader inkl. Retry/Semaphore), am 9e12a8e 2026-10-19 (tibi-types: ngram.stopwords/queryFilter/inMemory im Schema), am 21472bd 2026-10-19 (tibi-types: ngram.sizes 2–10, combinedCache im Schema), am cf44cd7 2026-10-19 (tibi-svelte-starter: tibi-types-Submodul auf 21472bd), am d0d699d 2026-10-20 (tibi-server: collection_facet_search.go, rrf.topK-Default 500, qLimit)