3行でいうと

  • キーワード検索は固有名詞や型番に強く、ベクトル検索は言い換えに強い。得意が違うので、合わせる価値がある。
  • 2つの点数は単位が違うので、足さずに「順位」で混ぜるRRFがよく使われる。式は 1÷(定数+順位) の合計。
  • リランキングは、検索で集めた数十〜数百件を、質問と一緒に読み直して並べ替える追加の段。
  • Anthropicの試験では、検索の取りこぼしが5.7%から、文脈つきの併用で2.9%、さらに並べ替えで1.9%になった。

この記事は、実際に使って確かめた記録ではありません。この記事は公式の文書や論文を読んでまとめたものです。

先に結論

前提:2つの検索

なぜ両方か

Pineconeの文書は、キーワード検索(BM25や疎ベクトル)は、商品コード、エラー文字列、名前のような特定の文字列に合い、ベクトル検索は意味に合う、と説明しています。「それぞれが取りこぼすものを、もう片方が拾う」という趣旨です。

Anthropicの記事の例は分かりやすいものです。技術サポートの資料に対し、「Error code TS-999」と聞くと、ベクトル検索はエラーコード全般の資料を見つけても、「TS-999」そのものを見逃すことがある。BM25はその文字列を探すので見つけられる、という説明です。

論文の側では、BEIR(NeurIPS 2021、18のデータセットでの検索方式の比較)が、BM25を「頑健な基準線」と評価し、再ランキング型と遅延相互作用型(質問と文書を細かい単位で突き合わせる方式)が平均で最良だが計算費用が高い、密なベクトル検索は効率が良いが他の方式より劣ることが多い、と報告しています。これは新しい領域に初めて当てたとき(ゼロショット)の結果で、2021年当時のモデルによるものです。

混ぜ方:スコアを足さず、順位を混ぜる(RRF)

2つの検索は点数の尺度が違います。Pineconeの文書は、BM25と密ベクトルの点数は「比べられない尺度」にあり、そのまま足す・平均するのは意味がなく、片方が支配的になりがち、と書いています。そこで RRF(Reciprocal Rank Fusion、逆順位融合)では、点数ではなく順位を使います。

式は、Elasticの文書でもPineconeの文書でも同じです。各検索の結果の中で、その文書が何位だったかを使って、

点数 = 各検索について 1 ÷(定数k + 順位)を合計する

順位は1位から数えます。その検索の結果に出ない文書は、その検索からは0点です。Pineconeの文書は、元の論文はk=60を使っていたと書いており、Elasticの rank_constant の既定値も60です。kを大きくすると、下位の文書の影響が増える、とElasticの文書は説明しています。

僕らの計算です。k=60で、次の3つの文書があるとします(順位は仮の例です)。

文書 キーワード検索の順位 ベクトル検索の順位 RRFの点数
B 3位 1位 1/63+1/61=0.03227
A 1位 5位 1/61+1/65=0.03178
C 2位 出ない 1/62=0.01613

片方で1位でも、もう片方に出ないCは低くなります。両方の上位に出るBが一番です。BはAより、片方の1位を持たないのに、両方で上位だったので上回ります。

公式文書のRRFの式と設定

公式文書の例です。この記事では、実行していません。Pineconeの文書にあるPythonの関数です。

def reciprocal_rank_fusion(rankings, k=60):
    scores = {}
    for ranking in rankings:
        for rank, doc_id in enumerate(ranking, start=1):
            scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
    return scores

設定の対応は次のとおりです。

製品 設定 内容
Elasticsearch rank_constant 既定は60。1以上。大きいほど下位の影響が増える
Elasticsearch rank_window_size 各検索から取る件数。大きいと関連度が上がるが性能は下がる
Pinecone(文書のサンプルコード) k サンプル関数の引数で、既定60。大きいと、1つの検索の上位の支配が弱まる
Pinecone(文書のサンプルコード) fetch_k APIの引数ではなく、例の中の変数。混ぜる前に各検索から取る深さで、最終のtop_k以上に
OpenAI(ファイル検索) hybrid_search.embedding_weight、text_weight RRFで、意味とキーワードの重みを調整。どちらか一方は0より大きく

