0Pricing
AI Engineering Academy · レッスン

スコア統合のための逆順位融合

互換性のない類似度スコアを正規化せずに、密検索と疎検索のランキング結果リストを統合する逆順位融合を実装します。

「スコア統合のための逆順位融合」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。

このレッスンの一部はまだ翻訳されておらず、英語で表示されています。

The Score Incompatibility Problem

Dense retrievers produce cosine similarity scores between -1 and 1, while BM25 produces unbounded positive scores that depend on corpus size and term frequencies. You cannot add these numbers directly — a BM25 score of 3.7 and a cosine similarity of 0.85 mean completely different things. Simple score normalization (dividing by max score) is brittle because outlier documents distort the scale. We need a method that is agnostic to absolute score values.

The Core Idea Behind RRF

Reciprocal Rank Fusion (RRF) sidesteps the score incompatibility problem by converting each retriever's results into ranked positions and fusing those ranks rather than raw scores. A document ranked first gets a high RRF contribution, a document ranked tenth gets a much lower one, and the final score is the sum of RRF contributions across all retrievers. The formula is: RRF(d) = sum(1 / (k + rank_i(d))) where k is a smoothing constant (typically 60).

# RRF formula
# For each retriever i, document d receives:
#   contribution = 1 / (k + rank_i(d))
# Final RRF score = sum of contributions from all retrievers
# k = 60 is the standard constant from the original 2009 paper

# Example:
# Document A: rank 1 in BM25, rank 4 in dense
#   RRF(A) = 1/(60+1) + 1/(60+4) = 0.01639 + 0.01563 = 0.03202
# Document B: rank 2 in BM25, rank 2 in dense
#   RRF(B) = 1/(60+2) + 1/(60+2) = 0.01613 + 0.01613 = 0.03226
# Document B scores slightly higher due to consistent top-2 ranking

Implementing RRF from Scratch

The implementation is surprisingly simple. For each retriever, iterate over its ranked result list and accumulate RRF scores into a dictionary keyed by document ID. Documents that appear in multiple retrievers accumulate contributions from each. Finally, sort by total RRF score descending to produce the merged ranking.

from collections import defaultdict

def reciprocal_rank_fusion(
    result_lists: list[list[str]],
    k: int = 60,
) -> list[tuple[str, float]]:
    '''Merge multiple ranked result lists using RRF.
    result_lists: each inner list is a ranked list of document IDs
    Returns: sorted list of (doc_id, rrf_score) tuples
    '''
    rrf_scores = defaultdict(float)

    for ranked_list in result_lists:
        for rank, doc_id in enumerate(ranked_list, start=1):
            rrf_scores[doc_id] += 1.0 / (k + rank)

    sorted_results = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)
    return sorted_results

End-to-End Hybrid Search with RRF

In practice, you run both retrievers, collect their ranked document ID lists, pass them to RRF, and look up the top-K document IDs in your document store. Notice that no score normalization is required — only the rank ordering of each retriever's results matters. This makes RRF extremely robust to distribution shifts and corpus size changes.

def hybrid_search_rrf(
    query: str,
    bm25_index,
    dense_retriever,
    documents: dict,  # id -> text
    top_k: int = 5,
) -> list[dict]:
    # Get ranked lists from each retriever
    bm25_ids = bm25_index.search(query, top_k=20)  # over-retrieve then fuse
    dense_ids = dense_retriever.search(query, top_k=20)

    # Run RRF
    fused = reciprocal_rank_fusion([bm25_ids, dense_ids])

    # Return top-K with scores
    results = []
    for doc_id, score in fused[:top_k]:
        results.append({
            'id': doc_id,
            'text': documents[doc_id],
            'rrf_score': round(score, 6),
        })
    return results

Why Over-Retrieve Before Fusing

Notice the pattern of retrieving 20 candidates from each retriever and then taking the top 5 after fusion. This over-retrieval strategy is important because a document that appears at rank 10 in BM25 but rank 1 in dense should bubble up in the fused list. If you only retrieve 5 from each, you miss such documents. A common practice is to retrieve top_k * 4 from each retriever before fusing and then return top_k final results.

The Smoothing Constant K

The constant k in the RRF formula controls how much weight is given to top-ranked versus lower-ranked documents. A small k amplifies the difference between ranks (rank 1 is much better than rank 2), while a large k flattens the distribution (all ranks matter roughly equally). The original paper used k=60 and empirical studies show values between 30 and 100 all perform similarly. k=60 is a robust default.

# Visualize effect of k on contribution weights
for k in [10, 60, 100]:
    weights = [1.0 / (k + rank) for rank in range(1, 11)]
    print(f'k={k}: rank1={weights[0]:.4f}, rank5={weights[4]:.4f}, rank10={weights[9]:.4f}')

# k=10:  rank1=0.0909, rank5=0.0667, rank10=0.0500  (steep)
# k=60:  rank1=0.0164, rank5=0.0154, rank10=0.0143  (flat)
# k=100: rank1=0.0099, rank5=0.0095, rank10=0.0091  (very flat)

Handling Documents Absent from One Retriever

