Baue den Index als SQLite mit FTS5 und sqlite-vec, dazu die Suche

Entscheidung Andreas 2026-09-04 (spine/core#8): kein Datenbankdienst. Der
Index ist eine abgeleitete Sicht auf die Markdown-Ablage in einer Datei im
Volume, jederzeit neu baubar. image/km/index.py zerlegt die Dateien in
Chunks (Chunking aus info/ai/scripts/index_docs.py übernommen), hält
Volltext in FTS5 und Vektoren aus bge-m3 in sqlite-vec, inkrementell über
SHA-256 je Datei. image/km/search.py sucht Volltext führend, Vektor
ergänzend, mit Filtern vor dem Ranking (0 Treffer statt gesperrt).

Ingest: Kommentare bei der Erstbefüllung repo-weit statt je Vorgang —
die erste Fassung hing bei live/live nach 371 Vorgängen; jetzt 2175 in zwei
Minuten. Gemessen: Volltext über 61.000 Chunks in 3 s, Vektoren 0,31 s je
Chunk auf der CPU-Box. Kein Bytecode mehr im Repo.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 18:04:35 +02:00
co-authored by Claude Fable 5.1
parent 1e08de2d05
commit 4986ef0575
9 changed files with 599 additions and 14 deletions
+89
View File
@@ -0,0 +1,89 @@
# Index und Suche — SQLite mit FTS5 und sqlite-vec
Entscheidung Andreas, 2026-09-04 (spine/core#8): kein Datenbankdienst. Der
Index ist eine **abgeleitete Sicht** auf die Markdown-Dateien der Ablage und
liegt als eine SQLite-Datei neben ihr. Er darf jederzeit gelöscht und neu
gebaut werden — die Wahrheit sind die Dateien.
Code: `image/km/index.py` (bauen), `image/km/search.py` (suchen).
Abhängigkeiten: `requests`, `sqlite-vec` (`image/requirements.txt`).
## Was drin ist
| Tabelle | Inhalt |
|---|---|
| `docs` | eine Zeile je Datei: Pfad, Namespace, Projekt, Klassifikation, Typ (`vorgang`, `doku`, …), Zustand, Titel, Zeitstempel, SHA-256, ob eingebettet |
| `chunks` | Absatz-Chunks je Datei (max. 600 Zeichen, 60 Überlappung — dasselbe Chunking wie der Wissens-MCP); der Titel ist Chunk 0 |
| `chunks_fts` | FTS5 über die Chunks, Tokenizer `unicode61` **ohne** Diakritika-Entfernung (ä bleibt ä) |
| `chunks_vec` | sqlite-vec, `bge-m3` mit 1024 Dimensionen, rowid = Chunk-ID |
| `verweise` | Kanten aus dem Frontmatter (`verweise:`), beide Richtungen abfragbar |
Die Filterspalten sind der Punkt, an dem die Zugriffsregel greift: Namespace,
Projekt, Klassifikation, Typ, Zustand sind Spalten in `docs`, und die Suche
filtert **vor** dem Ranking. Wer eine Klassifikation nicht sehen darf, bekommt
`0 Treffer` — nicht „vorhanden, aber gesperrt" (spine/core#18).
## Bauen
```bash
# Volltext (Sekunden) — nach jedem Ingest
image/km/index.py --ablage /srv/km/ablage
# dazu Vektoren fuer neue und geaenderte Dateien
image/km/index.py --ablage /srv/km/ablage --embed
# nur ausstehende Vektoren nachziehen, z. B. nachts
image/km/index.py --ablage /srv/km/ablage --embed-only [--embed-limit 200]
```
Inkrementell über den SHA-256 je Datei: unveränderte Dateien werden nicht
angefasst, geänderte komplett neu zerlegt (alte Chunks und Vektoren raus,
neue rein), verschwundene entfernt. Der Volltext ist sofort da; die Vektoren
folgen, sortiert nach `aktualisiert` absteigend — das Neueste zuerst.
## Suchen
```bash
search.py "worktree prune" # hybrid: Volltext + Vektor, RRF
search.py --mode fts "live/live#2181" # exakt
search.py --mode vec "warum braucht der bus keine persistenz mehr"
search.py --ns 42i --typ vorgang --zustand open "i18n"
search.py --classification lan --classification public "…" # Sichtkreis
search.py --json "…"
```
Volltext führend, Vektor ergänzend (spine/core#1). Hybrid fusioniert beide
Ranglisten per Reciprocal Rank Fusion; ausgegeben wird ein Treffer je Datei
mit dem besten Chunk als Anriss. Die FTS-Anfrage macht aus jedem Wort einen
Präfix (`worktree*`), Zeichenketten mit Sonderzeichen (`live/live#2181`)
werden wörtlich gesucht.
## Gemessen am 2026-09-04 (Mac, Ablage mit 3424 Vorgängen aus elf Repos)
| Schritt | Menge | Dauer |
|---|---|---|
| Volltext-Index, Erstbau | 2935 Dateien, 61.141 Chunks | 3,1 s |
| Volltext, inkrementell | 491 neue Dateien | 0,3 s |
| Vektoren | 1756 Chunks (30 Dateien) | 550 s → **0,31 s je Chunk** |
| Vektoren, hochgerechnet | 64.392 Chunks | **~5,5 h** |
| FTS-Suche | — | unter 50 ms |
| Vektorsuche | Embedding der Anfrage + Nachbarn | ~1 s, davon fast alles das Embedding |
Die Vektorzeit ist der Rechner, nicht die Leitung: `/api/embed` mit 32 Texten
im Batch liefert 0,31 s je Text, einzeln 1,0 s — die Ollama-Instanz auf dem
s18-CT rechnet auf der CPU (`OLLAMA_VULKAN=0`, siehe `info: lan/infra/wissens-mcp.md`).
Die Erstbefüllung fällt einmal an; danach kostet ein geändertes Ticket
Sekunden. Wer es schneller braucht, hängt ein GPU-Embedding ans Gateway —
das ist eine Betriebsfrage, keine am Index.
**Dateigröße:** 62 MB für den Volltext über 61.000 Chunks; mit Vektoren
(1024 × 4 Byte je Chunk) kommen rund 260 MB dazu.
## Was fehlt
- **Deutsche Wortformen.** `unicode61` stemmt nicht; „Sperrung" findet
„gesperrt" nur über die Vektorsuche. Falls das nicht reicht: Trigramm-
Tokenizer, keine neue Datenbank.
- **Kanten zu Doku-Seiten.** `verweise` kennt heute nur Vorgang→Vorgang.
- **Der Dienst.** Heute zwei Skripte; `read`/`search`/`summary` als HTTP-API
und MCP-Endpunkt kommen darüber (Reihenfolge in `image/README.md`).
+10 -8
View File
@@ -67,11 +67,15 @@ KM_GITEA_URL=https://git.home KM_NAMESPACE=familie KM_CLASSIFICATION=home \
|---|---|---|---|
| spine/core | 20 | 6 s | Erstbefüllung |
| spine/core | 0 gesehen | 0,3 s | inkrementell, nichts geändert |
| 42i/intern | 1165 | ~3 min | Erstbefüllung, ein API-Aufruf je Vorgang für die Kommentare |
| 42i/intern | 1165 | ~3 min | Erstbefüllung, noch ein API-Aufruf je Vorgang für die Kommentare |
| live/live | 2175 | ~2 min | Erstbefüllung mit repo-weitem Kommentar-Endpunkt (ein Aufruf je 50 Kommentare) |
| elf Repos (live, intern, spine/*, hive/*) | 2241 | 94 s | ein Lauf |
Die Erstbefüllung kostet einen Aufruf je Vorgang mit Kommentaren; das ist
die Last auf der Forge, von der #13 spricht. Sie fällt einmal an. Danach
kostet ein Lauf einen Aufruf je Repo plus einen je geändertem Vorgang.
Die erste Fassung holte die Kommentare je Vorgang und blieb bei live/live
nach 371 Vorgängen an einer offenen Verbindung hängen. Seitdem gilt: bei der
Erstbefüllung kommen die Kommentare **repo-weit** (`/issues/comments`, älteste
zuerst), inkrementell je geändertem Vorgang. Dazu ein Verbindungs-Timeout und
Fortschritt je 100 Vorgänge auf stderr.
## Was noch fehlt (Reihenfolge)
@@ -79,10 +83,8 @@ kostet ein Lauf einen Aufruf je Repo plus einen je geändertem Vorgang.
`wake.km` geweckt werden (spine/core#12: Gitea-Webhook → HTTP→Bus-Adapter
→ Bus → km) und dann den inkrementellen Lauf machen. Der periodische
Abgleich bleibt daneben Pflicht.
2. **Suche über die Ablage.** Volltext führend, Vektor ergänzend (spine/core#1).
Der bestehende Indexer `info/ai/scripts/index_docs.py` chunkt Markdown mit
Frontmatter — die Vorgangsdateien sind dafür gebaut. Welche Datenbank(en)
dahinter stehen, ist noch nicht entschieden (Stand 04.09.).
2. ~~Suche über die Ablage.~~ Gebaut: SQLite mit FTS5 und sqlite-vec, siehe
[index-suche.md](index-suche.md).
3. **Verknüpfung mit Doku und Memory.** Die `verweise:` sind erst Kanten
zwischen Vorgängen. Kanten zu Doku-Seiten (ein Ticket, das eine Seite
betrifft) und die Konsistenzprüfung gegen abgelegte Festlegungen kommen