為16,894個Obsidian檔案建構混合檢索器
對16,894個markdown檔案執行grep搜尋需要11至66秒(取決於搜尋詞),並且會回傳數百個低相關性的結果。向量搜尋能回傳語意相關的內容,卻會遺漏您輸入的確切函式名稱。而融合兩種方法的混合檢索器,能在23毫秒內(端對端,包含查詢嵌入)從單一83 MB的SQLite檔案中回傳正確答案,且完全不需要API呼叫。1
狂熱筆記者的問題不在於收集,問題在於檢索。Obsidian讓記錄變得毫無阻力。當檔案累積到一定數量,知識庫便成了唯寫資料庫:容易新增,卻無法查詢。按檔名搜尋在檔名變得毫無意義時便失效。全文搜尋在同一個關鍵字出現在400份文件中時便失效。標籤在您忘記標記某些內容時便失效。
一位HN評論者詢問了我為Obsidian知識庫建構的檢索系統的完整架構。2以下便是全部內容:分塊策略、嵌入模型、雙索引SQLite結構、附帶實際數據的融合數學,以及我在對系統執行數百次查詢後發現的失敗模式。
摘要
此檢索器將FTS5 BM25關鍵字搜尋與Model2Vec向量相似度搜尋結合,透過倒數排名融合(Reciprocal Rank Fusion, RRF)合併為單一排序列表。所有內容都在一個SQLite資料庫中本機執行:來自16,894個檔案的49,746個區塊,僅佔83 MB。完整重建索引需要四分鐘。增量更新在十秒內完成。系統透過hooks與Claude Code整合,讓代理程式無需將檔案載入上下文即可存取知識庫。BM25捕捉精確識別碼和函式名稱。向量搜尋捕捉跨越不同術語的語意匹配。RRF合併兩者的結果而無需分數校準。坦白說,其代價是:標籤完善但內容淺薄的筆記可能排名高於結構不佳的深度內容,因為BM25獎勵的是關鍵字密度,而非深度。
重點摘要
適合擁有大型知識庫的筆記者。 根據我的經驗,當檔案超過數千個時,僅靠全文搜尋已無法使用——而現有的Obsidian搜尋外掛(Smart Connections、Omnisearch)僅在應用程式內建立索引,無法作為外部程式庫供其他工具查詢。1在BM25之上加入向量搜尋,能捕捉那些您記得概念卻忘了關鍵字的查詢。檢索器完全在SQLite上執行,無需外部服務、無需GPU、無需API費用。Model2Vec以CPU速度進行嵌入,因為該模型是30 MB的靜態詞向量,而非transformer。3
適合建構檢索系統的開發者。 RRF是需要最少調參的融合方法。公式僅使用排名位置而非原始分數,因此您永遠不需要校準BM25分數與餘弦距離之間的對應關係。從k=60和等權重開始。只在根據自己的資料量測失敗案例後才進行調整。sqlite-vec擴充套件將向量KNN搜尋帶入SQLite,無需獨立的向量資料庫。4
適合Claude Code使用者。 檢索器作為程式庫透過hooks呼叫。PreToolUse hook在代理程式開始工作前查詢知識庫。代理程式看到的是2-3 KB帶有檔案路徑歸屬的精準結果,而非載入完整檔案。此整合保持上下文視窗精簡,同時讓代理程式存取16,894個檔案的知識。
最小可行版本。 最簡單的起點:為您的markdown檔案建立FTS5虛擬表(僅BM25,無需嵌入)。當關鍵字搜尋開始遺漏語意匹配時,加入sqlite-vec和Model2Vec。最後加入RRF融合。每一層都可獨立運作。完整技術堆疊僅需Python 3、一個30 MB的模型下載,以及pip install model2vec sqlite-vec。無需GPU、無需Docker、無需外部服務。16,894個檔案的總磁碟佔用:83 MB。
想要完整的操作指南? Obsidian AI基礎架構參考涵蓋知識庫架構、外掛設定、MCP伺服器設定、增量索引配方與疑難排解——這是本文架構深入解析的逐步實作指南。
為什麼僅靠關鍵字搜尋在規模化時會失效
全文搜尋在知識庫規模下會以可預見的方式崩潰。FTS5搭配BM25排名在精確匹配方面表現優異:搜尋requestAnimationFrame,每個包含該確切詞元的檔案都會出現,並按詞頻和文件長度排名。5 Robertson與Zaragoza對機率相關性模型的綜述證實了BM25的優勢:該演算法在關鍵字密集型查詢上表現良好,且僅需最少的參數調整。14其失敗模式在於同義詞和概念匹配。搜尋「如何處理認證失敗」,BM25會回傳每個分別提及「認證」或「失敗」的檔案,結果被大量僅有邊際相關的內容所稀釋。
向量搜尋解決了同義詞問題。將查詢嵌入後,找出嵌入向量在向量空間中距離接近的區塊。「如何處理認證失敗」能匹配到關於「登入錯誤復原」和「工作階段過期處理」的內容,因為嵌入捕捉了跨越不同術語的語意相似性。6 Karpukhin等人透過Dense Passage Retrieval(DPR)證明,密集嵌入在開放域問答的top-20準確率上超越BM25達9-19%,正是因為密集表示捕捉了超越詞彙重疊的語意。15其失敗模式恰恰相反:向量搜尋會遺漏精確識別碼。搜尋函式名稱_rrf_fuse,向量搜尋會回傳關於融合和排名演算法的內容,但可能將實際的函式定義排在概念性解釋之後。
兩種方法都無法單獨涵蓋兩種失敗模式。以下是一個說明差異的單一查詢範例(並非優越性的證明——彙總評估需要黃金標準集,而系統目前尚未建立)。查詢「PostToolUse hook for context compression」從每種方法回傳不同的前三名結果:
| 排名 | 僅BM25 | 僅向量 | 混合(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找到了確切的hook檔案和設定參考(關鍵字匹配「PostToolUse」),但遺漏了概念性的上下文工程筆記。向量搜尋找到了壓縮策略筆記(語意匹配「context compression」),但遺漏了具體的hook實作。RRF將對概念和實作都重要的筆記提升排名,將策略筆記和hook檔案分別置於第一和第二位。13
MS MARCO段落排名的研究在網路搜尋基準測試中支持此模式:混合檢索始終優於單獨使用BM25或密集檢索,在同時包含特定詞彙和抽象概念的查詢上增益最大。716
架構:三個相互增強的層次
系統有三個獨立的層次。每層都可獨立運作,但組合在一起時效果倍增。
第一層:輸入。 一個733行的Python評分管線根據四個維度對每個輸入訊號進行評分:相關性、可操作性、深度和權威性。得分0.55以上的訊號自動路由到12個領域資料夾之一。得分介於0.40至0.55之間的排入人工審核佇列。低於0.40的則被丟棄。該管線在14個月內處理了7,771個訊號,無需人工標記。1輸入層決定什麼進入知識庫。檢索層使其變得可搜尋。
第二層:檢索。 下文詳細介紹的混合搜尋引擎。引擎在標題邊界處對每個檔案進行分塊,使用Model2Vec嵌入區塊,並在SQLite中同時以vec0表進行向量KNN和FTS5虛擬表進行BM25索引。查詢同時對兩個索引執行,RRF將結果融合為單一排序列表。
第三層:整合。 將檢索器接入代理程式工作流程的Claude Code hooks。Hook在提交提示時觸發,查詢知識庫以獲取相關上下文,並將最佳結果注入對話中。代理程式看到的是帶有來源歸屬的精準區塊,而非原始檔案內容:
# 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...
每個結果攜帶章節標題和來源專案,上限為500個詞元的預算以避免上下文膨脹。
檢索器還啟用了第二個整合點:一個PostToolUse hook,在工具輸出進入對話前進行壓縮。原始工具輸出包含時間戳記、排序產物和冗長的格式,這些在每次執行間都會變化。檢索器以穩定、聚焦的子集取代原始輸出。代理程式不會看到雜訊,只看到相關的摘要。一個附帶好處是:由於檢索器的輸出對相同查詢是確定性的(相同的索引狀態產生相同的排名結果),壓縮後的輸出有助於提示快取。對未變更資料的重複查詢會產生相同的上下文區塊,而CLI的自動提示快取會重複使用已快取的前綴。
更廣泛的基礎架構故事解釋了hooks、skills和agents如何組合成圍繞模型的可程式化層。
這些層次在設計上是解耦的。輸入評分不了解嵌入。檢索器不了解訊號路由規則。但輸入確保知識庫包含高品質內容,檢索找到任何查詢的正確子集,而整合將該子集傳遞給代理程式而不造成上下文膨脹。我曾撰文探討上下文作為關鍵資源的理論框架。檢索器是其實際實作。
分塊:檢索品質的起點
分塊決定了搜尋結果的粒度。區塊太大,向量搜尋會回傳整個檔案,而其中只有一個段落是相關的。區塊太小,嵌入會失去語意匹配所需的上下文。RAG管線的研究證實,對於大多數使用情境,區塊大小對檢索品質的影響大於模型選擇,200-500個詞元的區塊在段落級檢索任務中表現最佳。18
分塊器在H2(##)標題邊界處分割,保留markdown結構。8一篇關於OAuth詞元輪換的筆記有三個H2段落,就會產生三個區塊,每個區塊都足夠自包含,讓嵌入能捕捉其含義。索引器將標題文字和父級筆記標題作為後設資料與每個區塊一同儲存,即使區塊文字本身較為稀疏,也能為BM25匹配提供上下文。
# 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
分塊器對超過2,000個字元的段落進行進一步分割:先在H3邊界處,然後在段落分隔處。它會丟棄少於30個字元的段落。分塊器也會跳過Related、See Also、Links和References段落,這些通常是wiki連結列表而非可搜尋的內容。
兩個設計選擇對檢索品質至關重要。首先,索引器將標題上下文字串("OAuth Token Rotation | note | security, authentication")儲存在獨立欄位中,並以較低的權重(0.3)在FTS5中索引,而區塊文字的權重為1.0。當區塊本文不包含搜尋詞時,BM25仍可匹配標題,但標題匹配的分數低於本文匹配。其次,分塊器提取frontmatter標籤和筆記類型並將其納入標題上下文,因此搜尋「security」時能匹配標記了security標籤的筆記,即使本文使用了不同的術語。
嵌入:30 MB模型、零次API呼叫
嵌入模型是Model2Vec的potion-base-8M,一個擁有760萬參數、產生256維向量的靜態詞嵌入模型。3在MTEB基準測試套件中,potion-base-8M達到all-MiniLM-L6-v2效能的89%(平均50.03 vs 56.09),推論速度快達500倍,使其適合在消費級硬體上索引大型語料庫。917一個需要注意的地方:該模型的MTEB檢索子分數明顯較低(31.71),相比分類(64.44)或STS(73.24)。MTEB的檢索基準測試的是網路語料庫上的文件級排名,而非同質markdown區塊上的段落級匹配。當區塊較短、主題聚焦且使用一致的詞彙時,這個差距影響較小。與基於transformer的嵌入模型不同,Model2Vec不對輸入執行注意力層。該模型將句子transformer的知識蒸餾為靜態詞元嵌入,透過加權平均而非序列計算產生向量。9
為什麼靜態嵌入適用於此使用情境?短markdown區塊(平均200-400個詞)包含關於單一主題的集中詞彙。這些詞元向量的加權平均值落在嵌入空間中有意義的區域,因為幾乎沒有離題稀釋。實務上,一篇涵蓋三個不同主題的2,000詞文件傾向產生一個模糊的質心,位於主題叢集之間而非其中之一。相比之下,一個關於OAuth詞元輪換的區塊產生的向量與其他認證內容緊密聚集。靜態嵌入犧牲了上下文消歧(「bank」在「river bank」vs「bank account」中的區別)以換取原始速度。在個人知識庫中,每個區塊涵蓋一個概念,歧義的代價很小,而論文報告高達500倍的推論加速。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]
實際效果:在Apple M3 Pro上,16,894個檔案的完整重建索引在四分鐘內完成。增量索引(僅處理變更的檔案,透過mtime比較偵測)在一般每日編輯量下不到十秒即可完成。1
模型運行在~/.claude/venvs/memory/的隔離虛擬環境中,以避免與其餘工具鏈的依賴衝突。嵌入器在首次使用時才延遲載入模型,而非在匯入時,因此當檢索器退回到僅BM25模式時,匯入模組的成本為零。
為什麼不用更大的模型?兩個原因。首先,256維向量使SQLite資料庫在49,746個區塊時僅佔83 MB。更高維度的向量(768或1,024)會使資料庫大小增加三到四倍,而對短markdown區塊的品質提升微乎其微。10其次,基於API的嵌入(例如OpenAI的text-embedding-3-small,每百萬詞元$0.02)引入了延遲、成本和網路依賴,而系統應該能夠離線運作。11整個知識庫重新嵌入以API價格計算約需$0.30,單獨看微不足道,但真正的成本是往返延遲乘以49,746個區塊,以及將個人筆記傳送至外部API的隱私問題。
模型雜湊機制追蹤嵌入相容性。索引器儲存一個由模型名稱和詞彙量大小衍生的雜湊值。如果模型變更,增量索引會偵測到不匹配並自動觸發完整重建索引。
SQLite結構:三張表、一個檔案
整個索引存放在一個SQLite檔案中(vectors.db,83 MB),使用WAL模式以確保並行讀取安全。12三張表服務於不同目的:
-- 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]
);
FTS5表使用content-sync模式:它直接參照chunks表而非儲存文字的副本。5有一個需要注意的問題:content-sync表不會自動傳播刪除操作。索引器必須在從chunks表移除列之前發出明確的INSERT INTO chunks_fts(chunks_fts, rowid) VALUES('delete', ?)命令,否則FTS5索引會靜默地變得不一致。BM25查詢中的欄位權重將區塊文字設為1.0、段落標題設為0.5、標題上下文設為0.3:
# vector_index.py: BM25 search with column weights
bm25(chunks_fts, 1.0, 0.5, 0.3) as score
sqlite-vec擴充套件將256維浮點向量以打包二進位資料儲存,並支援餘弦距離的KNN查詢。4Python的struct.pack用於序列化向量:
def _serialize_vector(vec):
return struct.pack(f"{len(vec)}f", *vec)
結構設計支援優雅降級。如果sqlite-vec載入失敗(缺少擴充套件、平台不相容),檢索器會退回到僅BM25搜尋。vec_available屬性追蹤向量搜尋是否可用。
倒數排名融合:使其運作的數學原理
RRF合併兩個排名列表而無需分數校準。7為什麼不直接組合原始分數?BM25回傳負值相關性分數(在SQLite的FTS5實作中,越負表示越相關),而餘弦距離回傳0到2之間的值。比較這些量度需要對查詢分佈敏感的正規化。RRF透過僅使用排名位置而非分數,完全迴避了這個問題。公式根據每個文件在各列表中的出現位置為其分配分數:
score(d) = Σ (weight_i / (k + rank_i))
其中k是常數(實作中為60,遵循Cormack等人的原始論文7),rank_i是文件在結果列表i中的排名,weight_i是每個列表的可選乘數(兩者預設為1.0)。
以下是一個附帶真實排名的計算範例。考慮一個查詢:「how does the review aggregator handle disagreements」。五個區塊出現在合併結果中:
| 區塊 | BM25排名 | Vec排名 | BM25 RRF | Vec RRF | 融合分數 |
|---|---|---|---|---|---|
| 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 |
第一個區塊勝出,因為它在兩個列表中排名都很靠前。BM25匹配了文字中的「review」、「aggregator」和「disagreements」。向量搜尋匹配了程式碼審查中衝突解決的語意概念。第二個區塊在BM25中排名第一(設定檔中「review」的精確關鍵字匹配),但在向量搜尋中排第八(設定JSON的語意較為稀疏)。RRF適當地將其排名拉低。最後一個區塊僅出現在向量結果中,因此只從一個來源獲得RRF分數。
# 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()]
預設候選池為融合前每個來源30個結果,最多產生60個候選項。檢索器回傳融合後的前10個結果。可選的max_tokens參數會截斷結果以符合詞元預算,以每詞元4個字元進行估算。
索引建立:完整與增量
索引器支援兩種模式。完整重建索引會清除資料庫並從頭重建。增量索引比較檔案修改時間(mtime_ns)與儲存的時間戳記,僅重新處理變更的檔案。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
嵌入以64個文字為一批進行,以分攤Model2Vec的額外開銷。8在完整重建索引期間,每500個檔案列印一次進度計數。SIGINT處理程式實現優雅關閉,在停止前完成當前檔案的處理。
設定檔使用允許清單模式來控制資料夾索引。知識庫有22個允許的資料夾和5個永久排除的資料夾(個人健康筆記、職涯文件、Obsidian內部目錄)。20索引器僅處理允許資料夾中的檔案,跳過其他所有內容。
一個關鍵的設計選擇:索引器在儲存每個區塊之前執行憑證過濾器。個人筆記包含除錯期間貼上的API金鑰、bearer詞元、資料庫連線字串和私密金鑰。憑證過濾器匹配21種供應商特定模式(OpenAI金鑰、GitHub PAT、AWS存取金鑰、Stripe詞元及其他17種)加上11種通用偵測器,涵蓋資料庫URL、JWT、bearer詞元、密碼指派和高熵base64字串。20過濾器以[REDACTED:pattern-name]詞元替換匹配內容,並記錄觸發了哪些模式,但絕不記錄密鑰本身。
# 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)
在不進行憑證過濾的情況下索引個人筆記,會建立一個可搜尋的密鑰資料庫。過濾器在嵌入之前執行,因此向量表示永遠不會編碼憑證模式。搜尋「API金鑰」會回傳討論API金鑰管理的筆記,而非包含實際金鑰的筆記。
出了什麼問題:誠實的失敗模式
在對生產索引執行數百次查詢後,四種失敗模式已經明確。
關鍵字密集的淺層內容排名高於深度內容。 一篇標記了security, authentication, oauth的短筆記,即使只有三句話的摘要,在BM25中的分數也高於一篇2,000字的OAuth實作深度解析——後者僅在引言中使用了一次該術語,然後轉向具體的協定細節。BM25獎勵的是相對於文件長度的詞頻,這是Robertson和Zaragoza記錄為演算法「詞頻飽和」元件的特性。514淺層筆記的關鍵字密度更高。RRF部分修正了這個問題,因為向量搜尋會將深度內容排名更高(嵌入捕捉了語意深度),但淺層筆記仍然出現在融合結果中,而它可能本不應該出現。
結構化資料索引效果不佳。 JSON設定檔、YAML frontmatter區塊和帶有變數名稱的程式碼片段會產生低品質的BM25匹配。搜尋「review configuration」會匹配每個包含review鍵的JSON檔案。向量搜尋處理結構化資料略好一些,因為嵌入捕捉了鍵值關係,但結構化內容在分塊上本質上比散文更困難。在嵌入前將JSON扁平化為鍵路徑:值的格式,可以改善設定密集型筆記的檢索品質。
區塊邊界切割了上下文。 分塊器將橫跨兩個H2段落邊界的段落切成兩個區塊。每個區塊包含一半的解釋。兩個區塊的嵌入效果都不好,因為嵌入缺少完整的上下文。分塊器透過標題上下文(將父標題帶入後設資料)來緩解此問題,但本文在邊界處仍然失去連續性。重疊視窗會有所幫助,但會增加區塊數量和資料庫大小。
時間相關性是不可見的。 檢索器沒有近期性的概念。一篇14個月前關於早期架構決策的筆記,與一篇昨天關於當前實作的筆記排名相同。對於一個不斷演進的知識庫,較新的筆記通常會取代較舊的筆記。檢索器不知道這一點。
下一步:擴展路線圖
五項新增功能將解決上述失敗模式並擴展系統的能力。
學習排名的重新排序層。 在RRF融合之後,一個輕量級的重新排序器可以根據後設資料訊號調整分數:筆記近期性、標籤與查詢領域的相關性、連結密度(被大量連結的筆記通常更具權威性)。重新排序器將在融合後的前30個結果上執行,而非整個語料庫,保持延遲在23毫秒基準以下。
查詢意圖分類。 不同的查詢需要不同的檢索策略。精確識別碼查找(_rrf_fuse)應重度加權BM25。概念性問題(「review如何處理分歧」)應加權向量搜尋。一個輕量級分類器根據每個查詢調整bm25_weight和vec_weight,可以在不改變融合架構的情況下提升精確度。
時間衰減。 對於關於當前狀態的查詢,略微提高較新筆記的權重。在融合後應用的衰減函式將降低最後修改時間超過N個月的檔案中區塊的分數。mtime_ns時間戳記已存在於結構中;衰減只需要在檢索器中加入一個加權函式。
帶有黃金查詢集的評估框架。 系統目前沒有自動化的品質衡量。一組50-100個精心策劃的查詢-答案配對,將能夠進行檢索品質的回歸測試:在任何分塊、嵌入或融合參數的變更後執行測試套件,驗證recall@10沒有下降。BEIR基準測試證明,檢索系統在不同查詢分佈上的nDCG@10可能相差20+分,這凸顯了領域特定評估的必要性。19沒有黃金標準集,改進只是軼事性的。
跨筆記關係索引。 Obsidian的wiki連結([[note-name]])編碼了筆記之間的顯式關係。目前系統完全忽略了連結結構。將連結目標作為後設資料索引,將讓檢索器能提升那些被許多其他高分筆記連結的區塊——類似知識庫的PageRank。
我對完整知識庫進行的嵌入空間拓撲分析揭示了這些改進在哪些方面影響最大。密集叢集(AI工具、安全性)檢索效果已經很好,因為術語是一致的。叢集之間的稀疏橋接區域是檢索器最為吃力的地方,也是關係索引和意圖分類能帶來最大增益之處。
常見問題
為什麼選擇SQLite而非專用向量資料庫?
整個檢索堆疊在一個檔案中執行,零外部依賴。SQLite的WAL模式處理來自多個Claude Code會話的並行讀取。sqlite-vec擴充套件新增向量KNN搜尋,無需獨立的Pinecone、Weaviate或Qdrant實例。4在49,746個區塊下,查詢延遲為23毫秒。1專用向量資料庫會增加運維複雜度(託管、備份、認證),而這僅是一個適合放在83 MB中的單一使用者知識庫。
為什麼選擇Model2Vec而非OpenAI嵌入或更大的模型?
三個原因:延遲、隱私和成本。Model2Vec以CPU速度在本機執行,無需網路呼叫。3個人筆記永遠不會離開機器。基於API的嵌入以當前知識庫大小計算,每次完整重建索引約需$0.3011,單獨看微不足道,但49,746個區塊的往返延遲和個人內容的隱私暴露才是真正的成本。
什麼是倒數排名融合,何時應該使用它?
RRF不需要訓練資料、不需要分數校準,且除了常數k之外不需要超參數調整。7學習型融合模型需要標記的相關性判斷來訓練,而個人知識庫不存在這些。RRF是產生有用結果門檻最低的融合方法。當您需要合併來自產生不相容分數類型的檢索方法的排名列表時,請使用RRF。
本機檢索器如何連接到Claude Code?
PreToolUse hook使用當前提示呼叫檢索器的search()方法,將最佳結果格式化為帶有檔案路徑和段落標題的上下文區塊,並將該區塊注入對話中。代理程式看到的是精準的區塊,而非原始檔案。max_tokens參數確保注入的上下文符合預算。
如何防止密鑰被索引到檢索系統中?
在儲存之前對每個區塊執行憑證過濾器。此系統中的過濾器匹配21種供應商特定模式和11種通用偵測器,涵蓋JWT、bearer詞元和私密金鑰。20它以[REDACTED:pattern-name]詞元替換匹配內容,並在嵌入之前執行,因此向量表示永遠不會編碼憑證模式。
參考文獻
-
Author’s production data. 49,746 chunks, 16,894 files, 83.56 MB SQLite database, 7,771 signals processed across 14 months. Query latency (23ms) measured via
time.perf_counter()in retriever.py, wrapping the full search path: BM25 lookup, query embedding via Model2Vec, vector KNN search, and RRF fusion.grep -rlmeasured at 11-66 seconds depending on term frequency (Apple M3 Pro, APFS). Full reindex measured at ~4 minutes on Apple M3 Pro. Incremental measured at <10 seconds for typical daily changes. FTS5-only search became unusable for the author above ~3,000 files due to keyword collision rates. ↩↩↩↩↩↩ -
HN thread: “Stop Burning Your Context Window”. Comments from danw1979 and tclancy requesting a detailed write-up. ↩
-
Model2Vec: Distill a Small Fast Model from any Sentence Transformer. Minish Lab, 2024. The potion-base-8M model uses static word embeddings distilled from a sentence transformer, producing 256-dimensional vectors without running attention layers. ↩↩↩
-
sqlite-vec: A vector search SQLite extension. Alex Garcia, 2024. Provides
vec0virtual tables for KNN vector search within SQLite, using the same query interface as standard tables. ↩↩↩ -
SQLite FTS5 Extension. SQLite documentation. FTS5 provides full-text search with BM25 ranking, content-sync tables, and configurable column weights via the
bm25()auxiliary function. ↩↩↩ -
Reimers, N. and Gurevych, I. Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. EMNLP, 2019. Foundational work on dense semantic similarity for text retrieval, establishing the vector search approach used in hybrid retrieval systems. ↩
-
Cormack, G.V., Clarke, C.L.A., and Buettcher, S. Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods. SIGIR, 2009. Introduces RRF with k=60 as a parameter-free method for combining ranked lists that outperforms trained fusion models. ↩↩↩↩
-
Author’s implementation.
chunker.pysplits at H2 boundaries in the_split_at_headingsfunction, with fallback to H3 then paragraph splitting for sections exceeding 2,000 characters. MIN_CHUNK_CHARS=30, MAX_CHUNK_CHARS=2000.index_vault.pyembeds in batches of 64 (BATCH_SIZE=64). ↩↩ -
van Dongen, T. et al. Model2Vec: Turn any Sentence Transformer into a Small Fast Model. arXiv, 2025. Describes the distillation approach producing static embeddings from sentence transformers with 50-500x inference speedup. ↩↩↩
-
Author’s measurement. 256-dim vectors at 49,746 chunks produce 83 MB SQLite. Extrapolating to 768-dim vectors: ~215 MB. To 1024-dim: ~280 MB. Marginal quality improvement on short markdown chunks (avg 200-400 words) does not justify the storage and latency increase. ↩
-
OpenAI Embeddings Pricing. text-embedding-3-small: $0.02 per million tokens. Estimated vault cost per full reindex: ~$0.30 based on average chunk length of ~200 tokens. ↩↩
-
SQLite Write-Ahead Logging. SQLite documentation. WAL mode allows concurrent readers with a single writer, suitable for the retriever’s read-heavy access pattern. ↩
-
Author’s query trace. Ran “PostToolUse hook for context compression” against BM25-only, vector-only, and hybrid modes. Results captured from retriever.py with
methodfield tracking which search path produced each result. ↩ -
Robertson, S. and Zaragoza, H. The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval, 2009. Survey of the BM25 family of ranking functions and their theoretical foundations. ↩↩
-
Karpukhin, V. et al. Dense Passage Retrieval for Open-Domain Question Answering. EMNLP, 2020. Demonstrated that learned dense representations outperform BM25 by 9-19% on open-domain QA benchmarks, establishing dense retrieval as a complement to lexical search. ↩
-
Luan, Y. et al. Sparse, Dense, and Attentional Representations for Text Retrieval. TACL, 2021. Analysis of hybrid sparse-dense retrieval on MS MARCO, showing consistent improvements over single-modality approaches. ↩
-
MTEB: Massive Text Embedding Benchmark. Muennighoff, N. et al., 2023. potion-base-8M scores 50.03 average MTEB vs 56.09 for all-MiniLM-L6-v2 (89.2% retention). Per-task breakdown: Classification 64.44, Clustering 32.93, Retrieval 31.71, STS 73.24. Source: Model2Vec results. ↩
-
Gao, Y. et al. Retrieval-Augmented Generation for Large Language Models: A Survey. arXiv, 2024. Survey of RAG architectures including analysis of chunking strategies and their impact on retrieval quality. ↩
-
Thakur, N. et al. BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. NeurIPS, 2021. Demonstrates high variance in retrieval performance across domains, underscoring the need for domain-specific evaluation. ↩
-
Author’s configuration and credential filter implementation.
memory-config.jsondefines 22allowed_foldersand 5excluded_alwaysentries.credential_filter.pydefines 21 vendor-specificCREDENTIAL_PATTERNS(OpenAI through Turnstile) plus 9 generic single-line patterns (DB URLs, bearer tokens, JWTs, passwords, secrets, API keys, auth tokens, base64 secrets) and 2 multiline patterns (RSA/SSH private keys, PGP keys). Total: 32 patterns. ↩↩↩