「社内のRAGシステムを入れたのに、製品型番やエラーコードで検索すると全然ヒットしない」——こんな声を、RAGを導入した企業からよく聞きます。
その原因は多くの場合、ベクトル検索だけに頼っているアーキテクチャにあります。Dense検索(ベクトル検索)は意味的な類似度には強い一方、表層の文字列一致が重要な固有名詞・型番・エラーコードに弱いという根本的な弱点を持ちます。
この記事では、BM25(キーワード検索)・Dense(ベクトル)・Sparse(SPLADE)・Late Interaction(ColBERT)の4つの検索アルゴリズムの仕組みと相補関係を整理し、Reciprocal Rank Fusion(RRF)によるスコア統合、Cross-Encoderリランカーの挿入位置、Qdrant/Elasticsearch/pgvectorでの実装例、そして日本語特有の形態素解析チューニングまでを体系的に解説します。
📌 この記事で学べること
・なぜベクトル検索だけでは固有名詞・型番がヒットしないのか
・BM25を足すだけで精度が跳ね上がる理由
・SPLADE・ColBERTの位置づけと導入コスト
・RRFとリランカーの使い分け
・日本語RAGに必要な形態素解析チューニング
・クエリ種別ごとのアルゴリズム選択ガイド
1. なぜ「ベクトル検索だけ」では不十分なのか
Dense検索の仕組みと弱点
Dense検索(密ベクトル検索)は、テキストをEmbeddingモデルで高次元のベクトルに変換し、コサイン類似度や内積でクエリと文書の類似度を計算します。text-embedding-3(OpenAI)、multilingual-e5(Microsoft)、ruri(NICT)などのモデルが代表的です。
Dense検索が得意とするのは意味的な言い換えへの対応です。「売上が落ちた」と「収益が悪化した」のような表現の違いを吸収し、意味的に近い文書を拾ってくれます。
一方で、Dense検索には明確な弱点があります。
| クエリ例 | 問題 |
|---|---|
| 「ERR_CONNECTION_REFUSED」 | エラーコードは意味的に薄く、ベクトル空間で散逸する |
| 「製品型番 ABC-1234-X」 | 型番の細かい差異(ABC-1234 vs ABC-1234-X)を区別しにくい |
| 「田中部長の稟議書」 | 固有名詞は学習データの偏りで埋め込みが不安定 |
| 「v2.3.1のリリースノート」 | バージョン番号の完全一致を保証できない |
Embeddingモデルは大量のテキストで学習されていますが、社内固有の型番・コード・人名・製品名は学習データに少なく、ベクトル空間での表現が不安定になります。これが「RAGを入れたのに型番で検索できない」問題の根本原因です。
BM25(キーワード検索)との相補関係
BM25(Best Match 25)はTF-IDFを改良した古典的な統計的検索アルゴリズムです。単語の出現頻度と文書長を考慮して文書をスコアリングします。
BM25の強みは表層一致の精度です。クエリに含まれる単語が文書に含まれていれば確実にスコアが上がります。型番「ABC-1234-X」をそのまま検索すれば、その型番を含む文書が上位に来ます。
Dense検索とBM25は互いの弱点を補う関係にあります:
- Dense検索が得意:意味的な言い換え、同義語、概念的な類似
- BM25が得意:固有名詞、型番、エラーコード、完全一致が重要なクエリ
実際に多くの実装評価で、Dense単独よりBM25+Denseのハイブリッドのほうが検索精度(Recall@k、MRR)が高いことが示されています。特に企業の社内文書では、型番・製品名・人名・プロジェクト名などBM25が強いクエリが多く、ハイブリッド化の効果が大きくなります。
2. 4つの検索アルゴリズムの全体マップ
ハイブリッド検索を設計する前に、主要な4つのアルゴリズムの位置づけを整理しておきましょう。
| アルゴリズム | 表現方式 | 強み | 弱み | 導入コスト |
|---|---|---|---|---|
| BM25 | 疎なTF-IDF統計 | 固有名詞・型番・完全一致 | 意味的な言い換えに弱い | 低 |
| Dense(ベクトル) | 密なEmbeddingベクトル | 意味的類似・言い換え対応 | 固有名詞・表層一致に弱い | 中 |
| Sparse(SPLADE) | 学習済み疎ベクトル | BM25の精度+語彙拡張 | 日本語モデルが少ない | 中〜高 |
| Late Interaction(ColBERT) | トークン単位のベクトル列 | 高精度・細粒度マッチング | インデックスサイズが大きい | 高 |
Sparse検索(SPLADE)の位置づけ
SPLADE(Sparse Lexical and Expansion model)はBM25とDense検索のハイブリッドとも言える手法です。BERTなどのTransformerモデルを使って意味的に関連する語彙への拡張(Query Expansion)をしながら、疎なベクトルを生成します。
例えば「車が壊れた」というクエリに対し、BM25なら「壊れた」「車」を含む文書しかヒットしませんが、SPLADEは「故障」「修理」「自動車」なども含む文書にスコアを与えます。かつBM25と同じ疎ベクトルの構造なので、転置インデックスで高速検索できます。
日本語SPLADEの現状:英語では複数の事前学習済みモデルが公開されていますが、日本語対応モデルはまだ少ない状況です。huggingface上で「japanese-splade」として公開されているモデルはありますが、英語ほど成熟していません。日本語でSPLADEを使う場合は、独自のファインチューニングが必要になるケースもあります。
Late Interaction(ColBERT)の違い
ColBERT(Contextualized Late Interaction over BERT)は、クエリと文書をそれぞれトークン単位のベクトル列として表現し、MaxSim演算(クエリの各トークンと最も類似した文書トークンの類似度を合計する)でスコアを計算します。
Denseモデルが文書全体を1つのベクトルに圧縮するのに対し、ColBERTはトークンレベルの細粒度マッチングを行うため、長文書・複雑なクエリに対する精度が高いのが特徴です。
ただしトークン単位でベクトルを保持するため、インデックスサイズがDenseの数倍〜数十倍になります。RAGzillaやRAGatouille(Pythonライブラリ)を使うと比較的導入しやすくなっています。
ColBERTの実用的な使い所は、精度が特に重要な段階(リランキング層)での利用です。初期検索は高速なBM25+Denseで行い、上位N件をColBERTで精密リランキングするアーキテクチャが現実的です。
3. スコア統合の手法:RRFとWeighted Fusion
複数の検索アルゴリズムの結果を組み合わせる方法には大きく2つあります。
Reciprocal Rank Fusion(RRF)
RRFは各アルゴリズムの検索結果リストを受け取り、順位ベースでスコアを統合します。計算式は以下の通りです:
RRF_score(d) = Σ 1 / (k + rank_i(d))
※ k=60(デフォルト)、rank_i(d) は i番目のアルゴリズムでの文書dの順位
RRFの最大の利点はスコアの正規化が不要な点です。BM25スコアとコサイン類似度は異なるスケールを持ちますが、RRFは順位だけを見るため、スコールを直接比較する必要がありません。
実装もシンプルで、Elasticsearch・Qdrant・Weaviateなど主要なベクトルDBはRRFを標準サポートしています。ゼロから実装する場合も以下のようなPythonコードで実現できます:
from collections import defaultdict
def reciprocal_rank_fusion(results_list: list[list[str]], k: int = 60) -> list[tuple[str, float]]:
"""
results_list: 各検索器の結果(文書IDのリスト)のリスト
k: ランクの調整パラメータ(デフォルト60)
"""
scores = defaultdict(float)
for results in results_list:
for rank, doc_id in enumerate(results, start=1):
scores[doc_id] += 1.0 / (k + rank)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
# 使用例
bm25_results = ["doc_A", "doc_C", "doc_B", "doc_E"]
dense_results = ["doc_B", "doc_A", "doc_D", "doc_C"]
fused = reciprocal_rank_fusion([bm25_results, dense_results])
# → [("doc_A", 0.032), ("doc_B", 0.031), ("doc_C", 0.029), ...]
Weighted Fusion(重み付き統合)
RRFに対し、スコアを直接重み付き加算する方法もあります:
Hybrid_score = α × BM25_score_normalized + (1-α) × Dense_score
この場合は各スコアを正規化(min-max normalizationやsoftmax)する必要があります。重みαをクエリ種別ごとに動的に変えることで、固有名詞クエリならBM25寄り、自然文クエリならDense寄りに調整できます。
どちらを選ぶか:パラメータチューニングの手間が少なく安定して高性能なRRFを最初に試し、評価データセットで検証した上でWeighted Fusionへの移行を検討するのが実践的なアプローチです。
4. Cross-Encoderリランカーの挿入位置
ハイブリッド検索で初期検索精度を上げた後、さらに精度を高める手段としてCross-Encoderリランカーがあります。
Bi-EncoderとCross-Encoderの違い
Dense検索で使われるEmbeddingモデルはBi-Encoderと呼ばれます。クエリと文書を独立してベクトル化するため高速ですが、クエリと文書の間のきめ細かい相互作用を捉えにくいという限界があります。
Cross-Encoderはクエリと文書を連結してTransformerに入力し、直接関連度スコアを出力します。クエリ-文書間の全トークンレベルのアテンションが計算されるため精度が高い一方、全文書に対してリアルタイムで計算すると非常に遅くなります。
Two-Stage Reranking アーキテクチャ
実用的な設計は以下の2ステージです:
Stage 1:初期検索(高速・広めに取る)
BM25 + Dense によるハイブリッド検索 → 上位50〜100件を取得
Stage 2:リランキング(精度重視)
Cross-Encoderで上位50〜100件を精密スコアリング → 上位5〜10件をLLMに渡す
日本語で使えるCross-Encoderリランカーの代表例:
| モデル | 言語 | 特徴 |
|---|---|---|
| bge-reranker-v2-m3(BAAI) | 多言語(日本語対応) | 高精度・最もよく使われる |
| jina-reranker-v2-base-multilingual | 多言語(日本語対応) | APIとモデル両方提供 |
| cross-encoder/ms-marco-MiniLM-L-6-v2 | 英語 | 軽量・英語限定 |
| hotchpotch/japanese-reranker-cross-encoder-large-v1 | 日本語特化 | 日本語STS/QAでファインチューニング済み |
リランカーの導入で通常、検索精度(NDCG@10)が5〜15ポイント程度改善することが多く、投資対効果は高いコンポーネントです。
5. 主要ベクトルDBでのハイブリッド実装
Qdrant
Qdrantはハイブリッド検索のサポートが充実しており、BM25(疎ベクトル)とDense(密ベクトル)を1つのコレクションに共存させることができます。
from qdrant_client import QdrantClient
from qdrant_client.models import (
VectorParams, SparseVectorParams, Distance,
NamedVector, NamedSparseVector, SparseVector
)
client = QdrantClient(":memory:")
# コレクション作成(Dense + Sparse)
client.create_collection(
collection_name="hybrid_docs",
vectors_config={
"dense": VectorParams(size=1536, distance=Distance.COSINE)
},
sparse_vectors_config={
"sparse": SparseVectorParams()
}
)
# ハイブリッド検索(RRF統合)
from qdrant_client.models import Prefetch, FusionQuery, Fusion
results = client.query_points(
collection_name="hybrid_docs",
prefetch=[
Prefetch(query=query_dense_vector, using="dense", limit=20),
Prefetch(query=SparseVector(indices=sparse_indices, values=sparse_values),
using="sparse", limit=20),
],
query=FusionQuery(fusion=Fusion.RRF),
limit=5,
)
Elasticsearch / OpenSearch
ElasticsearchはBM25による全文検索が元々の強みで、8.x以降でベクトル検索(kNN)を統合しました。ハイブリッド検索は`sub_searches`とRRFで実現できます。
POST /documents/_search
{
"sub_searches": [
{
"query": {
"match": { "content": "エラーコード ERR_1234" }
}
},
{
"knn": {
"field": "dense_vector",
"query_vector": [0.1, 0.2, ...],
"num_candidates": 50
}
}
],
"rank": {
"rrf": {
"window_size": 50,
"rank_constant": 60
}
},
"size": 10
}
既にElasticsearchを使っている企業では、追加インフラなしでハイブリッド化できる点が大きなメリットです。
pgvector(PostgreSQL拡張)
既存のPostgreSQLインフラを活かしたい場合、pgvectorとPostgreSQLの全文検索(tsvector/tsquery)を組み合わせる方法があります。
-- ハイブリッド検索クエリ(RRF実装)
WITH bm25_results AS (
SELECT id, content,
ts_rank(to_tsvector('japanese', content),
plainto_tsquery('japanese', $1)) AS bm25_score,
ROW_NUMBER() OVER (ORDER BY ts_rank(
to_tsvector('japanese', content),
plainto_tsquery('japanese', $1)) DESC) AS bm25_rank
FROM documents
WHERE to_tsvector('japanese', content) @@ plainto_tsquery('japanese', $1)
LIMIT 50
),
dense_results AS (
SELECT id, content,
1 - (embedding <=> $2::vector) AS cosine_sim,
ROW_NUMBER() OVER (ORDER BY embedding <=> $2::vector) AS dense_rank
FROM documents
LIMIT 50
),
rrf_scores AS (
SELECT
COALESCE(b.id, d.id) AS id,
COALESCE(b.content, d.content) AS content,
COALESCE(1.0 / (60 + b.bm25_rank), 0) +
COALESCE(1.0 / (60 + d.dense_rank), 0) AS rrf_score
FROM bm25_results b
FULL OUTER JOIN dense_results d ON b.id = d.id
)
SELECT id, content, rrf_score
FROM rrf_scores
ORDER BY rrf_score DESC
LIMIT 10;
pgvectorは専用ベクトルDBと比べてスケーラビリティに限界がありますが、小〜中規模(〜100万件程度)のRAGであれば十分実用的で、インフラコストを抑えられます。
6. 日本語RAGに必要な形態素解析チューニング
英語はスペースで単語が分かれているため、そのまま転置インデックスに使えます。日本語はスペースがないため、形態素解析で単語に分割してからインデックスを構築する必要があります。この工程をサボると、BM25の効果が大幅に落ちます。
SudachiとMeCabの選択
| Sudachi | MeCab | |
|---|---|---|
| 開発元 | WorksApplications | 奈良先端大学院 |
| 辞書 | UniDic(現代語・新語に強い) | IPAdic / UniDic |
| 表記正規化 | 標準搭載(全角→半角、送り仮名統一) | 別途実装が必要 |
| 分割モード | A/B/C モード(粒度選択可能) | なし |
| Elasticsearch連携 | analysis-sudachi プラグイン | analysis-kuromoji プラグイン |
| おすすめ用途 | 新語・技術用語が多い社内文書 | シンプルさ重視の用途 |
社内文書のRAGでは、Sudachiをおすすめします。理由は以下の通りです:
- 製品名・サービス名・社内用語などの新語への対応が優れている
- 表記ゆれ正規化(「AI」→「AI」、「人工知能」→「AI」など)が自動でかかる
- 分割モードA(最小単位)でインデックスし、モードC(最大単位)でクエリを出すといった組み合わせが可能
ユーザー辞書の整備
どの形態素解析器を使っても、社内固有の用語(製品コード、部署名、略語など)は標準辞書に収録されていません。ユーザー辞書の整備はRAG精度改善において費用対効果が高い作業です。
# Sudachi ユーザー辞書 CSVフォーマット例
# 表層形,左文脈ID,右文脈ID,コスト,品詞1,...,読み,正規化表記
ABC-1234-X,,,5000,名詞,固有名詞,一般,*,*,*,エービーシー1234エックス,ABC-1234-X
SRE推進室,,,3000,名詞,固有名詞,組織,*,*,*,エスアールイーすいしんしつ,SRE推進室
Denseモデルの日本語選択
Embeddingモデルの選択も日本語RAGの精度に大きく影響します:
| モデル | 特徴 | 推奨用途 |
|---|---|---|
| multilingual-e5-large(Microsoft) | 多言語・高精度・大きい | 精度重視の本番環境 |
| ruri-large(NICT) | 日本語特化・最高水準 | 日本語専用環境 |
| text-embedding-3-large(OpenAI) | APIで使いやすい・高精度 | クラウド環境 |
| intfloat/multilingual-e5-small | 軽量・ローカル動作可 | リソース制限のある環境 |
7. クエリ種別ごとのアルゴリズム選択ガイド
「すべてのクエリに最適な1つのアルゴリズム」は存在しません。実際の社内RAGでは、クエリの種類によって最適なアルゴリズムが異なります。
クエリ種別の分類と推奨設定
| クエリ種別 | 例 | 推奨設定 | 理由 |
|---|---|---|---|
| 固有名詞クエリ | 「製品ABC-1234の仕様書」「田中部長の稟議」 | BM25重視(α=0.7〜0.8) | 表層一致が重要 |
| 自然文クエリ | 「有給休暇の申請方法を教えて」 | Dense重視(α=0.2〜0.3) | 意味的理解が重要 |
| エラーコードクエリ | 「ERR_CONNECTION_REFUSED 対処法」 | BM25のみ or BM25強重視 | コード完全一致が必須 |
| 概念・方針クエリ | 「セキュリティポリシーに関する規定」 | Dense重視+リランカー | 概念的な関連度が重要 |
| 長文・複合クエリ | 「2025年度の新規事業展開における承認フローと必要書類の一覧」 | ハイブリッド+ColBERT | 細粒度マッチングが必要 |
クエリ分類の自動化
RAGのフロント側で軽量なクエリ分類器を挟み、クエリ種別に応じてαを動的に変える実装が可能です:
import re
def classify_query(query: str) -> dict:
"""クエリ種別を簡易分類してBM25重みを返す"""
# 英数字・型番・エラーコードが含まれるかチェック
has_code = bool(re.search(r'[A-Z0-9]{2,}-[A-Z0-9]{2,}|ERR_\w+|v\d+\.\d+', query))
# 自然文っぽさのチェック(助詞の存在)
is_natural = bool(re.search(r'[はがをにでとのも]', query))
if has_code:
return {"bm25_weight": 0.8, "dense_weight": 0.2}
elif is_natural and len(query) > 15:
return {"bm25_weight": 0.3, "dense_weight": 0.7}
else:
return {"bm25_weight": 0.5, "dense_weight": 0.5} # デフォルト均等
# 使用例
query = "製品ABC-1234の回路図はどこにありますか?"
weights = classify_query(query)
# → {"bm25_weight": 0.8, "dense_weight": 0.2}
8. 全体アーキテクチャと段階的な実装ロードマップ
推奨アーキテクチャ全体像
[ クエリ入力 ]
↓ クエリ分類(固有名詞 / 自然文 / コード)
↓ 並列検索
┌──────────────────────────────────────┐
│ BM25検索(形態素解析済みインデックス)│ → 上位50件
│ Dense検索(Embeddingベクトル) │ → 上位50件
└──────────────────────────────────────┘
↓ RRF または Weighted Fusion で統合 → 上位30件
↓ Cross-Encoder リランカー(bge-reranker等) → 上位5〜10件
↓ LLM へコンテキスト投入
[ 回答生成 ]
段階的な実装ロードマップ
すべてを一度に実装する必要はありません。以下の順番で段階的に導入し、各ステップで評価して次に進む方法を推奨します:
| フェーズ | 内容 | 期待効果 | 難易度 |
|---|---|---|---|
| Phase 1 | BM25単独導入(既存ベクトルDBに追加) | 型番・固有名詞のヒット率が大幅改善 | 低 |
| Phase 2 | BM25+Dense ハイブリッド(RRF統合) | 総合的な検索精度向上 | 中 |
| Phase 3 | Cross-Encoderリランカー追加 | ランキング精度のさらなる改善 | 中 |
| Phase 4 | 形態素解析チューニング+ユーザー辞書整備 | 日本語固有の精度向上 | 中 |
| Phase 5 | クエリ種別分類による動的重み調整 | クエリ特性に合わせた最適化 | 中〜高 |
| Phase 6 | SPLADE / ColBERT導入(必要に応じて) | 高精度化(コスト増も伴う) | 高 |
多くの企業のユースケースではPhase 1〜3で十分な精度改善を実感できることが多く、Phase 4以降はEval-Driven(評価データセットを使った改善サイクル)で必要性を確認してから進めることを推奨します。
9. よくある質問(Q&A)
Q1. 既存のRAGシステムに後からBM25を追加するのは大変ですか?
使っているベクトルDBによります。Qdrant・Elasticsearch・Weaviateは疎ベクトルのサポートが組み込まれており、既存コレクションに疎ベクトルフィールドを追加してBM25スコアを格納する形で対応できます。pgvectorは全文検索機能(tsvector)がPostgreSQLに元々あるため比較的追加しやすいです。LangChain / LlamaIndexを使っている場合はEnsembleRetrieverとして設定できます。
Q2. RRFのk=60というパラメータは変えるべきですか?
kは検索結果の上位グループをどれだけ均等に扱うかを制御します。kが大きいほど上位・下位の差が小さくなります。60はオリジナル論文の推奨値で、多くのケースで安定して機能します。評価データセットがある場合はチューニングする価値がありますが、なければまず60で試してください。
Q3. OpenAIのEmbeddingを使いながらBM25を追加できますか?
できます。OpenAI Embeddingはクラウドで計算されたDenseベクトルで、BM25はローカルのテキストインデックスです。両者は独立しており、組み合わせは自由です。例えばQdrantにDenseベクトル(OpenAI)と疎ベクトル(BM25スコア)を両方格納して、RRFで統合する実装が可能です。
Q4. SPLADEはBM25の代替として使えますか?
理論的にはSPLADEのほうがBM25より高性能ですが、日本語の事前学習済みモデルが少ない点がネックです。英語環境であれば積極的に採用する価値がありますが、日本語のプロダクション環境では現時点ではBM25+Denseのハイブリッドのほうが安定して高精度を得やすい状況です。
Q5. リランカーを使うとレスポンスが遅くなりませんか?
なります。Cross-Encoderは計算が重いため、クエリごとに50件のドキュメントをリランクすると100〜500ms程度の追加レイテンシが発生することがあります。対策として、GPUを使う(大幅に高速化)、リランキングするドキュメント数を絞る(50→20件)、リランカーを非同期処理にする、といった方法が有効です。bge-reranker-v2-m3はQuantize(4bit化)すると速度・精度のバランスが良くなります。
まとめ——ハイブリッド検索は「足し算」が正解
日本語社内文書のRAGにおいて、ベクトル検索だけに依存するのは検索精度の観点から不完全な設計です。
本記事のポイントを振り返ります:
- Dense検索とBM25は相補関係にある。固有名詞・型番・エラーコードにはBM25が必須
- RRFは実装が簡単でスコール正規化不要のため、ハイブリッド統合の第一選択肢
- Cross-EncoderリランカーはTwo-Stage Rerankingの2段目として挿入し、精度を5〜15ポイント改善
- 日本語形態素解析(Sudachi推奨)とユーザー辞書整備はBM25効果を最大化する基礎工事
- SPLADE・ColBERTは高精度だが導入コストが高く、まずBM25+Denseで評価してから検討
- 段階的なPhase導入が現実的——Phase1(BM25追加)だけでも大幅な改善を実感できることが多い
次のステップとして、現在のRAGシステムに対して評価データセットを作成し(Eval-Driven Developmentの記事も参照)、各フェーズの改善効果を数値で把握しながら進めることを強くおすすめします。
参考リンク
- Qdrant Hybrid Queries ドキュメント
- Elasticsearch RRF ドキュメント
- pgvector GitHub
- bge-reranker-v2-m3(HuggingFace)
- SudachiPy(GitHub)
- ruri-large 日本語Embeddingモデル(HuggingFace)
免責事項:本記事は2026年4月時点の公開情報に基づく情報提供であり、各ライブラリ・サービスのAPIや仕様は変更される場合があります。実装の際は各ツールの最新ドキュメントをご確認ください。

コメント