なぜハイブリッドなのか — DenseとSparseの相補性
第3章で見たように、Dense(ニューラル埋め込み)とSparse(BM25等)はそれぞれ別の強みを持ちます。 どちらか一方では必ず取りこぼします。両者を組み合わせることで、両方の弱点を相互に補えるのがハイブリッド検索の発想です。
| クエリ例 | Denseの強み | Sparseの強み |
|---|---|---|
| 「犬」と「ワンちゃん」 | ◎(意味類似) | ✕(表記違い) |
| 「GPT-4o」完全一致 | △(AI全般に引き寄せ) | ◎(語彙一致) |
| 「バグを直す」「欠陥を修正」 | ◎(言い換え対応) | ✕(別語彙) |
| 「APIメソッド createUser()」 | △(概念に引き寄せ) | ◎(識別子完全一致) |
| 多言語クエリ(日英混在) | ◎(多言語モデル) | △(言語分離必要) |
実務ベンチマークでは、ハイブリッド検索は単一手法に対してRecall +15〜30%を達成することが報告されています。 2026年時点で主要ベクトルDB(Weaviate、Milvus 2.5+、Qdrant v1.9+、LanceDB、Pinecone)はすべてハイブリッド検索を標準装備しています。
RRF — Reciprocal Rank Fusionの魔法
異なる検索結果をどう融合するかが問題です。生のスコア(コサイン類似度 vs BM25スコア)はスケールが違うので単純加算はできません。 ここで使われるのがRRF(Reciprocal Rank Fusion)です。
def rrf_fusion(*rankings, k=60):
"""複数の順位付き結果リストをRRFで融合"""
scores = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
return sorted(scores.items(), key=lambda x: -x[1])
# Dense検索とBM25を並走、RRFで融合
dense_ranked = vector_search(query, k=50)
bm25_ranked = bm25_search(query, k=50)
fused = rrf_fusion(dense_ranked, bm25_ranked, k=60)
top_k = fused[:10]Linear combination(α × dense + (1-α) × sparse)もありますが、αのチューニングにラベルデータが必要です。 実務ではまずRRFで動かし、ラベルが溜まってからLinear combinationへ移行するのが鉄板フローです。
Bi-encoder vs Cross-encoder — Rerankerの役割
第3章で触れたDense埋め込みは正確にはBi-encoderです。クエリと文書を独立にエンコードしてベクトル化するため、事前計算でき高速です。 一方、Cross-encoderはクエリと文書をペアで同時にTransformerに入力し、関連度スコアを直接出力します。
| Bi-encoder(検索用) | Cross-encoder(Rerank用) | |
|---|---|---|
| 入力 | クエリと文書を独立にエンコード | クエリと文書をペアで同時入力 |
| 速度 | 高速(ANNで数ms) | 低速(1ペア数十ms) |
| 事前計算 | 可能(インデックス化) | 不可 |
| 精度 | 中 | 高 |
| スケール | 数百万〜数十億件 | 〜数百件(再ランク用途) |
| 代表モデル | BGE、E5、Ruri、OpenAI-3 | Cohere Rerank、Jina v3、BGE Reranker |
graph LR Q[クエリ] --> Stage1[Stage 1: Retrieval\n Bi-encoder Hybrid\n Top-50 to 100] KB[(知識ベース\n 数百万件)] --> Stage1 Stage1 --> Stage2[Stage 2: Rerank\n Cross-encoder\n Top-5 to 10] Stage2 --> LLM[Stage 3: LLM Generation] style Stage1 fill:#8b5cf6,stroke:#6d28d9,color:#fff style Stage2 fill:#f97316,stroke:#ea580c,color:#fff style LLM fill:#14b8a6,stroke:#0d9488,color:#fff
2026年の主要Rerankerモデル
| モデル | ELO/精度 | レイテンシ | 特徴 |
|---|---|---|---|
| Zerank 2 | 1638 ELO(首位) | 中 | 2026年の最高精度Reranker |
| Cohere Rerank v4.0 Pro | 1629 ELO | ~600ms | 多言語・本番定番 |
| Jina Reranker v3 | BEIR 61.94 nDCG@10 | <200ms(最速級) | v2比+5.43%、サブ200ms唯一のトップ層 |
| BGE Reranker v2-m3 | 実用十分 | 自社ホスト可 | OSS定番・多言語・プライバシー◎ |
| japanese-reranker-base-v2 | JQaRA 0.7845 | CPU可 | 日本語特化、19層/512hidden |
Contextual Retrieval — Anthropicの革新
2024年9月、Anthropicが発表したContextual Retrievalは、RAGの精度を根本から引き上げる画期的手法です。 コアアイデアはシンプル:各チャンクに「全体文書の中でそのチャンクが何を意味するか」の文脈(50〜100トークン)を先頭に付与してから埋め込む。
なぜ効くのか
「この会社の売上は前四半期比3%増加した」というチャンクは、単体では「どの会社か・いつか」の文脈が失われています。 「ACME社のQ2 2023レポートより。前四半期(Q1 2023)の売上は3.14億ドル」という文脈プレフィックスを加えると、 「ACMEのQ2売上は?」というクエリに確実にヒットするようになります。
公式プロンプト(そのまま使える)
<document>
{{WHOLE_DOCUMENT}}
</document>
Here is the chunk we want to situate within the whole document
<chunk>
{{CHUNK_CONTENT}}
</chunk>
Please give a short succinct context to situate this chunk within the overall
document for the purposes of improving search retrieval of the chunk. Answer
only with the succinct context and nothing else.このプロンプトをClaude 3 Haikuで各チャンクに実行し、生成された文脈をチャンクの先頭に結合してからEmbeddingとBM25インデックスに投入します。
失敗率削減の実測値
| 手法 | 失敗率(top-20) | ベースライン比 |
|---|---|---|
| Naive Embeddings | 5.7% | - |
| Contextual Embeddings | 3.7% | -35% |
| + Contextual BM25 | 2.9% | -49% |
| + Reranking (Cohere) | 1.9% | -67% |
// TypeScript実装例(@anthropic-ai/sdk)
const response = await client.messages.create({
model: 'claude-3-haiku-20240307',
max_tokens: 150,
system: [
{
type: 'text',
text: `<document>\n${wholeDocument}\n</document>`,
cache_control: { type: 'ephemeral' }, // 文書をキャッシュ
},
],
messages: [
{
role: 'user',
content: `Here is the chunk...\n<chunk>${chunkContent}</chunk>\nPlease give a short succinct context...`,
},
],
});
const contextualChunk = `${response.content[0].text}\n\n${chunkContent}`;
// これをembedding・BM25両方に投入日本語ハイブリッド検索の落とし穴
日本語でBM25を使うには形態素解析によるトークナイズが必須です。 スペース区切りの英語と違い、日本語をそのまま単語分割せずにBM25に投入すると、検索品質が壊滅します。
| ツール | 速度 | 特徴 | 推奨用途 |
|---|---|---|---|
| MeCab | 高速 | 枯れた定番 | リアルタイム処理 |
| Sudachi | 中 | 3分割単位・固有表現に強い | 検索用トークナイズ(第一候補) |
| Janome | 遅 | Pure Python | プロトタイピング |
| GiNZA(ja_ginza) | 中 | spaCyベース・NER | エンティティ抽出併用 |
本番パイプラインの全体構成
graph TD Q[クエリ] --> QT[Query Transform\n HyDE/Multi-query] QT --> D[Dense検索\n Top-50] QT --> S[BM25検索\n Sudachi tokenize\n Top-50] D --> RRF[RRF融合\n k=60] S --> RRF RRF --> RR[Cross-encoder Rerank\n japanese-reranker-v2] RR --> F[Filter and Assembly\n 重要chunkを端に] F --> LLM[LLM Generation] style D fill:#8b5cf6,stroke:#6d28d9,color:#fff style S fill:#ec4899,stroke:#be185d,color:#fff style RRF fill:#f97316,stroke:#ea580c,color:#fff style RR fill:#14b8a6,stroke:#0d9488,color:#fff
次章への導線
ここまでで検索品質を極限まで引き上げる技術を押さえました。 しかし「評価なしで本番投入」が、2026年時点でもRAG失敗の最大の原因です。 次章ではRagasを中心としたRAG評価フレームワーク、合成データによる評価セット自動生成、LLM-as-Judgeの落とし穴を詳説します。
理解度チェック
RRF(Reciprocal Rank Fusion)でハイブリッド検索を行う際、業界標準で使われる定数kの値はいくつですか?
キーボード: 1〜4 で選択、Enter で回答