131 lines
5.7 KiB
Markdown
131 lines
5.7 KiB
Markdown
# JARVIS Knowledge Base (Phase 3) - Design
|
|
|
|
**Datum:** 2026-09-12
|
|
**Status:** Approved, bereit fuer Implementierungsplan
|
|
|
|
## Kontext
|
|
|
|
Phase 2 (Claude-Chat-Integration, Postgres-Persistenz, Health-Checks) ist live
|
|
auf dem VPS (72.61.186.98). Die JARVIS-API hat bereits Platzhalter-Endpunkte
|
|
fuer eine Knowledge Base (`POST/GET /api/v1/documents`), die aktuell nur
|
|
TODO-Stubs sind. Diese Spec beschreibt die echte Implementierung.
|
|
|
|
Die urspruengliche Architektur (siehe `JARVIS_HANDOFF.md`) sah Milvus als
|
|
Vector-DB vor. Das dafuer vorbereitete `docker-compose.yml` referenziert
|
|
`milvusdb/milvus:v0.4.0`, ein nicht existierendes Image, und ein veraltetes
|
|
Config-Format (nur etcd, kein MinIO) das mit modernem Milvus-Standalone nicht
|
|
kompatibel ist. Statt das nachzubauen, wird stattdessen `pgvector` in der
|
|
bereits laufenden Postgres-Instanz verwendet - siehe Entscheidung unten.
|
|
|
|
## Entscheidung: pgvector statt Milvus
|
|
|
|
| | pgvector (gewaehlt) | Milvus |
|
|
|---|---|---|
|
|
| Neue Container | keine | etcd, minio, milvus (3) |
|
|
| Betriebsaufwand | keiner (nutzt laufenden Postgres) | hoch (3 zusaetzliche Services) |
|
|
| Konsistenz | Transaktional mit `documents`/`conversations` | separates System, kein 2PC |
|
|
| Skalierung | ausreichend bis mehrere 100k Chunks | besser bei Millionen Vektoren |
|
|
|
|
Fuer eine Business-Knowledge-Base in diesem Umfang ist pgvector die
|
|
pragmatischere Wahl (YAGNI). Milvus bleibt als unbenutzter Platzhalter im
|
|
`docker-compose.yml` dokumentiert, falls spaetere Skalierung es noetig macht.
|
|
|
|
## Embeddings: Ollama (`nomic-embed-text`)
|
|
|
|
Anthropic bietet keine Embedding-API an. Da Ollama bereits im Stack laeuft,
|
|
wird lokal embedded statt ueber einen weiteren externen Anbieter (OpenAI/
|
|
Voyage) mit eigenem Key und laufenden Kosten:
|
|
|
|
- Modell: `nomic-embed-text` (768 Dimensionen), einmalig per
|
|
`docker exec jarvis-ollama ollama pull nomic-embed-text` zu laden.
|
|
- Aufruf ueber Ollama's `/api/embeddings` HTTP-Endpoint (aiohttp, analog zum
|
|
bestehenden Health-Check-Pattern in `main.py`).
|
|
|
|
## Datenmodell
|
|
|
|
Neue Tabelle `document_chunks`:
|
|
|
|
```sql
|
|
CREATE EXTENSION IF NOT EXISTS vector;
|
|
|
|
CREATE TABLE document_chunks (
|
|
id SERIAL PRIMARY KEY,
|
|
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
|
chunk_index INTEGER NOT NULL,
|
|
content TEXT NOT NULL,
|
|
embedding vector(768) NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_document_chunks_document_id ON document_chunks(document_id);
|
|
CREATE INDEX idx_document_chunks_embedding ON document_chunks
|
|
USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
|
|
```
|
|
|
|
Die bestehende `documents`-Tabelle speichert weiterhin Titel/Volltext/
|
|
Metadaten; `embedding_id` (bisher ungenutzt) bleibt vorerst unangetastet, da
|
|
die Beziehung jetzt ueber `document_chunks.document_id` laeuft.
|
|
|
|
## Datenfluss
|
|
|
|
**Upload (`POST /api/v1/documents`):**
|
|
1. Dokument-Zeile in `documents` anlegen (title, content, document_type).
|
|
2. Text in Chunks teilen: ~1000 Zeichen, 100 Zeichen Overlap (einfaches
|
|
Character-Chunking, kein Tokenizer noetig - ausreichend fuer Business-
|
|
Dokumente in diesem Umfang).
|
|
3. Pro Chunk: Embedding via Ollama holen, Zeile in `document_chunks`
|
|
einfuegen.
|
|
4. Response: `document_id`, Anzahl Chunks, Status `indexed`.
|
|
|
|
Passiert synchron im Request (kein Background-Queue-Mechanismus) - bei den
|
|
erwarteten Dokumentgroessen (Business-FAQs, Anleitungen) unkritisch fuer die
|
|
Latenz. Wird das spaeter zum Problem, ist ein Queue-basierter Ansatz ein
|
|
separates Follow-up.
|
|
|
|
**Suche (`GET /api/v1/documents?query=...`):**
|
|
1. Suchanfrage ueber Ollama embedden.
|
|
2. `document_chunks` per Cosine-Distanz (`<=>`-Operator) sortieren, Top N
|
|
(Parameter `limit`, Default 10).
|
|
3. Pro Treffer: Chunk-Inhalt, Distanz-Score, zugehoeriger Dokumenttitel.
|
|
|
|
## Fehlerbehandlung
|
|
|
|
- Ollama nicht erreichbar oder Modell nicht gepullt -> `503` mit klarer
|
|
Fehlermeldung (analog zum bestehenden `CLAUDE_API_KEY`-Check in `chat()`).
|
|
- Leerer/zu kurzer Query-String -> `422` (Pydantic-Validierung).
|
|
- Dokument ohne Inhalt -> `422`.
|
|
|
|
## Testing
|
|
|
|
- Unit-Test fuer die Chunking-Funktion (Grenzfaelle: leerer Text, Text
|
|
kuerzer als Chunk-Groesse, Text mit exaktem Vielfachen der Chunk-Groesse).
|
|
- Manueller End-to-End-Test nach Deployment: Dokument hochladen, danach mit
|
|
einer thematisch passenden Frage suchen und pruefen, dass der richtige
|
|
Chunk als Top-Treffer kommt (analog zum manuellen Chat-Test aus Phase 2,
|
|
da auf dem VPS kein automatisierter Test-Runner etabliert ist).
|
|
|
|
## Nachtrag (gefunden bei der Umsetzung, 2026-09-12)
|
|
|
|
Der in diesem Dokument spezifizierte `ivfflat`-Index auf `document_chunks.embedding`
|
|
hat bei sehr wenig Daten (getestet mit 1 Zeile) eine so geringe Recall-Rate,
|
|
dass `ORDER BY ... LIMIT` Anfragen **keine** Treffer zurückgeben (bestätigt:
|
|
funktioniert erst mit `SET ivfflat.probes = 10`). Genau das kündigt Postgres
|
|
schon beim Anlegen des Index an ("This will cause low recall... Drop the
|
|
index until the table has more data").
|
|
|
|
**Ruling:** Index per Migration `003_drop_low_data_ivfflat_index.sql` wieder
|
|
entfernt. Ein Sequential Scan ist bei der aktuellen Datenmenge korrekt und
|
|
schnell genug. Sobald `document_chunks` eine relevante Groessenordnung an
|
|
Zeilen hat (Richtwert pgvector-Doku: `lists ~= sqrt(row_count)`), sollte der
|
|
Index mit einem zur dann aktuellen Zeilenzahl passenden `lists`-Wert neu
|
|
angelegt werden - das ist ein eigenes Follow-up, keine sofortige Aufgabe.
|
|
|
|
## Out of Scope (bewusst nicht Teil dieser Phase)
|
|
|
|
- RAG-Integration in den Chat-Endpoint (automatisches Anreichern von
|
|
Claude-Antworten mit Suchtreffern) - eigenes Follow-up.
|
|
- Re-Embedding bei Dokument-Update/-Loeschung ausserhalb von `ON DELETE
|
|
CASCADE`.
|
|
- Zugriffskontrolle pro Nutzer (kein Auth-System vorhanden, siehe Phase 3
|
|
in `JARVIS_HANDOFF.md`).
|