當您同時擁有獨立運作的語意搜尋(向量嵌入)和詞彙搜尋(BM25)時,下一步就是將它們結合成一個統一的搜尋管線。這種混合方法利用了兩種方法的優勢,以提供更準確的結果。

建立統一介面
兩種搜尋實作共享幾乎相同的 API——它們都有 add_document() 和 search() 方法。這種一致性使得將它們包裝在單一的 Retriever 類別中變得簡單,該類別負責協調這兩種方法。

Retriever 作為協調者,負責:
- 接收使用者的問題
- 將其轉發給 VectorIndex 和 BM25Index
- 收集來自兩個系統的結果
- 使用排名演算法合併結果

倒數排名融合(Reciprocal Rank Fusion)
挑戰在於合併來自不同搜尋方法的結果。每個系統返回的結果都有不同的評分機制,因此您不能直接簡單地合併分數。相反,我們使用一種稱為倒數排名融合(Reciprocal Rank Fusion,RRF)的技術。

以下是 RRF 如何運作的實際範例。假設您的 VectorIndex 返回的結果排名為:Section 2、Section 7、Section 6。同時,您的 BM25Index 返回:Section 6、Section 2、Section 7。

為了合併這些結果,您建立一個組合表,顯示每個文字區塊在兩個系統中的排名:

RRF 公式為每個文件計算一個分數:
RRF_score(d) = Σ(1 / (k + rank_i(d)))其中 k 是一個常數(通常為 60,不過 1 在結果更清晰時效果也很好),而 rank_i(d) 是文件 d 在第 i 個排名系統中的排名。

對於每個文字區塊,您計算:
- Section 2:1.0/(1+1) + 1.0/(1+2) = 0.833
- Section 7:1.0/(1+2) + 1.0/(1+3) = 0.583
- Section 6:1.0/(1+3) + 1.0/(1+1) = 0.75
按分數排序後,最終排名變為:Section 2(第一)、Section 6(第二)、Section 7(第三)。

實作
Retriever 類別的實作很簡單:
class Retriever:
def __init__(self, *indexes):
self._indexes = list(indexes)
def add_document(self, document):
for index in self._indexes:
index.add_document(document)
def search(self, query_text, k=1, k_rrf=60):
# 從所有索引取得結果
all_results = []
for idx, results in enumerate(all_results):
for rank, (doc, _) in enumerate(results):
# 追蹤文件在各系統中的排名
# 套用 RRF 公式
# 回傳合併並排序後的結果
關鍵的洞見是,RRF 演算法透過考量每個文件在所有搜尋系統中的表現,來建立統一的排名,而不是依賴任何單一的評分方法。
測試混合方法
當使用像「what happened with INC-2023-Q4-011?」這樣的查詢進行測試時,混合方法提供的結果明顯優於單獨使用任一方法。您不再從純向量搜尋中得到意外的結果,而是首先得到最相關的網路安全事件報告,接著是相關的軟體工程內容。

可擴展性
這種設計的優點在於其模組化。由於每個搜尋索引都實作相同的介面(add_document() 和 search()),您可以輕鬆地將新的搜尋方法加入系統中。無論是不同的嵌入模型、專門的領域搜尋,還是任何其他檢索技術,只要遵循既定的 API,它就能無縫整合到混合管線中。

這種混合搜尋方法透過結合向量搜尋的語意理解與詞彙搜尋的精確關鍵字匹配,並透過數學上嚴謹的 RRF 排名演算法統一起來,在檢索準確性方面代表了重大的改進。