← Alle Beitrage

Einen Hybrid-Retriever für 16.894 Obsidian-Dateien entwickeln

From the guide: Claude Code Comprehensive Guide

Ein grep durch 16.894 Markdown-Dateien dauert je nach Suchbegriff 11–66 Sekunden und liefert Hunderte von kaum relevanten Treffern. Eine Vektorsuche liefert semantisch verwandte Inhalte, übersieht aber den exakten Funktionsnamen, den Sie eingegeben haben. Ein Hybrid-Retriever, der beide Methoden fusioniert, liefert die richtige Antwort in 23 Millisekunden (End-to-End, einschließlich Query-Embedding) aus einer einzigen 83 MB großen SQLite-Datei — ganz ohne API-Aufrufe.1

Das Problem des obsessiven Notizensammlers ist nicht das Sammeln. Das Problem ist das Wiederfinden. Obsidian macht das Erfassen reibungslos. Sammelt man genügend Dateien an, wird der Vault zu einer Write-Only-Datenbank: leicht zu befüllen, unmöglich abzufragen. Suche nach Dateinamen funktioniert, bis Dateinamen bedeutungslos werden. Volltextsuche funktioniert, bis dasselbe Schlüsselwort in 400 Dokumenten vorkommt. Tags funktionieren, bis man vergisst, etwas zu taggen.

Ein HN-Kommentator fragte nach der vollständigen Architektur hinter dem Retrieval-System, das ich für meinen Obsidian-Vault gebaut habe.2 Hier ist sie: die Chunking-Strategie, das Embedding-Modell, das Dual-Index-SQLite-Schema, die Fusions-Mathematik mit echten Zahlen und die Fehlermodi, die ich nach Hunderten von Abfragen an das System entdeckt habe.

TL;DR

Der Retriever kombiniert FTS5-BM25-Schlüsselwortsuche mit Model2Vec-Vektorähnlichkeitssuche, fusioniert über Reciprocal Rank Fusion (RRF) zu einer einzigen Rangliste. Alles läuft lokal in einer SQLite-Datenbank: 49.746 Chunks aus 16.894 Dateien in 83 MB. Eine vollständige Neuindexierung dauert vier Minuten. Inkrementelle Updates laufen in unter zehn Sekunden. Das System integriert sich über Hooks in Claude Code und gibt dem Agenten Zugriff auf das Wissen des Vaults, ohne Dateien in den Kontext zu laden. BM25 findet exakte Bezeichner und Funktionsnamen. Die Vektorsuche findet semantische Treffer über verschiedene Terminologie hinweg. RRF fusioniert beide, ohne dass eine Score-Kalibrierung erforderlich ist. Der ehrliche Kompromiss: Gut getaggter, oberflächlicher Inhalt kann schlecht strukturierten, tiefgehenden Inhalt im Ranking übertreffen, weil BM25 Schlüsselwortdichte belohnt, nicht Tiefe.


Kernerkenntnisse

Für Notizensammler mit großen Vaults. Nach meiner Erfahrung wurde die Volltextsuche allein ab einigen Tausend Dateien unbrauchbar — und bestehende Obsidian-Such-Plugins (Smart Connections, Omnisearch) indexieren innerhalb der App, nicht als externe Bibliothek, die andere Tools abfragen können.1 Vektorsuche zusätzlich zu BM25 fängt die Abfragen ab, bei denen Sie sich an das Konzept erinnern, aber nicht an das Schlüsselwort. Der Retriever läuft vollständig auf SQLite, ohne externe Dienste, ohne GPU und ohne API-Kosten. Model2Vec embeddet mit CPU-Geschwindigkeit, weil das Modell 30 MB statischer Wortvektoren ist, kein Transformer.3

Für Entwickler, die Retrieval-Systeme bauen. RRF ist die Fusionsmethode, die am wenigsten Tuning erfordert. Die Formel verwendet ausschließlich Rangpositionen, keine Roh-Scores, sodass Sie BM25-Scores nie gegen Kosinus-Distanzen kalibrieren müssen. Beginnen Sie mit k=60 und gleichen Gewichtungen. Optimieren Sie erst, nachdem Sie Fehlerfälle an Ihren eigenen Daten gemessen haben. Die sqlite-vec-Erweiterung bringt die Vektor-KNN-Suche in SQLite, ohne eine separate Vektordatenbank.4

Für Claude Code-Benutzer. Der Retriever läuft als Bibliothek, die Hooks aufrufen können. Ein PreToolUse-Hook fragt den Vault ab, bevor der Agent mit der Arbeit beginnt. Der Agent sieht 2–3 KB fokussierte Ergebnisse mit Dateipfad-Attribution, anstatt ganze Dateien zu laden. Die Integration hält Kontextfenster klein und gibt dem Agenten gleichzeitig Zugriff auf das Wissen aus 16.894 Dateien.

Minimal funktionsfähige Version. Der einfachste Einstiegspunkt: Erstellen Sie eine FTS5-Virtual-Table über Ihre Markdown-Dateien (nur BM25, keine Embeddings). Fügen Sie sqlite-vec und Model2Vec hinzu, wenn die Schlüsselwortsuche beginnt, semantische Treffer zu verpassen. Fügen Sie die RRF-Fusion zuletzt hinzu. Jede Schicht funktioniert unabhängig. Der gesamte Stack benötigt Python 3, einen 30 MB großen Modell-Download und pip install model2vec sqlite-vec. Kein GPU, kein Docker, keine externen Dienste. Gesamter Speicherbedarf für 16.894 Dateien: 83 MB.

Möchten Sie die vollständige Betriebsanleitung? Die Obsidian AI Infrastructure-Referenz behandelt Vault-Architektur, Plugin-Konfiguration, MCP-Server-Setup, inkrementelle Indexierungsrezepte und Fehlerbehebung — der Schritt-für-Schritt-Begleiter zum Architektur-Deep-Dive in diesem Beitrag.


Warum Schlüsselwortsuche allein bei großen Datenmengen versagt

