# RAG und RAGfind Dokumentzentrierte RAG-Plattform mit Ingestion, hybrider Suche, MCP-Integration, Admin-Werkzeugen und einer separaten lokalen Suchoberfläche namens `RAGfind`. Der Stack ingestiert Uploads, synchronisierte Verzeichnisse, gecrawlte Websites und Git-Repositories, extrahiert und strukturiert deren Inhalte, speichert Embeddings und Metadaten in PostgreSQL, stellt dokumentzentrierte APIs und MCP-Tools bereit und bietet zwei sichtbare Oberflächen: - die Admin- und Betriebskonsole auf Port `3311` - die Endnutzer-Suchoberfläche `RAGfind` auf Port `3312` ## Was Das Projekt Macht Dieses Repository ist für Teams gedacht, die mehr brauchen als reine Vektorsuche. Es kombiniert: - Ingestion für Uploads, lokale Verzeichnisse, Websites und Git-Repositories - OCR-Fallback für gescannte oder schwer extrahierbare Dokumente - hybride Suche über Vektor-, Keyword-, Fuzzy- und dokumentzentrierte Reranking-Signale - persistierte Dokumentstruktur mit Sections und Chunk-zu-Section-Zuordnung - Analyse-Workflows für Aufgaben, Entscheidungen, Fristen, Risiken, Anforderungen, Setup-Schritte, Config-Keys, API-Surfaces und Zusammenfassungen - MCP-Zugriff über HTTP und stdio für Open WebUI und andere MCP-fähige Clients - wissensdatenbankbewusste Admin-Steuerung und principalbasierte Zugriffsskopierung - `RAGfind` als separate Suchoberfläche mit lokalem Multisource-Viewer für HTML, Markdown, Code und Plaintext ## Aktuelle Laufzeitoberflächen ### Admin / API / MCP - URL: `http://localhost:3311` - stellt Operator-UI, Ingestion-Formulare, Dokumentbrowser, Admin-Einstellungen, Dokument-APIs und den MCP-Endpunkt bereit - Basic Auth ist für Admin-Oberfläche und Admin-APIs aktiv - Standard-Login ist `admin` / `admin`, bis es im UI geändert wird ### RAGfind - URL: `http://localhost:3312` - separater Such-Container und eigene Frontend-Oberfläche - der Such-Scope ist im Admin-UI auf Port `3311` konfigurierbar - gesucht wird nur in den für `RAGfind` freigegebenen Wissensdatenbanken - Suchergebnisse öffnen in einem lokalen Multisource-Viewer statt direkt auf externe Seiten zu springen ### MCP - HTTP-Endpunkt: `http://localhost:3311/mcp` - lokaler stdio-Einstieg: `npm run dev:mcp:stdio` oder `npm run start:mcp:stdio` ## Screenshots ### Admin-UI ![Admin-UI](./admin-ui.jpg) ### RAGfind ![RAGfind Suche](./ragfind1.jpg) ![RAGfind Viewer](./ragfind2.jpg) ## Kernfunktionen ### Ingestion - manuelle Uploads - Import-Verzeichnis-Sync über gemounteten Ordner, standardmäßig mit einem Unterverzeichnis je Wissensdatenbank (siehe [Verzeichnis-Sync je Wissensdatenbank](#verzeichnis-sync-je-wissensdatenbank)) - rekursives Website-Crawling mit Download-Unterstützung für Dateien - Git-Repository-Sync mit optionalem Branch- und Subpfad-Scope - Extraktion für PDF, DOCX, ODT, TXT, Markdown, HTML, JSON, YAML, SQL, JS, TS, Python, Shell-Skripte und andere Text-/Code-Formate - OCR-Fallback mit Tesseract und Ghostscript, wenn direkte Extraktion nicht ausreicht - SHA-256-Deduplizierung vor Chunk- und Vektorpersistenz - typabhängiges Chunking: die Ollama-Klassifizierung läuft vor dem Chunking und bestimmt sowohl den gespeicherten Dokumenttyp als auch Chunk-Größe und -Overlap (pro Typ in den Dokumenttyp-Einstellungen überschreibbar, sonst globaler Default) - Embedding läuft vollständig asynchron im Hintergrund: Chunks werden sofort als `pending` persistiert, sind direkt über Volltext-/Trigram-Suche auffindbar und werden von einem separaten Worker eingebettet, sobald Ollama erreichbar ist — die Ingestion-Transaktion hält dabei nie auf einen Ollama-Roundtrip ### Retrieval - semantische Vektorsuche in PostgreSQL plus pgvector - PostgreSQL-Fulltext-Suche - Fuzzy-Matching über Trigram-Indexe - Exact-Match-Booster für Titel, Source-Ref und Inhalt - dokumentzentriertes Reranking und Dokumentfokus-Verfeinerung - Small-to-Big-Kontexterweiterung um starke Treffer herum - Inventarmodus für Anfragen wie "welche Dokumente gibt es" - Suchverbesserungen für Repo- und Entity-lastige MCP- und Open-WebUI-Abfragen ### Dokumentzentrierter Zugriff - Volltextabruf kompletter Dokumente - persistierte Sections und Strukturnavigation - Originaldatei-Metadaten und stabile Download-URLs - Dokumentvergleich und Versionsvergleich - Cross-Reference-Abfragen über mehrere Dokumente hinweg - lokaler Viewer für gecrawlte Websites, Markdown, Code-Dateien und Plaintext ### Analyse - Extraktion von Meeting-Aufgaben - Entscheidungsextraktion - Fristenextraktion - Anforderungsextraktion - Extraktion von Config-Keys - Extraktion von Setup-Schritten - Extraktion von API-Surfaces - Extraktion operativer Hinweise - Risikoextraktion - Entitätenextraktion - Dokument- und Section-Zusammenfassungen ### Admin- und Multi-KB-Steuerung - Knowledge-Base-CRUD im Admin-UI - MCP-Principal-Verwaltung mit KB-Scope - Admin-User-Verwaltung und Passwortwechsel-Flow - editierbare Dokumenttyp-Einstellungen für Heuristik, Klassifikation, Smart Search sowie pro Typ überschreibbare Chunk-Größe und Chunk-Overlap (leer = globaler Default) - konfigurierbarer Knowledge-Base-Scope für `RAGfind` ## Architektur Zentrale Laufzeitkomponenten: - `ingestor-app`: Express-API, Admin-Dashboard, Dokument-APIs, MCP über HTTP - `ingestor-worker`: BullMQ-Worker für Hintergrund-Ingestion und Sync-Jobs - `ragfind`: separater Express-Runtime für die `RAGfind`-Suche und den lokalen Viewer - `rag-db`: PostgreSQL mit pgvector - `redis`: BullMQ-Backend - `elasticsearch`: optionale Hybrid-Suchsignalquelle - externer Ollama-Endpunkt: Embeddings, Zusammenfassungen und Dokumentklassifikation Primärer Ingestion-Flow: 1. Text aus Uploads, Syncs, Crawls oder Git-Inhalten extrahieren 2. bei unzureichender Extraktion auf OCR zurückfallen 3. Inhalte normalisieren und per Ollama klassifizieren — das Klassifikationsergebnis bestimmt sowohl den gespeicherten Dokumenttyp als auch die für diesen Typ konfigurierten (oder globalen) Chunk-Größen-/Overlap-Parameter 4. Inhalte anhand dieser Parameter in Chunks zerlegen und zusammen mit Dokumenten, Sections, Originaldatei-Metadaten und Analyse-Artefakten in PostgreSQL persistieren — Chunks erhalten zunächst `embedding_status = 'pending'` und sind sofort über Volltext- und Trigram-Suche auffindbar 5. Embeddings werden danach asynchron von einem Hintergrund-Worker erzeugt: ein Health-Check wartet auf einen erreichbaren Ollama-Endpunkt, transiente Fehler (Verbindungsabbruch, Timeout, 5xx) werden mit Backoff unbegrenzt wiederholt, permanente Konfigurationsfehler (z. B. Dimension-Mismatch) markieren die betroffenen Chunks sofort als `failed`, statt die Queue zu blockieren 6. Retrieval über HTTP, Admin-UI, MCP und `RAGfind` bereitstellen — der Live-Fortschritt des Embedding-Backlogs ist im Admin-Dashboard sichtbar (siehe „Dashboard und Admin-UI") ## Repository-Struktur ```text src/ config/ Environment-Handling db/ Pool, Migrationen, Startup-Migrationslauf mcp/ MCP-HTTP- und stdio-Einstiege ragfind/ separater RAGfind-Server-Einstieg routes/ HTTP-Endpunkte und gemeinsame Retrieval-Logik services/ Ingestion, Retrieval, OCR, Analyse, Sync, Crawl, Auth utils/ Chunking, Dateien, Hashing, Logging workers/ BullMQ-Worker-Runtime migrations/ PostgreSQL-Schema- und Index-Migrationen public/ Admin-/Operator-Frontend public/ragfind/ RAGfind-Frontend import-dir/ gemountetes Import-Verzeichnis für Sync-basierte Ingestion scripts/ Hilfsskripte für Deployment-Workflows ``` ## Verzeichnis-Sync je Wissensdatenbank Der Verzeichnis-Sync ordnet jedes direkte Unterverzeichnis von `IMPORT_DIR` genau einer Wissensdatenbank zu, damit sich Dokumente verschiedener Wissensräume nicht vermischen. Der Ordnername ist dabei der Slug der Wissensdatenbank: ``` import-dir/ default/ -> Wissensdatenbank "default" vertraege/ -> Wissensdatenbank "vertraege" technik/ -> Wissensdatenbank "technik" ``` Ablauf eines Syncs ohne explizit gewählte Wissensdatenbank: 1. Für jede aktive Wissensdatenbank wird der passende Ordner angelegt, falls er fehlt. 2. Jedes Unterverzeichnis wird rekursiv eingelesen und der Wissensdatenbank mit diesem Slug zugeordnet. 3. Ein Unterverzeichnis ohne passende Wissensdatenbank wird automatisch als neue Wissensdatenbank angelegt (abschaltbar über `SYNC_AUTO_CREATE_KNOWLEDGE_BASES=false`; dann wird der Ordner übersprungen und im Ergebnis gemeldet). 4. Dateien, die direkt im Root-Verzeichnis liegen, landen in `default`. Wird im Admin-UI oder per `POST /api/jobs/sync` eine konkrete `knowledgeBaseId` gesetzt, gilt weiterhin das alte Verhalten: der komplette Baum unterhalb des Root-Verzeichnisses geht in genau diese Wissensdatenbank. Mit `SYNC_KNOWLEDGE_BASE_SUBDIRS=false` lässt sich die Unterverzeichnis-Zuordnung global deaktivieren. Die Deduplizierung greift pro Wissensdatenbank (`content_hash` + `knowledge_base_id`), dieselbe Datei kann also bewusst in mehreren Wissensdatenbanken liegen. Das Job-Ergebnis in der Queue-Ansicht zeigt pro Wissensdatenbank, wie viele Dateien gescannt, importiert und als Duplikat erkannt wurden. ## Crawl-Optionen Beim Anlegen eines Crawls lässt sich getrennt steuern, was neben den Seiten selbst geholt wird: - **Verlinkte Dokumente mitladen** (Standard: an) — PDF, DOCX, ODT, TXT und Markdown werden heruntergeladen und wie ein Upload eingelesen, inklusive Textextraktion und OCR. - **Bilder mitladen (OCR)** (Standard: aus) — jedes gefundene Bild wird per OCR gelesen. Die Trennung hat einen praktischen Grund: früher hingen beide am selben Schalter, weshalb ein Crawl über eine bildlastige Seite jedes Logo und jede Kartenkachel durch Ghostscript und Tesseract schickte. Das dauert um Größenordnungen länger als der restliche Crawl. Bilder lohnen sich, wenn es gescannte Dokumente sind — sonst nicht. Ein abgewählter Dateityp wird auch nicht als Seite abgerufen, nicht nur nicht ingestiert. Ein Bild, in dem OCR keinen Text findet, ist kein Fehlerfall: es wird mit einer kurzen Beschreibung aus Dateiname und Herkunft aufgenommen (`ocrEmpty: true` in den Metadaten), ist über seinen Namen auffindbar und im RAGfind-Viewer als Bild zu sehen. Vorher warf die Ingestion dort `no text extracted`, und weil der Crawl seine Adressen nicht einzeln absicherte, riss diese eine Ausnahme den ganzen Lauf mit — bei einem Crawl in die Tiefe also den Großteil der Arbeit. Jede Adresse läuft jetzt für sich: eine nicht erreichbare Seite, eine Zeitüberschreitung oder eine Datei ohne lesbaren Inhalt wird protokolliert und gezählt, der Crawl macht weiter. Das Ergebnis enthält neben `pages`, `files` und `duplicates` deshalb auch `failed`. Über die API entsprechen die Schalter `downloadDocuments` und `downloadImages` in `POST /api/jobs/crawl`; fehlen sie, gelten dieselben Standardwerte. Für geplante Crawls können sie im Payload des Zeitplans stehen. ### Zugriffsweg: Proxy und Zertifikatsprüfung Beides ist optional und gilt nur für den jeweiligen Crawl — das Gegenstück zu: ```bash curl -v -x http://10.50.174.124:3128 https://lv.intra-ve.p -k ``` Im Admin-UI steht es unter **Zugriffsweg (optional)** im Crawl-Formular, über die API sind es zwei Felder in `POST /api/jobs/crawl`: ```json { "startUrl": "https://lv.intra-ve.p/", "knowledgeBaseId": 1, "proxyUrl": "http://10.50.174.124:3128", "ignoreTlsErrors": true } ``` - `proxyUrl` — HTTP- oder HTTPS-Proxy, über den die Seiten und verlinkten Dateien geholt werden. Das Schema darf fehlen (`10.50.174.124:3128` wird als `http://` gelesen), Zugangsdaten in der Adresse (`http://nutzer:kennwort@proxy:3128`) werden unterstützt. Eine unbrauchbare Adresse weist die API sofort mit `400` ab, statt sie erst im Worker als Crawl-Fehler auffallen zu lassen. - `ignoreTlsErrors` — übergeht die Zertifikatsprüfung für die Anfragen dieses Crawls. Für interne Seiten mit selbst ausgestelltem oder abgelaufenem Zertifikat. Ohne Angabe bleibt alles wie bisher: kein Proxy, Zertifikate werden geprüft. Gesetzt wird nichts global — `NODE_TLS_REJECT_UNAUTHORIZED` bleibt unberührt, und die Nachsicht gilt ausschließlich für die Verbindungen dieses einen Laufs. Geplante Crawls tragen beide Felder im Payload des Zeitplans. Der Weg wird am Dokument vermerkt (`crawlProxyUrl`, `crawlIgnoreTlsErrors` in den Metadaten, **ohne** Zugangsdaten) und von der Archivansicht in RAGfind wiederverwendet: die Stylesheets und Bilder einer gespeicherten Intranetseite werden später über denselben Proxy nachgeladen. Ohne das wäre die Kopie einer internen Seite nur Text, weil der Server die Adressen von sich aus nicht erreicht. Für Dokumente mit Proxy entfällt dabei die Sperre gegen private Adressen im Asset-Proxy — der Abruf läuft ausschließlich über den eingetragenen Proxy, und wer ihn gesetzt hat, hat den Weg in dieses Netz bewusst geöffnet. ## Aktualisieren `./update.sh` bringt eine laufende Installation auf den neuesten Stand, ohne Daten zu verlieren: ```bash ./update.sh # sichern, git pull, neu bauen, starten, pruefen ./update.sh --no-pull # nur neu bauen und starten ./update.sh --skip-backup # ohne Sicherung ``` Die Reihenfolge ist Absicht — erst sichern, dann ziehen. Vor jeder Aenderung landen ein PostgreSQL-Dump, ein Archiv der Originaldateien aus dem `app-data`-Volume und die `.env` unter `backups//`, zusammen mit einer Wiederherstellungs-Anleitung. Schlaegt etwas fehl, bricht das Skript ab, bevor es etwas veraendert hat. `docker compose down -v` kommt bewusst nicht vor: das wuerde die Volumes und damit Datenbank und Originaldateien loeschen. Elasticsearch wird nicht gesichert, weil sich der Index im Admin-UI jederzeit aus PostgreSQL neu aufbauen laesst. Nach dem Update meldet das Skript, welche Schluessel aus `.env.example` in der eigenen `.env` fehlen, und erinnert an Reindex beziehungsweise Re-Embedding, falls eine Aenderung das noetig macht. ## Retrieval-Qualität messen `npm run eval` misst Recall@k, MRR, nDCG@k und Precision@1 gegen ein Goldenset echter Fragen. Der Lauf geht über denselben Suchpfad wie RAGfind und MCP (`executeSmartSearchQuery`), inklusive Reranking und Small-to-Big. ```bash cp eval/goldenset.example.json eval/goldenset.json # oder: npm run eval:bootstrap # Template aus dem eigenen Bestand # Fragen in eval/goldenset.json eintragen npm run eval -- --tag baseline npm run eval -- --tag nachher --baseline eval/results/baseline.json ``` Der Vergleich gegen eine Baseline zeigt pro Frage, was besser und was schlechter wurde — Mittelwerte allein verstecken Regressionen. Details und der geplante Ausbau stehen in `optimierungsplan.md`. `eval/goldenset.json` und `eval/results/` sind bewusst nicht im Repository: sie enthalten Fragen und Trefferlisten aus dem jeweils eigenen Dokumentbestand. ## Deutsche Volltextsuche Neben den `simple`-tsvector-Spalten existieren deutsche Varianten (`*_tsv_de`, Migration 021) mit Snowball-Stemming und deutschen Stoppwörtern, damit „Dienstpläne" auch „Dienstplan" findet. Beide werden abgefragt, der bessere Rang gewinnt — `simple` bleibt zuständig für exakte Treffer auf Dateinamen, Aktenzeichen und Bezeichner. Elasticsearch spiegelt das über ein `.de`-Unterfeld mit dem Analyzer `german_rag`. Die Elasticsearch-Indizes tragen deshalb ein Versionssuffix (`rag-documents-v2`, `rag-chunks-v2`). Nach einem Deploy einmal *Reindex starten* im Admin-UI ausführen; die alten Indizes ohne Suffix können anschließend gelöscht werden. ## Cross-Encoder-Reranking Die besten Kandidaten einer Suche werden optional von einem Reranking-Modell nachsortiert, das Frage und Passage gemeinsam bewertet. Konfiguriert wird das im Admin-UI unter *Config-AI*: Basis-URL, Modellname und wie viele Kandidaten (`top_n`) an den Reranker gehen. Die Kosten wachsen linear mit der Kandidatenzahl (gemessen mit Qwen3-Reranker-0.6B: 8 Kandidaten ~740 ms, 12 ~1090 ms, 20 ~1800 ms), der Default liegt deshalb bei 12. Unterstützt wird jeder Server mit `POST /v1/rerank` (Fallback `/rerank`) im Cohere/Jina-Schema — llama.cpp mit `--reranking`, TEI, Infinity und vLLM. Empfehlung für deutschen Bestand: `bge-reranker-v2-m3` oder `Qwen3-Reranker-0.6B`. Der Reranker sitzt im Live-Suchpfad und ist deshalb abgesichert: harter Timeout, begrenzte Parallelität und 30 Sekunden Cooldown nach einem Fehler. Ist er aus, nicht erreichbar oder im Cooldown, greift automatisch das bisherige heuristische Reranking — die Suche liefert immer Ergebnisse, im Zweifel nur schlechter sortierte. `crossEncoderRerank` im Debug-Log und die `rerankMs`-Stufenzeit zeigen pro Anfrage, welcher Weg genommen wurde. ## Dokumentansicht in RAGfind Ein Suchtreffer öffnet das Dokument so, wie man es erwartet — nicht als Rohtext und nie als erzwungener Download. Welche Darstellung das ist, entscheidet `src/ragfind/viewerModel.ts` anhand von Dateityp, MIME-Typ und Herkunft: | Dokument | Ansicht | | --- | --- | | PDF | eingebetteter Betrachter auf Basis von pdf.js, mit Seitennavigation, Zoom und Volltextsuche im Dokument | | DOCX, ODT, PPTX, XLSX, RTF, CSV | von LibreOffice erzeugte PDF-Fassung, dann derselbe Betrachter | | gecrawlte Seite | die gespeicherte HTML-Kopie, mit Stylesheets, Bildern und Schriften | | Bild | das Bild selbst, der erkannte Text daneben | | Audio, Video | Abspieler | | Markdown | gerendert | | Quelltext | mit Syntaxhervorhebung | | sonstiges | extrahierter Text, notfalls eine Karte mit Dateiangaben und Download | Die Tabs richten sich nach dem, was es zu einem Dokument gibt: **Ansicht**, **Text** (die extrahierte Fassung mit hervorgehobenen Suchbegriffen und Sprung von Treffer zu Treffer), **Abschnitte** beziehungsweise **Inhalt** (bei PDFs mit eigener Gliederung deren Lesezeichen) und **Details**. Die Suchanfrage wird an die Ansicht weitergereicht: im PDF sucht der Betrachter sie selbst und springt zur ersten Fundstelle, im Text sind die Begriffe markiert. Ein Klick auf einen Textausschnitt in der Trefferliste springt an genau diese Stelle. pdf.js wird nicht ins Repository kopiert, sondern zur Laufzeit aus `node_modules/pdfjs-dist` unter `/pdfjs` ausgeliefert. PDF-Fassungen und Vorschaubilder liegen als abgeleitete Dateien neben dem Original unter `.derived/`; sie entstehen beim ersten Abruf und werden ungültig, sobald das Original neuer ist. ### Gespeicherte Webseiten Die Kopie wird über `/view//page` in einem Rahmen geladen. Beim Ausliefern werden alle Verweise auf Stylesheets, Bilder, Schriften und Medien auf `/view//asset?u=…` umgebogen; dieser Endpunkt holt die Datei serverseitig und liefert sie unter der Herkunft von RAGfind aus. Der Umweg ist notwendig, nicht bequem: viele Seiten senden auf ihren eigenen Dateien `Cross-Origin-Resource-Policy: same-site`. Der Browser blockiert sie damit für jedes fremde Dokument, ganz gleich wie großzügig dessen Content-Security-Policy ist — die Kopie erschien vorher als nackter Text. Der Proxy fasst nur öffentliche Adressen an (private und lokale Bereiche werden nach Namensauflösung abgewiesen), lässt nur Stylesheets, Bilder, Schriften und Medien durch und schreibt in Stylesheets auch die dort enthaltenen Adressen um. Links auf andere Seiten werden absolut gemacht und zeigen weiterhin auf das Original. Skripte bleiben außen vor: sie werden beim Speichern entfernt, die Content-Security-Policy des Endpunkts verbietet sie, und der Rahmen ist zusätzlich sandboxed. Seiten, die vor der Einführung dieser Speicherung gecrawlt wurden, haben keine HTML-Kopie und erscheinen als Text. Ein erneuter Crawl derselben Adresse trägt die Kopie nachträglich nach, auch wenn der Text unverändert ist. ## Trefferliste in RAGfind Die Liste zeigt je Dokument ein Vorschaubild (erste Seite bei PDF und Office, das Bild selbst bei Bildern), Dokumenttyp, Wissensdatenbank, Dateigröße und Datum, die Zusammenfassung aus der Klassifizierung und die Fundstellen im Text. Rechts stehen Filter für Dokumenttyp, Wissensdatenbank und Dateityp sowie die Sortierung nach Relevanz oder Datum; beides wirkt sofort und steht in der Adresse, ist also teilbar. Weitere Treffer kommen über einen Knopf dazu, statt die Liste auf zwölf Einträge zu beschneiden. Während der Suche steht ein Platzhalter statt einer leeren Seite — eine Anfrage kostet je nach Ollama-Antwortzeit mehrere Sekunden. Ergebnisse werden serverseitig fünf Minuten und im Browser für die Sitzung vorgehalten: Wer einen Treffer öffnet und zurückgeht, sieht die Liste sofort wieder. Ohne Treffer schlägt die Suche über Trigram-Ähnlichkeit ein anderes Wort vor. `/` springt ins Suchfeld, Pfeiltasten wählen einen Treffer, Enter öffnet ihn. ## Embedding-Input Embeddet wird nicht der rohe Chunk, sondern ein Kontext-Header aus Dokumenttitel und Abschnittsüberschrift plus der Chunk-Inhalt, versehen mit dem Task-Prefix des jeweiligen Modells (`search_document:` / `search_query:` bei nomic, `passage:` / `query:` bei e5 — automatisch aus dem Modellnamen abgeleitet, überschreibbar via `EMBEDDING_DOCUMENT_PREFIX` / `EMBEDDING_QUERY_PREFIX`). Der Header steht ausschließlich im Embedding-Input, nie in `document_chunks.content`. `document_chunks.embedding_input_version` hält fest, mit welcher Input-Form ein Vektor entstanden ist. Ändert sich die Form ohne Modellwechsel, findet der normale Re-Embedding-Lauf nichts — dafür gibt es *Re-Embedding starten* im Bereich Config-AI (entspricht `POST /admin/embeddings/reembed` mit `{"force": true}`). ## Betrieb mit vLLM vLLM spricht dieselbe OpenAI-kompatible API wie llama.cpp, LM Studio oder TGI. Im Admin-UI unter *Config-AI* deshalb Provider **OpenAI-kompatibel** wählen, als Basis-URL den `/v1`-Pfad eintragen (z. B. `http://host:8000/v1`) und das API-Key-Feld leer lassen — es ist optional und wird nur gebraucht, wenn der Server mit `--api-key` gestartet wurde. Die drei Rollen können auf getrennten vLLM-Instanzen laufen; Embedding und LLM teilen sich die Provider-Konfiguration, der Reranker hat eine eigene Basis-URL. ```bash # Embeddings vllm serve nomic-ai/nomic-embed-text-v1.5 \ --served-model-name nomic-embed-text --trust-remote-code \ --port 8000 --max-model-len 2048 # Reranker (Cross-Encoder) vllm serve BAAI/bge-reranker-v2-m3 \ --served-model-name bge-reranker-v2-m3 --port 8001 # LLM für Klassifikation und Zusammenfassung vllm serve Qwen/Qwen2.5-7B-Instruct --port 8002 ``` Die Flags zur Modellrolle heißen je nach vLLM-Version unterschiedlich (`--task embed` / `--task score` in älteren, `--runner pooling` in neueren Versionen). Statt sich darauf zu verlassen, lässt sich ein Endpunkt direkt prüfen: ```bash npm run check:provider -- \ --base-url http://host:8000/v1 \ --embedding-model nomic-embed-text \ --llm-model Qwen/Qwen2.5-7B-Instruct \ --reranker-url http://host:8001 \ --reranker-model bge-reranker-v2-m3 ``` Das Werkzeug fährt genau die Codepfade der Anwendung gegen den Endpunkt — Modell-Liste, Einzel- und Batch-Embedding, Dimension, Task-Prefixes, maximale Eingabelänge, Textgenerierung, JSON-Antwort für die Klassifikation und den Rerank-Endpunkt — und gibt am Ende die Werte aus, die in *Config-AI* einzutragen sind. Es braucht keine Datenbank und ändert nichts auf dem Server. Worauf zu achten ist: - **Eingabelänge:** Bei vLLM begrenzt `--max-model-len` eine einzelne Eingabe. Der Checker misst das Limit und sagt, ob es für die Chunk-Größen dieser Anwendung reicht. - **Embedding-Dimension:** Weicht sie vom Bestand ab, ist vor dem Umschalten `ALTER TABLE document_chunks ALTER COLUMN embedding TYPE VECTOR(n)` nötig, danach ein vollständiges Re-Embedding. Das Admin-UI prüft die Dimension beim Speichern und lehnt einen Konflikt ab. - **Task-Prefixes** werden aus dem Modellnamen abgeleitet. vLLM meldet oft den vollen HF-Pfad (`nomic-ai/nomic-embed-text-v1.5`), was erkannt wird; bei exotischen Namen helfen `EMBEDDING_DOCUMENT_PREFIX` / `EMBEDDING_QUERY_PREFIX`. - **Erste Inferenz schlägt fehl, Modell-Liste funktioniert:** Meldet der Server `Failed to find C compiler` oder verweist auf `triton.knobs.build.impl`, fehlt im vLLM-Container ein C-Compiler. vLLM kompiliert Triton-Kernel beim ersten echten Aufruf; `/v1/models` läuft deshalb, `/v1/embeddings` nicht. Abhilfe: `gcc` bzw. `build-essential` im vLLM-Image installieren oder `CC` auf einen vorhandenen Compiler setzen. Ein solcher Fehler wird jetzt als `AI provider at ... answered HTTP 500: ...` gemeldet — der Server war also erreichbar, die Ursache liegt bei ihm. - **Strukturierte Antworten:** Lehnt ein Server `response_format: json_object` ab, wird die Anfrage automatisch ohne dieses Feld wiederholt. Die Klassifikation funktioniert dadurch auch auf Servern ohne Guided Decoding. ## Eingabelimits des Modellservers Läuft das Embedding- oder Reranking-Modell auf llama.cpp, begrenzt dessen physische Batchgröße (`ubatch`, Default 512 Token) die Länge einer einzelnen Eingabe. Eine längere Eingabe wird mit HTTP 500 abgelehnt und würde ohne Gegenmaßnahme den ganzen Batch scheitern lassen. `CHUNK_SIZE` schützt davor nicht: es zählt mit `gpt-tokenizer`, während deutsche Texte im Modell-Tokenizer etwa doppelt so dicht kodieren — 300 „Chunk-Token" können 600 Modell-Token sein, OCR-Tabellen mit zusammengelaufenen Wörtern noch mehr. Der Stack geht damit zweistufig um: 1. `EMBEDDING_MAX_INPUT_CHARS` bzw. `RERANKER_DOCUMENT_MAX_CHARS` kappen die Eingabe vorab. 2. Lehnt der Server sie trotzdem ab, wird der Batch aufgeteilt, der auffällige Eintrag schrittweise gekürzt und erneut geschickt. Das ist unabhängig vom Tokenizer und funktioniert deshalb auch nach einem Modellwechsel. Sauberer ist es, den Server mit größerem `ubatch` zu starten (`llama-server -ub 2048 -b 2048`). Dann greifen die Kürzungen nie, und Reranking sowie Embedding sehen den vollständigen Text. ## Anforderungen - Node.js `22.23.4+`, `24.22.1+` oder `26.12.1+` – die Container laufen auf Node 24. Ältere Linien enthalten den fehlerhaften Brotli-Dekoder (CVE-2026-111497) und bekommen keinen Fix mehr; Crawl und Archiv-Abruf packen Antworten fremder Server aus und würden ihn erreichen. Jeder Dienst schreibt beim Start eine Warnung, wenn er auf einer solchen Laufzeit läuft. - PostgreSQL mit pgvector - Redis - externer Ollama-Endpunkt - Docker und Docker Compose für den einfachsten lokalen Betrieb - optionale OCR-Abhängigkeiten für gescannte Inhalte ## Schnellstart Mit Docker Compose 1. Environment-Vorlage kopieren. ```bash cp .env.example .env ``` 2. Mindestens diese Werte anpassen: - `OLLAMA_BASE_URL` (Seed-Wert; die laufende KI-Provider-Konfiguration wird danach im Admin-UI unter „Config-AI" verwaltet) - optional `PUBLIC_BASE_URL` 3. Gesamten Stack bauen und starten. ```bash docker compose up --build ``` 4. Admin-Konsole unter `http://localhost:3311` öffnen. 5. `RAGfind` unter `http://localhost:3312` öffnen. Der Standard-Compose-Stack startet: - Admin/API/MCP auf `3311` - `RAGfind` auf `3312` - PostgreSQL auf Host-Port `5433` - Redis auf Host-Port `6379` - Elasticsearch auf Host-Port `9200` ## Lokale Entwicklung 1. Abhängigkeiten installieren. ```bash npm install ``` 2. Environment-Datei kopieren und anpassen. ```bash cp .env.example .env ``` 3. PostgreSQL, Redis, optional Elasticsearch und den Ollama-Endpunkt starten. 4. Migrationen ausführen. ```bash npm run migrate ``` 5. API, Worker und optional `RAGfind` in getrennten Terminals starten. ```bash npm run dev ``` ```bash npm run dev:worker ``` ```bash npm run dev:ragfind ``` ## Verfügbare Skripte ```bash npm run dev # API im Watch-Modus starten npm run dev:worker # BullMQ-Worker im Watch-Modus starten npm run dev:ragfind # RAGfind-Server im Watch-Modus starten npm run dev:mcp:stdio # MCP-Server über stdio im Watch-Modus starten npm run build # TypeScript kompilieren npm run start # kompilierte API starten npm run start:worker # kompilierten Worker starten npm run start:ragfind # kompilierten RAGfind-Server starten npm run start:mcp:stdio # kompilierten MCP-stdio-Server starten npm run migrate # SQL-Migrationen ausführen ``` ## Wichtige Environment-Variablen Kernservices: - `PORT`: Admin/API-Port, Standard `3311` - `DATABASE_URL`: PostgreSQL-Connection-String - `REDIS_URL`: Redis-Connection-String - `PUBLIC_BASE_URL`: Basis für erzeugte Download-Links und externe Referenzen LLM und Embeddings (nur Seed-Werte für die Erstinstallation - die dauerhafte Konfiguration erfolgt danach im Admin-UI unter „Config-AI", siehe unten): - `OLLAMA_BASE_URL` - `EMBEDDING_MODEL` - `LLM_MODEL` - `DOCUMENT_CLASSIFIER_MODEL` - `EMBEDDING_DIMENSION` Speicherung und Ingestion: - `IMPORT_DIR` - `UPLOAD_DIR` - `ORIGINAL_STORAGE_DIR` - `GIT_REPO_CACHE_DIR` - `GIT_REPO_MAX_FILE_BYTES` - `CRAWL_DEFAULT_MAX_DEPTH` Retrieval-Tuning: - `QUERY_TOP_K` - `QUERY_CANDIDATE_K` - `QUERY_MAX_CHUNKS_PER_DOCUMENT` - `QUERY_VECTOR_WEIGHT` - `QUERY_KEYWORD_WEIGHT` - `QUERY_EXACT_MATCH_BOOST` - `QUERY_RERANK_TOP_N` - `QUERY_SMALL_TO_BIG_WINDOW` Suchschicht-Integration: - `ELASTICSEARCH_URL` - `ELASTICSEARCH_INDEX_PREFIX` Die aktuellen Defaults stehen in `.env.example`. ## Dashboard und Admin-UI Die Admin-Konsole auf Port `3311` ist über ein Navigationsmenü mit sieben Bereichen strukturiert (Hash-Routing, also direkt verlinkbar und mit Vor-/Zurück-Navigation des Browsers nutzbar): - **Übersicht** — Stats, System-Health (Ollama/Elasticsearch/Postgres) und Live-Fortschritt der asynchronen Embedding-Pipeline (Fortschrittsbalken „X / Y Chunks eingebettet" inkl. Hinweis auf fehlgeschlagene Chunks, sobald `failed > 0`) - **Ingestion** — Upload-, Crawl-, Directory-Sync-, Schedule- und Git-Import-Formulare, Queue-Jobs und Embedding-Fortschritt - **Dokumente** — Dokumentbrowser mit Vorschau, Filterung, Analyse-Werkbank und Unterstützung für Dokument-Reklassifikation - **Suche** — RAG-Query-Test gegen den Such-Stack - **Wissensbasis & Typen** — Knowledge-Base-Verwaltung, Dokumenttyp-Einstellungen (inkl. typabhängiger Chunk-Größe/-Overlap) und `RAGfind`-KB-Auswahl - **System** — MCP-Principal-Verwaltung, Admin-User-Verwaltung, Passwortänderung, Elasticsearch-Operationen, Git-Repository-Import-Status und Laufzeitkonfiguration - **Config-AI** — Konfiguration des KI-Providers (Ollama oder eine OpenAI-kompatible API): Server-URL, optionaler API-Key sowie je ein per Dropdown aus den auf dem Server tatsächlich verfügbaren Modellen wählbares Modell für Embedding, Zusammenfassung und Dokumentklassifizierung. Änderungen wirken sofort, ohne Neustart — ein Wechsel des Embedding-Modells wird beim Speichern per Testaufruf auf Dimensionskompatibilität geprüft und bei Konflikt mit einer verständlichen Fehlermeldung abgelehnt (siehe `EMBEDDING_DIMENSION` oben) Die Admin-Konsole ist die Stelle, an der der Such-Scope für `RAGfind` konfiguriert wird. Der Embedding-Fortschritt wird per Live-Polling (`GET /api/admin/embeddings/pending-status`) aktualisiert: solange Chunks noch `pending` oder `failed` sind, fragt die Oberfläche den Status alle 5 Sekunden ab und blendet die Anzeige aus, sobald alles eingebettet ist. ## RAGfind `RAGfind` ist ein separater Container und ein separates Frontend für die Endnutzer-Dokumentsuche. Aktuelles Verhalten: - sucht nur in den für `RAGfind` aktivierten Wissensdatenbanken - gruppiert Chunk-Treffer zu dokumentzentrierten Ergebnissen - zieht bei Bedarf direkte Titel- und Source-Ref-Treffer als Ergänzung nach - öffnet immer einen lokalen Viewer statt gecrawlte Seiten direkt auf der Live-Website aufzurufen - bietet einen Multisource-Viewer mit gerendertem HTML, gerendertem Markdown, syntaxhervorgehobenem Code und einem Plaintext-Tab ## Open-WebUI-Integration Open WebUI sollte nur über MCP angebunden werden. Empfohlener Endpunkt: ```text http://localhost:3311/mcp ``` Es gibt in diesem Repository keine mitverwalteten Open-WebUI-Python-Filter-, Tool- oder Action-Dateien mehr. ## MCP-Unterstützung Der Service stellt MCP in zwei Modi bereit. ### Streamable HTTP MCP Endpunkt: ```text http://localhost:3311/mcp ``` ### Lokales stdio-MCP Entwicklung: ```bash npm run dev:mcp:stdio ``` Produktions-Build: ```bash npm run build npm run start:mcp:stdio ``` ### MCP-Tool-Kategorien Verfügbare Tools decken ab: - Retrieval und Smart Search - Dokumentlisten und Dokument-Lookups - Volltext-, Section- und Strukturzugriff - Originaldatei-Metadaten - Dokumentanalysen und Zusammenfassungen - Dokumentvergleiche und Cross-Reference-Workflows ## opencode-MCP Neben dem allgemeinen MCP-Endpunkt gibt es einen zweiten, zugeschnitten auf einen Coding-Agenten: ```text http://localhost:3311/mcp/opencode ``` Der Grund ist nicht Geschmack, sondern Trefferquote. Der allgemeine Server bietet 25 Werkzeuge an — Fristen, Meeting-Aufgaben, Dokumentvergleich, Versionsvergleich. Für einen Coding-Agenten sind davon höchstens fünf brauchbar, der Rest belegt Kontext und verschlechtert die Werkzeugauswahl. Dieser Endpunkt bietet sieben an und teilt sich mit dem ersten alles darunter: Authentifizierung, Wissensdatenbank-Scope, Retrieval-Pipeline und Ingestion. ### Die Werkzeuge | Werkzeug | Zweck | | --- | --- | | `search_knowledge` | Hybride Suche über die zugeordneten Wissensdatenbanken. `kind` filtert auf `code`, `doc`, `note` oder `file`, `pathPrefix` auf den Pfadanfang. | | `get_file` | Eine Datei oder ein Dokument im Wortlaut, wahlweise als Zeilenbereich (`startLine`, `endLine`). Die Ausgabe ist durchnummeriert. | | `list_sources` | Inventar zum Navigieren: welche Dateien, Dokumente und Notizen es überhaupt gibt. | | `get_note` | Eine eigene Notiz im Wortlaut — Voraussetzung dafür, sie gezielt fortzuschreiben statt eine zweite anzulegen. | | `save_note` | Wissen unter einem stabilen Schlüssel ablegen. Derselbe Schlüssel **ersetzt** die vorherige Fassung. | | `save_file` | Eine Quelltextdatei unter ihrem Pfad ablegen; derselbe Pfad ersetzt den vorherigen Inhalt. | | `forget` | Eine eigene Notiz oder Datei löschen. Fasst ausschließlich Selbstgeschriebenes an, nie gecrawlte, hochgeladene oder per Git synchronisierte Dokumente. | ### Zugang und Schreibrecht Der Endpunkt verlangt **immer** ein MCP-Token — anders als `/mcp`, der ohne angelegte Zugänge jeden durchlässt. Was gelesen werden darf, bestimmt wie gehabt die Wissensdatenbank-Zuordnung des Zugangs. Schreiben ist ein eigenes Recht: **Schreiben erlaubt (opencode-MCP)** im Formular der MCP-Zugänge. Standard ist aus, auch nach einem Update bestehender Installationen — ein vorhandener Lesezugang bekommt dadurch kein Schreibrecht. Ohne das Recht antworten die drei Schreibwerkzeuge mit einer Ablehnung statt mit einem Fehler. ### Einstellungen Das Arbeitsmodell von opencode steht nicht im Code, sondern im Admin-UI unter **opencode-MCP** — dieselbe Installation läuft so in einer anderen Landschaft mit einem anderen Modell, ohne dass jemand Konstanten anfasst: - **Arbeitsmodell** und **Kontextfenster** — das Fenster belegt die Budgets vor (etwa vier Zeichen je Token, davon ein Sechzehntel für eine einzelne Antwort, höchstens 24 000 Zeichen). - **Antwortbudget** und **harter Deckel** — jede Antwort wird darauf gekürzt, und die Kürzung wird im Text sichtbar gemacht. Ein Agent, der nicht sieht, dass etwas fehlt, zieht aus einer halben Datei falsche Schlüsse. - **Treffer je Suche** und **Höchstzahl**. - **Ziel-Wissensdatenbank zum Schreiben** — bei genau einer zugeordneten entfällt die Angabe. Ein Eintrag für einen bestimmten Zugang übersteuert die Standardwerte vollständig. So bedient eine Installation mehrere opencode-Landschaften mit verschiedenen Modellen. ### Was beim Schreiben passiert - **Stabiler Schlüssel statt Anhäufen.** `save_note` schreibt nach `opencode://note/`, `save_file` nach `opencode://file/`. Eine vorhandene Fassung wird vollständig ersetzt, nicht danebengelegt — sonst widersprechen sich nach drei Überarbeitungen drei Dokumente. - **Keine Klassifizierung.** Der Dokumenttyp kommt vom Werkzeug. Der sonst übliche LLM-Roundtrip vor dem Chunking dauert je nach Provider bis zur Zeitüberschreitung; ein Schreibwerkzeug, das darauf wartet, benutzt kein Agent zweimal. Schreibvorgänge liegen damit im Bereich von Zehntelsekunden. - **Sofort auffindbar.** Chunks werden als `pending` gespeichert und sind unmittelbar über die Volltextsuche zu finden; die Vektoren zieht der Worker nach, sobald das Embedding-Modell erreichbar ist. Auch bei nicht erreichbarem Embedding bleibt Schreiben und Suchen also nutzbar. - **Herkunft bleibt sichtbar.** Jedes Dokument trägt `author`, `principal`, `writtenAt` und bei Ersetzung `supersededDocumentId` in den Metadaten. Weil Gelerntes in derselben Wissensdatenbank liegen darf wie der Code, ist der Herkunftstyp die Trennlinie: Das Admin-UI listet unter **opencode-MCP** alles auf, was der Agent selbst angelegt hat, mit Löschknopf je Eintrag. - **Mögliche Zugangsdaten werden markiert, nicht abgelehnt.** Private Keys, AWS- und GitHub-Tokens, JWTs, Zugangsdaten in URLs und Zuweisungen wie `password = …` setzen `containsSecrets` in den Metadaten, erzeugen einen Protokolleintrag und eine Warnung in der Antwort an den Agenten. Der Inhalt wird gespeichert — ein falscher Treffer soll kein Wissen verhindern — bleibt aber im Admin-UI auffindbar und gezielt löschbar. ### opencode einrichten In der `opencode.json` (Projekt oder `~/.config/opencode/`): ```json { "mcp": { "rag": { "type": "remote", "url": "http://localhost:3311/mcp/opencode", "enabled": true, "headers": { "Authorization": "Bearer mcp_dein-token" } } } } ``` Das Token stammt aus **MCP-Zugänge** im Admin-UI und wird genau einmal angezeigt. Für Schreibzugriff muss am selben Zugang **Schreiben erlaubt** gesetzt sein. ## Wichtige HTTP-API-Endpunkte ### Retrieval - `POST /api/smart-search` - `POST /api/cross-reference` ### Dokumente - `GET /api/documents` - `GET /api/documents/:id` - `GET /api/documents/:id/fulltext` - `GET /api/documents/:id/sections` - `GET /api/documents/:id/structure` - `GET /api/documents/:id/section` - `GET /api/documents/:id/original/meta` - `GET /api/documents/:id/original` ### Analyse - `GET /api/documents/:id/analysis/actions` - `GET /api/documents/:id/analysis/decisions` - `GET /api/documents/:id/analysis/deadlines` - `GET /api/documents/:id/analysis/requirements` - `GET /api/documents/:id/analysis/config-keys` - `GET /api/documents/:id/analysis/setup-steps` - `GET /api/documents/:id/analysis/api-surface` - `GET /api/documents/:id/analysis/operational-notes` - `GET /api/documents/:id/analysis/risks` - `GET /api/documents/:id/analysis/entities` - `GET /api/documents/:id/summary` - `GET /api/documents/:id/section-summary` - `GET /api/documents/:id/compare` - `GET /api/documents/:id/compare-version` ## Hinweise Zu Crawl und Git - Website-Crawling folgt same-site Links und herunterladbaren Dateien - weitergeleitete Domains wie `bmetallica.de -> www.bmetallica.de` werden über den Redirect-Ursprung hinweg korrekt gecrawlt - Git-Ingestion unterstützt optionalen Branch- und Subpfad-Scope und indexiert gängige Text- und Code-Formate ## GitHub-Repository-Vorbereitung Dieses Repository ist für die Veröffentlichung auf GitHub vorbereitet mit: - einer repositorytauglichen README - einer MIT-Lizenz - `.gitignore` für Node, Build, lokale Envs und Import-Artefakte - Anleitungen für containerbasierten und lokalen Betrieb - einer klaren Trennung zwischen Admin-Oberfläche und `RAGfind` ## Lizenz Dieses Projekt steht unter der MIT-Lizenz. Siehe `LICENSE`. ## Roadmap und Design-Notizen Für tiefere Produkt- und Retrieval-Notizen siehe: - `ROADMAP.md` - `rag-logik.md` ## Status Das Repository ist weiterhin in aktiver Entwicklung, die aktuelle Implementierung enthält aber bereits: - Multi-Source-Ingestion - typabhängiges Chunking auf Basis der vorgezogenen Ollama-Klassifizierung - vollständig asynchrone Embedding-Pipeline mit Health-Check, fehlerklassifiziertem Retry und Live-Fortschrittsanzeige im Dashboard - persistierte Struktur- und Originaldatei-Referenzen - Analyse- und Summary-Workflows - MCP-Integration - wissensdatenbankbewusste Admin-Konfiguration - Navigationsbasierte Admin-Oberfläche mit sieben klar getrennten Bereichen - separate `RAGfind`-Sucherfahrung mit lokalem Viewer