diff --git a/plans/audit-report.json b/plans/audit-report.json
index abda00a..1cea3a1 100644
--- a/plans/audit-report.json
+++ b/plans/audit-report.json
@@ -1,5 +1,5 @@
{
- "generatedAt": "2026-10-17T19:58:49.510Z",
+ "generatedAt": "2026-10-19T20:49:08.257Z",
"stats": {
"files": 70,
"byDir": {
@@ -13,9 +13,9 @@
"server": 5
},
"components": {
- "Aside": 108,
+ "Aside": 109,
"Badge": 30,
- "Steps": 87,
+ "Steps": 88,
"TabItem": 64,
"Tabs": 28,
"Card": 11,
@@ -33,6 +33,7 @@
"src/content/docs/projekt/mcp-nutzen.mdx",
"src/content/docs/projekt/navigation-i18n.mdx",
"src/content/docs/projekt/ssr-website.mdx",
+ "src/content/docs/projekt/suche.mdx",
"src/content/docs/reference/schema-properties.md"
],
"hollowMarkers": [],
diff --git a/plans/doku-ausbau.md b/plans/doku-ausbau.md
index 079fcf3..204bd00 100644
--- a/plans/doku-ausbau.md
+++ b/plans/doku-ausbau.md
@@ -55,7 +55,7 @@ Jede Fähigkeit von tibi-server / tibi-admin braucht genau eine Dokustelle. Die
| audit (TTL, Lesen) | projekt/audit-backup | ✅ |
| jobs (Cron) + actions | projekt/jobs-actions | ✅ |
| snapshots + backup/restore | projekt/audit-backup | ✅ |
-| search (Textindex, RRF-Hybrid, Regenerate) | projekt/suche (+ projekt/sammlungen-anlegen für Index-YAML) | ✅ |
+| search (Textindex, RRF-Hybrid, ngram.stopwords, Regenerate) | projekt/suche (+ projekt/sammlungen-anlegen für Index-YAML) | ✅ |
| embedding + vectorindex (inkl. Failover-Gruppen, Startprüfung/Quarantäne/Rückkehr, `goldenPath`, Status-Endpunkte) | server/konfiguration (Gruppen im Detail), server/betrieb (Status/Störungen), projekt/suche (Kurzüberblick) | ✅ |
| rankfusion (RRF-Formel) | projekt/suche | ✅ |
| permissions (public/user/token, Orgs/Teams-Matrix) | projekt/rechte-tokens | ✅ |
diff --git a/src/content/docs/projekt/suche.mdx b/src/content/docs/projekt/suche.mdx
index 79a55e5..482a736 100644
--- a/src/content/docs/projekt/suche.mdx
+++ b/src/content/docs/projekt/suche.mdx
@@ -108,6 +108,46 @@ Alle Modi aus `collection.go:314` — `fields` gilt für `regex` und `ngram`, `v
Aus Projektsicht musst du nichts einrichten — lass `autoRegenerate: true` in der YAML stehen, dann pflegen sich `fuzzy`- und `semantic`-Indizes selbst. Der Regenerate-Flow im tibi-admin (Test-Plan `41-search-index-status`) nutzt genau den Endpunkt aus Schritt 4.
+## 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, der alte Index enthält nur mehr N-Gramme als nötig. **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.
@@ -159,7 +199,7 @@ search:
```
- 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 nur für ngram-Quellen von `combined`-Suchen, und nur wenn er fertig geladen und im Budget ist. Sonst — und immer bei Einzel-ngram-Suchen sowie `_count` — läuft die Suche über MongoDB. Der Debug-Header zeigt `inMemory=ram|mongo(loading)|mongo(budget)|mongo(candidates)`.
+- Genutzt nur für ngram-Quellen von `combined`-Suchen, und nur wenn er fertig geladen und im Budget ist. Sonst — und immer bei Einzel-ngram-Suchen sowie `_count` — läuft die Suche über MongoDB. Der Debug-Header zeigt `inMemory=ram|mongo(loading)|mongo(budget)|mongo(candidates)|mongo(no-terms)`.
- 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.
@@ -183,10 +223,13 @@ search:
| 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 |
| 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 63a2848c 2026-10-19 (tibi-server: combinedCache, ngram.inMemory, _count-Fast-Path, Restore-Rebuild)
+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 69fc4d8 2026-10-19 (tibi-server: ngram.stopwords, Regenerate-Endpunkt, mongo(no-terms)), am 9e12a8e 2026-10-19 (tibi-types: ngram.stopwords/queryFilter/inMemory im Schema)
diff --git a/src/content/docs/reference/schema-properties.md b/src/content/docs/reference/schema-properties.md
index 8c9d9cc..36cec4f 100644
--- a/src/content/docs/reference/schema-properties.md
+++ b/src/content/docs/reference/schema-properties.md
@@ -400,4 +400,4 @@ Pflicht: `name`.
| `permissions` | Liste von string | nein | — | Bare permission names for team-based access (Phase 2). Empty = admin-only in Phase 1. |
| `enabled` | boolean | nein | `false` | Whether this snapshot profile is active. Disabled profiles are skipped by the scheduler but can still be triggered manually. |
-Generiert aus `tibi-types 3bf8055` (Stand 2026-10-17) — Änderungen an den Schemas dort erfordern `npm run gen:schemas`.
+Generiert aus `tibi-types 9e12a8e` (Stand 2026-10-19) — Änderungen an den Schemas dort erfordern `npm run gen:schemas`.