RRF(Reciprocal Rank Fusion)とは?ハイブリッド検索のスコア統合を数式と実験で徹底解説

RRF(Reciprocal Rank Fusion)とは何かを一次情報つきで解説する。BM25とベクトル検索のスコアを単純に足せない理由、k=60の由来、合成データによる融合実験でnDCGとrecallの実測差を示し、Milvus・OpenSearch・Elasticsearchでの実装まで扱う。

1. ハイブリッド検索のスコア統合問題: BM25とベクトルスコアは足せない

キーワード検索(BM25などの全文検索)とベクトル検索(埋め込みの類似度検索)を組み合わせたハイブリッド検索は、いまや検索基盤の標準構成になっている。ベクトル検索は意味的な近さに強い一方で固有名詞や型番のような「表記の完全一致」に弱く、BM25は完全一致に強い一方で語彙のミスマッチ(言い換え)に弱いという、互いに補い合う弱点を持つからだ(この対比の詳細は ANN基礎編 第1章で扱っている)。

問題は、両者の検索結果をどう1つのランキングにまとめるかである。素朴には「両方のスコアを足す」ことを考えたくなるが、これはうまくいかない。理由は主に2つある。1つはスケールが違うことで、BM25のスコアは理論上上限がなくクエリの語数やIDF(逆文書頻度)次第で桁が変わるのに対し、コサイン類似度は \([-1,1]\) (正規化済みベクトルなら実質 \([0,1]\) )に収まる。単純に足すと値の大きい方がほぼ一方的に結果を支配してしまい、しかもそのスコア分布はクエリごとに揺れるため、固定の係数での事前正規化も当てにならない。もう1つは意味が異なることで、BM25のスコアは「統計的にどれだけ珍しい語が一致したか」の指標、コサイン類似度は「ベクトル空間上の角度」の指標であり、単位も意味も異なる量を足し合わせること自体に数学的な正当性がない。

この問題への実務的な解の1つが、スコアそのものを比較するのをやめ、各リストにおける「順位」だけを使って統合するという発想である。順位は常に \(1, 2, 3, \dots\) という共通のものさしなので、BM25だろうがベクトル検索だろうが、どんな検索エンジンの出力であっても公平に扱える。この発想を定式化したのが本記事の主題、Reciprocal Rank Fusion(RRF) である。

2. RRFの定義と手計算で確かめる