RRF handles missing documents gracefully: a document that appears in only one retriever still accumulates that retriever's RRF contribution. It simply does not receive a second contribution. This means documents that both retrievers agree on will naturally score higher than documents found by only one. This consensus signal is exactly what you want in a hybrid system.

# Example showing consensus effect
example_docs = {
    'doc_A': [1, 3],   # ranks in [bm25, dense]
    'doc_B': [2, 2],   # both retrievers like it
    'doc_C': [3, None],  # only in BM25
    'doc_D': [None, 1],  # only in dense
}

k = 60
for doc, ranks in example_docs.items():
    score = sum(1/(k + r) for r in ranks if r is not None)
    print(f'{doc}: RRF = {score:.5f}')
# doc_B ranks highest because both retrievers agree
# doc_D (rank 1 in dense only) may outscore doc_A (1,3) despite lower consensus

RRF with More Than Two Retrievers

RRF scales naturally to three or more retrievers. You might combine BM25, a dense embedding retriever, and a sparse learned retriever (like SPLADE) that explicitly optimizes for keyword matching in a learned sparse space. Each additional retriever adds another RRF contribution per document, and documents that rank highly across all retrievers win by a clear margin.

# Three-way hybrid: BM25 + dense + SPLADE
bm25_ids = ['doc_B', 'doc_A', 'doc_C', 'doc_D']
dense_ids = ['doc_D', 'doc_A', 'doc_B', 'doc_E']
splade_ids = ['doc_B', 'doc_D', 'doc_A', 'doc_F']

fused = reciprocal_rank_fusion([bm25_ids, dense_ids, splade_ids])
print('Fused ranking:')
for doc_id, score in fused:
    print(f'  {doc_id}: {score:.5f}')
# doc_A and doc_B likely dominate because they appear in all three lists

Weighted RRF for Asymmetric Retrievers

Standard RRF treats all retrievers equally, but in practice one retriever may be more reliable than another for your specific domain. Weighted RRF multiplies each retriever's contribution by a weight before summing. A weight of 1.5 on the dense retriever and 0.5 on BM25 emphasizes semantic matching, while equal weights of 1.0 treat them symmetrically. Tune weights on a validation set.

def weighted_rrf(
    result_lists: list[list[str]],
    weights: list[float],
    k: int = 60,
) -> list[tuple[str, float]]:
    from collections import defaultdict
    scores = defaultdict(float)
    for ranked_list, weight in zip(result_lists, weights):
        for rank, doc_id in enumerate(ranked_list, start=1):
            scores[doc_id] += weight * (1.0 / (k + rank))
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

# Emphasize dense retrieval (0.7) over BM25 (0.3)
fused = weighted_rrf([bm25_ids, dense_ids], weights=[0.3, 0.7])

RRF vs Score Normalization: Why RRF Wins

Alternatives to RRF include min-max normalization (scale each retriever's scores to [0,1]) and z-score normalization. Both are sensitive to outlier documents that inflate the maximum score and collapse other scores toward zero. RRF is robust to outliers because it only uses rank positions, not raw scores. Empirical benchmarks consistently show RRF outperforms normalization-based fusion approaches.

Implementing RRF with LangChain

LangChain's EnsembleRetriever implements RRF fusion under the hood. It accepts a list of retrievers and optional weights, runs them in parallel, and returns RRF-merged results. This lets you integrate hybrid search into LCEL chains with minimal boilerplate and swap out individual retrievers without changing the fusion logic.

from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

# Dense retriever
vectorstore = FAISS.from_documents(langchain_docs, OpenAIEmbeddings())
vector_retriever = vectorstore.as_retriever(search_kwargs={'k': 20})

# Sparse retriever
bm25_retriever = BM25Retriever.from_documents(langchain_docs, k=20)

# Hybrid ensemble using RRF internally
ensemble = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.4, 0.6],
)
results = ensemble.invoke('hybrid search reciprocal rank fusion')

Quick Check

Test your understanding of reciprocal rank fusion from this lesson.

Lesson Recap

In this lesson you learned: RRF merges ranked lists without normalizing incompatible scores, the formula 1/(k + rank) gives each document a contribution from each retriever, and documents ranked highly by multiple retrievers earn the highest fused scores. The smoothing constant k=60 is a robust default. LangChain's EnsembleRetriever implements RRF natively. Next up we configure hybrid search directly in Pinecone and pgvector.

よくある質問

「スコア統合のための逆順位融合」レッスンは無料ですか?

はい。「スコア統合のための逆順位融合」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。

「スコア統合のための逆順位融合」で何を学びますか?

互換性のない類似度スコアを正規化せずに、密検索と疎検索のランキング結果リストを統合する逆順位融合を実装します。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

AI Engineering Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「スコア統合のための逆順位融合」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAI Engineering Academyレッスンでコードを書いて実行できますか?

はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. 密検索と疎検索の比較:トレードオフ
  2. BM25キーワード検索を実装する
  3. スコア統合のための逆順位融合
  4. Pineconeとpgvectorでハイブリッド検索
← AI Engineering Academyに戻る