我在做文件搜尋時,同一個問題會跑兩條路。
一條是關鍵字搜尋。產品代號、錯誤碼和專有名詞對得很準。另一條是向量搜尋。使用者講得很口語,文件裡沒有出現同一組字,它還是有機會找到意思相近的段落。
兩條都留著不難,麻煩的是它們各自交回一份排名。我最後只能給模型一份文件清單,兩份要怎麼合?
我一開始想得很直覺:分數加起來就好了。很快就發現不行。
10.5 不能直接加 0.87
關鍵字搜尋常用 BM25。它的分數可能是 10.5、8.3、5.1,而且沒有一個固定上限。向量搜尋的分數則跟所用的距離算法和產品實作有關,常落在另一個完全不同的範圍。
如果我直接把兩邊相加,數字比較大的那一邊自然會主導結果。那不是融合,只是讓其中一種搜尋換個方式贏兩次。
可以先做 min-max 或 z-score 正規化,但這又帶來新的選擇:用哪一批結果算?遇到離群值怎麼辦?換一個 embedding 模型後門檻要不要重調?
我後來用的 RRF(Reciprocal Rank Fusion)乾脆把原始分數丟掉,只留下名次。
RRF 分數 = Σ 1 / (k + 名次)
這裡的 Σ 代表每一份搜尋排名都算一次再加總。k 是讓名次差距變平緩的常數,常見起點是 60。
真的算一次就懂了
假設 BM25 和向量搜尋各找出 5 篇文件:
BM25:A=1, B=2, C=3, D=4, E=5
向量:B=1, C=2, E=3, D=4, A=5
用 k = 60 計算後:
| 文件 | BM25 名次 | 向量名次 | RRF 總分 | 最後名次 |
|---|---|---|---|---|
| B | 2 | 1 | 0.03252 | 1 |
| C | 3 | 2 | 0.03200 | 2 |
| A | 1 | 5 | 0.03178 | 3 |
| E | 5 | 3 | 0.03126 | 4 |
| D | 4 | 4 | 0.03125 | 5 |
B 在兩邊都排前面,所以贏了。A 雖然拿到一次第 1 名,另一邊只有第 5,最後掉到第 3。
這正是我要的效果:不是獎勵某一邊特別有自信,而是讓兩種不同方法都認為不錯的文件往前走。
k = 60 在控制什麼
如果直接用 1 / 名次,第 1 名是 1,第 2 名是 0.5。一次第 1 名的影響太大,另一份排名很難拉得回來。
加上 60 後,第 1 名是 1 / 61 = 0.01639,第 2 名是 1 / 62 = 0.01613。前幾名還是比較高,但差距沒有大到一票定生死。
60 不是不能動的自然定律。它是一個實務上常用的起點,Microsoft 和 Elasticsearch 的官方說明也使用或預設這個值。數字要不要調,還是要拿自己的查詢和相關性標註去測。
20 行內可以寫完
我用 Python 寫的最小版本是這樣:
def rrf(rankings, k=60):
"""rankings 是多份 {文件 ID: 名次}。"""
scores = {}
for ranking in rankings:
for doc_id, rank in ranking.items():
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
return sorted(scores.items(), key=lambda item: item[1], reverse=True)
bm25 = {"A": 1, "B": 2, "C": 3, "D": 4, "E": 5}
vector = {"A": 5, "B": 1, "C": 2, "D": 4, "E": 3}
print(rrf([bm25, vector]))
輸出第一筆會是 B,分數約 0.03252。實際接搜尋引擎時,我不一定會自己寫這段,因為不少產品已經內建 RRF;但自己算過一次後,遇到排名不如預期會比較知道該查哪裡。
出問題的不是公式,是進 RRF 之前
有次融合後的結果一直不理想,我花時間調 k,從 60 試到 30、100,排名有動,但答案沒有真的變好。
症狀是某些完全不相關的段落一直出現在前 5 名。原因是向量搜尋本身撈回來的 50 筆品質就不好,關鍵字搜尋又因為文件切得太碎,把同一篇文章的相似片段佔滿排名。
RRF 只會融合兩份排名,不會治好原本的搜尋。垃圾排第 1,經過公式還是很有影響力。
我後來先做三件事:
檢查文件怎麼切段,避免同一來源洗滿前幾名。
分別量 BM25 和向量搜尋的召回率,不先看融合結果。
用一組人工標過相關文件的問題,比較融合前後的 Recall@K。
兩邊品質差很多時,也可以改成加權 RRF:
加權 RRF 分數 = Σ 權重ᵢ / (k + 名次ᵢ)
不過一加權就要有資料支持。只因為某次 Demo 看起來不順,就把喜歡的那條搜尋乘 2,通常只是把人工偏好寫進公式。
它放在 RAG 的哪裡
我的流程大致是:
同一個問題送進 BM25 和向量搜尋。
兩邊各取一批候選文件。
用 RRF 合成一份排名。
視需要再做較昂貴的 rerank。
只把最前面的少量內容交給 LLM。
RRF 解決的是第 3 步,而且只解決這一步。它不負責切段、不負責 embedding,也不保證最後答案正確。
如果只有一種搜尋,我不會多放一層 RRF。有足夠的標註資料、需要追求更高效果時,也可能直接做 Learning to Rank 或模型 reranking。
對我來說,RRF 的價值很樸素:兩份分數單位不同,又沒有一批訓練資料時,它讓我先用一個看得懂、算得出來的方式把排名合起來。先有穩定基準,再決定值不值得做更複雜的東西。
參考資料: