--- title: 'Suche' description: 'Textindex, search-Blöcke, Hybrid-Suche, Neuaufbau des Index aus Projektsicht.' --- import { Aside, Steps } from '@astrojs/starlight/components'; 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. ## Index anlegen `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). ## search-Blöcke ```yaml title="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 | ## Lesen mit q Grundlagen (Filter, Lookup, Facetten) stehen unter [Lesen und suchen](/projekt/abfragen-lesen/). Hier nur die Suche: ```text title="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). - `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. ## Hybrid-Kurzüberblick `combined` mischt Vektor- und n-gram-Treffer per Reciprocal Rank Fusion (`internal/rankfusion`, tibi-server): ```text title="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`). - 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. ## Suchmodi im Überblick 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 | ## Index aufbauen und prüfen 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`. ## Stoppwörter für `ngram` (`ngram.stopwords`) 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. ```yaml title="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`). ### Nach Änderungen neu aufbauen `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. ## Performance-Optionen (Cache und In-Memory-Index) Beide Optionen sind **standardmäßig aus** und ändern ohne Konfiguration nichts. ### Ergebnis-Cache für `combined` (`combinedCache`) Wiederholte Combined-Queries (Katalog-Filter, Blättern durch dieselbe Treffermenge) können die gemergte Rangliste kurzzeitig wiederverwenden: ```yaml 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). ### In-Memory-Index für `ngram` (`ngram.inMemory`) Für große Sammlungen kann der ngram-Anteil einer Combined-Suche aus dem RAM beantwortet werden statt aus MongoDB: ```yaml # 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 (Projektion, `lookup` und Aggregation bleiben also nutzbar). 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)` und `mongo(hook-pipeline)`, wenn die Anfrage Schritte braucht, die in der MongoDB-Pipeline laufen. 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 `_id`s). ## Vektor-Index: Laden, Speicher, Altformate 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. ## Was der Vektor-Pfad zusätzlich braucht - **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`). ## Fehlerbilder | 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)` | Anfrage nutzt Schritte, die in der MongoDB-Pipeline laufen | Erwartetes Verhalten; ohne `lookup`/`aggregate`-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 Suche wirkungslos | Facetten sind eigene Route (`…/_facet`) | [Lesen und suchen](/projekt/abfragen-lesen/) beachten | Weiter: [Lesen und suchen](/projekt/abfragen-lesen/), [Sammlungen anlegen](/projekt/sammlungen-anlegen/), [Felder und Widgets](/projekt/felder-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 2a8922eb 2026-10-19 (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)