上級 11 分最適化

ハイブリッド検索:BM25 + vectoriel

端的な回答

ハイブリッド検索では、同じクエリに対してキーワード検索(BM25)とベクトル検索を並行して実行し、その後、両方のランキングを、通常は Reciprocal Rank Fusion (RRF) によって融合します。これにより、埋め込みが失敗するケース(識別子、固有名詞、稀な用語)を補完しつつ、言い換えの理解を損なうことなく対応できます。Qdrant、Weaviate、Elasticsearch はこれをネイティブでサポートしており、ChromaDB を使用する場合でも、Python の 30 行で構築できます。

ベクトルデータベースは意味に基づいて検索しますが、文字列そのものの一致は捉えません。「RG/2024-117」のような参照番号やチケット番号を見逃します。逆に、キーワード検索は「automobile」と「voiture」が同じものを指すことを理解できません。このガイドでは、両者を組み合わせる方法、選ぶべき検索結果の融合手法、QdrantとWeaviateの機能、そして気づかないうちにBM25部分を台無しにするフランス語のトークン化の落とし穴を紹介します。

著者 Mohamed Meguedmi·更新 2026-09-30·Windows・macOS・Linuxでテスト済み

#ベクトル検索だけでは一部の質問に対応できない理由

ベクトル検索は、各文章をその全体的な意味を表すベクトルに変換し、質問のベクトルに最も近いベクトルを持つ文章を返します。この圧縮は言い換えには非常に有効ですが、識別子、エラーコード、人名、社内の略語など、一字一句正確に見つける必要があるものには不向きです。埋め込みでは「PROD-4817」と「PROD-4871」が似ているため、無関係なチケットが正しいチケットより上位に表示されることがあります。ハイブリッド検索は、この欠点を補います。単語が完全一致で含まれているかどうかに基づく別のランキングを追加し、両方を統合することで、それぞれの手法がもう一方の弱点を補うようにします。

識別子および参照
チケット番号、案件番号、契約番号、SKU、エラーコードは、埋め込みでは確実に保持できない文字列ですが、BM25なら該当箇所に含まれていれば検索できます。
使用頻度の低い専門用語
医学的、法的または技術的用語で頻度が低いもの:BM25では稀な単語の重みが高くなるが、その埋め込みベクトルは曖昧になる可能性がある。
非常に短い検索クエリ
「facture 2024」のような2語だけでは、埋め込みに使える情報がほとんどありません。一方、キーワードはそのまま比較できます。
言い換えと表現の変更
逆のケースでは、「contrat à durée déterminée」(有期雇用契約)と「CDD」、あるいは「voiture」(車)と「automobile」(自動車)は、共通する単語を含みません。これらを関連付けられるのは、ベクトル検索だけです。
i
典型的なケース
チケットのデータベースで「PROD-4817」をベクトル検索だけで検索すると、内容が似たチケットが返されます。一方、キーワード検索なら、その番号を持つ唯一のチケットをすぐに特定できます。

#BM25:キーワード検索が行っていること

ローカルRAGキット

あなたのドキュメント、あなたの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のドキュメントでは、調整が不要で、各関連度指標が互いに関係している必要もない手法として紹介されています。

RRF式
score(doc) = somme, sur chaque liste i où le doc apparaît, de 1 / (k + rang_i(doc))

