如果你出貨過純向量搜尋的 RAG,你大概有遇過這種震驚:使用者打了清楚關鍵字 query 像「退費政策」,結果檢索沒抓到那份標題就叫「退費政策」的文件。這不是 bug,是向量搜尋本來就會這樣。
Hybrid search —— BM25(詞彙 / 關鍵字)加 dense vector(語意)—— 就是來解這個的。它不是 2026 的新發明,在 production RAG 系統裡已經當黃金標準兩年了。如果你在做認真的檢索,該用。
為什麼純向量會漏東西
Dense embedding 抓意思強,但抓「精確識別字」很爛:
- 產品 SKU
XB-447-Z。embedding 模型沒看過它,跟任何東西的 cosine 相似度都沒意義。 - 人名「Ahmadinejad」。罕見 token,embedding 變異很大。
- 縮寫
RAG跟單字rag。embedding 一樣。 - 最新新聞「DeepSeek V4 release」—— 如果在 embedding 模型訓練截止之後,它基本上是隨機的。
BM25 是反過來。它擅長精確比對。「退費政策」會找到含這些字的文件;它抓不到改寫 —— 「退貨流程」不會比對到「退費政策」即使意思一樣。
Hybrid 同時拿到兩種能力。
BM25 30 秒解釋
BM25(Best Match 25)是 1990 年代基於詞頻的評分函式。對 query Q 跟文件 D:
- Q 裡每個詞,數它在 D 裡出現幾次(term frequency)。
- 用 inverse document frequency 懲罰常見詞(the、a)。
- 對文件長度做正規化(避免長文件偏誤)。
- 每個詞的分數加總。
單一個方程式,有對的索引能在百萬文件上毫秒回應,在 Elasticsearch 跟 Lucene 裡當預設搜尋演算法已經幾十年。現代變體(BM25F、BM25+)調 edge case,核心想法沒變。
BM25 不需要 GPU、embedding 或訓練。倒排索引就好。
食譜:平行 + 結合
基本 hybrid pattern:
- 把使用者 query 平行送到兩個系統。
- 各回 top K 結果加分數。
- 把排名結合成單一清單。
- 可選:用 cross-encoder 重排。
結合那步是有選擇的地方。三種常見做法:
RRF(Reciprocal Rank Fusion)
最簡單而且意外地好用:
對任一結果清單裡的文件 d:
score(d) = 1/(k + rank_bm25(d)) + 1/(k + rank_vector(d))
k 是常數(60 是標準)。在任一清單排前面的文件拿高分,在兩邊都排前面的拿最高。RRF 不在乎兩個系統的絕對分數,只看相對排名。這很穩,因為 BM25 分數跟 cosine 相似度不能直接比。
這是預設值。沒理由不用就用這個。
加權分數融合
把兩邊分數正規化,加權加總:
final = alpha * normalized_bm25 + (1 - alpha) * normalized_vector
控制更多。你可以針對領域調 alpha(法律 / 醫療多重詞彙、客服聊天多重語意)。要做超參數搜尋,所以大部分人就用 RRF。
Cross-encoder 重排
貴但最準的選項:
- BM25 拿 top 50 + 向量拿 top 50,去重。
- 大約 100 個候選全部跑 cross-encoder(例如 BAAI/bge-reranker-v2-m3、Cohere Rerank、Jina Reranker v2)。
- 拿 cross-encoder 的 top 10。
Cross-encoder 用 attention 同時比對 query 跟文件,所以抓到 bi-encoder embedding 漏掉的細節。它較慢(每個 (query, doc) 對都要推論)但 top-K 明顯更好。
2026 年認真系統的做法:BM25 + 向量候選 → reranker → 最終 K。
2026 的實作
看你的 stack 三條路:
路徑 1:Postgres + pgvector
資料已經在 Postgres,加 pg_trgm 做 trigram BM25-ish 搜尋,加 pgvector 做向量:
-- BM25-ish 用 tsvector + tsrank
SELECT id, ts_rank(content_tsv, plainto_tsquery('退費政策')) AS bm_score
FROM docs
ORDER BY bm_score DESC LIMIT 50;
-- 向量
SELECT id, 1 - (embedding <=> $1) AS vec_score
FROM docs
ORDER BY embedding <=> $1 LIMIT 50;
-- 在應用程式碼裡用 RRF 結合
Production 級 Postgres BM25 用 paradedb extension(2024 釋出),提供真正 SQL 內 BM25。2026 年最乾淨的單資料庫 hybrid 設定。
路徑 2:Elasticsearch / OpenSearch
Elasticsearch 自 2022 就有 dense_vector field type,BM25 一直都有。跑兩個 query(文字 + kNN),用 RRF 或 rank block 結合:
{
"retriever": {
"rrf": {
"retrievers": [
{ "standard": { "query": { "match": { "content": "退費政策" } } } },
{ "knn": { "field": "embedding", "query_vector": [...], "k": 50 } }
]
}
}
}
ES 8.8 加入了原生 RRF。已經在用 Elasticsearch 的話這是最乾淨設定。
路徑 3:Vector DB + 獨立 BM25 服務
用 Pinecone、Qdrant、Weaviate、LanceDB?大部分現在都內建 BM25 / sparse vector 支援:
- Qdrant。 2024 年原生 sparse vector + BM25 內建。
- Weaviate。 原生
hybridquery 模式,alpha 參數調校。 - Pinecone。 Sparse-dense hybrid 用他們的 splade 風格 sparse vector。
- LanceDB。 FTS + 向量內建融合。
不用 Postgres 或 Elastic 的話挑這些其中一個。
調校建議
Hybrid 接好後:
- 跑你的 eval set(你有對吧?看 RAG evaluation 那篇)。比較 hybrid 跟純向量的 recall@K。Hybrid 在真實 query 上通常贏純向量 5-15% recall,在多關鍵字 query 上更多。
- 不要過度調 alpha。 RRF k=60 是穩固基線。只有你的領域嚴重偏向(法律文件需要更多詞彙、聊天需要更多語意)才調。
- 檢索看起來 OK 之後加 reranker。 重排會疊加效益:hybrid + reranker 通常比純向量單獨好 20-30%。
- 盯延遲。 兩次檢索 + rerank = 200-400ms。使用者感覺得到就把 BM25 跟向量呼叫平行化(它們獨立),而且考慮把 reranker 放 GPU 上跑。
什麼時候不必做
- 小 corpus(<1000 文件)。 純向量就好。recall 差距是雜訊。
- 你的 query 都是自然語言改寫。 純語意實際贏。使用者從不打關鍵字,BM25 沒幫助。
- 你還沒建 eval set。 先做那個,沒它你判斷不出 hybrid 有沒有真的幫到。
務實預設
從零開始要一個決定:Qdrant + Cohere Rerank(2026 年)。Qdrant 給你原生 hybrid、快、好操作。Cohere Rerank 是託管 API 中品質最高的 reranker(每 1000 個文件重排 $0.002,基本免費)。總基礎建設:一個 Qdrant cluster、一個 API key、三行接合程式碼。這設定贏 95% 客製檢索管線。
不能用外部 API,把 Cohere 換成單張 GPU 自架的 BAAI/bge-reranker-v2-m3。
下一步
- Pretrained Transformers for Text Ranking: BERT and Beyond —— dense retrieval 經典。
- BEIR benchmark 論文 —— 跨領域檢索評測,驗證 hybrid 優越性。
- Vespa / Pinecone / Qdrant 部落格的 production hybrid setup 文章。
- 查這些詞:SPLADE、ColBERT、BM25 vs SPLADE、query expansion。