ハイブリッド検索とは、キーワード検索(BM25)とベクトル検索を同時に走らせ、2つのランキングを1つに束ねる検索方式です。RAGの回答品質は、LLMに何を渡せたかでほぼ決まります。型番や社内用語のような表記が一致する語はキーワード検索が、言い換えられた質問はベクトル検索が拾う。その取りこぼしを互いに埋めるのがハイブリッド検索の狙いです。ただし、束ね方(スコア統合)を間違えると単独検索より精度が落ちます。本記事は定義と処理の流れ、統合方式の違い、主要プラットフォームの実装コード、日本語環境で逆効果になる条件、そして導入の手順までを扱います。
まとめ:ハイブリッド検索とは何か、統合方式と導入判断の要点
- ハイブリッド検索=BM25とベクトル検索の並列実行+ランキング統合。片方が苦手なクエリをもう片方が補う。
- 統合方式は順位ベース(RRF)と値ベース(スコア正規化)の2択。BM25スコアは上限なし、コサイン類似度は概ね0〜1と尺度が違うため、素の足し算は成立しない。
- RRFの計算式は
1/(k + rank)、定数kは60が事実上の標準。調整できるのはElasticsearchとOpenSearchで、Azure AI Searchのkは60固定。順位しか見ないので「僅差の1位」と「圧倒的な1位」を区別できない。スコア差を活かしたいなら正規化+加重平均を選ぶ。 - 重み付けは各製品で書ける時代になった。Elasticsearchは9.2系でrrfの子retrieverごとの
weightと、正規化+加重和の linear retriever を一般提供し、OpenSearchもRRFにweightsを渡せる。 - 日本語では、BM25側を作り込まない限りハイブリッド化で精度が下がる。公開実験では、ベクトル単独のHit Rate@10 0.8355に対し、形態素解析だけ入れた既定設定のBM25を足すと0.7819へ低下した。上回ったのは、アナライザ・正規化・リランカーまで作り込んだ構成(0.8954)だけだった。
- 導入判断は「BM25側を作り込めるか」と「評価セットで差を測れるか」で決まる。全文検索の品質が低いまま束ねると、弱い方のランキングがノイズとして混ざる。
ハイブリッド検索とは:定義と、RAGでキーワード検索と併用する理由
ハイブリッド検索は「複数の検索方式を1回の問い合わせで並べて実行し、結果を1本のランキングに統合する構成」の総称です。実務でこの言葉が指すのは、ほぼ例外なく、表記の一致を見るキーワード検索と、意味の近さを見るベクトル検索の組み合わせです。個々の検索方式ではなく、組み合わせ方の名前だと捉えると混乱しません。
キーワード検索とベクトル検索がそれぞれ取りこぼす検索クエリの具体例
キーワード検索は転置インデックスとBM25(Okapi BM25)のスコアリングで、クエリ語と文書語の表記一致を評価します。「ERR-4021」「MFC-J6983CDW」のような型番・エラーコード・固有名詞に強く、その語が本文に無ければヒットしない、という素直な挙動を持ちます。この素直さは、検索結果の理由を説明できるという利点でもあるのです。
ベクトル検索は文をEmbeddingモデルで密ベクトル化し、コサイン類似度などの距離で近い文書を返します。「印刷が途中で止まる」と「用紙が詰まって停止する」を近いものとして扱えるかわりに、学習データに無い社内固有の型番は他の型番と潰し合い、上位に出てこないことがあります。仕組みの詳細を整理した記事が、ベクトル検索とセマンティック検索の違いです。どの埋め込みモデルを選ぶかでも取りこぼし方は変わり、日本語モデルの選び方は埋め込みモデルの仕組みと選定基準で扱っています。
この2つの弱点は綺麗に相補的です。片方が0点のクエリで、もう片方が高得点を出す。ハイブリッド検索はその性質を利用します。3方式の違いを並べると次のとおりです。
| 観点 | キーワード検索 | ベクトル検索 | ハイブリッド検索 |
|---|---|---|---|
| 一致の基準 | 表記の一致 | 意味の近さ | 両方 |
| 強いクエリ | 型番・固有名詞 | 言い換え・自然文 | 混在するクエリ |
| 弱いクエリ | 同義語・言い換え | 未知の型番・略語 | 片側が弱いとき |
| スコアの値域 | 上限なし(BM25) | 概ね0〜1 | 統合方式で決まる |
| 運用するもの | アナライザ・辞書 | 埋め込みモデル | 両方+統合設定 |
RAGの回答品質が生成ではなく検索段階の文書選定で決まってしまう理由
RAG(Retrieval-Augmented Generation、検索拡張生成)は、質問に関連する文書を検索し、その本文をプロンプトに詰めてLLMに答えさせる構成です。全体像はRAGの仕組みとファインチューニングとの違いにまとめています。検索が誤った文書を上位に返せば、LLMはその誤りを流暢な日本語で述べます。生成側のプロンプトをどれだけ磨いても、渡していない情報は出てきません。
だからRAGの改善は、まず検索の再現率(Recall)を上げる作業になります。上位k件(k=5〜10が一般的)に正解文書が含まれていなければ、そこで負けが確定する。ハイブリッド検索が効くのは、この「上位k件に正解を入れる」確率を、2系統の検索で二重化できるからです。検索工程そのものの範囲と処理順はリトリーバルの工程と実装の判断基準で整理しています。
ハイブリッド検索の処理の流れ:並列検索・融合・再ランキングの3段
1回の問い合わせの中で起きることは、どの製品でも同じ3段に分けて考えられます。
- 並列検索:同じクエリ文字列をBM25用のアナライザにかけて全文検索し、同時に埋め込みモデルでベクトル化して近傍検索する。それぞれ上位50〜100件程度の候補を取る。
- 融合:2本の候補リストを、RRF(順位)かスコア正規化(値)で1本にする。ここで各リストの候補数(ウィンドウ幅)を狭くしすぎると、片側にしか出ない正解が融合の対象から外れる。
- 再ランキング(任意):融合後の上位数十件を、クロスエンコーダ型のリランカーで並べ直し、上位5件前後をLLMへ渡す。
3段目は必須ではありませんが、後述の日本語の実験でベクトル単独を上回ったのはリランカーを足した構成でした。リランカーの種類と選び方はリランクモデルの種類と選び方を参照してください。
密ベクトルと疎ベクトルを組み合わせる型もハイブリッド検索と呼ぶ
「キーワード側」をBM25ではなく、学習済みモデルで作る疎ベクトル(語ごとの重みを持つ、ほとんどが0のベクトル)で置き換える構成もあります。SPLADE系のモデルや、Elasticsearchの sparse_vector、OpenSearchのneural sparse検索がこの型です。語の一致を見る性質は残しつつ、関連語にも重みが付くため、BM25より言い換えに強くなります。
ただし、疎ベクトルのモデルは英語で学習されたものが多く、日本語で使えるかはモデルごとに確認が要ります。日本語の社内文書を扱うなら、まずはBM25+形態素解析で組み、疎ベクトルは評価セットで比較してから置き換えを検討する順が安全です。統合の考え方(RRFか正規化か)は、BM25でも疎ベクトルでも変わりません。
スコア統合の2方式:順位で束ねるRRFと、値で束ねるスコア正規化
BM25スコアとコサイン類似度をそのまま足してはいけない理由
2つの検索結果を統合する際、最初に直面するのが尺度の不一致です。Microsoftの公式ドキュメント「Relevance scoring in hybrid search using RRF」は、Azure AI Searchが返すスコアの範囲を次のように明示しています。
| 検索方式 | スコアリング | 値域 |
|---|---|---|
| 全文検索 | BM25 | 上限なし |
| ベクトル検索 | HNSW+類似度指標 | 0.333〜1.00(コサイン) |
| ハイブリッド | RRF | 融合するクエリ数×約1/kが上限 |
| セマンティックランカー | 再ランキング | 0.00〜4.00 |
BM25は文書長やクエリ語数によって3にも30にもなり、上限がありません。一方コサイン類似度は1を超えません。これを単純加算すれば、常にBM25の側だけで順位が決まります。統合するには、順位に変換するか(RRF)、値域を揃えるか(正規化)のどちらかが必要です。
RRF(Reciprocal Rank Fusion):順位の逆数だけを見て統合する
Reciprocal Rank Fusion(RRF)は、各ランキングでの順位の逆数を足し合わせます。2009年のSIGIRで発表されたCormackらの原論文が出どころで、文書dのスコアは各リストについて 1/(k + rank(d)) を計算し、合算した値です。定数kはAzure AI Searchのドキュメントで「実験上、60のような小さい値が最も良く機能する」と説明され、Elasticsearchの rank_constant、OpenSearchの score-ranker-processor、Weaviateのranked fusionも同じ60を既定にしています。
ただし、kを触れるかは製品によって違います。Elasticsearchの rank_constant とOpenSearchの combination.rank_constant は変更できますが、Azure AI Searchのkはアルゴリズム内部の定数で公開パラメータがなく、60から動かせません(ベクトル検索の近傍数kとは別物である点も公式が注意喚起しています)。Weaviateの60はranked fusionを明示的に選んだときの定数で、既定のrelative score fusionでは使われません。
kが大きいほどスコアは平坦になり、上位の重みが薄れます。小さくすると1位と2位の差が開き、トップの結果が支配的になる仕組みです。アルゴリズムの導出やRAG-Fusionでの使い方はRRF(Reciprocal Rank Fusion)の仕組みとスコア計算で扱っています。
RRFの弱点は、元のスコアを捨てることです。BM25で圧勝した文書も、僅差で1位になった文書も、同じ「1位」として 1/61 になる。確信度の情報が消えます。この弱点を補うため、各製品はRRFに重みを掛ける手段を足してきました。重みは順位の逆数に掛け算されるので、「ベクトル側の1位をキーワード側の1位より2倍重く見る」といった調整ができます。
スコア正規化と加重平均:検索スコアの値の差を残したまま統合する方式
もう一方の流儀が、各ランキングのスコアをmin-max正規化などで0〜1に写像してから加重平均する方式です。OpenSearchのnormalization-processorは正規化に min_max(既定)・l2・z_score、結合に arithmetic_mean(既定)・geometric_mean・harmonic_mean を提供し、サブクエリごとの重みを合計1.0で指定できます。Elasticsearchでは linear retriever が同じ役割を担い、normalizer に minmax か l2_norm を指定します。Weaviateがv1.24から既定にしたrelative score fusionも同じ発想です。
使い分けの判断基準は、スコアの差に意味があるかどうかです。BM25側のスコア分布が安定している社内文書検索なら正規化+加重平均で重み調整の余地が生まれます。コーパスやクエリの性質が変わりやすく、スコア分布が読めないならRRFの方が壊れにくい。迷ったらRRFで始めるのが安全です。パラメータ1つ(k)で済み、片側のスコアが暴れても引きずられません。
min-max正規化には固有の癖もあります。1回の検索の中で最大値を1、最小値を0に写すため、BM25側の候補がすべて弱い一致でも、その中の1位は1.0になります。つまり「キーワードでは何も当たっていない」状態が見えなくなる。キーワード一致の有無そのものに意味がある検索(型番検索など)では、RRFか、BM25スコアに下限の閾値を設けてから正規化する設計を選びます。
プラットフォーム別のハイブリッド検索の実装コードと設定の要点
主要5環境の違いを先に一覧にしておきます。版は2026年9月時点の各公式ドキュメントに基づきます。
| 環境 | RRF | 正規化+加重 | RRFの定数k | 重み指定 |
|---|---|---|---|---|
| Elasticsearch | rrf retriever | linear retriever | 変更可(既定60) | 9.2系からweight |
| OpenSearch | score-ranker | normalization | 変更可(既定60) | weights(合計1.0) |
| Azure AI Search | 自動で適用 | なし | 60固定 | ベクトル側のweight |
| Weaviate | rankedFusion | relativeScore(既定) | 60 | alpha |
| PostgreSQL+pgvector | SQLで自作 | SQLで自作 | 任意 | 任意 |
Elasticsearch:rrfとlinearのretriever構文による統合
Elasticsearchはretriever構文でBM25とkNNを束ねます。注意点は rank_window_size です。rrf retrieverのリファレンスでは既定10とされており、各サブクエリの上位10件しかRRFの対象になりません。実運用での設定は、50〜100へ広げる形です。子retrieverは2本以上必要で、num_candidates は k より大きい値にします。ベクトル側のフィールド設計とkNNの基本はElasticsearchのベクトル検索の実装で扱っています。
POST my-index/_search
{
"retriever": {
"rrf": {
"retrievers": [
{
"standard": {
"query": { "match": { "content": "プリンタ 給紙 停止" } }
}
},
{
"knn": {
"field": "content_vector",
"query_vector": [0.023, -0.117, 0.451],
"k": 50,
"num_candidates": 200
}
}
],
"rank_constant": 60,
"rank_window_size": 50
}
},
"size": 5
}
9.2系からは、子retrieverを {"retriever": ..., "weight": 2.0} の形で包むと、RRFスコアに重みを掛けられます。スコアの値そのものを残したい場合は、linear retrieverで正規化してから加重和を取ります。normalizer の指定は9.2系で一般提供になった機能で、それより前の版では既定が正規化なし(none)です。
POST my-index/_search
{
"retriever": {
"linear": {
"retrievers": [
{
"retriever": {
"standard": {
"query": { "match": { "content": "プリンタ 給紙 停止" } }
}
},
"weight": 0.3
},
{
"retriever": {
"knn": {
"field": "content_vector",
"query_vector": [0.023, -0.117, 0.451],
"k": 50,
"num_candidates": 200
}
},
"weight": 0.7
}
],
"normalizer": "minmax",
"rank_window_size": 50
}
},
"size": 5
}
OpenSearch:hybridクエリと検索パイプラインのプロセッサで統合する
OpenSearchは統合ロジックを検索パイプライン側に置きます。hybridクエリはサブクエリを最大5本まで受け取り、正規化と結合を担当するのはパイプラインのプロセッサです。RRFを使うならscore-ranker-processor(2.19で導入)、値ベースの正規化なら normalization-processor を選びます。
PUT /_search/pipeline/hybrid-pipeline
{
"phase_results_processors": [
{
"normalization-processor": {
"normalization": { "technique": "min_max" },
"combination": {
"technique": "arithmetic_mean",
"parameters": { "weights": [0.3, 0.7] }
}
}
}
]
}
POST /my-index/_search?search_pipeline=hybrid-pipeline
{
"query": {
"hybrid": {
"queries": [
{ "match": { "content": "プリンタ 給紙 停止" } },
{
"neural": {
"content_vector": {
"query_text": "プリンタ 給紙 停止",
"model_id": "<your-model-id>",
"k": 50
}
}
}
]
}
}
}
重みの配列は合計1.0でなければエラーになります。上の例はキーワード側3割・ベクトル側7割の配分です。model_id は、インデックスに既定モデルを設定していれば省略できます。正規化に z_score を選んだ場合、結合は arithmetic_mean しか使えません。
RRFで重みを付ける場合は、score-ranker-processorの combination.parameters.weights に同じ形の配列を渡します。rank_constant は1〜10000の範囲で変更でき、公式の例は40に下げて上位の差を広げています。
PUT /_search/pipeline/rrf-pipeline
{
"phase_results_processors": [
{
"score-ranker-processor": {
"combination": {
"technique": "rrf",
"rank_constant": 40,
"parameters": { "weights": [0.3, 0.7] }
}
}
}
]
}
Azure AI Search:RRF統合とセマンティックランカーを重ねる構成
Azure AI Searchは search(全文)と vectorQueries を同一リクエストに入れるだけでRRF統合が走ります。特徴は、RRFで融合した後段にセマンティックランカー(0.00〜4.00の @search.rerankerScore)を挟める点です。
{
"search": "プリンタが給紙途中で停止する",
"vectorQueries": [
{
"kind": "vector",
"vector": [0.023, -0.117, 0.451],
"fields": "contentVector",
"k": 50,
"weight": 1.5
}
],
"queryType": "semantic",
"semanticConfiguration": "default",
"top": 5
}
kは60固定ですが、ベクトルクエリ側には weight で倍率を掛けられます。既定は1.0で、上の例のように1.5にするとベクトル側の順位の効きが強まり、0.5にすると弱まります。倍率を掛けられるのはベクトルクエリで、全文検索側には掛けられない点に注意してください。top を省略すると、統合後の上位50件が返ります。
全文検索側の候補数は maxTextRecallSize(既定1,000)で制限されます。これはBM25側の候補数だけを制御するもので、ベクトル側の k は増えません。件数の多いコーパスで再現率が伸び悩むときに疑う値ですが、執筆時点ではプレビュー機能で、プレビュー版APIかつ ハイブリッド検索の設定オブジェクト内での指定が必要です。
Weaviate:alphaパラメータ1つでキーワードとベクトルの比率を決める
Weaviateは統合比率を alpha という単一パラメータに集約しています。1.0がベクトル検索のみ、0.0がキーワード検索のみ。融合方式は公式の解説のとおりv1.24以降、relative score fusionが既定です。製品全体の特徴と選定基準はWeaviateのハイブリッド検索と選定基準で扱っています。
ここで既定値に罠があります。サーバー側の既定alphaは0.75ですが、公式はこの値が効くのは「alphaを付けずにリクエストが届いたときだけ」と説明し、クライアントライブラリとサーバーの版の組み合わせによっては実効値が0.75からずれ、純粋なキーワード検索になる場合もあると注意しています。alphaは常に明示指定するのが安全です。書き方はハイブリッド検索のコード例に各言語版があります。
collection = client.collections.use("Document")
response = collection.query.hybrid(
query="プリンタが給紙途中で停止する",
alpha=0.5, # 0=BM25のみ / 1=ベクトルのみ。既定に頼らず明示する
limit=5,
)
PostgreSQL+pgvector:RDB内で完結するRRFのSQL自前実装
専用の検索エンジンを増やしたくない場合、PostgreSQLの全文検索(tsvector)とpgvectorによるベクトル検索を1本のSQLでRRF統合できます。pgvectorは2026年9月時点で0.8系(v0.8.6)で、コサイン距離の演算子 <=> とHNSWインデックスを提供しています。要点は、順位付けを50件に絞った後で行うことです。RANK() をCTEの中で LIMIT と同居させると、ウィンドウ関数がWHERE通過後の全行に対して評価されるため、pgvectorのHNSWインデックスが効かなくなり全件ソートに落ちます。
WITH kw AS (
SELECT id, ts_rank_cd(tsv, q) AS s
FROM docs, plainto_tsquery('simple', :query) q
WHERE tsv @@ q
ORDER BY s DESC
LIMIT 50
),
kw_r AS (SELECT id, ROW_NUMBER() OVER (ORDER BY s DESC) AS r FROM kw),
vec AS (
SELECT id, embedding <=> :qvec AS d -- <=> はコサイン距離
FROM docs
ORDER BY d
LIMIT 50 -- ウィンドウ関数を挟まないのでHNSWが効く
),
vec_r AS (SELECT id, ROW_NUMBER() OVER (ORDER BY d) AS r FROM vec)
SELECT doc.id, doc.title,
COALESCE(1.0 / (60 + k.r), 0) + COALESCE(1.0 / (60 + v.r), 0) AS rrf
FROM kw_r k
FULL OUTER JOIN vec_r v ON k.id = v.id
JOIN docs doc ON doc.id = COALESCE(k.id, v.id)
ORDER BY rrf DESC
LIMIT 5;
LIMIT の前に必ず ORDER BY を置く点も外せません。PostgreSQLのLIMITとOFFSETの章は、順序を指定しない LIMIT について「予測不能な部分集合が返る」と明記しています。ウィンドウ関数と LIMIT を同じCTEに書くと、たまたま正しく見えて本番で崩れます。
部署やテナントで絞り込むWHERE句をベクトル側に足す場合も注意が要ります。近似インデックスは絞り込みをスキャン後に適用するため、50件を指定しても条件に合う件数がそれより少なく返ることがあります。pgvectorは0.8.0からiterative index scansを備え、足りない分を追加でスキャンできるので、フィルタ付きのハイブリッド検索では有効化を検討してください。
日本語では plainto_tsquery('simple', ...) のままでは語が切れません。PGroongaやpg_bigmを導入するか、アプリ側で形態素解析してから空白区切りで渡す必要があります。SQLiteで小さく試すなら、FTS5のbm25関数と日本語対応でも同じ構造を組めます。この問題は次章の本題そのものです。
ハイブリッド検索が逆効果になる条件:日本語環境で精度が落ちる落とし穴
BM25側のトークナイズが既定のままだとハイブリッド化で精度は下がる
ハイブリッド検索は常に単独検索より強い、という前提は誤りです。ランク融合は弱いランキングもそのまま混ぜるため、片方の品質が低ければ全体を引き下げます。
この点を実測した公開実験があります(Ahogrammer「ハイブリッド検索で必ずしも検索性能が上がるわけではない」2023年12月)。尼崎市のQAデータ784問に対し、ベクトル検索単独(text-embedding-ada-002)とハイブリッド構成を比較したものです。
| 構成 | Hit Rate@10 | MRR@10 |
|---|---|---|
| ベクトル単独(ada-002) | 0.8355 | 0.6440 |
| BM25単独(MeCab) | 0.5918 | 0.3955 |
| ベクトル+BM25(MeCab・他は既定) | 0.7819 | 0.5855 |
| ベクトル+BM25(ja.microsoft) | 0.7997 | 0.5859 |
| ベクトル+BM25(ja.lucene) | 0.8252 | 0.6185 |
| ベクトル+改善版全文検索+リランカー | 0.8954 | 0.6616 |
読むべきは差分の向きです。形態素解析器(MeCab)を入れてなお、他を既定のままにしたBM25を足すと、ベクトル単独の0.8355から0.7819へ5ポイント以上落ちています。アナライザをja.microsoft、ja.luceneと替えても、まだベクトル単独に届きません。BM25単独が0.5918しかない以上、その弱いランキングを融合すれば全体が引きずられる、という素直な帰結です。
ベクトル単独を超えたのは最後の行だけで、その中身は全文検索側の全面的な作り込みです。原典によれば、トークナイザをSudachi(mode A)に替え、基本形化・品詞フィルタ・正規化・nGramを施し、BM25RetrieverをTF-IDFベースに変更したうえ、bge-reranker-largeによる再ランキングを追加し、RRFの定数kを60から10へ下げています。つまり「BM25を足したら精度が上がる」のではなく、BM25側を検索エンジンとして作り込めた場合にだけ上がる。この差を認識せずに導入すると、工数をかけて精度を下げます。RRF統合の後段に置く再スコアリングの選択肢としては、トークン単位で突き合わせるColBERTの遅延相互作用による再ランクもあります。
実装上の起点は形態素解析です。Elasticsearch/OpenSearchならkuromojiやSudachiのアナライザ、Azure AI Searchなら ja.lucene を指定します。kuromojiのanalyzer構成と辞書追加の手順はElasticsearchの日本語全文検索の設定にまとめました。LangChainの BM25Retriever は preprocess_func の既定が text.split()、つまり空白区切りなので、日本語では形態素解析器を渡さない限りそもそも語が切れません(上の実験もMeCabを渡したうえでの数値です)。日本語トークナイザの渡し方はLangChain BM25Retrieverの日本語対応で扱っています。
ハイブリッド検索を採用すべきでない場面と運用体制を踏まえた見送りの判断条件
次の条件に当てはまるなら、ハイブリッド化は見送るべきです。
- 検索対象が意味の言い換えばかりで、固有名詞や型番をほとんど含まない:BM25の出番が無く、運用コストだけが増える。
- 全文検索側を設定する体制が無い:アナライザ・シノニム辞書・ストップワードを誰も保守しないなら、弱いランキングを混ぜるだけになる。
- レイテンシ要件が厳しい:2系統の検索を並列実行して融合する分、単独検索よりレスポンスは遅い。上位k件を広げるほど顕著になる。
- そもそも文書がチャンク分割に馴染まない構造をしている:見出し階層で辿るべき長大な仕様書などは、PageIndexのような推論型RAGやGraphRAG的な構造化の方が筋が良い。
反対に、採用を強く勧められるのは、型番・製品名・条文番号・エラーコードのような「表記が一致しないと意味が無い語」と、自然文の質問が同じ検索窓に混在する場面です。社内のヘルプデスク、製品マニュアル、規程集の検索がその典型で、ベクトル単独では型番の取り違えが起き、BM25単独では言い換えを拾えません。
評価セットを作ってハイブリッド化の効果を単独構成との数値比較で測定する方法
ハイブリッド化の是非は、体感ではなく評価セットで決めます。用意するのは「質問」と「正解文書ID」の対を数十〜数百件並べたCSV1枚です。指標はHit Rate@k(正解が上位k件に入った割合)とMRR@k(正解の順位の逆数の平均)で足ります。前掲の実験も784問でこの2指標を測っています。検索と生成を分けて測る指標の全体像はRAG評価の指標と実装を参照してください。
比較すべき構成は最低4つ。BM25単独、ベクトル単独、RRF統合、正規化+加重平均です。ハイブリッドが単独を下回るケースは実在するため、単独構成をベースラインとして必ず残します。BM25単独のスコアは、そのままBM25側の作り込み度合いを示す健康診断になる。ここが極端に低ければ、統合方式をいじる前にアナライザを直す番です。改善が0.01ポイント未満なら、2系統を運用する保守コストに見合いません。
2指標はPythonの数行で計算できます。各構成の検索結果(質問ごとの文書IDの上位リスト)を同じ形で出力しておけば、構成を入れ替えるたびに同じ関数で比べられます。
def hit_rate_and_mrr(results, gold, k=10):
"""results: {質問ID: [文書ID, ...]} / gold: {質問ID: 正解文書ID}"""
hits, rr = 0, 0.0
for qid, answer in gold.items():
ranked = results.get(qid, [])[:k]
if answer in ranked:
hits += 1
rr += 1.0 / (ranked.index(answer) + 1)
n = len(gold)
return hits / n, rr / n
# 例: bm25・vector・rrf の3構成を同じ評価セットで比較する
# for name, res in {"bm25": r1, "vector": r2, "rrf": r3}.items():
# print(name, hit_rate_and_mrr(res, gold))
ハイブリッド検索の導入手順:評価セット作りから本番運用までの5工程
ここまでの内容を、実際に導入するときの順番に並べ直します。順番を守る理由は1つで、統合方式やパラメータより先に、BM25側とベクトル側それぞれの素の実力を測っておかないと、ハイブリッド化の効果を判定できないからです。
工程1〜2:評価セットの作成と、単独構成それぞれの検索精度の測定
最初に、実際の問い合わせログや想定質問から評価セットを作ります。型番を含む質問と自然文の質問の比率は、本番の問い合わせに近づけます。型番の質問が1割しかないのに評価セットで5割にすると、BM25の効果を過大に見積もることになるためです。
次に測定するのは、BM25単独とベクトル単独の検索精度です。BM25単独のHit Rate@10がベクトル単独より大幅に低いなら、この時点でアナライザ(形態素解析・正規化・シノニム辞書)の調整に戻ります。ベクトル側が低いなら、埋め込みモデルの変更か、チャンク分割の単位の見直しを先に行います。
工程3〜5:RRFで統合し、重みとリランカーを足してから本番へ
工程3では、既定値のRRF(k=60・ウィンドウ幅50〜100)で統合し、単独構成と比べます。ここで単独を上回らなければ、重みの調整では解決しないことが多く、工程2に戻る判断をします。
工程4で、重み(Elasticsearchの weight、OpenSearchの weights、Weaviateの alpha)を0.1刻みで振り、必要なら正規化+加重平均と比べます。工程5は、リランカーを足し、レイテンシと精度の両方を確認してから本番に載せる段階です。本番後は、問い合わせログから「回答できなかった質問」を定期的に評価セットへ追加し、文書の追加や埋め込みモデルの更新のたびに同じ評価を回します。
検索基盤を自社で組むか外部に相談するかを分ける運用体制の3つの判断基準
ハイブリッド検索そのものは、上のコードのとおり数十行で動きます。難しいのは動かした後です。次のどれかに当てはまるなら、検索基盤の設計から相談できる相手を探した方が工数を抑えられます。
- 日本語のアナライザと辞書を保守できる担当者がいない:前掲の実験のとおり、ここが弱いとハイブリッド化で精度が下がる。
- 評価セットを作って比較する時間が取れない:効果を測れないまま本番に出すと、改善したのか悪化したのかが分からない。
- 既存の文書管理や権限設定と検索を連携させる必要がある:部署ごとの閲覧権限を検索結果に反映させる設計は、検索精度とは別の工数がかかる。
一創のRAG構築支援では、評価セットの設計、日本語アナライザの調整、ハイブリッド検索とリランカーの組み込み、権限を考慮した検索基盤の構築までを受託しています。既存のRAGで回答精度が伸び悩んでいる段階からの相談にも対応します。
よくある質問
ハイブリッド検索とRRFは同じものですか?
違います。ハイブリッド検索は「複数の検索方式を併用する」という構成の名前で、RRFはその結果を1つに束ねる融合アルゴリズムの1つです。RRFの代わりにスコア正規化+加重平均を使ってもハイブリッド検索です。
RRFの定数kは60から変えるべきですか?
まずは60のままで構いません。そもそも変更できるのはElasticsearch(rank_constant)とOpenSearch(combination.rank_constant)で、Azure AI Searchのkは内部定数のため60から動かせません。上位の結果をより強く効かせたい場合だけ小さくし(前掲の実験は10に下げています)、評価セットで効果を確認してから採用します。
キーワード検索とベクトル検索の重みはどう決めますか?
Weaviateなら alpha、OpenSearchなら weights、Elasticsearch 9.2系なら子retrieverの weight、Azure AI Searchならベクトルクエリの weight で調整します。均等配分から始め、評価セットのHit Rateを見ながら0.1刻みで振るのが実務的です。日本語で重み調整が効かないときは、重みではなくBM25側のトークナイズを疑ってください。
ベクトルデータベースを新たに導入しないとハイブリッド検索はできませんか?
できます。PostgreSQL+pgvectorなら既存のRDBだけでBM25相当の全文検索とベクトル検索をRRF統合できます。データ量が数十万件規模までなら現実的な選択肢です。専用エンジンとの違いはベクトルデータベースの仕組みを参照してください。
ハイブリッド検索を入れればハルシネーションは無くなりますか?
減りますが、無くなりません。正しい文書を渡せる確率が上がるだけで、LLMが渡された文書を読み違える余地は残ります。検索側の改善と、生成側の指示(引用の強制、根拠が無ければ答えない指示)は別の対策として両方必要です。
関連記事
- RRF(Reciprocal Rank Fusion)とは?RAG-Fusionでの仕組み・スコア計算・実装を解説
- BM25とは?読み方・計算式とk1・bの意味、Python実装(rank_bm25・bm25s)の落とし穴
- ベクトル検索とセマンティック検索の違い|包含関係・仕組み・使い分けを実装目線で整理
- LangChain BM25Retrieverの使い方|インストール・日本語対応・ハイブリッド検索
- リランクモデルとは?ベクトル検索と組み合わせる種類・仕組み・選び方【2026年版】
- ベクトルデータベースとは?ベクトル化・インデックス構造・製品比較【2026年版】
- PageIndexとは?ベクトル不要の推論型RAGの仕組み・使い方・従来RAGとの違い