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:
@@ -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`).
|
||||
Reference in New Issue
Block a user