Die Volltextsuche bricht bei Vault-Größenordnungen auf vorhersehbare Weise zusammen. FTS5 mit BM25-Ranking ist hervorragend bei exakten Treffern: Suchen Sie nach requestAnimationFrame und jede Datei, die genau diesen Token enthält, erscheint, sortiert nach Termhäufigkeit und Dokumentlänge.5 Robertsons und Zaragozas Überblick über probabilistische Relevanzmodelle bestätigt die Stärke von BM25: Der Algorithmus liefert mit minimalem Parameter-Tuning gute Ergebnisse bei schlüsselwortlastigen Abfragen.14 Der Fehlermodus sind Synonyme und Konzeptabgleich. Suchen Sie nach „how to handle authentication failures” und BM25 liefert jede Datei, die „authentication” oder „failures” einzeln erwähnt, und verwässert die Ergebnisse mit nur tangential verwandtem Inhalt.

Die Vektorsuche löst das Synonymproblem. Embedden Sie die Abfrage und finden Sie Chunks, deren Embeddings im Vektorraum nahe beieinander liegen. „How to handle authentication failures” trifft auf Inhalte über „login error recovery” und „session expiration handling”, weil das Embedding semantische Ähnlichkeit über verschiedene Terminologie hinweg erfasst.6 Karpukhin et al. zeigten mit Dense Passage Retrieval (DPR), dass dichte Embeddings BM25 beim Open-Domain Question Answering um 9–19 % in der Top-20-Genauigkeit übertreffen, gerade weil dichte Repräsentationen Bedeutung jenseits lexikalischer Überlappung erfassen.15 Der Fehlermodus ist genau umgekehrt: Die Vektorsuche übersieht exakte Bezeichner. Suchen Sie nach dem Funktionsnamen _rrf_fuse und die Vektorsuche liefert Inhalte über Fusions- und Ranking-Algorithmen, ordnet aber möglicherweise die eigentliche Funktionsdefinition hinter einer konzeptionellen Erklärung ein.

Keine der beiden Methoden deckt allein beide Fehlermodi ab. Eine einzelne Abfrage veranschaulicht den Unterschied (kein Beweis für Überlegenheit — eine aggregierte Evaluation erfordert ein Golden Set, das das System noch nicht hat). Die Abfrage „PostToolUse hook for context compression” liefert unterschiedliche Top-3-Ergebnisse bei jeder Methode:

Rang Nur BM25 Nur Vektor Hybrid (RRF)
1 hook-stdlib.sh „PostToolUse Handler” context-is-the-new-memory.md „Compression Layers” context-is-the-new-memory.md „Compression Layers”
2 settings.json „PostToolUse Events” token-budget-analysis.md „Context Engineering” hook-stdlib.sh „PostToolUse Handler”
3 compress-output.sh „Tool Output Filter” agent-memory-patterns.md „Retrieval Integration” compress-output.sh „Tool Output Filter”

BM25 fand die exakte Hook-Datei und die Settings-Referenz (Schlüsselworttreffer auf „PostToolUse”), verpasste aber die konzeptionelle Context-Engineering-Notiz. Die Vektorsuche fand die Kompressionsstrategie-Notizen (semantischer Treffer auf „context compression”), verpasste aber die spezifische Hook-Implementierung. RRF beförderte die Notizen nach oben, die sowohl für das Konzept als auch für die Implementierung relevant sind, und platzierte die Strategienotiz und die Hook-Datei auf Position eins und zwei.13

Forschung zum MS MARCO Passage Ranking bestätigt dieses Muster in Web-Such-Benchmarks: Hybride Retrieval-Verfahren übertreffen durchgängig sowohl reines BM25 als auch reine Dense-Retrieval-Verfahren, mit den größten Zugewinnen bei Abfragen, die sowohl spezifische Begriffe als auch abstrakte Konzepte enthalten.716


Die Architektur: Drei Schichten, die sich gegenseitig verstärken

Das System hat drei unabhängige Schichten. Jede funktioniert ohne die anderen, aber zusammen verstärken sie sich.

Schicht 1: Aufnahme. Eine 733-zeilige Python-Scoring-Pipeline bewertet jedes eingehende Signal auf vier Dimensionen: Relevanz, Umsetzbarkeit, Tiefe und Autorität. Signale mit einem Score von 0,55 oder höher werden automatisch in einen von 12 Domänenordnern geleitet. Signale zwischen 0,40 und 0,55 werden zur manuellen Überprüfung eingereiht. Unter 0,40 verwirft die Pipeline das Signal. Die Pipeline hat 7.771 Signale über 14 Monate verarbeitet, ohne manuelles Tagging.1 Die Aufnahmeschicht bestimmt, was in den Vault gelangt. Die Retrieval-Schicht macht es auffindbar.

Schicht 2: Retrieval. Die hybride Suchmaschine, die im Folgenden detailliert beschrieben wird. Die Engine zerlegt jede Datei an Überschriftengrenzen in Chunks, embeddet die Chunks mit Model2Vec und indexiert sie in SQLite sowohl mit einer vec0-Tabelle für Vektor-KNN als auch mit einer FTS5-Virtual-Table für BM25. Eine Abfrage läuft gleichzeitig gegen beide Indizes, und RRF fusioniert die Ergebnisse zu einer einzigen Rangliste.

Schicht 3: Integration. Claude Code-Hooks, die den Retriever in den Workflow des Agenten einbinden. Ein Hook feuert bei der Prompt-Übermittlung, fragt den Vault nach relevantem Kontext ab und injiziert die Top-Ergebnisse in die Konversation. Der Agent sieht fokussierte Chunks mit Quellenangabe anstelle von rohen Dateiinhalten:

# Illustrative output (format matches production, content simplified)
## Relevant Memory Context

### OAuth Token Rotation (security-patterns)
Rotate tokens on 401 response. Store refresh token in keychain,
not environment variable. Implement retry with backoff...

### Session Expiration Handling (auth-architecture)
Three expiration modes: absolute (24h), sliding (30min idle),
refresh (7d with rotation). Hook into 401 interceptor...

Jedes Ergebnis trägt die Abschnittsüberschrift und das Quellprojekt, begrenzt auf ein Budget von 500 Token, um Kontext-Aufblähung zu vermeiden.

