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>
4.1 KiB
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
# 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
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.
unicode61stemmt nicht; „Sperrung" findet „gesperrt" nur über die Vektorsuche. Falls das nicht reicht: Trigramm- Tokenizer, keine neue Datenbank. - Kanten zu Doku-Seiten.
verweisekennt heute nur Vorgang→Vorgang. - Der Dienst. Heute zwei Skripte;
read/search/summaryals HTTP-API und MCP-Endpunkt kommen darüber (Reihenfolge inimage/README.md).