Applied AI School
v0 · 規劃中
Anthropic

Multi-index pipeline

Semantic search 不夠精準時——加 BM25 lexical search、再用 reranker 合併結果。production RAG 的標準長相。

TL;DR

  • 純 vector search 對「精確關鍵字」(API name、錯誤碼、incident ID)出乎意料地爛
  • Hybrid = BM25(關鍵字)+ vector(語意)平行跑,再用 RRF 或 reranker 合併
  • Reranker(Voyage rerank-2 / Cohere rerank)是「retrieve 太多後讓 model 重新排」的便宜後處理

一個情境:vector 找不到的 incident ID

文件裡有 INC-2023-Q4-011 這個事故編號,出現在「軟體工程」跟「資安」兩節。

User 問:「INC-2023-Q4-011 發生什麼事?

純 vector search 結果:

Rank結果有提到 ID 嗎
1資安事件報告
2財務分析✗(完全沒提到 incident)
3軟體工程

第 2 名跑出財務分析,是因為 embedding model 看到「事故、發生」這類詞,就把財務的「異常損益」也算成相關了。INC-2023-Q4-011 這種精確字串對 embedding 來說是 noise,但對 user 來說是搜尋的全部重點。

這就是為什麼要加 lexical search。

Semantic vs Lexical:互補

Semantic(vector)Lexical(BM25)
強項同義詞、概念相關精確 token、罕見詞
弱項罕見 ID / 專有名詞同義詞、改寫
例:「員工修了多少 bug」✓ 找到「engineering 修了 47 個 issue」✗ 字面沒 "bug" 抓不到
例:「INC-2023-Q4-011」✗ 把概念相近的也撈進來✓ 直接找包含這串的 chunk
計算embedding API + vector DB純文字統計,便宜很多

兩個剛好補對方的弱點。production RAG 預設都該 hybrid,除非你已經量過純 vector recall 夠高。

BM25 一分鐘

BM25(Best Match 25) 是 1990 年代的搜尋演算法,至今仍是 lexical search 的 baseline。原理三句話:

  1. 把 query 跟 doc 都 tokenize(拆成 word)
  2. 罕見 token(在所有 doc 裡很少出現)權重高、常見 token("a"、"the"、"的")權重低
  3. 含越多高權重 token 的 doc 排越前
from rank_bm25 import BM25Okapi

# 把每個 chunk tokenize(簡單做就 split 空白;中文要先斷詞)
tokenized = [chunk.split() for chunk in chunks]
bm25 = BM25Okapi(tokenized)

q = "What happened with INC-2023-Q4-011?".split()
scores = bm25.get_scores(q)  # 每個 chunk 一個分數
top_k = sorted(range(len(scores)), key=lambda i: -scores[i])[:3]

跑同一個 query,結果:

RankBM25 結果有提到 ID 嗎
1軟體工程(提到 ID 三次)
2資安事件
3方法論

完全沒漏掉精確匹配。但 BM25 自己也有弱點:「員工修了多少 bug」這種概念查詢,它會錯過沒寫 "bug" 但寫了 "issue" 的 chunk。

Hybrid pipeline 流程

                  user query
                      │
           ┌──────────┴──────────┐
           ▼                     ▼
    [Vector index]        [BM25 index]
       top-10                 top-10
           │                     │
           └──────────┬──────────┘
                      ▼
          [Reciprocal Rank Fusion]
                      │
                      ▼
                 top-5 chunks
                      │
                      ▼
               (optional rerank)
                      │
                      ▼
                final top-3 → Claude

兩個 index 平行跑,各自拿 top-N,再用一個策略合併。

Reciprocal Rank Fusion(RRF)

合併兩種 search 不能直接相加分數——BM25 分數可能 0–20、cosine 是 0–1,尺度根本不一樣。RRF 改用「排名」而不是「分數」:

RRF(d) = Σ  1 / (k + rank_i(d))
         i

k 通常 60。對每個 doc:在每個 index 裡查到它排第幾,套公式相加。在兩個 index 都排前面的 doc 自然得分高。

from collections import defaultdict

def rrf(rankings: list[list[str]], k: int = 60) -> dict[str, float]:
    """rankings: [[doc_id 排序好的 list], ...]"""
    scores = defaultdict(float)
    for ranks in rankings:
        for i, doc_id in enumerate(ranks):
            scores[doc_id] += 1 / (k + i + 1)
    return scores

merged = rrf([vector_top_ids, bm25_top_ids])
final = sorted(merged, key=merged.get, reverse=True)[:5]

無 ML、不用訓練、跑得快,是 production 跑 hybrid 的標配方法。

Rerank:再加一層精準度

RRF 是 rule-based 合併;要再準一點,上 reranker:用專門的小 model 把 (query, chunk) pair 重新打分。

result = vo.rerank(
    query=q,
    documents=retrieved_chunks,  # RRF 後的 top-20
    model="rerank-2",
    top_k=3,
)
final = [r.document for r in result.results]

順序:vector + BM25 各拿 top-20 → RRF 合併拿 top-10 → rerank 最後挑 top-3。主流選擇:Voyage rerank-2Cohere rerank-v3,或自架 cross-encoder(bge-reranker)。Rerank 比 embedding 貴,但只跑 top-N,總成本可控。

Multi-index:不只一個 vector store

廣義的 multi-index 是指不同類型內容用不同 retrieval 策略:FAQ 短 QA 用 embedding、規格文件用 hybrid、程式碼以 BM25 為主(symbol 名稱)、客服 transcript 加時間 metadata filter。每個 index 自己一條 pipeline,外層 Retriever 抽象成一個 protocol(add(doc) + search(query, k)),fan-out 後再合併。新加一個 index 不用改外層。

接下來

到這裡 RAG section 完整了——你知道為什麼要 RAG、怎麼切 chunk、怎麼算 similarity、怎麼 hybrid + rerank。下一個 section 換到 Claude 進階能力,第一篇是 Prompt caching——RAG 之後馬上會撞到「同樣 system prompt 重複送很貴」的問題,prompt caching 就是針對這個來的。