Der Retriever ermöglicht auch einen zweiten Integrationspunkt: einen PostToolUse-Hook, der Tool-Ausgaben komprimiert, bevor sie in die Konversation gelangen. Rohe Tool-Ausgaben enthalten Zeitstempel, Sortierartefakte und ausführliche Formatierungen, die zwischen Durchläufen variieren. Der Retriever ersetzt den rohen Dump durch eine stabile, fokussierte Teilmenge. Der Agent sieht niemals das Rauschen, nur den relevanten Extrakt. Ein Nebeneffekt: Da die Ausgabe des Retrievers für dieselbe Abfrage deterministisch ist (derselbe Indexzustand liefert dieselben Ranking-Ergebnisse), unterstützt die komprimierte Ausgabe das Prompt-Caching. Wiederholte Abfragen gegen unveränderte Daten erzeugen identische Kontextblöcke, und das automatische Prompt-Caching der CLI verwendet den gecachten Präfix wieder.

Die umfassendere Infrastruktur-Geschichte erklärt, wie Hooks, Skills und Agenten zu einer programmierbaren Schicht um das Modell herum komponiert werden.

Die Schichten sind bewusst entkoppelt. Die Aufnahme-Bewertung weiß nichts über Embeddings. Der Retriever weiß nichts über Signal-Routing-Regeln. Aber die Aufnahme stellt sicher, dass der Vault hochwertige Inhalte enthält, das Retrieval die richtige Teilmenge für jede Abfrage aufdeckt, und die Integration diese Teilmenge ohne Kontext-Aufblähung an den Agenten liefert. Ich habe über den theoretischen Rahmen geschrieben, der Kontext als kritische Ressource betrachtet. Der Retriever ist die praktische Umsetzung.


Chunking: Wo Retrieval-Qualität beginnt

Chunking bestimmt die Granularität der Suchergebnisse. Sind die Chunks zu groß, liefert die Vektorsuche ganze Dateien zurück, in denen nur ein Absatz relevant ist. Sind sie zu klein, verliert das Embedding den Kontext, der für semantischen Abgleich nötig ist. Forschung zu RAG-Pipelines bestätigt, dass die Chunk-Größe für die meisten Anwendungsfälle einen größeren Einfluss auf die Retrieval-Qualität hat als die Modellwahl, wobei Chunks von 200–500 Token für Absatzebenen-Retrieval am besten abschneiden.18

