0Pricing
AI Engineering Academy · 강의

점수 병합을 위한 상호 순위 융합

상호 순위 융합을 구현해 밀집 검색기와 희소 검색기에서 나온 순위 결과 목록을 병합합니다. 서로 호환되지 않는 유사도 점수를 정규화하지 않아도 됩니다.

점수 병합을 위한 상호 순위 융합은(는) CoddyKit의 무료 AI Engineering Academy 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 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/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Engineering Academy 강의 전체를 잠금 해제할 수 있습니다. AI Engineering Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“점수 병합을 위한 상호 순위 융합”에서 뭘 배우나요?

상호 순위 융합을 구현해 밀집 검색기와 희소 검색기에서 나온 순위 결과 목록을 병합합니다. 서로 호환되지 않는 유사도 점수를 정규화하지 않아도 됩니다. 브라우저에서 직접 실행하는 실습 코드로 AI Engineering Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

AI Engineering Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 AI Engineering Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“점수 병합을 위한 상호 순위 융합” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 AI Engineering Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 AI Engineering Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 밀집 검색과 희소 검색의 비교: 상충 관계
  2. BM25 키워드 검색 구현
  3. 점수 병합을 위한 상호 순위 융합
  4. Pinecone과 pgvector의 하이브리드 검색
← AI Engineering Academy(으)로 돌아가기