RRFは、Cormack・Clarke・Büttcherの2009年のSIGIR論文( Reciprocal Rank Fusion outperforms Condorcet and Individual Rank Learning Methods , SIGIR'09)で提案された、複数のランキングを統合するための素朴なスコアリング式である。文書集合 \(D\) と、複数のランキング(順位付け)の集合 \(R\) が与えられたとき、文書 \(d \in D\) のRRFスコアは次式で計算される。

\[ \text{RRF}(d) = \sum_{i \in R} \frac{1}{k + \text{rank}_i(d)} \]

ここで \(\text{rank}_i(d)\) はランキング \(i\) における文書 \(d\) の順位(1位から始まる整数)、\(k\) は平滑化パラメータである。統合後は、このRRFスコアが大きい順に文書を並べ替えれば、それが最終的な統合ランキングになる。

論文はこの式を選んだ理由を、「上位の文書ほど重要だが、指数関数のような急減衰する関数と違い下位文書の重要度がゼロに消えてしまわないようにした。定数 \(k\) は一部の外れ値的なシステムが特定の文書を過度に高順位へ押し上げてしまう影響を緩和する」(原文: “while highly-ranked documents are more important, the importance of lower-ranked documents does not vanish… The constant k mitigates the impact of high rankings by outlier systems.")と説明している。

言葉だけでは掴みにくいので、5件の文書 D1〜D5 に対する2つの順位リスト(キーワード検索とベクトル検索)を使って、\(k=60\) で実際に計算してみる。

RRF計算の概念図。キーワード検索(BM25)の順位リストとベクトル検索の順位リストがそれぞれ独立に文書を並べ、両方の順位を1/(k+rank)の和で統合してRRF融合スコアを計算する。統合後はD2, D1, D3とD4(同点), D5の順になる

文書キーワード検索の順位ベクトル検索の順位\(\frac{1}{60+\text{rank}_{kw}}\)\(\frac{1}{60+\text{rank}_{vec}}\)RRFスコア
D1140.0163930.0156250.032018
D2310.0158730.0163930.032266
D3250.0161290.0153850.031514
D4520.0153850.0161290.031514
D5430.0156250.0158730.031498

統合ランキングは D2 (0.032266) > D1 (0.032018) > D3 = D4 (0.031514, 完全な同点) > D5 (0.031498) となる。D3とD4がRRFスコアで完全に一致しているのは偶然ではない。D3の順位は \((2, 5)\) 、D4の順位は \((5, 2)\) で、単に2つのランキングでの役割が入れ替わっているだけであり、\(\frac{1}{k+2}+\frac{1}{k+5}\) と \(\frac{1}{k+5}+\frac{1}{k+2}\) は加算の順序が違うだけの同じ値になる。RRFは「どのランキングでその順位を取ったか」ではなく「順位の多重集合」だけを見ているという性質が、この数値例からも確認できる。

3. なぜ順位ベースが強いか: スコアスケール不変性

RRFが実務で好まれる最大の理由は、任意の単調変換に対してスコアが不変という性質にある。ランキング \(i\) の生スコアに対して、順位を保存する変換(定数倍、定数を足す、対数を取る、シグモイドをかけるなど、狭義単調増加であれば何でもよい)を施しても、\(\text{rank}_i(d)\) は一切変化しない。つまりBM25の実装が変わってスコアのスケールが10倍になろうが、埋め込みモデルを差し替えてコサイン類似度の分布が変わろうが、RRFの統合結果は変わらない。

これは、素朴なスコア加算はもちろん、min-max正規化のような「スコアを揃えてから統合する」手法とも一線を画す性質である。min-max正規化はクエリごとに最小値・最大値を計算し直す必要があり、外れ値が1件混じるだけで正規化後の分布全体が歪みうる。RRFはそもそもスコアの値を見ないため、この種の較正作業が原理的に不要になる。論文も「RRFは特定のランキング手法が返す恣意的なスコアを一切気にせず順位だけを組み合わせられる」「順位さえ分かれば1システムずつ逐次的にスコアを積み上げられ、全ランキングを同時にメモリに保持する必要がない」という実装上の利点を指摘している(原文: “RRF… combines ranks without regard to the arbitrary scores returned by particular ranking methods… ranks may be computed and summed one system at a time, avoiding the necessity of keeping all rankings in memory.")。この「スコアスケール不変性」が実際にどれだけ効くのかを、以降の実験で定量的に確認する。

4. 実験: 2つの不完全なランカーの融合

実験設定

\(N=1000\) 件の文書からなる合成データを作り、そのうち50件をランダムに「正解の関連文書」とする。この50件をさらに25件ずつ2グループに分割し、片方を「キーワード寄りランカーが得意なグループ」、もう片方を「意味寄りランカーが得意なグループ」と呼ぶ。各ランカーのスコアは次のように生成する(NumPyのdefault_rngでシード固定)。

  • 得意グループの文書: 平均 \(\mu_{hi}=2.0\) 、標準偏差 \(\sigma=1.0\) の正規分布からスコアを生成(強い正のシグナル)
  • 不得意グループの文書: 平均 \(\mu_{lo}=1.0\) 、同じ \(\sigma=1.0\) の正規分布(弱いが確実に正のシグナル)
  • 無関連文書(950件): 平均 \(0\) 、同じ \(\sigma=1.0\) (シグナルなし)

キーワード寄りランカーAは「キーワードグループ」で \(\mu_{hi}\) 、「意味グループ」で \(\mu_{lo}\) を使い、意味寄りランカーBはその逆(「意味グループ」で \(\mu_{hi}\) 、「キーワードグループ」で \(\mu_{lo}\) )を使う。つまりどちらのランカーも、自分の不得意な半分の関連文書に対して完全に無力なわけではないが、弱いシグナルしか持たない。これはBM25が言い換えを部分的にしか拾えない、ベクトル検索が完全一致の希少語をある程度は拾えるものの決定打には欠ける、という実際のハイブリッド検索の非対称性を単純化したモデルである。

評価指標には、\(\text{rel}_i\) を順位 \(i\) の文書の関連度(\(1\) =関連、\(0\) =無関連)として、次のnDCG(正規化割引累積利得)を使う。

\[ \text{DCG@k} = \sum_{i=1}^{k} \frac{\text{rel}_i}{\log_2(i+1)}, \qquad \text{nDCG@k} = \frac{\text{DCG@k}}{\text{IDCG@k}} \]

\(\text{IDCG@k}\) は理想的な(関連文書を上位に並べた)順序でのDCG@kで、nDCGを \([0,1]\) に正規化する。recall@kは「上位k件に含まれる関連文書数 ÷ 関連文書の総数」である(本設定では関連文書が50件なので、recall@10の理論上限は \(10/50=0.20\) になる)。

実装(抜粋)

import numpy as np

def ndcg_at_k(rel_in_rank_order, k):
    rel = np.asarray(rel_in_rank_order[:k], dtype=float)
    dcg = np.sum(rel / np.log2(np.arange(2, len(rel) + 2)))
    ideal = np.sort(rel_in_rank_order)[::-1]
    idcg = np.sum(ideal[:k] / np.log2(np.arange(2, len(ideal[:k]) + 2)))
    return dcg / idcg if idcg > 0 else 0.0

def rrf_fuse(orders, k=60, n_docs=1000):
    """orders: 各ランキングの文書index配列(0番目が1位)"""
    total = np.zeros(n_docs)
    for order in orders:
        rank = np.empty(n_docs, dtype=int)
        rank[order] = np.arange(1, n_docs + 1)
        total += 1.0 / (k + rank)
    return np.argsort(-total)  # 融合後の順位配列

実測結果

シード固定のうえ300試行を平均した結果は以下の通り(標準偏差も併記)。

指標キーワード寄りランカー単独意味寄りランカー単独RRF融合(k=60)
nDCG@100.7346 ± 0.09140.7260 ± 0.10270.8807 ± 0.0983
recall@100.1365 ± 0.02780.1355 ± 0.02900.1713 ± 0.0224

単独ランカーとRRF融合のnDCG@10・recall@10比較の棒グラフ。nDCG@10はキーワード寄り0.735、意味寄り0.726に対しRRF融合0.881。recall@10はキーワード寄り0.136、意味寄り0.136に対しRRF融合0.171(理論上限0.20)

RRF融合はnDCG@10で単独ランカーを14〜15ポイント、recall@10で3.5ポイント前後上回った。recall@10は理論上限0.20に対し、単独ランカーが0.136(上限の約68%)にとどまるのに対し、RRF融合は0.171(上限の約85%)まで到達している。これは、各関連文書が(強弱の差はあれ)2つのランカーの少なくとも一方から正のシグナルを受け取っており、順位ベースの和がその「複数系統からの弱い合意」を積み上げて浮かび上がらせているためである。単独のランカーでは自分の不得意な半分の関連文書を安定して上位に上げられないが、RRF融合はどちらか一方が強く支持する文書をきちんと拾い上げる。これは実際のハイブリッド検索でBM25とベクトル検索を組み合わせる動機そのものであり、合成データという単純化された設定でも同じ効果が定量的に確認できたことになる。

5. 実験: kの感度とk=60の由来

RRFの唯一のパラメータ \(k\) を、実験4と同じ300試行・同じスコア生成過程を使い、\(k=1\) から \(k=1000\) まで対数間隔で振ってnDCG@10がどう変化するかを測定した。

kパラメータの感度掃引。横軸は対数スケールのk(1〜1000)、縦軸はnDCG@10。k=10〜100付近でなだらかな山を描き、実測ピークはk=32(nDCG@10=0.899)、k=60では0.881。k=1000まで振ってもnDCG@10は0.853と大きくは崩れない

\(k\)11032(実測ピーク)601003161000
nDCG@100.8320.8680.8990.8810.8700.8570.853

実測ピークは \(k=32\) (nDCG@10=0.899)だが、\(k=10\) から \(k=100\) の範囲ではnDCG@10は0.868〜0.899の間に収まっており、非常になだらかな山になっている。極端な \(k=1\) でも0.832、\(k=1000\) でも0.853と、ピークから10ポイント弱しか落ちない。つまり \(k\) を多少ずらしても致命的な性能劣化は起きず、RRFはこのパラメータに対して広く頑健であることが、この合成データでも定量的に確認できた。

この頑健性は、Cormack・Clarke・Büttcherの原論文自体が最初に発見していたことでもある。論文のTable 1では、30個のモデルシステムの結果をTREC topics 351–400で融合する予備実験において、\(k\) を0から500まで振ってMAP(Mean Average Precision)を測定している。

\(k\)0102030405060708090100500
MAP.2072.2123.2134.2139.2138.2144.2145.2146.2147.2145.2142.2098

論文はこの結果について次のように明言している。

The results of the first, shown in table 1, indicated that k = 60 was near-optimal, but that the choice was not critical.

つまり \(k=60\) という値そのものに数理的な特別さはなく、著者らの予備実験でたまたま近傍最適だった値を「以降の検証では変更しなかった」だけである(原文: “where k = 60 was fixed during a pilot investigation and not altered during subsequent validation”)。それにもかかわらずMilvus・OpenSearch・Elasticsearchのいずれもデフォルト値として \(k=60\) (あるいはそれに準じる値)を採用しているのは、この論文以来ほぼ20年にわたって多くの追試で「悪くない」ことが確認されてきた、業界の慣習的なデフォルトという性格が強い。

なお同論文は、RRFがCondorcet FuseやCombMNZ(スコアと順位を組み合わせた別の統合手法)よりも一貫して優れることも示している。TREC Robust・TREC 3・TREC 5・TREC 9の4タスクで実際の参加者の順位を融合した実験(Table 2)ではRRFのMAPがCondorcet Fuseを全4ケース、CombMNZを4ケース中3ケースで上回り、LETOR 3データセット(583,850件のクエリ・文書ペア)での最終実験(Table 3)ではRankSVM・RankBoost・AdaRank-MAP・ListNetなど学習型ランキング手法を含む個々の手法すべてを \(p<.003\) で上回った(最良個別手法との差はMAPで0.02、約4%)。

6. min-max正規化ベース融合との使い分け

RRFの「スコアスケール不変性」が実際どれだけ効くのかを確かめるため、実験4と同じ2つのランカーのスコアに意図的に極端なスケール差を持たせた融合実験を行った。ランカーAのスコアはそのまま、ランカーBのスコアには単調変換 \(\text{score}_B' = 400 \times \text{score}_B + 5000\) を施す(BM25風の小さな素点のAと、内積スコア風の桁の大きいBを模したもの。単調変換なので統合結果には本来影響しないはずの操作である)。この状況で(1)生スコア加算、(2)クエリ内min-max正規化後に加算、(3)RRF、の3通りを比較した。

\[ \text{minmax}(x) = \frac{x - \min(x)}{\max(x) - \min(x)} \]
指標ランカーA単独ランカーB単独生スコア加算min-max正規化加算RRF(k=60)
nDCG@100.73320.73200.73350.90460.8858
recall@100.13700.13560.13590.17530.1719

生スコアをそのまま加算した場合のnDCG@10は0.7335で、これはランカーB単独(0.7320)とほぼ同じ値である。つまりスケールの大きいランカーBがほぼ一方的に統合結果を支配してしまい、ランカーAの情報はノイズに埋もれて実質的に無視されている。これが冒頭で述べた「素朴な加算はうまくいかない」の実測での再現である。

一方、min-max正規化してから加算した場合はnDCG@10が0.9046まで回復し、この実験ではRRF(0.8858)をわずかに上回った。min-max正規化はスコアの「大きさの情報」(たとえば類似度0.95と0.60の差)を保持したまま統合できるため、その差が意味を持つ場面ではRRFより有利に働くことがある。実際、OpenSearchは normalization-processor というスコアベースの統合手法(min-max・L2正規化 × arithmetic/geometric/harmonic平均の組み合わせ)を用意しており、RRFと並ぶ選択肢として公式にサポートしている。

ただしmin-max正規化には、クエリごとに毎回min/maxを再計算する必要があり、外れ値1件で分布全体の正規化結果が歪みうるという弱点がある。RRFはスコアの値自体を一切見ないためこの種の較正が原理的に不要で、埋め込みモデルの差し替えやスコア分布の変化に構造的に頑健である。使い分けの目安は、各リストのスコアが同じ意味論で較正されており「差の大きさ」自体に価値がある場合はmin-max正規化、スコアの意味論や分布がリスト間・クエリ間で大きく変わりうる場合はRRFを優先するという判断になる。両者は排他的ではなく、OpenSearchのように両方を検索パイプラインとして提供し用途に応じて切り替える設計が実務上も合理的である。

7. 実装: Milvus・OpenSearch・Elasticsearchでの使い方

Milvus: RRFRanker

Milvusはhybrid_search() APIとRRFRankerクラスでRRFによる統合をサポートしている。デフォルトの \(k\) は60で、有効範囲は \((0, 16384)\) である( Milvus公式ドキュメント: RRF Ranker )。Milvusの内部アーキテクチャ(Query Node・segment・consistency levelなど)については Milvus内部構造編 で詳しく扱っている。

from pymilvus import MilvusClient, AnnSearchRequest, RRFRanker

client = MilvusClient(uri="http://localhost:19530")

# BM25全文検索(sparse_vectorフィールド)
req_bm25 = AnnSearchRequest(
    data=["ベクトル検索の仕組みとは"],
    anns_field="sparse_vector",
    param={"metric_type": "BM25"},
    limit=20,
)
# 埋め込みベクトル検索(dense_vectorフィールド)
req_dense = AnnSearchRequest(
    data=[query_embedding],
    anns_field="dense_vector",
    param={"metric_type": "COSINE"},
    limit=20,
)

ranker = RRFRanker(k=60)  # デフォルトk=60、範囲は(0, 16384)

results = client.hybrid_search(
    collection_name="docs",
    reqs=[req_bm25, req_dense],
    ranker=ranker,
    limit=10,
)

OpenSearch: score-ranker-processor(RRF)とnormalization-processor

OpenSearchはバージョン2.19でNeural Searchプラグインにscore-ranker-processorを追加し、rrfという統合手法(combination technique)としてRRFをサポートした( Introducing reciprocal rank fusion for hybrid search - OpenSearchScore ranker - OpenSearch Documentation )。rank_constantのデフォルトは60(最小値1)である。search pipelineとして定義し、検索リクエストのクエリパラメータで指定する。OpenSearchのk-NN検索の内部実装については OpenSearch内部構造編 を参照してほしい。

PUT /_search/pipeline/rrf-pipeline
{
  "description": "RRFによるハイブリッド検索の統合パイプライン",
  "phase_results_processors": [
    {
      "score-ranker-processor": {
        "combination": {
          "technique": "rrf",
          "rank_constant": 60
        }
      }
    }
  ]
}
POST my_index/_search?search_pipeline=rrf-pipeline

前節で触れた通り、OpenSearchはスコアベースの統合手法であるnormalization-processor(min-max・L2正規化)も別途提供しており、要件に応じてRRFと使い分けられる( Normalization processor - OpenSearch Documentation )。

Elasticsearch: rrfリトリーバー

Elasticsearchはrrfリトリーバーとして、複数の子リトリーバー(標準クエリ・knnクエリなど)を統合する機能を提供している( Reciprocal rank fusion - Elasticsearch Reference )。パラメータ名はrank_constant(デフォルト60、最小値1)で、統合対象の件数を決めるrank_window_sizeもあわせて指定する。

{
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": { "match": { "text": "ベクトル検索" } }
          }
        },
        {
          "knn": {
            "field": "vector",
            "query_vector": [0.12, 0.34, -0.05],
            "k": 50,
            "num_candidates": 100
          }
        }
      ],
      "rank_window_size": 50,
      "rank_constant": 60
    }
  }
}