k = 60 par défaut (valeur par défaut d'Elasticsearch)
rang_i = 1 pour le premier de la liste, 2 pour le deuxième, etc.
Un doc absent d'une liste n'ajoute rien pour cette liste.

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 を提供しており、後者は生スコアを保持しつつ、結合前にその分布(平均と標準偏差)を正規化します。スコアの分布が不明な場合、ランクによる融合の方がより堅牢です。測定可能な場合、スコアによる融合の方がより微細な調整が可能です。

#どのツールを選ぶか:ハイブリッド検索に標準対応したエンジン

ツール別のハイブリッド検索(公式ドキュメント、2026年9月)
ツールハイブリッド検索の標準対応融合重みの調整
Qdrantはい、API Query(バージョン1.10以降で利用可能)を介して可能ですRRFまたはDBSF最近のバージョンでは、クエリごとの重みと定数kを調整できます
Weaviateはい、hybrid演算子で対応相対的な順位またはスコア(1.24以降のデフォルト)アルファパラメータ:1 = ベクトルのみ、0 = キーワードのみ
Elasticsearchはい、rrfリトリーバーありRRFrank_constant(デフォルトで60)および rank_window_size
SQLite FTS5 + ベクトル拡張組み合わせが必要要作成あなたに
ChromaDB + rank_bm25Python で組み合わせる必要があります自分で実装する必要あり(RRF は6行で実装)あなたに

本質的な違いは、数行のコードで実装できる検索結果の統合そのものではなく、インデックスにあります。ネイティブエンジンは両方のインデックスを一緒に最新の状態に保ちますが、自作の構成では BM25 インデックスをメモリに保持し、ドキュメントを追加するたびに再構築する必要があります。数千の文章断片からなる、めったに変更されないコーパスなら、自作の構成で十分です。それ以上の規模、あるいはドキュメントが毎日変更される場合には、ネイティブエンジンを使うことで両インデックス間の不整合を防げます。Weaviate についてのガイドでは、このツールを詳しく紹介しています。

#自作の実装:ChromaDB、rank_bm25、RRF

以下のコードは3つの要素を組み立てます。例でよく見られる欠陥を修正しています:正規化関数はフィルタリングの前にダイアクリティカルマーク(発音区別符号)を除去する必要があります。そうしないと、アクセント付きの各文字が単語を2つに分割してしまいます。

BM25 + ChromaDBハイブリッド(RRFあり)
import re, unicodedata
import chromadb
from rank_bm25 import BM25Okapi

def normalize(txt):
    txt = unicodedata.normalize("NFKD", txt.lower())
    txt = "".join(c for c in txt if not unicodedata.combining(c))  # retire les accents
    return re.findall(r"[a-z0-9]+", txt)

# Indexation BM25 (en mémoire) : l'indice de la liste = l'identifiant du passage
all_docs = [d["text"] for d in load_docs()]
bm25 = BM25Okapi([normalize(d) for d in all_docs])

# ChromaDB : les ids doivent être les mêmes, sous forme de chaînes "0", "1", ...
coll = chromadb.PersistentClient("./chroma_db").get_collection("docs")

def rrf_fuse(rankings, weights=None, k=60):
    weights = weights or [1.0] * len(rankings)
    scores = {}
    for ranking, w in zip(rankings, weights):
        for rank, doc_id in enumerate(ranking, start=1):
            scores[doc_id] = scores.get(doc_id, 0) + w / (k + rank)
    return sorted(scores, key=scores.get, reverse=True)

def hybrid_search(question, top_k=5, n_candidates=20):
    s = bm25.get_scores(normalize(question))
    bm25_top = sorted(range(len(all_docs)), key=lambda i: -s[i])[:n_candidates]
    bm25_ranking = [str(i) for i in bm25_top]
    vec_ranking = coll.query(query_texts=[question], n_results=n_candidates)["ids"][0]
    fused = rrf_fuse([bm25_ranking, vec_ranking])
    return [all_docs[int(i)] for i in fused[:top_k]]
!
フランス語のトークン化における罠
一般的な正規化「NFKDに変換し、a-zまたは数字以外のすべてをスペースに置き換える」では、「référence」は「re fe rence」になります。各アクセントは結合文字を残し、それがスペースに置き換えられます。BM25部分は識別子に対して引き続き機能しますが、アクセント付きの単語をすべて取りこぼします。インデックス作成前に、3つの文でnormalizeをテストしてください。

実用上の注意点は2つあります。まず、両方のインデックスで識別子を一致させる必要があります。ここでは、リストの添字を文字列に変換したものを Chroma の識別子として使います。次に、メモリ上の BM25 インデックスはプログラムの終了時に失われます。起動時に再構築するか、文書のリストをデータベースと同じ場所に保存してください。数万件のテキスト断片でも、再構築には数秒しかかかりません。

#BM25とベクトル検索のバランスを調整する

デフォルトでは、RRFは2つのリストに同じ重みを与えます。コーパスに参照情報(判例、チケット、カタログ)が多い場合はBM25の重みを大きくしてください。質問が会話的な場合は、重みのバランスを維持するか、ベクトル検索を優先してください。前述の関数では、weights=[0.6, 0.4] を渡すだけで、BM25の重みを60%にできます。Qdrantでも同じ調整が可能です。ドキュメントによると、各クエリの重みはデフォルトで1であり、その場合は元のRRFの式になります。また、最近のバージョンでは定数kも調整できます。Weaviateではalphaを設定します。1ならベクトル検索のみ、0ならキーワード検索のみになります。

コーパスの種類に応じた初期設定
コーパスと質問BM25/ベクトルの初期重み監視している内容
参照情報、番号、固有名詞(法律関連文書、チケット、カタログ)60 / 40IDに基づく質問は最初に表示されますか?
文章で書かれたドキュメント、自然言語での質問40 / 60質問を言い換えても、適切な箇所を検索できますか?
混合または未知のコーパス50 / 50実際の質問30〜50件でのRecall@5(再現率)
業界用語および略語55 / 45、BM25側に同義語辞書を採用略称と正式名称で同じ箇所が検索されるか?
→
調整する前に測定する
これらの値は出発点であり、測定結果ではありません。実際の質問を30〜50問用意し、それぞれに検索で見つかるべき文章の抜粋を対応させてください。BM25のみ、ベクトル検索のみ、ハイブリッド検索の順に、上位5件の検索結果における再現率を計算し、自分の文書でハイブリッド検索の成績が上回った場合に限って採用してください。

#結果の統合後:リランカーを追加する

結果を統合すると、各手法を単独で使う場合よりも多様な候補が得られます。その後、リランカーが各パッセージを質問と併せて読み、候補群を並べ替えることができます。つまり、この2つの手法は併用できます。通常は、ハイブリッド検索、結果の統合、上位20〜50件へのリランカーの適用、最も適した3〜5件のパッセージをプロンプトに含める、という順序です。リランカーのガイドではこの最後の工程を詳しく説明し、チャンキングのガイドでは、パッセージの大きさがBM25にも埋め込みにも影響する理由を説明しています。

#ハイブリッド検索に関するよくある質問

FAQ
RAGにおけるハイブリッド検索とは何ですか?+
同じ質問に対して、意味の近い文章部分を検索するベクトル検索と、同じ用語を含む文章部分を検索するキーワード検索(BM25)を組み合わせる方法です。2つの検索結果の順位を、通常はReciprocal Rank Fusionで1つに統合してから、上位の文章部分をモデルに渡します。
BM25とベクトル検索のスコアは、足し合わせる前に正規化する必要がありますか?+
RRFでは順位だけを使うため、正規化は必要ありません。むしろ、それこそが狙いです。両者のスコア尺度は直接比較できません。スコアを組み合わせたい場合は、Weaviateの相対スコア融合やQdrantのDBSFと同様に、まず正規化し、コーパスが変わっても結果が安定していることを確認する必要があります。
RRFでkの値をどのように選ぶべきですか?+
変更する理由がない限り、Elasticsearchのデフォルト値である60のままにしてください。値を小さくすると最上位の順位がより優遇され、大きくすると下位の順位の影響が強まります。この設定の影響は、トークン化の品質や各リストの候補数に比べると小さいです。
ChromaDBでハイブリッド検索はできますか?+
はい。Chromaのベクトル検索、PythonのBM25インデックス(rank_bm25ライブラリ)、数行のコードによるRRF融合を組み合わせれば実現できます。両方のインデックスで同じ識別子を使用し、トークン化の際にアクセント記号を除去してください。頻繁に更新されるコーパスでは、QdrantやWeaviateのようにハイブリッド検索を標準で備えるエンジンを使えば、2つのインデックスを維持する手間を省けます。
ハイブリッド検索はクエリの処理を大きく遅らせるでしょうか?+
通常、遅くなるのはごくわずかです。メモリ上のインデックスを使うBM25は非常に高速で、ベクトル検索はもともと行われています。実際の負荷は、その後に必要に応じて実行するリランカーと、語彙インデックスのメモリ使用量によるものです。各段階を個別に測るのではなく、自分のクエリで処理全体の所要時間を測定してください。
自分のドキュメントにハイブリッド検索を使う価値があるかどうかは、どう判断すればよいですか?+
検索で見つかるべき箇所が分かっている実際の質問を30〜50件用意し、BM25のみ、ベクトル検索のみ、ハイブリッド検索それぞれの上位5件の再現率を比較してください。ハイブリッド検索で改善がなければ、コーパスが純粋に会話的な内容で、ベクトル検索だけで十分なのかもしれません。主に識別子の検索で改善が見られるなら、BM25の重みを増やしてください。
このガイドは役に立ちましたか?

ご意見、誤りのご指摘、補足はありますか?ぜひお知らせください。皆さんにとってより良いガイドにするために役立ちます。