對應原題(Common ‧ RAG and Retrieval)
- How do you choose between a sparse retriever (BM25) and a dense retriever? When do you need both?
— Asked at: Microsoft, Perplexity, Glean
🎯 什麼情境該想到我
當你「向量搜尋搜不到明明存在的東西」的時候——查詢是 ERR_CONN_4012 或 getUserById,向量搜尋回來一堆「語意相近」但完全不對的結果。
⚙️ 怎麼用
一、稀疏檢索:BM25
本質:倒排索引 + 一個很好調的詞頻加權公式。向量是「詞表大小」維、絕大多數是 0(所以叫稀疏)。
三個組件各自在做什麼(能講清楚這三件事就答完了):
| 組件 | 作用 | 參數 |
|---|---|---|
| IDF | 罕見詞權重高。「的」幾乎不加分,ERR_4012 加很多分 | — |
| 詞頻飽和 | 出現 20 次不該比出現 10 次加倍得分——分母裡有 ,所以會飽和 | (越大越晚飽和) |
| 長度正規化 | 長文件天然含更多詞,要扣分 | ( 不正規化、 完全正規化) |
「為什麼 BM25 比 TF-IDF 好」的一句話:TF 是線性的,BM25 讓它飽和,並加入了長度正規化。這兩件事讓它在真實語料上穩定勝出——而且它是 1994 年的東西,至今仍是強到不合理的 baseline。
★ BM25 至今沒死的四個理由:
- 精確詞匹配 —— 產品編號、錯誤碼、API 名稱、人名、版本號、罕見術語
- 零訓練成本,新文件寫進索引立刻可搜(不用 embed)
- 可解釋 —— 能明確指出「因為命中了這幾個詞」
- 對分布外查詢穩健 —— embedding 模型沒見過的領域,dense 會崩,BM25 不會
二、密集檢索:Bi-Encoder
本質:查詢與文件各自獨立編碼成一個稠密向量(512–4096 維),比較內積或餘弦。
查詢 ──▶ Encoder ──▶ q ∈ ℝᵈ ─┐
├──▶ sim(q, d) = q·d
文件 ──▶ Encoder ──▶ d ∈ ℝᵈ ─┘ (文件端可離線算好)
「獨立編碼」是關鍵:文件向量可以離線算完存進索引,查詢時只要算一次 query 的 embedding + ANN 搜尋。這是它能在上億文件規模下即時回應的原因,也是它和 cross-encoder 的本質差別(見 Reranker 重排序)。
擅長:同義(「離職」↔「辭職」)、改寫、跨語言、概念相近、口語化提問。
不擅長:沒見過的專有名詞。getUserById 對它可能只是「某個駝峰命名的東西」,和 getOrderById 幾乎一樣近。
三、★ 對照表
| 面向 | BM25(稀疏) | Dense |
|---|---|---|
| 精確詞/代號/縮寫 | ✅ 強 | ❌ 弱 |
| 同義與改寫 | ❌ 完全不行 | ✅ 強 |
| 罕見/新造詞 | ✅ IDF 會給高權重 | ❌ 落在無意義的位置 |
| 跨語言 | ❌ | ✅(多語模型) |
| 長尾查詢 | ✅ 穩健 | ⚠️ 看訓練分布 |
| 新增文件 | 立即 | 要 embed(改模型要全部重算) |
| 可解釋性 | ✅ | ❌ 黑盒 |
| 索引體積 | 小 | 大 |
| 冷啟動 | ✅ 零成本 | 要選模型/可能要微調 |
★ 它們的失敗模式是互補的,不是重疊的。 這就是混合檢索存在的全部理由。
四、什麼時候需要兩者(題目的重點)
判準是查詢分布:
| 查詢分布 | 建議 |
|---|---|
| 幾乎都是自然語言概念問題(FAQ、客服) | Dense 為主即可 |
| 幾乎都是精確識別碼(日誌檢索、程式碼搜尋) | BM25 為主即可 |
| 混合(企業搜尋、技術文件、電商、法律) | ★ 必須 hybrid |
企業內部搜尋(Glean、Microsoft 的場景)幾乎一定是混合的——同一個搜尋框裡會出現「上季 OKR 進度」和
PROJ-4821。
面試答法:「我會先把真實查詢日誌抽樣分類,量出精確匹配型查詢的比例。這個比例決定要不要 hybrid,以及融合的權重。 沒看過查詢分布就選檢索器是拍腦袋。」
五、★ 怎麼融合:RRF 而不是加權和
天真作法(加權分數和)的問題:
- BM25 分數無上界且隨語料變動;cosine 落在 —— 尺度完全不可比
- 正規化(min-max / z-score)依賴當次結果集的分布,同一份文件在不同查詢下的分數會漂
- 權重 要調,而且對查詢類型敏感(一組權重不可能同時對兩種查詢都好)
Reciprocal Rank Fusion(Cormack et al., 2009)—— 只用排名,不用分數:
def rrf(rankings: list[list[str]], k: int = 60, top_n: int = 20):
"""rankings: 多個檢索器各自的有序 doc_id 清單"""
scores = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)[:top_n]為什麼好用:
- 免調參( 是原論文的經驗值,幾乎不用動)
- 尺度無關 —— 可以塞任意多個檢索器進去(BM25 + dense + SPLADE + 知識圖譜…)
- 對單一檢索器的崩潰有韌性 —— 一個檢索器排爛了,其他的還能撐
- 的作用是壓低第一名的絕對優勢,讓「兩個檢索器都排第 3」勝過「一個排第 1、一個沒排到」
實務預設:BM25 + dense,用 RRF 合併取 top-50,再丟 reranker 取 top-5。 這是目前 RAG 的標準三段式。
六、第三條路:Learned Sparse(SPLADE)
用 MLM 模型為每個文件產生一個稀疏但經過語意擴展的詞權重向量——文件裡沒出現的相關詞也會被賦予權重。
兼有兩者優點(精確匹配 + 語意擴展),且能直接跑在既有的倒排索引基礎設施上。代價是索引體積膨脹、編碼較慢。
⚠️ 注意 / 什麼時候不適用
- ★ 別忘了 query / passage 前綴。很多 embedding 模型(E5、BGE、GTE 系列)是非對稱訓練的,查詢端要加
"query: "、文件端要加"passage: "。漏了這件事,效果會無聲地掉一大截,而且測起來「好像有在動」。 - 相似度函式要和訓練時一致。用 cosine 去搜一個用內積訓練的模型(或反過來、或忘了正規化),結果會微妙地錯。
- 換 embedding 模型 = 重算全部向量。這是不可逆的營運成本,選型時就要想清楚。
- BM25 對中文要先分詞。用錯分詞器(或沒分詞)會讓它退化成字元匹配。
- hybrid 讓延遲變成兩條路的較慢者,且要維護兩套索引。小規模時未必划算。
- RRF 會丟失分數資訊。需要用絕對分數做閾值過濾(「相似度低於 0.7 就說不知道」)時,RRF 幫不上忙——那要另外保留原始分數。
- 檢索不好不一定是檢索器的錯。先確認切塊與解析(見 切塊與文件解析)。
🧪 我實際套用的紀錄
- 2026-09-20:從面試題庫展開。待辦:做 RAG 前先抽樣查詢日誌,量精確匹配型查詢的比例。
🔗 相關
- 切塊與文件解析 —— 上游,決定索引裡放的是什麼
- Reranker 重排序 —— 下游,hybrid 取回 top-50 之後的精修
- 向量索引 ANN —— dense 那一側怎麼做到即時
- 查詢改寫與 HyDE —— 從查詢端改善召回
- RAG 評估與營運 —— 怎麼證明 hybrid 真的比較好
- 工具-用trigram索引加速模糊查詢 —— PostgreSQL 上的另一種字面匹配加速