<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>BM25 |</title><link>https://aretascodes.dev/de/tags/bm25/</link><atom:link href="https://aretascodes.dev/de/tags/bm25/index.xml" rel="self" type="application/rss+xml"/><description>BM25</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>de-DE</language><lastBuildDate>Fri, 12 Jun 2026 00:00:00 +0000</lastBuildDate><image><url>https://aretascodes.dev/media/icon_hu_2ab4f4763b27c75b.png</url><title>BM25</title><link>https://aretascodes.dev/de/tags/bm25/</link></image><item><title>CogniVault Backend erklärt, Teil 3 · Wie aus einer Frage eine belegte Antwort wird</title><link>https://aretascodes.dev/de/blog/backend-explained-rag-agent/</link><pubDate>Fri, 12 Jun 2026 00:00:00 +0000</pubDate><guid>https://aretascodes.dev/de/blog/backend-explained-rag-agent/</guid><description>
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;Alle Abkürzungen werden im Anhang am Ende der Seite vollständig erklärt.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Du tippst eine Frage ein. Ein paar Sekunden später bekommst du eine Antwort mit Fußnoten — genaue Angabe der Dokumente und Seiten, aus denen sie stammt. Dieser Teil geht alles durch, was dazwischen passiert.&lt;/p&gt;
&lt;p&gt;In
haben wir die Wissensbasis aufgebaut: jedes Dokument gechunkt, embedded und indiziert. Jetzt fangen wir an, sie zu &lt;em&gt;nutzen&lt;/em&gt; — und hier hört CogniVault auf, nur eine Pipeline zu sein, und fängt an, spannend zu werden.&lt;/p&gt;
&lt;h2 id="zwei-bibliothekare-weil-einer-dich-immer-wieder-hängen-lässt"&gt;Zwei Bibliothekare, weil einer dich immer wieder hängen lässt&lt;/h2&gt;
&lt;p&gt;Stell dir eine Bibliothek vor mit einer Bibliothekarin, die alles nach &lt;em&gt;Vibes&lt;/em&gt; ordnet. Frag sie nach &amp;ldquo;Prozeduren bei Server-Ausfall&amp;rdquo; und sie ist genial — sie versteht, was du meinst, und findet Dokumente, die das Konzept diskutieren, egal welche Wörter sie benutzen. Aber frag sie nach &amp;ldquo;Fehlercode 404B&amp;rdquo;, zuckt sie mit den Schultern und reicht dir allgemeine Netzwerk-Guides. Mit exakten Zeichenketten kann sie nichts anfangen.&lt;/p&gt;
&lt;p&gt;Am Ende des Flurs sitzt ein zweiter Bibliothekar mit einem Zettelkasten. Er findet den genauen String &amp;ldquo;404B&amp;rdquo; sofort — aber stell ihm eine konzeptionelle Frage, die anders formuliert ist als im Quelltext, und er findet überhaupt nichts.&lt;/p&gt;
&lt;p&gt;Das sind die zwei Hälften der Suche:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Semantische Suche (FAISS)&lt;/strong&gt; — deine Frage wird in einen Vektor umgewandelt (embedded), und der Index findet Chunks, deren Vektoren in die gleiche Richtung zeigen (technisch gesehen: Cosinus-Ähnlichkeit — wie gut zwei Pfeile übereinstimmen). Super für die Bedeutung, blind für exakte Identifikatoren.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keyword-Suche (BM25)&lt;/strong&gt; — eine Bewertungsformel (Scoring), die Chunks belohnt, die deine &lt;em&gt;exakten&lt;/em&gt; Wörter enthalten, gewichtet danach, wie markant diese Wörter sind. Super für Identifikatoren, blind für Synonyme.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CogniVault fragt &lt;strong&gt;jedes Mal beide Bibliothekare&lt;/strong&gt;, und verschmilzt dann ihre Antworten mit &lt;strong&gt;Reciprocal Rank Fusion (RRF)&lt;/strong&gt; — einer Formel, die gerankte Listen kombiniert, indem sie nur die &lt;em&gt;Positionen&lt;/em&gt; nutzt:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;score(chunk) = summe aus beiden Listen von 1 / (60 + rang)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ein Chunk, der von einem der beiden Bibliothekare hoch gerankt wird, punktet gut; ein Chunk, den beide gut fanden, schwimmt ganz nach oben. Die Eleganz liegt darin, was &lt;em&gt;fehlt&lt;/em&gt;: Du musst niemals die Ähnlichkeits-Scores von FAISS mit der komplett anderen Skala von BM25 abgleichen, weil Ränge (Ranks) der einzige Input sind. Die Konstante 60 stammt direkt aus dem ursprünglichen Research-Paper von 2009, und ja, sie ist auch im Code zitiert.&lt;/p&gt;
&lt;p&gt;Ein paar Implementierungsdetails, die du kennen solltest: Beide Suchen holen absichtlich zu viel (mindestens 20 Kandidaten jeweils), damit die Fusion Material zum Arbeiten hat; sehr schwache semantische Treffer werden fallengelassen, aber ein perfekt auf Keywords passender Chunk kann durch die Fusion trotzdem noch gerettet werden; und die finale Antwort nutzt die Top-7-Chunks. Ich habe dieses ganze Setup in
gegen eine reine Vektorsuche gebenchmarkt, falls du die Kriegsgeschichten dazu lesen willst.&lt;/p&gt;
&lt;h2 id="der-agent-ein-modell-das-selbst-entscheidet"&gt;Der Agent: Ein Modell, das selbst entscheidet&lt;/h2&gt;
&lt;p&gt;Hier ist der zweite Punkt, der Anfänger oft ins Straucheln bringt: Der Chat von CogniVault ist nicht einfach &amp;ldquo;Kopiere Chunks in einen Prompt, bekomme eine Antwort.&amp;rdquo; Es ist ein &lt;strong&gt;Agent&lt;/strong&gt; — ein Modell, das in einer Schleife läuft, in der es sich &lt;em&gt;entscheiden&lt;/em&gt; kann, Tools aufzurufen, deren Ergebnisse zu lesen und erst dann zu antworten.&lt;/p&gt;
&lt;p&gt;Gebaut mit dem Strands Agents SDK, bekommt der Agent sechs Tools:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Aufgabe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;search_knowledge_base&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Das Kern-RAG-Tool — führt die hybride Suche von oben aus, liefert Chunks mit Quelle und Seite zurück&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_documents&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nachschauen, was im Vault (Tresor) liegt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;analyze_document&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strukturierte Analyse eines Dokuments: Themen, Entitäten, Fakten, Zusammenfassung&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compare_documents&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Beantwortung einer Frage durch den direkten Vergleich von zwei Dokumenten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;calculator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sicheres Rechnen — der Ausdruck wird in einen Syntaxbaum (AST) geparst und nur erlaubte Operatoren werden ausgeführt. Niemals &lt;code&gt;eval()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;current_time&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Datum und Uhrzeit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Es gibt hier kein fest programmiertes Routing. Das &lt;em&gt;Modell&lt;/em&gt; liest deine Frage und entscheidet, welche Tools es aufruft, geleitet von seinem System-Prompt. Fragst du &amp;ldquo;Vergleiche die zwei Verträge hinsichtlich der Kündigungsklauseln&amp;rdquo;, greift es zum &lt;code&gt;compare_documents&lt;/code&gt;; fragst du &amp;ldquo;Was sind 15% von 2.340&amp;rdquo;, nutzt es den Taschenrechner, anstatt Mathematik zu halluzinieren.&lt;/p&gt;
&lt;p&gt;Zwei Sicherheitsdetails, auf die Anfänger achten sollten, weil sie den Unterschied zwischen einem Spielzeug und einem Produkt ausmachen: &lt;strong&gt;Für jeden Request wird ein frischer Agent gebaut&lt;/strong&gt; (kein geteilter State, der zwischen parallelen Chats überspricht), und die Dokumentenanalyse-Tools rufen das Modell &lt;em&gt;direkt&lt;/em&gt; auf statt über den Agenten — sonst könnte ein Agent, der ein Tool aufruft, das wiederum den Agenten aufruft, in einer Endlosschleife feststecken.&lt;/p&gt;
&lt;h2 id="dem-modell-beim-denken-zusehen"&gt;Dem Modell beim Denken zusehen&lt;/h2&gt;
&lt;p&gt;Wenn du eine Nachricht absendest, streamt die Antwort als &lt;strong&gt;NDJSON&lt;/strong&gt; (Newline-Delimited JSON — jede Zeile des Streams ist ein eigenes kleines JSON-Objekt). Und das passiert in zwei Phasen:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phase 1 — Denken.&lt;/strong&gt; Gemmas Argumentationskette (Reasoning Chain) streamt zuerst und wird im aufklappbaren Panel über der Antwort gerendert. Es ist absichtlich so gebaut, dass es nicht zwingend klappen muss (Best-Effort): Falls es aus irgendeinem Grund fehlschlägt, kommt die Antwort trotzdem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phase 2 — Die Agenten-Antwort.&lt;/strong&gt; Tools laufen, Zitate (Quellenangaben) tauchen im Quellen-Panel auf, sobald die Suche abgeschlossen ist — &lt;em&gt;bevor&lt;/em&gt; die Antwort fertig geschrieben ist — und der Antworttext streamt herein.&lt;/p&gt;
&lt;div class="mermaid"&gt;flowchart TB
Q["Deine Frage&lt;br/&gt;(plus optionale Bilder, Dateien, Scope)"] --&gt; P1
subgraph STREAM["POST /rag — ein NDJSON-Stream"]
P1["Phase 1: Denken&lt;br/&gt;Reasoning-Chunks streamen zuerst"]
P1 --&gt; P2["Phase 2: Agent&lt;br/&gt;frisch pro Request, Historie wiederhergestellt"]
P2 --&gt;|"entscheidet sich aufzurufen"| T["search_knowledge_base"]
T --&gt; D["FAISS&lt;br/&gt;semantisch"]
T --&gt; S["BM25&lt;br/&gt;Keywords"]
D --&gt; RRF["RRF Fusion — Top 7 Chunks"]
S --&gt; RRF
RRF --&gt;|"Chunks + Quellenangaben"| P2
P2 --&gt; OUT["Quellenangaben, dann Antworttext,&lt;br/&gt;dann ein Speicher-Nutzungs-Report"]
end
&lt;/div&gt;
&lt;p&gt;Jede Zeile im Stream ist typisiert: &lt;code&gt;thinking&lt;/code&gt;, &lt;code&gt;metadata&lt;/code&gt; (eine Quelle/Zitat), &lt;code&gt;text&lt;/code&gt; (Antwort), &lt;code&gt;memory&lt;/code&gt; (wie voll das Konversations-Budget ist) oder &lt;code&gt;error&lt;/code&gt;. Das Frontend liest einfach die Zeilen und leitet sie in das richtige Panel weiter. Ich habe dieses Design zerlegt — und erklärt, warum das Denken &lt;em&gt;vor&lt;/em&gt; den Tool-Aufrufen kommt — in
.&lt;/p&gt;
&lt;h2 id="ein-speicher-budget-kein-fassloses-loch"&gt;Ein Speicher-Budget, kein fassloses Loch&lt;/h2&gt;
&lt;p&gt;Gemmas Context Window (die Textmenge, die das Modell auf einmal betrachten kann) beträgt 128K Token, aber CogniVault lässt den Chatverlauf nicht über das komplette Fenster wuchern. Jede Chat-Session bekommt ein Budget von 48.000 Zeichen — grob 12.000 Token. Überschreitest du es, fällt das &lt;em&gt;älteste&lt;/em&gt; Frage-Antwort-Paar leise als erstes heraus. So bleibt der Großteil des Fensters frei für das, was wirklich zählt: deine aktuelle Frage und die abgerufenen Chunks.&lt;/p&gt;
&lt;p&gt;Zwei Resilienz-Tricks, die du für deine eigenen Projekte klauen solltest:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reboots überleben.&lt;/strong&gt; In-Memory-Verlauf stirbt mit dem Prozess. Deshalb baut die erste Nachricht in einer Session nach einem Backend-Neustart ihren Verlauf aus dem Chat-Log wieder auf, den das Frontend persistiert hat. Multi-Turn-Gedächtnis überlebt Neustarts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bearbeiten und neu generieren.&lt;/strong&gt; Wenn du eine frühere Nachricht bearbeitest, wird der gespeicherte Verlauf auf genau diesen Punkt zurückgespult, bevor neu gefragt wird — das Modell vergisst buchstäblich die Zeitlinie, die jetzt nicht mehr existiert.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="scope-die-ki-auf-bestimmte-dokumente-festnageln"&gt;Scope: Die KI auf bestimmte Dokumente festnageln&lt;/h2&gt;
&lt;p&gt;Noch ein letztes Feature, und eine Lektion über kleine lokale Modelle. Du kannst einen Chat auf bestimmte Dateien oder eine Kategorie pinnen (Scope). Dieser Filter reist mit dem Request &lt;em&gt;und&lt;/em&gt; eine zwingende Such-Anweisung wird sowohl in den System-Prompt als auch in deine eigentliche Nutzer-Nachricht injiziert.&lt;/p&gt;
&lt;p&gt;Warum in beide? Weil kleine Modelle manchmal Anweisungen ignorieren, die nur im System-Prompt stehen — aber sie können nicht ignorieren, was direkt in der Frage steckt. Gürtel und Hosenträger. Wenn du mit 4-Milliarden-Parameter-Modellen arbeitest statt mit den größten Frontrunnern, lernst du, Anweisungen so zu platzieren, dass man sie unmöglich übersehen kann, anstatt nur zu hoffen, dass sie befolgt werden.&lt;/p&gt;
&lt;h2 id="fazit"&gt;Fazit&lt;/h2&gt;
&lt;p&gt;Eine belegte Antwort ist das Zusammenspiel von vier Systemen: Zwei Retriever decken gegenseitig ihre blinden Flecken ab, eine Fusionsformel, die nichts weiter braucht als Ränge, ein Agent, der sich seine Tools selbst aussucht, und ein Stream, der seinen Lösungsweg offenlegt. Keines der vier ist für sich genommen exotisch — das eigentliche Produkt ist ihre Zusammenarbeit.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="anhang-abkürzungen-in-diesem-post"&gt;Anhang: Abkürzungen in diesem Post&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Abkürzung&lt;/th&gt;
&lt;th&gt;Volle Form&lt;/th&gt;
&lt;th&gt;Bedeutung&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Retrieval-Augmented Generation&lt;/td&gt;
&lt;td&gt;Hole erst relevante Passagen aus deinen eigenen Dokumenten; lass das Modell daraus antworten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FAISS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Facebook AI Similarity Search&lt;/td&gt;
&lt;td&gt;Die semantische (bedeutungsbasierte) Hälfte der hybriden Suche&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BM25&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Best Match 25&lt;/td&gt;
&lt;td&gt;Die Keyword-Hälfte — eine klassische Ranking-Formel aus dem Okapi Information-Retrieval-System&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RRF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reciprocal Rank Fusion&lt;/td&gt;
&lt;td&gt;Vereint die beiden gerankten Listen und nutzt dafür nur den Rang jedes Chunks: &lt;code&gt;score = Σ 1/(60 + rang)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NDJSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Newline-Delimited JSON&lt;/td&gt;
&lt;td&gt;Ein Stream, bei dem jede Zeile ein eigenes komplettes JSON-Objekt ist — das Format der Chat-Antwort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JSON&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JavaScript Object Notation&lt;/td&gt;
&lt;td&gt;Das universelle Textformat für strukturierte Daten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AST&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Abstract Syntax Tree&lt;/td&gt;
&lt;td&gt;Die geparste Form eines Ausdrucks — wie der Taschenrechner rechnet, ohne &lt;code&gt;eval()&lt;/code&gt; zu nutzen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LLM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Large Language Model&lt;/td&gt;
&lt;td&gt;Ein neuronales Netz, trainiert mit riesigen Textmengen, das Sprache lesen und erzeugen kann&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SDK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Software Development Kit&lt;/td&gt;
&lt;td&gt;Eine Bibliothek von Bausteinen — hier Strands, das die Agenten-Schleife bereitstellt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;K&lt;/strong&gt; (in 128K)&lt;/td&gt;
&lt;td&gt;Kilo (Tausend)&lt;/td&gt;
&lt;td&gt;128K Token ≈ 128.000 Token — Gemmas Context Window&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Als Nächstes:&lt;/strong&gt;
— die gleiche Maschinerie, aber ausgerichtet auf das Erstellen von Quizzes, Workshops, Karteikarten und Mindmaps, plus eine Tabelle mit jedem Byte, das die App speichert und wo genau es lebt.&lt;/p&gt;</description></item><item><title>Teil 2 · Hybrid Retrieval in der Praxis: FAISS + BM25, verschmolzen mit RRF</title><link>https://aretascodes.dev/de/blog/hybrid-retrieval-faiss-bm25-rrf/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://aretascodes.dev/de/blog/hybrid-retrieval-faiss-bm25-rrf/</guid><description>
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;Teil einer Serie über die Entwicklung von
, einem vollständig lokalen KI-Lernbegleiter. Zuvor:
.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote class="border-l-4 border-neutral-300 dark:border-neutral-600 pl-4 italic text-neutral-600 dark:text-neutral-400 my-6"&gt;
&lt;p&gt;Alle Abkürzungen werden im Anhang unten auf der Seite vollständig erklärt.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Die erste Version von CogniVault nutzte reines Dense Retrieval – die Suchanfrage mit &lt;code&gt;embeddinggemma&lt;/code&gt; einbetten, in einem FAISS-Index suchen und die Top-7-Chunks an das Modell übergeben. Es funktionierte. Es funktionierte &lt;em&gt;hervorragend&lt;/em&gt; – bis ein Nutzer ein PDF mit deutschen Gesetzestexten hochlud und nach &amp;ldquo;§3 Absatz 2&amp;rdquo; fragte.&lt;/p&gt;
&lt;p&gt;Das Modell konnte es nicht finden.&lt;/p&gt;
&lt;p&gt;Der Chunk war &lt;em&gt;genau da&lt;/em&gt;. Das PDF war indiziert. Aber &amp;ldquo;§3 Absatz 2&amp;rdquo; lässt sich nicht in etwas Semantisch Sinnvolles einbetten – es ist ein Identifikator auf Token-Ebene, kein Konzept. Der dichte Vektor für die Suchanfrage landete nicht einmal in der Nähe des dichten Vektors für den Chunk, obwohl der Chunk exakt den String enthielt, nach dem der Nutzer gefragt hatte.&lt;/p&gt;
&lt;p&gt;Dieser Bug hat reines Dense Retrieval für mich erledigt. In diesem Beitrag geht es darum, womit ich es ersetzt habe.&lt;/p&gt;
&lt;h2 id="zwei-arten-von-ähnlich"&gt;Zwei Arten von &amp;ldquo;ähnlich&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;Du nutzt bereits jeden Tag beide Arten der Suche. Wenn Spotify ein &amp;ldquo;Song Radio&amp;rdquo; basierend auf einem Track erstellt, den du magst, vergleicht es das &lt;em&gt;Gefühl&lt;/em&gt; – Tempo, Stimmung, Genre – und spielt dir gerne einen Song vor, dessen Titel kein einziges Wort mit dem Original gemeinsam hat. Aber wenn du &lt;code&gt;Bohemian Rhapsody remastered 2011&lt;/code&gt; in die Suchleiste tippst, willst du kein &lt;em&gt;Gefühl&lt;/em&gt;. Du willst genau diesen String, und &amp;ldquo;ein ähnliches opernhaftes Rock-Epos&amp;rdquo; ist die falsche Antwort.&lt;/p&gt;
&lt;p&gt;Suchsysteme formalisieren diese Unterscheidung in zwei Konzepte von Ähnlichkeit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lexikalische Ähnlichkeit&lt;/strong&gt; – &amp;ldquo;Teilen diese Strings seltene Wörter?&amp;rdquo; Das ist es, was TF-IDF und BM25 modellieren. Sie glänzen bei Identifikatoren, Namen, Code, Fachbegriffen und direkten Zitaten.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Semantische Ähnlichkeit&lt;/strong&gt; – &amp;ldquo;Sprechen diese Passagen über dieselbe Idee, auch wenn sie andere Wörter verwenden?&amp;rdquo; Das ist es, was Embeddings modellieren. Sie glänzen bei Paraphrasen, konzeptionellen Anfragen und natürlichsprachlichen Fragen.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keines der beiden schließt das andere ein. Ein Nutzer, der fragt: &lt;em&gt;&amp;ldquo;Wie ist die praktische Prüfung aufgebaut?&amp;rdquo;&lt;/em&gt;, braucht die &lt;strong&gt;semantische&lt;/strong&gt; Suche – im Dokument steht nämlich nicht zwingend &amp;ldquo;Aufbau der praktischen Prüfung&amp;rdquo;. Ein Nutzer, der &lt;em&gt;&amp;quot;§3 Absatz 2&amp;quot;&lt;/em&gt; fragt, braucht die &lt;strong&gt;lexikalische&lt;/strong&gt; Suche – da gibt es kein Konzept zum Einbetten, nur einen wörtlichen String.&lt;/p&gt;
&lt;p&gt;Production-RAG muss beides können. CogniVault macht beides und führt die Ergebnislisten dann mit &lt;strong&gt;Reciprocal Rank Fusion (RRF)&lt;/strong&gt; zusammen.&lt;/p&gt;
&lt;h2 id="der-stack"&gt;Der Stack&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Query
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── embed via embeddinggemma ──► FAISS IndexFlatIP ──► top-K dense
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── tokenize + lowercase ──► BM25Okapi ──► top-K sparse
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Reciprocal Rank Fusion ◄──┘
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; top-7 fused chunks
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Beide Indizes liegen &lt;strong&gt;im Arbeitsspeicher&lt;/strong&gt;, davor sitzt ein &lt;code&gt;VectorDB&lt;/code&gt;-Singleton. FAISS führt eine Inner-Product-Suche über normalisierte Embeddings durch (das Skalarprodukt entspricht also dem Kosinus). BM25 ist &lt;code&gt;BM25Okapi&lt;/code&gt; aus &lt;code&gt;rank_bm25&lt;/code&gt;, gefüttert mit denselben Chunks, die durch einen einfachen Lowercase-und-Split-Tokenizer in Tokens zerlegt wurden.&lt;/p&gt;
&lt;p&gt;Die Korpora werden synchron gehalten: Wenn man die Chunks einer Datei weich löscht, löst das einen BM25-Rebuild über die verbleibenden aktiven Chunks aus, und das Singleton lädt beide Indizes aus &lt;code&gt;vector_store.faiss&lt;/code&gt; + &lt;code&gt;vector_store.json&lt;/code&gt; (Chunk-Metadaten + Rohtext) nach jedem Ingestion-Lauf und beim App-Start neu.&lt;/p&gt;
&lt;h2 id="warum-faiss-indexflatip-und-nicht-hnsw-oder-ivf"&gt;Warum FAISS &lt;code&gt;IndexFlatIP&lt;/code&gt; und nicht HNSW oder IVF?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;IndexFlatIP&lt;/code&gt; ist eine exakte Brute-Force-Suche. Es scannt jeden Vektor für jede Anfrage. Bei zehntausenden Chunks ist das völlig in Ordnung – unter einer Millisekunde auf einem Laptop. CogniVault ist eine &lt;strong&gt;lokale Single-User&lt;/strong&gt;-App; der Index wird nie Milliarden von Vektoren haben. Um Recall für Geschwindigkeit über HNSW oder IVF einzutauschen, würde hier nichts bringen und nur die &amp;ldquo;Exakt&amp;rdquo;-Garantie kosten. Langweilig, korrekt, schnell genug.&lt;/p&gt;
&lt;p&gt;Wenn das Korpus so groß wird, dass Brute-Force zu zäh wird, ist der Wechsel nur eine Zeile Code. Bis dahin gewinnt der einfachste Index.&lt;/p&gt;
&lt;h2 id="reciprocal-rank-fusion"&gt;Reciprocal Rank Fusion&lt;/h2&gt;
&lt;p&gt;Der naive Weg, zwei geordnete Listen zu kombinieren, ist, sie zu scoren und zu addieren. Das klingt sinnvoll, bis du dich daran erinnerst, dass FAISS Inner-Product-Scores in einem begrenzten Bereich liefert und BM25 Scores in einem unbegrenzten – sie sind ohne Normalisierung nicht vergleichbar, und jede Normalisierung, die du wählst, ist irgendwie willkürlich.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RRF umgeht das Problem komplett.&lt;/strong&gt; Es schaut sich nur &lt;em&gt;Ränge&lt;/em&gt; an, keine Scores. Für jede Ergebnisliste trägt ein Item auf Rang &lt;code&gt;r&lt;/code&gt; mit &lt;code&gt;1 / (k + r)&lt;/code&gt; zu seinem End-Score bei (mit &lt;code&gt;k = 60&lt;/code&gt; per Konvention – groß genug, um den Tail abzuflachen, klein genug, damit die Top-Items noch dominieren). Items, die in beiden Listen auftauchen, werden summiert.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Simplified — the real implementation also de-duplicates chunks&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# by (source, chunk_id, page) before scoring.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reciprocal_rank_fusion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result_lists&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result_lists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Das ist schon der ganze Algorithmus. Kein Tuning, keine Kalibrierung, keine Gewichte pro Korpus. Ein Chunk, der bei BM25 auf Platz 1 und bei FAISS auf Platz 4 liegt, schlägt problemlos einen Chunk, der nur in einer der Listen auf Platz 2 ist. Ein Chunk, bei dem sich &lt;em&gt;beide&lt;/em&gt; Indizes einig sind, steigt deterministisch an die Spitze.&lt;/p&gt;
&lt;p&gt;Das Ergebnis für die &amp;ldquo;§3 Absatz 2&amp;rdquo;-Anfrage: BM25 findet den exakten Treffer und platziert ihn auf Rang 1. FAISS findet nichts Brauchbares (seine Top-Treffer handeln allgemein von Prüfungsordnungen). RRF bringt den BM25-Treffer an die Spitze der fusionierten Liste. Problem gelöst.&lt;/p&gt;
&lt;h2 id="scope-filterung-mit-contextvar-isolierung"&gt;Scope-Filterung mit ContextVar-Isolierung&lt;/h2&gt;
&lt;p&gt;Ein Detail, das man leicht falsch macht: Der Retriever muss sich seines &lt;em&gt;Scopes&lt;/em&gt; bewusst sein. In CogniVault können Nutzer eine Frage auf eine einzelne Kategorie oder bestimmte Dateien beschränken. Der Scope wird durch den Request gesetzt, aber die Suche wird tief im Inneren des Strands-Agent-Loops aufgerufen, der wiederum von einem streamenden FastAPI-Handler aufgerufen wird – möglicherweise mit mehreren parallelen Requests pro Worker.&lt;/p&gt;
&lt;p&gt;Den Scope durch jeden Funktionsaufruf durchzureichen, wäre unschön. Eine globale Variable ist unsicher. Das richtige Mittel dafür ist Pythons
, das dir einen task-lokalen, isolierten State gibt, den sowohl &lt;code&gt;asyncio&lt;/code&gt; als auch Threads respektieren.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;contextvars&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ContextVar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;_doc_scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;DocScope&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;doc_scope&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_doc_scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DocScope&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_doc_scope&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;current_doc_scope&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DocScope&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_doc_scope&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Der &lt;code&gt;/rag&lt;/code&gt;-Request-Handler setzt den Scope ganz am Anfang jeder Streaming-Antwort; das Such-Tool liest ihn; und weil der Wert task-lokal ist, stirbt er mit dem Request. Keine globalen Variablen, kein Durchbohren von Parametern, keine Race Conditions über gleichzeitige Nutzer hinweg.&lt;/p&gt;
&lt;p&gt;Das ist eine dieser Designentscheidungen, die nach Over-Engineering aussehen, bis du zwei Browser-Tabs offen hast und merkst, dass ohne sie der Scope-Filter von Tab A in die Frage von Tab B leaken würde.&lt;/p&gt;
&lt;h2 id="chunking-entscheidungen-die-sich-später-auszahlen"&gt;Chunking-Entscheidungen, die sich später auszahlen&lt;/h2&gt;
&lt;p&gt;Hybrid Retrieval ist nur so gut wie seine Chunks. CogniVault nutzt einen &lt;code&gt;RecursiveCharacterTextSplitter&lt;/code&gt; mit &lt;strong&gt;1.000 Zeichen und 100 Zeichen Overlap&lt;/strong&gt; für unstrukturierten Text – klein genug, um das Retrieval präzise zu halten, groß genug, um Kontext für das Modell zu liefern.&lt;/p&gt;
&lt;p&gt;Für strukturierte Formate ändert sich die Strategie:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Markdown&lt;/strong&gt; → &lt;code&gt;MarkdownHeaderTextSplitter&lt;/code&gt; liefert einen Chunk pro H1/H2/H3-Abschnitt, wobei die Überschriftenhierarchie als Brotkrümel vorangestellt wird (&amp;ldquo;Privacy &amp;gt; Vault Audit &amp;gt; Indicators&amp;rdquo;). BM25 liebt Brotkrümel – sie lassen Anfragen mit Überschriften-Keywords sauber matchen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSV&lt;/strong&gt; → Kopfzeile + 20 Zeilen pro Batch als Chunk, sodass eine Suche nach einem Spaltennamen im richtigen Block landet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PPTX&lt;/strong&gt; → ein Chunk pro Folie, Titel und Body-Text zusammen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XLSX&lt;/strong&gt; → Kopfzeile + Zeilen-Batches pro Sheet, mit einem &lt;code&gt;[Sheet: name]&lt;/code&gt; Präfix.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Winzige Fragmente werden gefiltert: Unstrukturierter Text braucht mindestens &lt;strong&gt;100 Zeichen&lt;/strong&gt;, um ein Chunk zu werden, während die strukturierten Formate die Messlatte auf &lt;strong&gt;20&lt;/strong&gt; senken – ein zweizeiliger Markdown-Abschnitt oder ein Sheet, das nur aus Überschriften besteht, ist zwar kurz, aber immer noch aussagekräftig. Der rekursive Splitter ist altbekanntes Terrain, aber die formatabhängigen Strategien sind viel wichtiger, als man ihnen oft zugesteht.&lt;/p&gt;
&lt;h2 id="was-ich-anders-machen-würde"&gt;Was ich anders machen würde&lt;/h2&gt;
&lt;p&gt;Ein paar Dinge, die ich noch einmal überdenken würde, wenn ich noch einmal von vorn anfangen würde:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Aufhören, für BM25 mit &lt;code&gt;str.split()&lt;/code&gt; zu tokenisieren.&lt;/strong&gt; Es ist okay, aber ein echter Tokenizer, der mit Satzzeichen und deutschen Komposita umgehen kann, würde den Recall bei den rechtlichen Dokumenten deutlich verbessern.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Einen kleinen Reranker hinzufügen.&lt;/strong&gt; RRF findet das richtige &lt;em&gt;Set&lt;/em&gt;, aber ein Cross-Encoder-Rerank auf den Top 20 würde die &lt;em&gt;Reihenfolge&lt;/em&gt; aufpolieren. Natürlich lokal gehostet – da gibt es mittlerweile gute kleine Modelle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query Expansion für dünne Anfragen.&lt;/strong&gt; Zwei-Wort-Fragen wie &amp;ldquo;§3 Prüfung&amp;rdquo; könnten vor dem Retrieval über einen schnellen &lt;code&gt;gemma4&lt;/code&gt;-Aufruf erweitert werden. Kostet Latenz, bringt aber Recall.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nichts davon ist bisher an Bord. RRF über FAISS + BM25 ist schon so viel besser als jedes für sich allein, dass ich noch nicht den Drang gespürt habe, weiter zu optimieren.&lt;/p&gt;
&lt;h2 id="fazit"&gt;Fazit&lt;/h2&gt;
&lt;p&gt;Wenn dein Retrieval &amp;ldquo;embed + cosine + top-k&amp;rdquo; ist, wird es genau auf dieselbe Weise scheitern wie meins – bei Anfragen, die wortwörtliche Identifikatoren enthalten, für die dein Modell kein Embedding hat. Die Lösung ist kein besseres Embedding-Modell. Es ist ein zweiter Retriever, der nicht so tut, als wäre alles ein Konzept.&lt;/p&gt;
&lt;p&gt;FAISS für Ideen. BM25 für Strings. RRF entscheidet, wer heute Recht hat.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="anhang-abkürzungen-in-diesem-beitrag"&gt;Anhang: Abkürzungen in diesem Beitrag&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Abkürzung&lt;/th&gt;
&lt;th&gt;Vollform&lt;/th&gt;
&lt;th&gt;Bedeutung&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RAG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Retrieval-Augmented Generation&lt;/td&gt;
&lt;td&gt;Rufe zuerst relevante Passagen aus deinen eigenen Dokumenten ab; lass das Modell dann basierend darauf antworten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FAISS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Facebook AI Similarity Search&lt;/td&gt;
&lt;td&gt;Metas Bibliothek zum Speichern von Vektoren und zum schnellen Finden der ähnlichsten&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BM25&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Best Match 25&lt;/td&gt;
&lt;td&gt;Eine Keyword-Ranking-Formel – die 25. Ranking-Funktion, die im Informationsretrieval-System Okapi entwickelt wurde&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RRF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reciprocal Rank Fusion&lt;/td&gt;
&lt;td&gt;Führt geordnete Listen nur anhand der Ränge zusammen: Jedes Item punktet mit &lt;code&gt;Σ 1/(k + rank)&lt;/code&gt; über alle Listen hinweg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TF-IDF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Term Frequency–Inverse Document Frequency&lt;/td&gt;
&lt;td&gt;Der Vorfahre von BM25: Bewertet Wörter danach, wie oft sie hier auftauchen vs. wie selten sie überall sonst sind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP&lt;/strong&gt; (in &lt;code&gt;IndexFlatIP&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Inner Product&lt;/td&gt;
&lt;td&gt;Das Ähnlichkeitsmaß, das FAISS berechnet; bei normalisierten Vektoren entspricht es der Kosinus-Ähnlichkeit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HNSW&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hierarchical Navigable Small World&lt;/td&gt;
&lt;td&gt;Eine beliebte Struktur für &lt;em&gt;approximative&lt;/em&gt; Vektor-Indizes – hier bewusst nicht verwendet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IVF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inverted File Index&lt;/td&gt;
&lt;td&gt;Ein weiterer approximativer FAISS-Indextyp – ebenfalls bewusst nicht verwendet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AEVO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ausbildereignungsverordnung&lt;/td&gt;
&lt;td&gt;Das deutsche Gesetz, dessen Anfrage &amp;ldquo;§3 Absatz 2&amp;rdquo; das reine Dense Retrieval zum Scheitern brachte&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CSV / PPTX / XLSX&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Comma-Separated Values / PowerPoint / Excel (Office Open XML)&lt;/td&gt;
&lt;td&gt;Strukturierte Formate mit ihren eigenen Chunking-Strategien&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;H1/H2/H3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Heading levels 1–3&lt;/td&gt;
&lt;td&gt;Die Markdown-Überschriftenebenen, die zum Aufteilen von Abschnitten verwendet werden&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Als Nächstes:&lt;/strong&gt;
— wie der &lt;code&gt;/rag&lt;/code&gt;-Endpoint von CogniVault das &lt;em&gt;Denken&lt;/em&gt; von Gemma 4 streamt, bevor Tool-Aufrufe starten.&lt;/p&gt;</description></item></channel></rss>