3系統とも計算式そのものは論文のRRFと同一で、パラメータ名(k / rank_constant)とデフォルト値(60)が事実上の業界標準として揃っている点が確認できる。

まとめ

  • RRFは \(\text{RRF}(d) = \sum_{i} \frac{1}{k+\text{rank}_i(d)}\) という素朴な式で、複数の検索結果を「順位だけ」を使って統合する手法である。スコアの値そのものを見ないため、BM25とベクトル検索のようにスケールも意味も異なるスコアを、較正なしで公平に統合できる。
  • 合成データを使った実験では、2つの不完全なランカー(キーワード寄り・意味寄り)を単独で使うよりも、RRF融合の方がnDCG@10・recall@10ともに明確に上回った(nDCG@10で+0.146〜0.155、recall@10で理論上限比68%→85%)。
  • \(k\) の掃引実験では、\(k=10\) 〜\(100\) の広い範囲でnDCG@10がなだらかに高い水準を保ち、原論文自身も「\(k=60\) は近傍最適だが選択自体はクリティカルではない」と述べている。
  • スコアスケールが極端に異なる設定では、素朴な加算は失敗し(スケールの大きい方に支配される)、min-max正規化とRRFはどちらも良好な結果を回復した。用途に応じて両者を使い分けるのが実務的である。
  • Milvus・OpenSearch・Elasticsearchはいずれも \(k\) (またはrank_constant)のデフォルトを60とするRRF実装を公式にサポートしている。

