Không cộng thẳng vì hai thang điểm không cùng đơn vị: BM25 trả điểm không chặn trên, phụ thuộc độ dài tài liệu và tần suất từ trong corpus; cosine similarity nằm trong khoảng hẹp quanh 0.7-0.9. Cộng hoặc lấy trung bình có trọng số thì trọng số phải chỉnh lại mỗi khi corpus đổi.
Reciprocal Rank Fusion (RRF) bỏ điểm, chỉ dùng thứ hạng:
def rrf(rankings, k=60):
scores = defaultdict(float)
for ranking in rankings: # e.g. [bm25_ids, vector_ids]
for rank, doc_id in enumerate(ranking, start=1):
scores[doc_id] += 1 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)Vì sao chạy tốt:
- Không cần chuẩn hóa, không cần chỉnh trọng số theo corpus.
- Hằng số k (thường 60) làm phẳng phần đầu bảng, nên một tài liệu đứng hạng 1 ở một nhánh không áp đảo được kết quả.
- Tài liệu xuất hiện ở cả hai nhánh được cộng dồn, đó chính là tín hiệu đồng thuận cần khai thác.
Hai nhánh bù cho nhau: BM25 bắt khớp từ chính xác — mã lỗi, tên hàm, số hợp đồng, từ hiếm mà embedding hay bỏ qua; vector bắt diễn đạt khác từ nhưng cùng nghĩa. Trong thực tế nên lấy top-50 mỗi nhánh, fuse, rồi đưa top-20 sang reranker thay vì trả thẳng cho model.