API 開發

檢索品質不夠好?用 BM25 與混合檢索補上語意搜尋的盲點

語意搜尋擅長理解意思,卻常漏掉編號、代碼這類需要精確比對的字串。解法是同時執行 BM25 關鍵字搜尋與向量檢索,再用 reciprocal rank fusion 把兩邊的排名合併成一份結果。兩種方法互補,混合起來的檢索比單獨任何一種都穩。
檢索品質不夠好?用 BM25 與混合檢索補上語意搜尋的盲點:文章重點卡

檢索品質不夠好?用 BM25 與混合檢索補上語意搜尋的盲點

系列前幾篇把 RAG 的基本管線搭起來了:切塊、embedding、向量檢索。但實際跑起來你很快會發現:光靠語意搜尋,結果不一定最好

這篇要補上最後一塊拼圖:什麼情境語意搜尋會失手、老牌的 BM25 關鍵字搜尋怎麼救場,以及怎麼把兩路結果合併成一份更可靠的排名。

你將學到什麼

語意搜尋的盲點

查特定編號時,語意搜尋為什麼會失手。

BM25 的原理

四個步驟看懂這個經典的關鍵字搜尋演算法。

混合檢索架構

兩路搜尋並行,各自發揮擅長的事。

RRF 合併排名

用 reciprocal rank fusion 公平地融合兩份排名。

語意搜尋的盲點:查編號查不到

先回顧一下:前一篇的語意搜尋把提問與切塊都轉成 embedding,再用相似度找出意思最接近的段落。大多數時候這招很好用,但有個情境會點出它的罩門:你要在文件裡查一個特定的事件編號「INC-2023-Q4-011」。

語意搜尋很會理解上下文與概念,但它回傳的結果裡,除了真的含有這個編號的資安章節,還混進了一段完全沒提到這個編號的財務分析。因為語意搜尋比的是概念相似,不是字面有沒有出現。

對「幫我解釋這個概念」這類提問,語意搜尋是強項;但對編號、代碼、專有名詞這種必須精確命中的查詢,它會失手。這就是要引入字面搜尋(lexical search)的原因。

BM25:給稀有詞更高權重的關鍵字搜尋

BM25(Best Match 25)是 RAG 系統裡最常用的字面搜尋演算法。它處理一個查詢的過程可以拆成四步:

  1. 斷詞:把提問拆成一個個詞,例如「a INC-2023-Q4-011」拆成兩個詞。
  2. 統計詞頻:計算每個詞在所有文件裡出現的次數。
  3. 依稀有度加權:越少出現的詞越重要。常見的「a」權重很低,罕見的「INC-2023-Q4-011」權重很高。
  4. 找最佳匹配:含有較多高權重詞的文件,排名越前面。

用起來跟向量檢索幾乎一樣,只是把索引換成 BM25:

chunks = chunk_by_section(text)

store = BM25Index()
for chunk in chunks:
    store.add_document({"content": chunk})

results = store.search("What happened with INC-2023-Q4-011?", 3)

for doc, distance in results:
    print(distance, "\n", doc["content"][:200], "\n----\n")

同一個查詢改跑 BM25,回傳的就是真正含有那個編號的章節。它特別擅長技術名詞、編號與特定片語,因為它做的正是語意搜尋不做的事:認字,不猜意思

對照一下兩者的個性就很清楚:語意搜尋把「車子壞了」與「汽車故障」視為近親,BM25 則把它們當成兩組不同的字;反過來,一長串沒有語意的事件編號,在 embedding 眼裡面目模糊,在 BM25 眼裡卻是全場最醒目的稀有詞。

混合檢索:兩條路並行,再合併

既然兩種方法各有擅場,最好的策略就是都用:同一個提問同時送進向量索引與 BM25 索引,兩邊各自回傳一份排名,再把兩份排名融合成一份。語意搜尋負責理解概念,字面搜尋確保精確詞不漏接。

兩路搜尋彼此獨立、可以並行執行,不會互相拖慢,合併只是最後一步輕量的計算。

使用者提問向量索引語意搜尋,理解概念BM25 索引字面搜尋,精確命中RRF 排名融合輸出最相關的段落
混合檢索:一個提問、兩路搜尋、一份融合後的排名。

reciprocal rank fusion:排名怎麼公平合併

合併不能只是把兩份清單接在一起,因為兩種搜尋的計分方式完全不同。reciprocal rank fusion(RRF)的做法是不看分數、只看名次:每份排名裡,文件的名次越前面得分越高,再把各份排名的得分加總。

這個設計的好處是完全不需要理解各索引的分數怎麼算,只要每個索引吐得出一份排序,就能參與融合。公式是:

RRF_score(d) = sum( 1 / (k + rank_i(d)) )

# k 是常數,實務上常用 60;這裡為了讓數字好讀,示範時用 k = 1
# rank_i(d) 是文件 d 在第 i 份排名裡的名次

實際跑一次:查詢那個事件編號後,向量索引回傳的排名是章節二、章節七、章節六;BM25 回傳的是章節六、章節二、章節七。用 k 等於 1 帶進公式:

文件向量索引名次BM25 名次RRF 得分
章節二120.833
章節六310.75
章節七230.583

最終排名變成章節二、章節六、章節七。這很符合直覺:在兩邊都表現不錯的文件,自然浮到最上面。實測也印證了效果:原本向量檢索把不相干的財務分析排到第二名,混合檢索之後,回傳的前兩名變成真正相關的資安事件章節與軟體工程章節。

同一個介面,隨時加新的索引

實作上有個漂亮的設計:VectorIndex 與 BM25Index 的 API 幾乎一模一樣,都有加入文件與搜尋兩個方法。所以可以再包一層 Retriever,把提問轉發給所有索引、收集結果、做 RRF 融合:

class Retriever:
    def __init__(self, *indexes: SearchIndex):
        if len(indexes) == 0:
            raise ValueError("At least one index must be provided")
        self._indexes = list(indexes)

    def add_document(self, document: Dict[str, Any]):
        for index in self._indexes:
            index.add_document(document)

    def search(self, query_text: str, k: int = 1, k_rrf: int = 60):
        # 1. 把查詢送進每一個索引
        # 2. 記錄每份文件在各索引的名次
        # 3. 套 RRF 公式加總得分,排序後回傳
        ...
這個架構的延展性因為所有索引都實作同一個介面,之後想加關鍵字索引、圖譜式搜尋或領域專用的索引,只要照介面實作,Retriever 就會自動把它納入融合。每種搜尋各自獨立、各自可測,最後在同一個地方合流。

到這裡,RAG 系列的核心拼圖就齊了:切塊、embedding、向量檢索,加上這篇的 BM25 與混合融合。兩種搜尋是互補的,語意搜尋懂脈絡,字面搜尋不漏字,合起來才是一套經得起實戰的檢索系統。

參考出處本文取材自 Anthropic 官方 Claude Academy 免費課程「Building with the Claude API」,由酒Ann 消化後以自己的視角重新編寫。想看英文原版課程,可到 Claude Academy 修習。

延伸學習

把這篇文章分享給需要的人FacebookLINEThreadsX

常見問答

有了語意搜尋,為什麼還需要 BM25?
語意搜尋比對的是概念上的相似,遇到事件編號、產品代碼這類必須精確出現的字串反而容易漏。BM25 專門做字面比對,正好補上這個洞。
BM25 怎麼決定哪個詞重要?
看稀有程度。全部文件裡很常見的詞(例如英文的 a)權重低,很少出現的專有詞權重高,含有較多高權重詞的文件就排前面。
什麼是 reciprocal rank fusion?
一種合併多份排名的方法:每份排名裡名次越前面的文件得分越高,把各份排名的得分加總後重新排序。在兩邊都表現好的文件自然浮上來。
混合檢索還能再加入其他搜尋方式嗎?
可以。只要新的索引實作同樣的介面(加入文件與搜尋兩個方法),就能直接掛進同一個 Retriever,一起參與排名融合。