よくある質問(FAQ)

Q. kはいくつにすべきですか?

原論文の予備実験(Table 1)でも本記事の実験でも、\(k=10\) 〜\(100\) 程度の範囲であればnDCG@10はなだらかに変化するだけで致命的な劣化は起きない。まずはMilvus・OpenSearch・Elasticsearchいずれも採用している既定値の60から始め、自分のデータセットで評価指標(nDCGやrecall)を測り、必要なら10〜100の範囲で軽く振ってみるのが現実的である。60という数値自体に数理的な必然性はなく、20年近く多くの追試を経てきた実績ある既定値、という位置づけで捉えるとよい。

Q. RRFとスコア正規化(min-max等)のどちらを使うべきですか?

各検索エンジンのスコアが同じ意味論で較正されており「差の大きさ」自体に意味がある場合(例: 類似度0.9と0.5の差を信頼できる)は、min-max正規化のようなスコアベースの統合がより多くの情報を活かせる。複数のエンジンやモデルを組み合わせる、あるいはスコア分布がクエリごとに大きく揺れる状況では、スケールに一切依存しないRRFの頑健性が有利になる。OpenSearchのように両方を検索パイプラインとして提供し、要件に応じて切り替えられる設計が現実的な落としどころである。

Q. 3系統以上の検索結果の融合もできますか?

できる。RRFの式 \(\sum_{i \in R} \frac{1}{k+\text{rank}_i(d)}\) は、\(|R|=2\) の場合に限定された式ではなく、任意の本数のランキングに対して和を取るだけでよい。たとえばBM25・密ベクトル検索・SPLADEのような疎ベクトル検索・メタデータベースのビジネスルールスコアの4系統を融合する場合も、単純に4項の和を取ればよい。系統ごとに重要度を変えたい場合は、MilvusのWeightedRankerのように各項に重みを掛ける拡張版(weighted RRF)を使うこともできる。

関連記事

ベクトル検索実践入門(真鍋知博、技術評論社)

他の分野の定番書は エンジニアにおすすめの技術書10選 にまとめている。

参考文献

論文

公式ドキュメント