RAGの検索品質は、取得する文書の精度に依存します。「意味が近い」だけで検索するベクトル検索と、「単語が一致する」キーワード検索は、それぞれ得意・不得意があります。このページでは、主要な検索手法の仕組みと使い分けを整理します。
3つの主要な検索手法
Section titled “3つの主要な検索手法”RAGで使われる検索手法は、大きく3つに分けられます。
| 手法 | 仕組み | 得意 | 不得意 |
|---|---|---|---|
| キーワード検索(BM25) | 単語の一致頻度で文書をスコアリング | 製品名、型番、固有名詞の完全一致 | 言い換え、同義語、意味的な類似 |
| ベクトル検索 | 埋め込みベクトルの類似度で文書を取得 | 意味的な類似、言い換え、多言語 | 完全一致が必要なキーワード |
| ハイブリッド検索 | 両方を組み合わせてスコアを統合 | 上記の両方をカバー | 設定と調整が必要 |
キーワード検索(BM25)の仕組み
Section titled “キーワード検索(BM25)の仕組み”BM25(Best Match 25)は、キーワード検索の定番アルゴリズムです。検索クエリに含まれる単語が文書にどれだけ登場するか(TF: Term Frequency)と、その単語がどれだけ珍しいか(IDF: Inverse Document Frequency)を組み合わせてスコアを計算します。[1]
「Python エラー 解決」というクエリがあった場合、「Python」「エラー」「解決」の3語が多く含まれる文書が高くスコアされます。「Python」が多くの文書に出現する一般的な単語なら、IDF によってスコアが調整されます。
BM25 の特徴:
- 単語の完全一致に強い(製品コード「SKU-2048」などの完全一致検索)
- インデックス構築が高速
- ベクトル化が不要(計算コストが低い)
- 言い換えや同義語には対応できない(「スマホ」と「スマートフォン」を別物として扱う)
ベクトル検索(ANN)の仕組み
Section titled “ベクトル検索(ANN)の仕組み”ベクトル検索は、埋め込み で変換したベクトル同士の距離を計算して、意味的に近い文書を探します。
大量のベクトルから最も近いものを正確に探す(全探索)と計算コストが高くなるため、実務ではANN(Approximate Nearest Neighbor、近似最近傍探索)が使われます。ANN は「厳密に最も近い」ではなく「かなり近い」ものを高速に見つける方法です。HNSW(Hierarchical Navigable Small World)などのアルゴリズムが広く使われています。
ベクトル検索の特徴:
- 「検索のコツ」と「使い方を教えて」のように、言い換えや言葉のゆらぎに強い
- 多言語テキストの意味的な近さに対応
- 完全一致が必要な場合は精度が落ちることがある
なぜハイブリッド検索が効果的か
Section titled “なぜハイブリッド検索が効果的か”BM25とベクトル検索はそれぞれ異なる「見方」で文書を評価するため、組み合わせることで補完関係が生まれます。
graph TD
Q["ユーザーの質問"] --> BM25["BM25キーワード検索"]
Q --> VEC["ベクトル検索(ANN)"]
BM25 --> M["スコア統合(RRF など)"]
VEC --> M
M --> R["統合済みランキング"]
R --> RE["リランカー(オプション)"]
RE --> TOP["上位K件 → LLMへ"]ハイブリッド検索では、2つの検索結果を統合してランキングを作ります。スコア統合の方法としては、RRF(Reciprocal Rank Fusion、逆順位融合)がシンプルかつ実績のある手法です。RRF は各結果の「順位」を使ってスコアを計算するため、スコールの単位が異なる2つの検索をそのまま組み合わせられます。
def reciprocal_rank_fusion(bm25_results, vector_results, k=60):
"""
RRF でBM25とベクトル検索の結果を統合する
bm25_results: [(doc_id, score), ...]
vector_results: [(doc_id, score), ...]
k: RRFの定数(通常60)
"""
scores = {}
for rank, (doc_id, _) in enumerate(bm25_results):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (rank + k)
for rank, (doc_id, _) in enumerate(vector_results):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (rank + k)
# スコアの高い順に並び替え
return sorted(scores.items(), key=lambda x: x[1], reverse=True)リランキングとは
Section titled “リランキングとは”リランカー(Reranker)は、最初の検索で得た候補文書を「質問への関連度」で並べ直すコンポーネントです。
検索(BM25 + ベクトル)が「候補を広く集める」段階だとすると、リランカーは「本当に答えになる文書を選ぶ」段階です。
リランカーには クロスエンコーダー(Cross-Encoder)がよく使われます。クロスエンコーダーは、質問と文書のペアを一緒に処理して関連度スコアを計算します。[2]
# pip install sentence-transformers
from sentence_transformers import CrossEncoder
# Cohere や sentence-transformers のリランカーモデルを使用
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
query = "Pythonでファイルを読み込む方法"
candidates = [
"Pythonのopen()関数でファイルを開く方法を説明します",
"Javaでファイルを読み込む基本的な手順",
"Pythonのwith文を使ったファイル操作のベストプラクティス",
]
# 質問と各文書のペアでスコアを計算
pairs = [(query, doc) for doc in candidates]
scores = reranker.predict(pairs)
# スコアの高い順に並び替え
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
for doc, score in ranked:
print(f"{score:.4f}: {doc[:40]}...")リランカーのコストトレードオフ
Section titled “リランカーのコストトレードオフ”リランカーは精度向上に有効ですが、追加のレイテンシとコストが発生します。
| 段階 | 処理する件数 | 目的 |
|---|---|---|
| 検索(BM25+ベクトル) | 全文書(数万〜数百万) | 候補を絞る(上位50〜100件) |
| リランカー | 上位50〜100件 | 最終的な上位5〜20件に絞る |
| LLMへの入力 | 上位5〜20件 | 回答生成 |
検索段階で上位100件に絞ってからリランカーにかけることで、計算コストを抑えつつ精度を上げられます。
クエリ拡張とクエリ書き換え
Section titled “クエリ拡張とクエリ書き換え”ユーザーの質問は、そのままでは良い検索クエリにならないことがあります。クエリを前処理することで検索精度を改善できます。
クエリ書き換え(Query Rewriting)
Section titled “クエリ書き換え(Query Rewriting)”「それってどうやるの?」のように文脈依存の質問を、検索に使える具体的なクエリに書き換えます。
from openai import OpenAI
client = OpenAI()
def rewrite_query(conversation_history, user_question):
"""会話履歴を考慮してクエリを書き換える"""
messages = [
{
"role": "system",
"content": (
"以下の会話と質問から、文書検索に使える独立したクエリを1つ生成してください。"
"会話の文脈を踏まえ、指示語(それ・これ)を具体的な言葉に置き換えてください。"
"クエリだけを出力し、説明は不要です。"
)
},
*conversation_history,
{"role": "user", "content": f"質問: {user_question}"}
]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
max_tokens=100
)
return response.choices[0].message.contentクエリ拡張(Query Expansion / HyDE)
Section titled “クエリ拡張(Query Expansion / HyDE)”HyDE(Hypothetical Document Embeddings)は、質問に対して「こういう文書があれば答えられる」という仮想的な文書をLLMに生成させ、その文書のベクトルで検索する手法です。[4]
質問「Pythonでメモリリークを検出する方法は?」に対して、LLMが「Pythonのメモリリーク検出には tracemalloc モジュールを使います…」のような仮想回答を生成し、その文章で検索します。これにより、質問文そのものよりも文書に近い形で検索できます。
実務での設計指針
Section titled “実務での設計指針”まずハイブリッド検索から始める
Section titled “まずハイブリッド検索から始める”プロジェクト開始時の推奨構成:
- ハイブリッド検索(BM25 + ベクトル)を基本とする
- 精度が要求される場合はリランカーを追加する
- 質問の文脈が重要な場合はクエリ書き換えを追加する
「まずベクトル検索だけで試して、精度が出なければハイブリッドに移行する」という順序も有効です。ただし、製品名・型番・エラーコードを含む用途では、最初からハイブリッドにすることをお勧めします。
ハイブリッド検索の実装例
Section titled “ハイブリッド検索の実装例”Weaviate、Qdrant、Elasticsearch などの検索基盤は、BM25 とベクトル検索を組み合わせるハイブリッド検索を提供しています。Cohere も、初期検索後に候補を並べ替える Rerank API を提供しています。[3]
# Weaviate を使ったハイブリッド検索の例
# pip install weaviate-client
import weaviate
client = weaviate.connect_to_local()
collection = client.collections.get("Documents")
# hybrid() メソッドで BM25 + ベクトル検索を同時実行
results = collection.query.hybrid(
query="Pythonでファイルを読み込む方法",
alpha=0.5, # 0=BM25のみ, 1=ベクトルのみ, 0.5=均等に統合
limit=10
)
for obj in results.objects:
print(obj.properties["content"][:100])alpha パラメータで BM25 とベクトル検索の比重を調整できます。完全一致が重要な用途では alpha=0.3(BM25 寄り)、意味的な検索が重要な用途では alpha=0.7(ベクトル寄り)など、評価データを使って調整します。
- キーワード検索(BM25)は完全一致に強く、ベクトル検索は意味的な類似に強い
- ハイブリッド検索はRRFで2つを統合し、それぞれの弱点を補完する
- リランカー(クロスエンコーダー)は精度を上げるが、レイテンシとコストが増加する
- クエリ書き換えは会話の文脈を検索に活かすための前処理として有効
- 実務ではハイブリッド検索を基本として、精度要件に応じてリランカーを追加する順序が現実的
よくある質問
Section titled “よくある質問”Q: 常にハイブリッド検索を使うべきですか?
A: 多くの場合、ハイブリッド検索はベクトル検索のみよりも安定した精度を示します。ただし、文書が意味的に均一で言い換えが少ない場合(たとえば短い定型文FAQのみ)はベクトル検索だけで十分なことがあります。最初はハイブリッドで構築し、評価データに基づいて判断することをお勧めします。
Q: リランキングは常にコストに見合いますか?
A: 精度が重要なユースケース(法律・医療・顧客対応)では、リランキングの追加コストは十分に見合います。一方、レイテンシが最優先(例:チャットのリアルタイム応答)または精度の要求が低い場合は、リランカーなしで運用することも合理的です。評価セットを用意して、精度改善の程度を測定してから判断することが重要です。
Q: BM25 は日本語でも使えますか?
A: 使えますが、日本語は英語のように単語がスペースで区切られていないため、形態素解析(MeCab、SudaChiなど)による分かち書きが必要です。適切なトークナイザーを設定しない場合、BM25 の精度が大きく低下します。
Q: クエリ書き換えは毎回LLMを呼び出すのですか?
A: 基本的にはそうです。ただし、GPT-4oよりも軽量なモデル(GPT-4o-mini、Claude Haiku など)を使うことでコストを抑えられます。また、単一ターンの質問(会話履歴がない)では、クエリ書き換えの効果は限定的なため、省略することが多いです。