Der Chunker teilt an H2-Überschriftengrenzen (##) und bewahrt dabei die Markdown-Struktur.8 Eine Notiz über OAuth-Token-Rotation mit drei H2-Abschnitten wird zu drei Chunks, jeder eigenständig genug, damit das Embedding seine Bedeutung erfassen kann. Der Indexer speichert den Überschriftentext und den Titel der übergeordneten Notiz als Metadaten neben jedem Chunk, was BM25-Matching auch dann ermöglicht, wenn der Chunk-Text selbst dünn besetzt ist.

# chunker.py: H2 splitting with heading context
MIN_CHUNK_CHARS = 30
MAX_CHUNK_CHARS = 2000

def _split_at_headings(body):
    sections = []
    current_heading = ""
    current_lines = []
    for line in body.split("\n"):
        if line.startswith("## "):
            if current_lines:
                text = "\n".join(current_lines).strip()
                if text:
                    sections.append((current_heading, text))
            current_heading = line[3:].strip()
            current_lines = []
        else:
            current_lines.append(line)
    if current_lines:
        text = "\n".join(current_lines).strip()
        if text:
            sections.append((current_heading, text))
    return sections

Der Chunker unterteilt Abschnitte über 2.000 Zeichen weiter: zuerst an H3-Grenzen, dann an Absatzumbrüchen. Abschnitte unter 30 Zeichen werden verworfen. Der Chunker überspringt außerdem Related-, See Also-, Links- und References-Abschnitte, die typischerweise Listen von Wiki-Links sind und keinen suchbaren Inhalt darstellen.

Zwei Designentscheidungen sind für die Retrieval-Qualität wichtig. Erstens: Der Indexer speichert den Überschriftenkontext-String ("OAuth Token Rotation | note | security, authentication") in einer separaten Spalte und indexiert ihn in FTS5 mit einem niedrigeren Gewicht (0,3) als den Chunk-Text (1,0). BM25 trifft weiterhin auf die Überschrift, wenn der Chunk-Body den Suchbegriff nicht enthält, aber der Überschriftentreffer wird niedriger bewertet als ein Body-Treffer. Zweitens: Der Chunker extrahiert Frontmatter-Tags und Notiztyp und fügt sie in den Überschriftenkontext ein, sodass eine Suche nach „security” auch Notizen trifft, die mit security getaggt sind, selbst wenn der Body-Text andere Terminologie verwendet.


Embedding: 30 MB Modell, null API-Aufrufe

Das Embedding-Modell ist Model2Vecs potion-base-8M, ein statisches Wort-Embedding-Modell mit 7,6 Millionen Parametern, das 256-dimensionale Vektoren erzeugt.3 Auf der MTEB-Benchmark-Suite erreicht potion-base-8M 89 % der Leistung von all-MiniLM-L6-v2 (50,03 vs. 56,09 Durchschnitt) bei bis zu 500-facher Inferenzgeschwindigkeit, was die Indexierung großer Korpora auf Consumer-Hardware praktikabel macht.917 Ein Vorbehalt: Der MTEB-Retrieval-Subscore des Modells ist deutlich niedriger (31,71) als seine Classification- (64,44) oder STS-Scores (73,24). Die Retrieval-Benchmarks von MTEB testen Dokument-Level-Ranking auf Web-Korpora, nicht Absatz-Level-Matching auf homogenen Markdown-Chunks. Die Lücke ist weniger relevant, wenn Chunks kurz, thematisch fokussiert und in einem konsistenten Vokabular geschrieben sind. Anders als transformerbasierte Embedding-Modelle führt Model2Vec keine Attention-Schichten über den Input aus. Das Modell destilliert das Wissen eines Sentence Transformers in statische Token-Embeddings und erzeugt Vektoren durch gewichtete Mittelung statt sequentieller Berechnung.9

Warum funktionieren statische Embeddings für diesen Anwendungsfall? Kurze Markdown-Chunks (durchschnittlich 200–400 Wörter) enthalten konzentriertes Vokabular zu einem einzigen Thema. Der gewichtete Durchschnitt dieser Token-Vektoren landet in einer bedeutungsvollen Region des Embedding-Raums, weil es kaum thematische Verwässerung gibt. In der Praxis neigt ein 2.000-Wörter-Dokument, das drei verschiedene Themen abdeckt, dazu, einen unscharfen Schwerpunkt zu erzeugen, der zwischen Themenclustern liegt statt innerhalb eines. Ein Chunk über OAuth-Token-Rotation hingegen erzeugt einen Vektor, der eng mit anderen Authentifizierungsinhalten clustert. Statische Embeddings tauschen kontextuelle Disambiguierung (das Wort „Bank” in „Flussbank” vs. „Bankfiliale”) gegen reine Geschwindigkeit. In einer persönlichen Wissensdatenbank, in der jeder Chunk ein Konzept behandelt, ist die Mehrdeutigkeitsstrafe gering, und die Veröffentlichung berichtet von bis zu 500-facher Inferenzbeschleunigung.9

# embedder.py: lazy-loading Model2Vec in a dedicated venv
DEFAULT_MODEL = "minishlab/potion-base-8M"
EMBEDDING_DIM = 256

class Model2VecEmbedder:
    def __init__(self, model_name=DEFAULT_MODEL):
        self._model_name = model_name
        self._model = None

    def _ensure_model(self):
        if self._model is not None:
            return
        _activate_venv()  # Add memory venv to sys.path
        from model2vec import StaticModel
        self._model = StaticModel.from_pretrained(self._model_name)

    def embed_batch(self, texts):
        self._ensure_model()
        vecs = self._model.encode(texts)
        return [v.tolist() for v in vecs]

Die praktische Konsequenz: Eine vollständige Neuindexierung von 16.894 Dateien wird in vier Minuten auf einem Apple M3 Pro abgeschlossen. Inkrementelle Indexierung (nur geänderte Dateien, erkannt durch mtime-Vergleich) läuft bei typischen Tagesänderungen in unter zehn Sekunden.1

Das Modell läuft in einer isolierten virtuellen Umgebung unter ~/.claude/venvs/memory/, um Abhängigkeitskonflikte mit dem Rest der Toolchain zu vermeiden. Der Embedder lädt das Modell verzögert bei der ersten Verwendung, nicht beim Import, sodass das Importieren des Moduls nichts kostet, wenn der Retriever auf den reinen BM25-Modus zurückfällt.

Warum kein größeres Modell? Zwei Gründe. Erstens halten die 256-dimensionalen Vektoren die SQLite-Datenbank bei 83 MB für 49.746 Chunks. Höherdimensionale Vektoren (768 oder 1.024) würden die Datenbankgröße verdreifachen oder vervierfachen, bei marginalem Qualitätsgewinn für kurze Markdown-Chunks.10 Zweitens führen API-basierte Embeddings (zum Beispiel OpenAIs text-embedding-3-small für 0,02 $ pro Million Token) zu Latenz, Kosten und einer Netzwerkabhängigkeit für ein System, das offline funktionieren sollte.11 Die vollständige Vault-Neuembedding kostet bei API-Preisen etwa 0,30 $, isoliert betrachtet trivial, aber die eigentlichen Kosten sind die Round-Trip-Latenz multipliziert mit 49.746 Chunks sowie die Datenschutzimplikation, persönliche Notizen an eine externe API zu senden.

Ein Modell-Hash-Mechanismus verfolgt die Embedding-Kompatibilität. Der Indexer speichert einen Hash, der aus dem Modellnamen und der Vokabulargröße abgeleitet wird. Wenn sich das Modell ändert, erkennt die inkrementelle Indexierung die Abweichung und löst automatisch eine vollständige Neuindexierung aus.


Das SQLite-Schema: Drei Tabellen, eine Datei

Der gesamte Index befindet sich in einer SQLite-Datei (vectors.db, 83 MB) im WAL-Modus für sichere parallele Lesezugriffe.12 Drei Tabellen dienen verschiedenen Zwecken:

-- Chunk content and metadata
CREATE TABLE chunks (
    id INTEGER PRIMARY KEY,
    file_path TEXT NOT NULL,
    section TEXT NOT NULL,
    chunk_text TEXT NOT NULL,
    heading_context TEXT DEFAULT '',
    mtime_ns INTEGER NOT NULL,
    embedded_at REAL NOT NULL
);

-- FTS5 for BM25 search (content-synced to chunks)
CREATE VIRTUAL TABLE chunks_fts USING fts5(
    chunk_text, section, heading_context,
    content=chunks, content_rowid=id
);

-- sqlite-vec for vector KNN search
CREATE VIRTUAL TABLE chunk_vecs USING vec0(
    id INTEGER PRIMARY KEY,
    embedding float[256]
);

Die FTS5-Tabelle verwendet ein Content-Sync-Muster: Sie referenziert die chunks-Tabelle direkt, anstatt eine Duplikatkopie des Textes zu speichern.5 Ein Fallstrick: Content-Sync-Tabellen propagieren Löschvorgänge nicht automatisch. Der Indexer muss explizite INSERT INTO chunks_fts(chunks_fts, rowid) VALUES('delete', ?)-Befehle ausgeben, bevor Zeilen aus der chunks-Tabelle entfernt werden, sonst wird der FTS5-Index still inkonsistent. Spaltengewichtungen in BM25-Abfragen weisen dem Chunk-Text 1,0, den Abschnittsüberschriften 0,5 und dem Überschriftenkontext 0,3 zu:

# vector_index.py: BM25 search with column weights
bm25(chunks_fts, 1.0, 0.5, 0.3) as score

Die sqlite-vec-Erweiterung speichert 256-dimensionale Float-Vektoren als gepackte Binärdaten und unterstützt KNN-Abfragen mit Kosinus-Distanz.4 Pythons struct.pack serialisiert die Vektoren:

def _serialize_vector(vec):
    return struct.pack(f"{len(vec)}f", *vec)

Das Schema behandelt graceful Degradation konstruktionsbedingt. Falls sqlite-vec nicht geladen werden kann (fehlende Erweiterung, inkompatible Plattform), fällt der Retriever auf reine BM25-Suche zurück. Die Eigenschaft vec_available verfolgt, ob die Vektorsuche betriebsbereit ist.


Reciprocal Rank Fusion: Die Mathematik, die es funktionieren lässt

RRF fusioniert zwei Ranglisten, ohne Score-Kalibrierung zu erfordern.7 Warum nicht die Roh-Scores direkt kombinieren? BM25 liefert negative Relevanz-Scores (negativer = relevanter in der FTS5-Implementierung von SQLite), während Kosinus-Distanz Werte zwischen 0 und 2 liefert. Diese Skalen zu vergleichen erfordert eine Normalisierung, die empfindlich auf die Abfrageverteilung reagiert. RRF umgeht das Problem vollständig, indem es ausschließlich Rangpositionen verwendet, keine Scores. Die Formel weist jedem Dokument einen Score zu, basierend darauf, wo es in jeder Liste erschien:

score(d) = Σ (weight_i / (k + rank_i))

Dabei ist k eine Konstante (60 in der Implementierung, gemäß dem Original-Paper von Cormack et al.7), rank_i der Rang des Dokuments in Ergebnisliste i und weight_i ein optionaler Multiplikator pro Liste (Standard 1,0 für beide).

Hier ein durchgerechnetes Beispiel mit echten Rängen. Betrachten Sie eine Abfrage: „how does the review aggregator handle disagreements.” Fünf Chunks tauchen in den kombinierten Ergebnissen auf:

Chunk BM25-Rang Vec-Rang BM25 RRF Vec RRF Fusionierter Score
review-aggregator.py „Disagreement Resolution” 3 1 1/63 = 0,0159 1/61 = 0,0164 0,0323
deliberation-config.json „Review Weights” 1 8 1/61 = 0,0164 1/68 = 0,0147 0,0311
code-review MOC „Multi-Agent Review” 7 2 1/67 = 0,0149 1/62 = 0,0161 0,0310
jiro-artisan.sh „Review State Machine” 2 12 1/62 = 0,0161 1/72 = 0,0139 0,0300
quality-loop.md „Evidence Gate” - 3 0 1/63 = 0,0159 0,0159

Der erste Chunk gewinnt, weil er in beiden Listen gut abschneidet. BM25 traf auf „review”, „aggregator” und „disagreements” im Text. Die Vektorsuche traf auf das semantische Konzept der Konfliktlösung im Code Review. Der zweite Chunk lag auf Rang eins bei BM25 (exakter Schlüsselworttreffer auf „review” in der Konfigurationsdatei), aber auf Rang acht bei der Vektorsuche (der JSON-Inhalt der Konfigurationsdatei ist semantisch dünn). RRF stufte ihn angemessen herab. Der letzte Chunk erschien nur in den Vektorergebnissen und erhielt daher nur einen RRF-Score aus einer Quelle.

# retriever.py: RRF fusion core
RRF_K = 60

def _rrf_fuse(self, bm25_results, vec_results,
              bm25_weight=1.0, vec_weight=1.0):
    scores = {}
    for rank, r in enumerate(bm25_results, start=1):
        cid = r["id"]
        if cid not in scores:
            scores[cid] = {"rrf_score": 0.0, ...}
        scores[cid]["rrf_score"] += bm25_weight / (self._rrf_k + rank)
        scores[cid]["bm25_rank"] = rank

    for rank, r in enumerate(vec_results, start=1):
        cid = r["id"]
        if cid not in scores:
            scores[cid] = {"rrf_score": 0.0, ...}
        scores[cid]["rrf_score"] += vec_weight / (self._rrf_k + rank)
        scores[cid]["vec_rank"] = rank

    return [SearchResult(chunk_id=cid, **data)
            for cid, data in scores.items()]

Der Standard-Kandidatenpool umfasst 30 Ergebnisse aus jeder Quelle vor der Fusion, was bis zu 60 Kandidaten ergibt. Der Retriever liefert die Top 10 der fusionierten Ergebnisse. Ein optionaler max_tokens-Parameter kürzt Ergebnisse, um in ein Token-Budget zu passen, wobei mit 4 Zeichen pro Token geschätzt wird.


Indexierung: Vollständig und inkrementell

Der Indexer unterstützt zwei Modi. Vollständige Neuindexierung löscht die Datenbank und baut sie von Grund auf neu auf. Inkrementelle Indexierung vergleicht Dateiänderungszeiten (mtime_ns) mit den gespeicherten Zeitstempeln und verarbeitet nur geänderte Dateien neu.1

# index_vault.py: incremental detection
stale = index.get_stale_files(vault_mtimes)  # mtime changed or new
deleted = index.get_deleted_files(vault_paths)  # no longer in vault

Embedding läuft in Batches von 64 Texten, um den Overhead von Model2Vec zu amortisieren.8 Ein Fortschrittszähler gibt bei vollständiger Neuindexierung alle 500 Dateien eine Meldung aus. Ein SIGINT-Handler ermöglicht ein kontrolliertes Herunterfahren und beendet die aktuelle Datei, bevor der Prozess stoppt.

Die Konfigurationsdatei verwendet ein Allowlist-Modell zur Steuerung der Ordner-Indexierung. Der Vault hat 22 erlaubte Ordner und 5 dauerhaft ausgeschlossene Ordner (persönliche Gesundheitsnotizen, Karrieredokumente, interne Obsidian-Verzeichnisse).20 Der Indexer verarbeitet nur Dateien innerhalb erlaubter Ordner und überspringt alles andere.

Eine kritische Designentscheidung: Der Indexer führt einen Credential-Filter auf jedem Chunk aus, bevor er gespeichert wird. Persönliche Notizen enthalten API-Schlüssel, Bearer-Token, Datenbankverbindungs-Strings und private Schlüssel, die während Debugging-Sitzungen eingefügt wurden. Der Credential-Filter erkennt 21 herstellerspezifische Muster (OpenAI-Schlüssel, GitHub-PATs, AWS-Zugriffsschlüssel, Stripe-Token und 17 weitere) sowie 11 generische Detektoren für Datenbank-URLs, JWTs, Bearer-Token, Passwort-Zuweisungen und Base64-Strings mit hoher Entropie.20 Der Filter ersetzt erkannte Inhalte durch [REDACTED:pattern-name]-Token und protokolliert, welche Muster ausgelöst wurden, aber niemals das Geheimnis selbst.

# chunker.py: credential filtering before storage
cleaned_text, scan_result = clean_content(sub_text)
if not scan_result.is_clean:
    logger.info("Scrubbed %d credential(s) from %s [%s]",
                scan_result.match_count, file_path, sub_heading)

Persönliche Notizen ohne Credential-Filterung zu indexieren würde eine durchsuchbare Datenbank von Geheimnissen erzeugen. Der Filter läuft vor dem Embedding, sodass die Vektorrepräsentationen niemals Credential-Muster kodieren. Eine Abfrage nach „API key” liefert Notizen, die API-Schlüsselverwaltung diskutieren, nicht Notizen, die tatsächliche Schlüssel enthalten.


Was schiefgeht: Ehrliche Fehlermodi

Nach Hunderten von Abfragen gegen den Produktionsindex sind vier Fehlermuster klar erkennbar.

Schlüsselwortdichter, oberflächlicher Inhalt schlägt tiefgehenden Inhalt im Ranking. Eine kurze Notiz mit den Tags security, authentication, oauth und einer Drei-Satz-Zusammenfassung erhält bei BM25 einen höheren Score als ein 2.000 Wörter langer Deep Dive zur OAuth-Implementierung, der die Terminologie einmal in der Einleitung verwendet und dann zu spezifischen Protokolldetails wechselt. BM25 belohnt Termhäufigkeit relativ zur Dokumentlänge, eine Eigenschaft, die Robertson und Zaragoza als „Term-Frequency-Saturation”-Komponente des Algorithmus dokumentiert haben.514 Die oberflächliche Notiz hat eine höhere Schlüsselwortdichte. RRF korrigiert das Problem teilweise, weil die Vektorsuche den tiefgehenden Inhalt höher rankt (das Embedding erfasst die semantische Tiefe), aber die oberflächliche Notiz erscheint immer noch in den fusionierten Ergebnissen, obwohl sie es wahrscheinlich nicht sollte.

Strukturierte Daten werden schlecht indexiert. JSON-Konfigurationsdateien, YAML-Frontmatter-Blöcke und Code-Snippets mit Variablennamen erzeugen minderwertige BM25-Treffer. Eine Suche nach „review configuration” trifft jede JSON-Datei mit einem review-Schlüssel. Die Vektorsuche verarbeitet strukturierte Daten etwas besser, weil das Embedding die Schlüssel-Wert-Beziehungen erfasst, aber strukturierter Inhalt ist grundsätzlich schwieriger zu chunken als Prosa. JSON vor dem Embedding zu Schlüsselpfad:Wert-Paaren abzuflachen würde die Retrieval-Qualität für konfigurationslastige Notizen verbessern.

Chunk-Grenzen zerschneiden den Kontext. Der Chunker teilt einen Absatz, der die Grenze zwischen zwei H2-Abschnitten überspannt, in zwei Chunks. Jeder Chunk enthält die Hälfte der Erklärung. Keiner der Chunks wird gut eingebettet, weil dem Embedding der vollständige Kontext fehlt. Der Chunker mildert das Problem durch Überschriftenkontext (die übergeordnete Überschrift wird in die Metadaten übertragen), aber der Body-Text verliert an der Grenze dennoch seine Kontinuität. Überlappende Fenster würden helfen, erhöhen aber die Chunk-Anzahl und Datenbankgröße.

Zeitliche Relevanz ist unsichtbar. Der Retriever hat kein Konzept von Aktualität. Eine Notiz von vor 14 Monaten über eine frühe Architekturentscheidung wird genauso gerankt wie eine Notiz von gestern über die aktuelle Implementierung. Für eine Wissensdatenbank, die sich weiterentwickelt, ersetzen neuere Notizen oft ältere. Der Retriever weiß das nicht.


Was als Nächstes kommt: Die Erweiterungs-Roadmap

Fünf Ergänzungen würden die Fehlermodi adressieren und die Fähigkeiten des Systems erweitern.

Learning-to-Rank-Reranking-Schicht. Nach der RRF-Fusion könnte ein leichtgewichtiger Reranker die Scores auf Basis von Metadaten-Signalen anpassen: Aktualität der Notiz, Tag-Relevanz zur Abfragedomäne, Link-Dichte (stark verlinkte Notizen sind oft autoritativer). Der Reranker würde auf den fusionierten Top-30-Ergebnissen laufen, nicht auf dem gesamten Korpus, und die Latenz unter der 23-ms-Baseline halten.

Abfrageintent-Klassifikation. Verschiedene Abfragen benötigen verschiedene Retrieval-Strategien. Ein exakter Bezeichner-Lookup (_rrf_fuse) sollte BM25 stark gewichten. Eine konzeptionelle Frage („how does review handle disagreements”) sollte die Vektorsuche gewichten. Ein leichtgewichtiger Klassifikator, der bm25_weight und vec_weight pro Abfrage anpasst, würde die Präzision verbessern, ohne die Fusionsarchitektur zu ändern.

Zeitlicher Abklingfaktor. Neuere Notizen bei Abfragen zum aktuellen Stand leicht höher gewichten. Eine Abklingfunktion, die nach der Fusion angewendet wird, würde den Score von Chunks aus Dateien reduzieren, die seit mehr als N Monaten nicht mehr geändert wurden. Der mtime_ns-Zeitstempel existiert bereits im Schema; der Abklingfaktor benötigt nur eine Gewichtungsfunktion im Retriever.

Evaluierungs-Harness mit Golden Queries. Das System hat derzeit keine automatisierte Qualitätsmessung. Ein Satz von 50–100 kuratierten Abfrage-Antwort-Paaren würde Retrieval-Qualitäts-Regressionstests ermöglichen: die Testsuite nach jeder Änderung an Chunking, Embedding oder Fusionsparametern ausführen und verifizieren, dass Recall@10 sich nicht verschlechtert. Der BEIR-Benchmark zeigte, dass Retrieval-Systeme über verschiedene Abfrageverteilungen hinweg um mehr als 20 Punkte in nDCG@10 variieren können, was domänenspezifische Evaluation unerlässlich macht.19 Ohne ein Golden Set sind Verbesserungen anekdotisch.

Querverknüpfungs-Indexierung über Notizen hinweg. Obsidian-Wiki-Links ([[note-name]]) kodieren explizite Beziehungen zwischen Notizen. Das aktuelle System ignoriert die Link-Struktur vollständig. Link-Ziele als Metadaten zu indexieren würde dem Retriever ermöglichen, Chunks aus Notizen höher zu gewichten, auf die viele andere hochbewertete Notizen verlinken — ähnlich wie PageRank für den Vault.

Die Embedding-Space-Topologie-Analyse, die ich auf dem gesamten Vault durchgeführt habe, zeigt, wo diese Verbesserungen den größten Effekt hätten. Dichte Cluster (AI-Tooling, Security) liefern bereits gute Retrieval-Ergebnisse, weil die Terminologie konsistent ist. Dünn besetzte Brückenregionen zwischen Clustern sind der Bereich, in dem der Retriever am meisten Schwierigkeiten hat — und wo Querverknüpfungs-Indexierung und Intent-Klassifikation die größten Gewinne bringen würden.


FAQ

Warum SQLite statt einer dedizierten Vektordatenbank?

Der gesamte Retrieval-Stack läuft in einer Datei ohne externe Abhängigkeiten. SQLites WAL-Modus verarbeitet parallele Lesezugriffe aus mehreren Claude Code-Sitzungen. Die sqlite-vec-Erweiterung fügt Vektor-KNN-Suche hinzu, ohne eine separate Pinecone-, Weaviate- oder Qdrant-Instanz zu benötigen.4 Bei 49.746 Chunks beträgt die Abfragelatenz 23 ms.1 Eine dedizierte Vektordatenbank würde operationale Komplexität hinzufügen (Hosting, Backups, Authentifizierung) für eine Einbenutzer-Wissensdatenbank, die in 83 MB passt.

Warum Model2Vec statt OpenAI-Embeddings oder einem größeren Modell?

Drei Gründe: Latenz, Datenschutz und Kosten. Model2Vec läuft lokal mit CPU-Geschwindigkeit ohne Netzwerkaufruf.3 Persönliche Notizen verlassen niemals den Rechner. API-basierte Embeddings würden bei der aktuellen Vault-Größe etwa 0,30 $ pro vollständiger Neuindexierung kosten,11 isoliert betrachtet vernachlässigbar, aber die Round-Trip-Latenz über 49.746 Chunks und die Datenschutzexposition persönlicher Inhalte sind die eigentlichen Kosten.

Was ist Reciprocal Rank Fusion und wann sollte man es verwenden?

RRF erfordert keine Trainingsdaten, keine Score-Kalibrierung und kein Hyperparameter-Tuning über die Konstante k hinaus.7 Ein gelerntes Fusionsmodell würde gelabelte Relevanzurteile zum Training erfordern, die für eine persönliche Wissensdatenbank nicht existieren. RRF ist die Fusionsmethode mit der niedrigsten Einstiegshürde für brauchbare Ergebnisse. Verwenden Sie RRF, wenn Sie Ranglisten aus Retrieval-Methoden kombinieren, die inkompatible Score-Typen erzeugen.

Wie verbindet sich ein lokaler Retriever mit Claude Code?

Ein PreToolUse-Hook ruft die search()-Methode des Retrievers mit dem aktuellen Prompt auf, formatiert die Top-Ergebnisse als Kontextblock mit Dateipfaden und Abschnittsüberschriften und injiziert den Block in die Konversation. Der Agent sieht fokussierte Chunks, keine rohen Dateien. Ein max_tokens-Parameter stellt sicher, dass der injizierte Kontext in ein Budget passt.

Wie verhindert man, dass Geheimnisse in einem Retrieval-System indexiert werden?

Führen Sie einen Credential-Filter auf jedem Chunk vor der Speicherung aus. Der Filter in diesem System erkennt 21 herstellerspezifische Muster und 11 generische Detektoren für JWTs, Bearer-Token und private Schlüssel.20 Er ersetzt erkannte Inhalte durch [REDACTED:pattern-name]-Token und läuft vor dem Embedding, sodass Vektorrepräsentationen niemals Credential-Muster kodieren.


Referenzen


  1. Produktionsdaten des Autors. 49.746 Chunks, 16.894 Dateien, 83,56 MB SQLite-Datenbank, 7.771 Signale verarbeitet über 14 Monate. Abfragelatenz (23 ms) gemessen via time.perf_counter() in retriever.py, umfassend den gesamten Suchpfad: BM25-Lookup, Query-Embedding via Model2Vec, Vektor-KNN-Suche und RRF-Fusion. grep -rl gemessen mit 11–66 Sekunden abhängig von der Termhäufigkeit (Apple M3 Pro, APFS). Vollständige Neuindexierung gemessen mit ca. 4 Minuten auf Apple M3 Pro. Inkrementell gemessen mit <10 Sekunden für typische tägliche Änderungen. Reine FTS5-Suche wurde für den Autor ab ca. 3.000 Dateien durch Schlüsselwortkollisionsraten unbrauchbar. 

  2. HN-Thread: „Stop Burning Your Context Window”. Kommentare von danw1979 und tclancy mit der Bitte um einen detaillierten Artikel. 

  3. Model2Vec: Distill a Small Fast Model from any Sentence Transformer. Minish Lab, 2024. Das potion-base-8M-Modell verwendet statische Wort-Embeddings, die aus einem Sentence Transformer destilliert wurden, und erzeugt 256-dimensionale Vektoren ohne Attention-Schichten. 

  4. sqlite-vec: A vector search SQLite extension. Alex Garcia, 2024. Stellt vec0-Virtual-Tables für KNN-Vektorsuche innerhalb von SQLite bereit, mit derselben Abfrageschnittstelle wie Standardtabellen. 

  5. SQLite FTS5 Extension. SQLite-Dokumentation. FTS5 bietet Volltextsuche mit BM25-Ranking, Content-Sync-Tabellen und konfigurierbare Spaltengewichtungen über die bm25()-Hilfsfunktion. 

  6. Reimers, N. und Gurevych, I. Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. EMNLP, 2019. Grundlagenarbeit zu dichter semantischer Ähnlichkeit für Text-Retrieval, die den Vektorsuchansatz etablierte, der in hybriden Retrieval-Systemen verwendet wird. 

  7. Cormack, G.V., Clarke, C.L.A. und Buettcher, S. Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods. SIGIR, 2009. Führt RRF mit k=60 als parameterfreie Methode zum Kombinieren von Ranglisten ein, die trainierte Fusionsmodelle übertrifft. 

  8. Implementierung des Autors. chunker.py teilt an H2-Grenzen in der _split_at_headings-Funktion, mit Fallback auf H3- dann Absatzteilung für Abschnitte über 2.000 Zeichen. MIN_CHUNK_CHARS=30, MAX_CHUNK_CHARS=2000. index_vault.py embeddet in Batches von 64 (BATCH_SIZE=64). 

  9. van Dongen, T. et al. Model2Vec: Turn any Sentence Transformer into a Small Fast Model. arXiv, 2025. Beschreibt den Destillationsansatz, der statische Embeddings aus Sentence Transformern mit 50–500-facher Inferenzbeschleunigung erzeugt. 

  10. Messung des Autors. 256-dimensionale Vektoren bei 49.746 Chunks ergeben 83 MB SQLite. Hochrechnung auf 768-dimensionale Vektoren: ~215 MB. Auf 1024-dimensionale: ~280 MB. Marginaler Qualitätsgewinn bei kurzen Markdown-Chunks (durchschnittlich 200–400 Wörter) rechtfertigt den Speicher- und Latenzanstieg nicht. 

  11. OpenAI Embeddings Pricing. text-embedding-3-small: 0,02 $ pro Million Token. Geschätzte Vault-Kosten pro vollständiger Neuindexierung: ~0,30 $ basierend auf durchschnittlicher Chunk-Länge von ~200 Token. 

  12. SQLite Write-Ahead Logging. SQLite-Dokumentation. WAL-Modus erlaubt parallele Leser mit einem einzigen Schreiber, geeignet für das leseintensive Zugriffsmuster des Retrievers. 

  13. Abfrage-Trace des Autors. „PostToolUse hook for context compression” gegen BM25-only, Vector-only und Hybrid-Modus ausgeführt. Ergebnisse erfasst aus retriever.py mit method-Feld, das verfolgt, welcher Suchpfad jedes Ergebnis erzeugte. 

  14. Robertson, S. und Zaragoza, H. The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval, 2009. Überblick über die BM25-Familie von Ranking-Funktionen und ihre theoretischen Grundlagen. 

  15. Karpukhin, V. et al. Dense Passage Retrieval for Open-Domain Question Answering. EMNLP, 2020. Zeigte, dass gelernte dichte Repräsentationen BM25 bei Open-Domain-QA-Benchmarks um 9–19 % übertreffen, und etablierte Dense Retrieval als Ergänzung zur lexikalischen Suche. 

  16. Luan, Y. et al. Sparse, Dense, and Attentional Representations for Text Retrieval. TACL, 2021. Analyse von hybridem Sparse-Dense-Retrieval auf MS MARCO, die konsistente Verbesserungen gegenüber Einzelmodalitäts-Ansätzen zeigt. 

  17. MTEB: Massive Text Embedding Benchmark. Muennighoff, N. et al., 2023. potion-base-8M erreicht 50,03 durchschnittlichen MTEB vs. 56,09 für all-MiniLM-L6-v2 (89,2 % Beibehaltung). Aufschlüsselung pro Aufgabe: Classification 64,44, Clustering 32,93, Retrieval 31,71, STS 73,24. Quelle: Model2Vec-Ergebnisse

  18. Gao, Y. et al. Retrieval-Augmented Generation for Large Language Models: A Survey. arXiv, 2024. Überblick über RAG-Architekturen einschließlich Analyse von Chunking-Strategien und deren Auswirkung auf die Retrieval-Qualität. 

  19. Thakur, N. et al. BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. NeurIPS, 2021. Zeigt hohe Varianz in der Retrieval-Leistung über verschiedene Domänen und unterstreicht die Notwendigkeit domänenspezifischer Evaluation. 

  20. Konfiguration und Credential-Filter-Implementierung des Autors. memory-config.json definiert 22 allowed_folders und 5 excluded_always-Einträge. credential_filter.py definiert 21 herstellerspezifische CREDENTIAL_PATTERNS (OpenAI bis Turnstile) plus 9 generische Einzeiler-Muster (DB-URLs, Bearer-Token, JWTs, Passwörter, Geheimnisse, API-Schlüssel, Auth-Token, Base64-Geheimnisse) und 2 mehrzeilige Muster (RSA/SSH-Privatschlüssel, PGP-Schlüssel). Insgesamt: 32 Muster. 

Verwandte Beiträge

The Blind Judge: Scoring Claude Code vs Codex in 36 Duels

Claude Code vs Codex CLI, scored blind on 5 dimensions across 36 duels. The winner matters less than the synthesis combi…

14 Min. Lesezeit

Thinking With Ten Brains: How I Use Agent Deliberation as a Decision Tool

You cannot debias yourself by trying harder. 10 AI agents debating each other is a structural intervention for better de…

15 Min. Lesezeit

Topology of a Second Brain: What 15,000 Signals Look Like in Embedding Space

15,800 notes in embedding space reveal three knowledge topologies. Each has different failure modes practitioners can di…

15 Min. Lesezeit