# 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`).