はじめに:RAGの精度は「検索」で9割決まる
「RAG(検索拡張生成)システムを構築したけれど、期待したほど回答精度が上がらない」
これは、RAGシステムを構築する多くのプロジェクトで直面する課題です。LLMのプロンプトを何度調整しても、ハルシネーション(もっともらしい嘘)が減らない。その原因の多くは、実はLLM側ではなく、その前段階である「検索(Retrieval)」に潜んでいます。
LLMに渡すコンテキスト(参考情報)が的確でなければ、どんなに優秀なモデルでも正しい回答は生成できません。ここで重要になるのが、キーワード検索(Keyword Search)とベクトル検索(Vector Search)を組み合わせる「ハイブリッド検索」です。
多くの記事では「ハイブリッド検索は精度が良い」と結論付けられていますが、実務で成果を出すには「とりあえず両方混ぜる」だけでは不十分です。データの特性やクエリの意図に合わせて、どのように重み付けを行うかという戦略が求められます。
この記事では、静的な検索統合からスタートし、最終的にはAI(Cross-Encoder)を用いて検索結果を動的に最適化する「Re-ranking(リランキング)」の実装まで、順を追って解説します。Pythonコードを交えながら、検索の質を高め、実用的なAI導入へと繋げるための具体的なアプローチを見ていきましょう。
学習パスの概要:検索精度向上のための「最後の1マイル」
ハイブリッド検索の実装は、単なる技術的な設定作業ではありません。ユーザーが求める情報に、システムがいかに近づけるかという「対話の設計」でもあります。PoC(概念実証)で終わらせず、実際のビジネス課題を解決するシステムを構築するためには、この設計が不可欠です。
本記事では、以下の4つのステップで、検索システムを段階的に高度化させていきます。
- 特性理解: キーワード検索(BM25)とベクトル検索(Dense Vector)の決定的な違いと、それぞれの得意領域を理解します。
- 静的統合: RRF(Reciprocal Rank Fusion)アルゴリズムを用いた基本的なハイブリッド検索の実装を行います。
- AIリランキング: Cross-Encoderや最新のリランクモデル(Cohere Rerank等)を導入し、検索結果を文脈に合わせて再順位付けする仕組みを構築します。
- 動的戦略: クエリの意図に応じて、検索ロジックを動的に使い分ける高度な戦略を実装します。
なぜベクトル検索だけでは不十分なのか
「ベクトル検索が登場した今、キーワード検索はもう古い技術なのでは?」と疑問を持つ方もいるかもしれません。しかし、実務の現場では、むしろその重要性が再評価されています。ベクトル検索は単語の意味や文脈を捉えるのに優れていますが、「完全一致」が求められるシーンでは限界があるからです。
例えば、製造業の部品データベースで「型番:AX-900」を検索すると仮定しましょう。ベクトル検索では「AX-800」や「BX-900」といった「意味的に似ているが違うもの」を上位に表示してしまうリスクがあります。一方で、BM25などのキーワード検索アルゴリズムは、こうした固有名詞や特定の数値をピンポイントで特定することに長けています。
実際、MilvusやAzure AI Searchといった主要な検索プラットフォームでも、BM25の実装は継続的に最適化されており、ベクトル検索と組み合わせることで相互の弱点を補う構成が標準となっています。この両者の「いいとこ取り」をし、さらにAIの力で順位を最適化するのが、今回目指すゴールです。
Step 1:異種検索の特性を理解する(BM25 vs Dense Vector)
まずは、それぞれの検索手法がどのような結果を返すのか、Pythonを使って実際に比較してみましょう。特性を知ることが、適切なチューニングへの第一歩です。
キーワード検索(BM25)とベクトル検索の得意・苦手
| 特徴 | キーワード検索 (BM25) | ベクトル検索 (Dense Retrieval) |
|---|---|---|
| マッチング原理 | 単語の出現頻度と逆文書頻度 | ベクトル空間上の距離(コサイン類似度等) |
| 得意なクエリ | 型番、人名、専門用語、完全一致 | 曖昧な質問、自然言語、表記ゆれ |
| 苦手なクエリ | 同義語(「車」で「自動車」はヒットしない) | 厳密なキーワード一致、未知語 |
【演習】同じクエリでの検索結果比較実験
ここでは、代表的なライブラリである rank_bm25 と sentence-transformers を使用して、簡単な比較実験を行います。
※以下のコードは概念実証用の簡易サンプルです。
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer, util
import numpy as np
# サンプルドキュメント(製造業のFAQを想定)
corpus = [
"製品Aの型番はAX-100です。電源が入らない場合はケーブルを確認してください。",
"製品B(BX-200)は防水仕様です。屋外での使用に適しています。",
"AX-100の修理受付は2023年で終了しました。",
"製品の電源トラブルシューティングガイド。"
]
# 1. BM25(キーワード検索)の準備
# ※注意: 日本語の実運用ではMeCabやSudachi等の形態素解析器が必須です。
# ここではデモ用に簡易的な文字単位トークナイズ(リスト化)を行います。
tokenized_corpus = [list(doc) for doc in corpus]
bm25 = BM25Okapi(tokenized_corpus)
# 2. ベクトル検索の準備
# Hugging Face等で公開されている汎用的な軽量モデルを使用
model = SentenceTransformer('all-MiniLM-L6-v2')
corpus_embeddings = model.encode(corpus)
# クエリ: "AX-100 電源"
query = "AX-100 電源"
# BM25での検索
tokenized_query = list(query) # クエリも同様にトークナイズ
bm25_scores = bm25.get_scores(tokenized_query)
print("--- BM25 Scores ---")
for i, score in enumerate(bm25_scores):
print(f"Doc {i}: {score:.4f} | {corpus[i]}")
# ベクトル検索
query_embedding = model.encode(query)
cos_scores = util.cos_sim(query_embedding, corpus_embeddings)[0]
print("\n--- Vector Scores ---")
for i, score in enumerate(cos_scores):
print(f"Doc {i}: {score:.4f} | {corpus[i]}")
このコードを実行すると、BM25は「AX-100」という特定の文字列が含まれるドキュメントに高いスコアを与え、ベクトル検索は「電源」や文脈全体の意味が近いドキュメントを評価する傾向が見て取れるはずです。
この「評価軸のズレ」こそが、ハイブリッド検索が必要な理由です。単一の手法では取りこぼしてしまう情報を、相互に補完し合うことが可能になります。
Step 2:静的なハイブリッド検索を実装する
2つの検索結果が得られたら、次はそれを統合します。ここでよくある間違いが、「BM25のスコアとコサイン類似度を単純に足してしまう」ことです。
BM25のスコアは実装(Elasticsearch、Milvus、Azure AI Searchなど)によって算出基準が異なり、基本的に上限がありません。一方でコサイン類似度は0から1の間です。尺度が異なる数値を単純に足しても、意味のある結果にはなりません。
特に、Milvusの最新版などではBM25の統計情報処理が最適化されたり、Azure AI Searchではハイブリッドランク付けの制御機能が追加されたりと、各プラットフォームで独自の改良が進んでいます。こうした環境差を吸収し、安定した統合結果を得るために推奨されるのが、Reciprocal Rank Fusion (RRF) というアルゴリズムです。
Reciprocal Rank Fusion (RRF) とは
RRFは、スコアそのものではなく「順位(Rank)」を使って統合する方法です。「1位のドキュメントには高い点数、10位には低い点数」というように、順位の逆数を加算して新たなスコアを算出します。
$ RRF_score(d) = \sum_{r \in R} \frac{1}{k + rank(d, r)} $
ここで $k$ は定数(通常は60が使われます)です。順位のみを見るため、スコアの分布やエンジンの違いを気にする必要がなく、非常に堅牢で実装しやすいのが特徴です。
【演習】PythonでのRRF関数の実装
def reciprocal_rank_fusion(results_list, k=60):
"""
results_list: 複数の検索結果リスト(ドキュメントIDのリストのリスト)
Example: [[doc_a, doc_b], [doc_b, doc_c]]
"""
fused_scores = {}
for results in results_list:
for rank, doc_id in enumerate(results):
if doc_id not in fused_scores:
fused_scores[doc_id] = 0
# 順位は0始まりなので +1 する
fused_scores[doc_id] += 1 / (k + rank + 1)
# スコアの高い順にソート
reranked_results = sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
return reranked_results
# 仮想的な検索結果(ドキュメントID)
# キーワード検索結果: Doc 0 が1位
keyword_results = [0, 2, 3, 1]
# ベクトル検索結果: Doc 3 が1位
vector_results = [3, 0, 1, 2]
# 統合実行
final_ranking = reciprocal_rank_fusion([keyword_results, vector_results])
print("--- RRF Result ---")
for doc_id, score in final_ranking:
print(f"Doc {doc_id}: Score {score:.4f}")
この手法により、どちらか一方の検索で上位に来ているドキュメントだけでなく、「両方の検索でそこそこの順位にいるドキュメント」も適切に評価されるようになります。
なお、より高度な精度を求める場合、最近のトレンドとしてはCohere Rerankのようなクロスエンコーダーを用いたリランキングモデルを後段に配置する手法や、Milvusなどのベクトルデータベースが提供する組み込みのハイブリッド検索機能を活用するケースも増えています。しかし、RRFは追加の学習や外部API呼び出しが不要で高速に動作するため、最初のベースラインとして非常に優秀です。
Step 3:Cross-EncoderによるAI重み付け(Re-ranking)の実装
ここからが本記事のハイライトです。RRFで統合した結果はあくまで「静的」な統合に過ぎません。さらに精度を高め、実用的なレベルへと引き上げるために、Cross-Encoderを用いたRe-ranking(再ランク付け)を導入します。
Bi-EncoderとCross-Encoderの違い
- Bi-Encoder (通常のベクトル検索): クエリとドキュメントを別々にベクトル化し、距離を計算します。高速ですが、文脈の細かいニュアンス(否定語や関係性)を捉えきれないことがあります。
- Cross-Encoder (Re-ranking): クエリとドキュメントをペアとしてAIモデルに入力し、「このペアはどれくらい関連しているか?」を直接推論させます。計算コストは高いですが、精度は非常に高くなります。
実務的なアーキテクチャとしては、以下の「2段階検索(Two-Stage Retrieval)」が一般的です。
- Retrieve (収集): ベクトル検索やキーワード検索で、高速に候補を絞る(例:全100万件から上位50件を取得)。
- Re-rank (精査): 絞り込んだ50件に対してCross-Encoderを実行し、並び替える。
【演習】Hugging Faceのモデルを用いたリランカーの実装
sentence-transformersライブラリを使えば、Cross-Encoderもスムーズに実装可能です。実運用では、対象言語に対応した最新のモデルを選定することが重要です。
from sentence_transformers import CrossEncoder
# Cross-Encoderモデルのロード
# ※実務では多言語対応の最新Re-rankerモデル(Hugging Face Hub等で確認)を選定してください
# 以下はデモ用に軽量なモデルを指定しています
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
# クエリ
query = "電源が入らない場合の対処法"
# Retrieveフェーズで取得した候補ドキュメント(RRFなどで統合済みと仮定)
candidate_docs = [
"製品Aの電源トラブルシューティングガイド。",
"製品Bは防水仕様です。",
"電源が入らない場合は、まずコンセントを確認し、次にケーブルを交換してください。",
"AX-100の修理受付終了のお知らせ。"
]
# ペアを作成 [query, doc] のリスト
pairs = [[query, doc] for doc in candidate_docs]
# スコアリング実行
scores = reranker.predict(pairs)
# 結果の表示
scored_docs = sorted(zip(scores, candidate_docs), key=lambda x: x[0], reverse=True)
print("--- Re-ranking Result ---")
for score, doc in scored_docs:
print(f"{score:.4f} | {doc}")
このプロセスを経ることで、キーワードが含まれているだけの関連性の低いドキュメントを排除し、質問の意図に合致するドキュメントを上位に提示できるようになります。これは、RAGシステムにおいて、LLMに渡すコンテキストの質を高めるための非常に有効なアプローチです。
Step 4:クエリに応じた動的な重み付け戦略
さらに一歩進んで、システムをより人間に近づけるアプローチを紹介します。それは、「ユーザーの質問タイプに合わせて検索戦略を変える」という手法です。
すべてのクエリに対して一律に「キーワード50%:ベクトル50%」で統合するのが最適とは限りません。
- 具体的な検索(例:「エラーコード E-503 意味」)
- 戦略:キーワード検索重視。完全一致が必要。
- 抽象的な相談(例:「最近のセキュリティトレンドについて教えて」)
- 戦略:ベクトル検索重視。概念的な広がりが必要。
クエリ意図判定(Query Routing)の実装イメージ
これを実現するには、検索を実行する前に軽量なLLMや分類器でクエリを判定させます。
# 擬似コード:LangChainなどを用いたルーティングのイメージ
def route_query(query):
prompt = f"次のクエリは『具体的(Specific)』か『抽象的(Abstract)』か分類せよ: {query}"
# LLMによる判定(実際にはOpenAI APIなどを呼び出す)
intent = call_llm(prompt)
if intent == "Specific":
# キーワード検索の結果を優先、あるいはフィルターを厳格に
return run_hybrid_search(query, alpha=0.8) # alphaはキーワードの重み
else:
# ベクトル検索の結果を優先
return run_hybrid_search(query, alpha=0.2)
このように、入り口で交通整理を行うことで、不要なノイズを減らし、より的確なコンテキストをLLMに渡すことが可能になります。これを「Agentic RAG(エージェント型RAG)」の初期段階と捉えることもできます。
実務適用のためのヒントとトラブルシューティング
最後に、これらを本番環境に導入し、ROIを最大化するための注意点を共有します。
1. レイテンシ(応答速度)の壁
Cross-Encoderは強力ですが、計算コストが高いです。全件に対して行うのは不可能です。「Retrieveで上位50〜100件に絞り、Re-rankで上位5〜10件をLLMに渡す」という設計が黄金比です。また、Re-ranking処理は非同期で行うか、キャッシュを活用してUXを損なわない工夫が必要です。
2. ドメイン特化用語への対応
汎用的なEmbeddingモデルでは、社内用語や業界特有の略語を理解できないことがあります。この場合、モデル自体のファインチューニングを検討する前に、「同義語辞書(Synonym Dictionary)」をキーワード検索側(Elasticsearch等)に追加する方が、コストパフォーマンスが良い場合が多いです。
3. 継続的な精度評価
「なんとなく良くなった」で終わらせないために、評価セットを作りましょう。質問と「正解ドキュメント」のペアを用意し、MRR (Mean Reciprocal Rank) や NDCG といった指標で、リリースのたびにスコアを計測するパイプラインを構築してください。
まとめ:進化する検索システムへ
ハイブリッド検索の実装は、以下の段階を経て進化します。
- Level 1: キーワード検索とベクトル検索の特性を理解し、使い分ける。
- Level 2: RRFを用いて、両者を静的に統合する。
- Level 3: Cross-EncoderによるRe-rankingで、文脈理解を深める。
- Level 4: クエリ意図に応じて戦略を変える動的なシステムへ。
ここまで読み進めた方は、単にライブラリを使うだけでなく、検索の仕組みそのものを論理的かつ体系的に設計できる知識を手にしています。しかし、実際のプロジェクトでは、データの汚れやインフラの制約など、記事には書ききれない泥臭い課題に直面することでしょう。
検索技術は日進月歩です。AIを単なる手段として捉え、ビジネス課題の解決とROI最大化に貢献する、実用的なRAGシステムを構築していきましょう。
コメント