Pineconeの文書(10月22日に読んだ版)は、比較表でハイブリッド検索の形を3つ挙げています。Vectors APIの節では、さらに、密と疎を1つの索引に入れる形と、2つの索引にIDでつなぐ形(別々に検索して、手元で結果を混ぜる)に分けて説明しています。以下は、比較表の3つです。

  • 絞ってから順位付け:キーワードの条件で候補を絞り、そこをベクトルで並べる。1回の要求で済み、点数の正規化が不要。
  • クライアント側で融合:2つを別々に検索し、RRFで混ぜる。
  • サーバー側の組み合わせ:1つの索引に密と疎のベクトルを持たせ、重み(alpha)で調整する。疎の点数には上限がないため、正規化が必要。

並べ替え(リランキング)

仕組み

Sentence Transformersの文書が、2段構えの標準形を説明しています。

  1. 検索(retrieve): 速い方法(バイエンコーダー=質問と文書を別々にベクトル化する方式、またはBM25)で、100件ほどの候補を取る。
  2. 並べ替え(re-rank): 遅いが精密な方法(クロスエンコーダー=質問と文書をいっしょにモデルに入れて、関連度を0〜1で出す方式)で、候補を採点し直す。

クロスエンコーダーは、質問と文書の間で注意(どの語に注目するか)を働かせるので、精度が高い。ただし、全文書を採点するのは遅すぎるので、検索で絞った候補にだけ使う、と書かれています。

Nogueiraらの論文(2019)は、BERTによる質問と文章の再ランキングが、MS MARCOの文章検索で当時の最高を、MRR@10(上位10件のうち最初の正解の順位の逆数の平均)で相対27%上回った、と報告しています。BEIRの論文でも、再ランキング型が平均で最良、ただし計算費用が高い、とされています。

実際のAPI

CohereのRerank APIの文書では、質問と文書のリストを渡すと、関連が高い順に並べて返します。

Anthropicの数字

Anthropicの記事(2024年9月19日)の試験は、複数の領域(コード、小説、論文)、埋め込みの組み合わせで、「上位20件に正解が入らなかった割合」を測りました。

構成 取りこぼし(上位20件) 基準からの減り方
文脈なしの埋め込み(基準) 5.7% ―
文脈つきの埋め込み 3.7% 35%減
文脈つきの埋め込み+文脈つきBM25 2.9% 49%減
上の構成+並べ替え(上位150件から20件へ) 1.9% 67%減

並べ替えにはCohereのモデルを使い、Voyageの再ランク器は試していない、と記事にあります。まとめの項では、「埋め込み+BM25は、埋め込み単独より良い」「並べ替えは、しないより良い」「上位20件を渡すのは、10件や5件より良い」と書かれています。

実務での使い分け(見立てです)

見立てです。以下は、読んだ資料からの整理で、検証したものではありません。

状況 考え方
型番、人名、エラーコードで探す場面が多い キーワード検索を必ず入れる
言い換えや質問文で探す場面が多い ベクトル検索を中心にして、キーワードを補助に
両方ある RRFで混ぜる。重みの調整は、まず既定のまま始め、測ってから
上位の質が足りない 検索で多めに取り(例:50〜150件)、並べ替えで絞る
遅延や費用が厳しい 並べ替えの対象件数を減らす。Anthropicも、件数と性能に損得がある、と書いている

日本語では、キーワード検索の質が、文章の切り方(分かち書き)に左右されます。Elasticsearchには日本語用の分析プラグイン(kuromoji)があり、空白で区切らない言語向けにはn-gram(文字を重ねて切る方法)の機能もあります。この選び方でRRFの結果がどう変わるかを比べた資料は、読んだ範囲にありません。

つまずきやすい点

気をつけること

出典

更新履歴