ハイブリッド検索:BM25 + vectoriel
ハイブリッド検索では、同じクエリに対してキーワード検索(BM25)とベクトル検索を並行して実行し、その後、両方のランキングを、通常は Reciprocal Rank Fusion (RRF) によって融合します。これにより、埋め込みが失敗するケース(識別子、固有名詞、稀な用語)を補完しつつ、言い換えの理解を損なうことなく対応できます。Qdrant、Weaviate、Elasticsearch はこれをネイティブでサポートしており、ChromaDB を使用する場合でも、Python の 30 行で構築できます。
ベクトルデータベースは意味に基づいて検索しますが、文字列そのものの一致は捉えません。「RG/2024-117」のような参照番号やチケット番号を見逃します。逆に、キーワード検索は「automobile」と「voiture」が同じものを指すことを理解できません。このガイドでは、両者を組み合わせる方法、選ぶべき検索結果の融合手法、QdrantとWeaviateの機能、そして気づかないうちにBM25部分を台無しにするフランス語のトークン化の落とし穴を紹介します。
#ベクトル検索だけでは一部の質問に対応できない理由
ベクトル検索は、各文章をその全体的な意味を表すベクトルに変換し、質問のベクトルに最も近いベクトルを持つ文章を返します。この圧縮は言い換えには非常に有効ですが、識別子、エラーコード、人名、社内の略語など、一字一句正確に見つける必要があるものには不向きです。埋め込みでは「PROD-4817」と「PROD-4871」が似ているため、無関係なチケットが正しいチケットより上位に表示されることがあります。ハイブリッド検索は、この欠点を補います。単語が完全一致で含まれているかどうかに基づく別のランキングを追加し、両方を統合することで、それぞれの手法がもう一方の弱点を補うようにします。
- 識別子および参照
- チケット番号、案件番号、契約番号、SKU、エラーコードは、埋め込みでは確実に保持できない文字列ですが、BM25なら該当箇所に含まれていれば検索できます。
- 使用頻度の低い専門用語
- 医学的、法的または技術的用語で頻度が低いもの:BM25では稀な単語の重みが高くなるが、その埋め込みベクトルは曖昧になる可能性がある。
- 非常に短い検索クエリ
- 「facture 2024」のような2語だけでは、埋め込みに使える情報がほとんどありません。一方、キーワードはそのまま比較できます。
- 言い換えと表現の変更
- 逆のケースでは、「contrat à durée déterminée」(有期雇用契約)と「CDD」、あるいは「voiture」(車)と「automobile」(自動車)は、共通する単語を含みません。これらを関連付けられるのは、ベクトル検索だけです。
#BM25:キーワード検索が行っていること
あなたのドキュメント、あなたのAI:あなたのPDF、メモ、メールを扱う信頼性の高いローカルRAG。何もクラウドに送信しません。
- 永久に利用できるオンラインスペース
- PDF + ファイル
- 生涯アップデート
BM25は、Lucene、Elasticsearch、OpenSearchに実装されている語彙ベースのランキング関数であり、SQLiteのFTS5モジュールでも提供されています(ドキュメントでは、bm25()関数がクエリとの行の一致の質を示す値を返すと説明されています)。BM25は3つの考え方に基づいています。1つ目は、頻度の低い単語は頻度の高い単語よりも重み付けされること、2つ目は、ある一定の出現回数を超えると、繰り返し出現する単語の寄与が減少していくこと、3つ目は、長い文書は短い文書に比べてわずかに減点されることです。パラメータk1はこの飽和を調整します。Elasticの説明によると、k1はクエリの単一タームがドキュメントのスコアに与える影響を制限します。
- 強み
- 出現頻度の低い語、識別子、短いクエリ、専門用語に強い。モデルの読み込みや学習が不要で、インデックスはコンパクト。結果も説明可能です(どの単語によってその箇所が検索結果に上がったかが分かります)。
- 弱点
- 類義語、別の表現、言い換え、タイプミスが弱点です。見出し語化(レンマ化)を行わなければ、フランス語の「signé」と「signature」は別々の単語として扱われます。
- 前提条件
- 適切なトークン化:小文字化、アクセント記号の除去、必要に応じたステミング(語幹化)。自作実装の多くが間違えるのはこの部分です。詳しくは後述します。
#ハイブリッド検索:その原理
同じ質問に対して2種類の検索を実行し、それぞれから候補リスト(20〜50件の文章の抜粋)を取得した後、2つのリストを統合して1つのランキングにします。効果が得られる理由は単純です。両方のリストに含まれる抜粋はほぼ常に質問に関連しており、さらに各手法が、もう一方の手法では見落とした抜粋も取得するからです。Weaviateはハイブリッド検索を、ベクトル検索とキーワード検索の2つの結果セットを統合して組み合わせる検索と定義しています。統合手法と相対的な重みは設定できます。どの程度の改善が得られるかはコーパスと質問によって異なります。一般に当てはめられる信頼できる数値はなく、ご自身の文書で測定する必要があります(後述)。
#正規化せずに融合:相互順位融合(Reciprocal Rank Fusion)
よくある落とし穴は、生のスコアをそのまま足し合わせることです。BM25のスコアは上限のない正の数ですが、ベクトル類似度のスコアは、範囲が限られた距離またはコサイン値です。両者の尺度は比較できず、コーパスが少し変わるだけでもずれてしまいます。Reciprocal Rank Fusionは、順位だけを使うことでこの問題を回避します。Elasticsearchのドキュメントでは、調整が不要で、各関連度指標が互いに関係している必要もない手法として紹介されています。
BM25 で一位、ベクトル検索で五位にランクインしたパスは 1/61 + 1/65、つまり約 0.0318 を得ます。両方のリストで十五位にランクインしたパスは 2/75、つまり約 0.0267 を得ます。定数 k は一位の優位性を緩和します:k が大きいほど、遠いランクの重みが増します。Elasticsearch はこの定数を rank_constant という名前でドキュメント化しており、デフォルト値は 60 です。また、融合前の各リストの長さを決めるウィンドウサイズ(rank_window_size)もあります。ドキュメントによると、より大きなウィンドウはパフォーマンスを犠牲にして関連性を向上させます。
#スコアによる融合は?
一部のエンジンは、各リストのスコアを正規化してから重みで結合するという代替手段を提供しています。Weaviate は、ランクによるランキングと相対スコアによる融合という 2 つの方法を文書化しており、後者はバージョン 1.24 以降のデフォルト方法です。また、ハイブリッド演算子で autocut を使用するにはこの方法が必須です。Qdrant は RRF と DBSF を提供しており、後者は生スコアを保持しつつ、結合前にその分布(平均と標準偏差)を正規化します。スコアの分布が不明な場合、ランクによる融合の方がより堅牢です。測定可能な場合、スコアによる融合の方がより微細な調整が可能です。
#どのツールを選ぶか:ハイブリッド検索に標準対応したエンジン
| ツール | ハイブリッド検索の標準対応 | 融合 | 重みの調整 |
|---|---|---|---|
| Qdrant | はい、API Query(バージョン1.10以降で利用可能)を介して可能です | RRFまたはDBSF | 最近のバージョンでは、クエリごとの重みと定数kを調整できます |
| Weaviate | はい、hybrid演算子で対応 | 相対的な順位またはスコア(1.24以降のデフォルト) | アルファパラメータ:1 = ベクトルのみ、0 = キーワードのみ |
| Elasticsearch | はい、rrfリトリーバーあり | RRF | rank_constant(デフォルトで60)および rank_window_size |
| SQLite FTS5 + ベクトル拡張 | 組み合わせが必要 | 要作成 | あなたに |
| ChromaDB + rank_bm25 | Python で組み合わせる必要があります | 自分で実装する必要あり(RRF は6行で実装) | あなたに |
本質的な違いは、数行のコードで実装できる検索結果の統合そのものではなく、インデックスにあります。ネイティブエンジンは両方のインデックスを一緒に最新の状態に保ちますが、自作の構成では BM25 インデックスをメモリに保持し、ドキュメントを追加するたびに再構築する必要があります。数千の文章断片からなる、めったに変更されないコーパスなら、自作の構成で十分です。それ以上の規模、あるいはドキュメントが毎日変更される場合には、ネイティブエンジンを使うことで両インデックス間の不整合を防げます。Weaviate についてのガイドでは、このツールを詳しく紹介しています。
#自作の実装:ChromaDB、rank_bm25、RRF
以下のコードは3つの要素を組み立てます。例でよく見られる欠陥を修正しています:正規化関数はフィルタリングの前にダイアクリティカルマーク(発音区別符号)を除去する必要があります。そうしないと、アクセント付きの各文字が単語を2つに分割してしまいます。
実用上の注意点は2つあります。まず、両方のインデックスで識別子を一致させる必要があります。ここでは、リストの添字を文字列に変換したものを Chroma の識別子として使います。次に、メモリ上の BM25 インデックスはプログラムの終了時に失われます。起動時に再構築するか、文書のリストをデータベースと同じ場所に保存してください。数万件のテキスト断片でも、再構築には数秒しかかかりません。
#BM25とベクトル検索のバランスを調整する
デフォルトでは、RRFは2つのリストに同じ重みを与えます。コーパスに参照情報(判例、チケット、カタログ)が多い場合はBM25の重みを大きくしてください。質問が会話的な場合は、重みのバランスを維持するか、ベクトル検索を優先してください。前述の関数では、weights=[0.6, 0.4] を渡すだけで、BM25の重みを60%にできます。Qdrantでも同じ調整が可能です。ドキュメントによると、各クエリの重みはデフォルトで1であり、その場合は元のRRFの式になります。また、最近のバージョンでは定数kも調整できます。Weaviateではalphaを設定します。1ならベクトル検索のみ、0ならキーワード検索のみになります。
| コーパスと質問 | BM25/ベクトルの初期重み | 監視している内容 |
|---|---|---|
| 参照情報、番号、固有名詞(法律関連文書、チケット、カタログ) | 60 / 40 | IDに基づく質問は最初に表示されますか? |
| 文章で書かれたドキュメント、自然言語での質問 | 40 / 60 | 質問を言い換えても、適切な箇所を検索できますか? |
| 混合または未知のコーパス | 50 / 50 | 実際の質問30〜50件でのRecall@5(再現率) |
| 業界用語および略語 | 55 / 45、BM25側に同義語辞書を採用 | 略称と正式名称で同じ箇所が検索されるか? |
#結果の統合後:リランカーを追加する
結果を統合すると、各手法を単独で使う場合よりも多様な候補が得られます。その後、リランカーが各パッセージを質問と併せて読み、候補群を並べ替えることができます。つまり、この2つの手法は併用できます。通常は、ハイブリッド検索、結果の統合、上位20〜50件へのリランカーの適用、最も適した3〜5件のパッセージをプロンプトに含める、という順序です。リランカーのガイドではこの最後の工程を詳しく説明し、チャンキングのガイドでは、パッセージの大きさがBM25にも埋め込みにも影響する理由を説明しています。
#ハイブリッド検索に関するよくある質問
RAGにおけるハイブリッド検索とは何ですか?+
BM25とベクトル検索のスコアは、足し合わせる前に正規化する必要がありますか?+
RRFでkの値をどのように選ぶべきですか?+
ChromaDBでハイブリッド検索はできますか?+
ハイブリッド検索はクエリの処理を大きく遅らせるでしょうか?+
自分のドキュメントにハイブリッド検索を使う価値があるかどうかは、どう判断すればよいですか?+
- パイプラインにリランカーを追加する
- チャンキング戦略
- Weaviate:ハイブリッド検索とマルチテナント対応
- ChromaDB と Ollama を使ったローカル RAG
- フランス語対応の優れた埋め込みモデル
- 出典:Qdrantのハイブリッドクエリ
- ソース:Weaviate、ハイブリッド検索
- ソース:Elasticsearch、逆順位融合(Reciprocal Rank Fusion)
- ソース:SQLite FTS5、関数bm25()
ご意見、誤りのご指摘、補足はありますか?ぜひお知らせください。皆さんにとってより良いガイドにするために役立ちます。