
Embedding検索は、質問と文書を数値vectorへ変換し、vector空間で近い文書を探す方法です。単語が一致しなくても、学習済みencoderが関連する表現を近づけていれば、「休暇の残りを知りたい」という質問から「年次有給休暇の残日数確認」を含む文書を取得できます。
ただし、文章をvector化するだけで検索が完成するわけではありません。何をpositive(正例)、何をnegative(負例)として学習したか、どのsimilarity(類似度)を使うか、近似indexが正解候補を落としていないかまでが検索品質を決めます。
この記事では、Dense Passage Retrieval(DPR、密なvector表現を使う文章検索)の原論文を軸に、Dual Encoder(質問と文書を別々にencodeする構成)の学習、文書index、ANN(Approximate Nearest Neighbor、近似最近傍探索)、評価、失敗診断を順に解説します。
3行要約
- Dense Retrievalは、queryと文書を固定長のdense vectorへ変換し、内積やcosine similarityで上位文書を探します。
- DPRは、関連passageのscoreを上げ、negative passageのscoreを下げるように2つのencoderを学習します。
- 本番品質はencoderだけで決まりません。exact検索との差、metadata filter、index version、domain別のRecall@kを分けて確認します。
Embedding検索とDense Retrievalとは
Dense Retrievalは、query(検索質問)とdocumentまたはpassage(検索単位の文章)を、要素の多くが非ゼロである低次元のdense vectorへ写し、その近さで文書集合(corpus)を順位付けする検索方式です。一般向けには「Embedding検索」や「ベクトル検索」と呼ばれることがあります。
「セマンティック検索」は、意味や意図を使う検索体験・system全体を指す場合があります。Dense Retrievalはその実装手段の一つです。セマンティック検索が必ず1本のdense vector検索だけで構成されるわけではありません。keyword、metadata、rerankerを併用するsystemもあります。
キーワード検索との違い
Dense Passage Retrieval for Open-Domain Question Answeringは、従来のTF-IDFやBM25を高次元のsparse表現、DPRを低次元のdense表現として対比しています。
| 比較軸 | sparse retrieval(BM25など) | dense retrieval |
|---|---|---|
| 主な手がかり | 語の一致、頻度、希少性 | 学習したvector空間での近さ |
| 得意な例 | 製品型番、エラーコード、人名 | 同義語、言い換え、自然な質問 |
| index | inverted index(転置index) | vector index |
| 学習 | 強力な未学習baselineを作りやすい | encoderの学習data・domainに依存 |
| 説明しやすさ | 一致語を追いやすい | 各vector次元の意味は解釈しにくい |
| 主な弱点 | vocabulary mismatch(語彙不一致) | exact token、domain shift、近似誤差 |
たとえばqueryが「PCが熱いときの対処」、文書が「ノートパソコンの過熱時は吸気口を清掃する」なら、dense retrievalは「PC」と「ノートパソコン」、「熱い」と「過熱」の近さを学習していれば取得できます。
一方、「E1042」や「AB-19X」のような文字列は、意味の近さより完全一致が重要です。Dense Retrievalだけに置き換えるのではなく、まずBM25をbaselineとして残し、queryの種類ごとに比較する必要があります。

BEIRも、複数domainのzero-shot評価でBM25が堅牢なbaselineである一方、dense retrieverにはdomain外一般化の余地があると報告しています。
token Embeddingと検索用Embeddingは役割が違う
順番3で扱ったtoken Embeddingは、token IDをTransformerが計算できるvectorへ変える入口でした。Dense Retrievalで比較するのは、query全体やpassage全体を代表する検索用vectorです。
| 粒度 | 入力 | 出力 | 主目的 |
|---|---|---|---|
| token embedding | token ID | tokenごとのvector | neural network内部の入力表現 |
| contextualized representation | token列 | 文脈を反映したtoken表現 | 分類、抽出、生成など |
| retrieval embedding | queryまたはpassage | 検索単位を代表するvector | 関連文書のranking |

検索用vectorは、単にBERTの出力を取り出せば自動的に良くなるわけではありません。「このqueryにはこのpassageが関連する」というdataを使い、検索順位に合うようencoderを調整します。
Embeddingの基礎から確認したい場合は、Embeddingとは?単語や文章をベクトルで表す仕組みも参照してください。
DPR論文が解いた問題
DPRが対象にしたopen-domain QA(大規模な文書集合から答える質問応答)では、すべての文書をreaderへ入力できません。先にretrieverが小さな候補集合を選び、その後にreaderが答えを抽出します。
corpusを C、questionを q、取得件数を k とすると、retrieverは概念的に次の写像です。
DPR論文では、top-kにanswer spanを含むpassageが少なくとも1件あるquestionの割合をtop-k retrieval accuracyとして評価しました。retrieverが正解passageを候補へ入れなければ、後段readerが高性能でも答えられません。
論文はNatural Questions、TriviaQA、WebQuestions、CuratedTREC、SQuADの5 datasetで評価し、強いLucene-BM25 baselineに対してtop-20 passage retrieval accuracyを多くの場合9〜19 absolute point改善したと報告しています。ただし、これは論文の英語Wikipedia、質問応答dataset、学習条件での結果です。「どのcorpusでもdense retrievalがBM25より優れる」と一般化はできません。
Dual Encoderは質問と文書を別々に処理する
DPRはquestion encoder E_Q とpassage encoder E_P を使います。原論文では独立した2つのBERT-baseを用い、それぞれの[CLS]表現から768次元vectorを作りました。
question q とpassage p のscoreは内積です。
関連passageのvectorがquestion vectorと近い方向・大きな内積になれば、scoreが上がります。

なぜCross EncoderではなくDual Encoderなのか
queryとpassageを連結し、両者のtokenをcross attentionで詳しく比較するmodelは、pairごとにneural network推論が必要です。corpusが100万passageなら、1 queryに対して100万pairを重いmodelへ通すことになります。
Dual Encoderでは、passage側をqueryと独立に計算できます。文書登録時に全passageをencodeしてindexへ保存し、検索時はqueryを1回encodeしてvector同士を比較します。
| 観点 | Dual Encoder | Cross Encoder |
|---|---|---|
| 入力 | queryとpassageを別々にencode | queryとpassageを同時に入力 |
| passage事前計算 | 可能 | 原則できない |
| 大規模候補生成 | 向く | 全corpus比較には重い |
| token間の詳細比較 | encoder後の単一vectorに圧縮 | query-document間attentionを使える |
| 典型的な役割 | first-stage retrieval | 少数候補のreranking |
Sentence-BERTも、文同士を毎回pairwiseにBERTへ入力する計算を避け、各文のembeddingを比較する構成を示しました。速さは「BERTが軽くなった」からではなく、corpus側の計算を使い回せることから生まれます。
Cross Encoderとrerankerの詳細は、次回のChunking / Rerank編で扱います。
positiveとnegativeから検索空間を学ぶ
DPRの学習instanceは、question q_i、関連するpositive passage p_i^+、関連しないnegative passages p_{i,j}^- で構成されます。
positive passageのsoftmax確率を高めるlossは次の形です。
このlossは、positiveのscoreだけを上げるのではなく、同じ候補集合にいるnegativeよりpositiveを上へ並べるよう学習します。
negativeの難しさが学習を変える
DPRは次のnegativeを比較しました。
| negative | 作り方 | 学べること | 注意点 |
|---|---|---|---|
| random negative | corpusから無作為に選ぶ | 明らかに無関係な文書を離す | 簡単すぎて細かな順位差を学びにくい |
| BM25 hard negative | 語は一致するがanswerを含まない上位文書 | 表面的に似た誤候補を見分ける | 実は正解であるfalse negative混入に注意 |
| in-batch negative | 他questionのpositiveをnegativeとして再利用 | 1 batchで多数の比較を作る | 同じ話題の別正解を誤ってnegativeにし得る |
batch sizeを B とし、question vector matrixを Q、passage vector matrixを P とすると、score matrixは次の計算で一度に作れます。
対角成分 S_ii がpositive、同じrowの他要素がnegativeです。1 batchで B^2 個のquestion-passage scoreを再利用できます。

DPRの実験では、in-batch negativesにBM25 hard negativeを1件加える構成が有効でした。2件へ増やしても同じ条件では改善しなかったため、hard negativeは多ければよいわけではありません。
false negativeを監査する
「answer文字列を含まないからnegative」と決めると、別表現で正しく答えるpassageをnegativeにする場合があります。社内文書でも、旧制度の説明、別部署の同義文、表の見出しを失ったchunkが混ざります。
training dataでは次を記録します。
- query ID
- positive source IDと選定根拠
- negative source IDと採掘方法
- encoder version
- annotation version
- false negative確認status
lossが下がっても、誤ったlabelへ適合していれば検索品質は上がりません。
Indexingとquery-timeを分ける
Dense Retrievalの運用は、文書を準備するoffline側と、質問へ応答するonline側に分かれます。
Indexing
- source documentを検索単位へ分ける。
- passage textとtitleをpassage encoderへ入力する。
- vectorとpassage ID、source ID、version、metadataを対応付ける。
- exactまたはANN indexへ追加する。
- index versionを固定し、評価済みsnapshotとして切り替える。
Query-time
- queryを同じmodel familyのquestion encoderでvector化する。
- tenantやdocument種別などのfilter条件を確定する。
- indexからtop-k候補を検索する。
- passage IDから本文と出典metadataを取得する。
- score、rank、encoder/index versionをtraceへ残す。

DPR公式repositoryは、static documentのrepresentation生成が並列化可能である一方、規模によっては長い処理になると説明しています。encoderを更新すると、古いpassage vectorと新しいquery vectorが同じ空間にある保証がなくなります。したがって、新encoder用indexを別versionで作り、評価後に切り替える方が安全です。
| 更新対象 | 必要な対応 |
|---|---|
| passage本文 | 対象passageを再encodeしindex更新 |
| metadataだけ | vectorを変えずmetadata/filter indexを更新できる場合がある |
| passage encoder | 原則としてcorpus全体を再encode |
| question encoderだけ | offline vectorは残せるが、互換性を評価する |
| similarity/normalization | vector再計算またはindex再構築が必要になり得る |
内積・cosine similarity・L2距離を揃える
「vectorが近い」の定義は一つではありません。
Inner product
内積は方向だけでなくvector norm(長さ)の影響を受けます。DPRは内積を使います。
Cosine similarity
queryとdatabase vectorをL2 normalizeしてunit vectorにすれば、cosine similarityは内積と一致します。
L2 distance
unit vector同士では、Faissのmetric資料が示すように次の関係になります。
したがって、両側を正規化した条件では「cosine similarity最大」「inner product最大」「L2 distance最小」は同じ順位になります。正規化していないvectorでは同じではありません。

model評価時はcosine、indexはinner product、queryだけnormalizeという不一致があると、Notebookと本番で順位が変わります。metric名だけでなく次を一組で固定します。
- encoder名とversion
- pooling方法
- vector次元
- query/documentのnormalization有無
- index metric
- scoreが大きいほど良いか、小さいほど良いか
exact searchとANNを分ける
exact searchは、query vectorと対象vectorをすべて比較します。正しいmetricのもとで真のtop-kを返せますが、vector数が増えるほど計算量とmemory帯域が問題になります。
ANNは、graph(近傍関係)、cluster(vectorのまとまり)、quantization(量子化)などを使って探索対象や表現量を絞ります。高速化やmemory削減と引き換えに、exact top-kを落とす可能性があります。
Faissは、index選択の比較軸としてsearch time、search quality、memory、training time、adding timeなどを挙げています。
| 方式 | 検索結果 | latency | memory | build/update | 向く用途 |
|---|---|---|---|---|---|
| Flat exact | metric上のexact top-k | corpus増加で重くなる | 生vectorを保持 | 単純 | 小規模、品質baseline |
| graph ANN(HNSWなど) | 近似 | 高recallと速度を調整 | graph edge分が増える | 構築・削除制約を確認 | memoryを確保できる低latency検索 |
| IVF系 | cluster候補だけ探索 | probe数で調整 | 構成による | trainingとparameter調整 | 大規模corpus |
| quantized index | 圧縮vectorで近似 | 高速化しやすい | 削減できる | 圧縮学習・再構築 | memory制約が強い環境 |

HNSW論文は、階層化したproximity graphをたどるANN手法を提案しています。ここで大切なのは製品名ではなく、同じencoder・同じcorpusでexact結果を基準にANN recallとlatencyを測ることです。
2種類の「検索漏れ」を分ける
- encoder ranking error: exact検索でもgold passageのscoreが低い。
- ANN approximation error: exact検索ならtop-kに入るが、ANNが候補を見つけられない。
ANN parameterだけを調整しても、encoder ranking errorは直りません。反対にmodelを再学習しても、ANNの探索幅が狭すぎればgold passageは落ちます。
検索器をどう評価するか
RAGの最終回答だけを見ると、検索漏れと生成誤りを区別できません。retriever単体のtest setを作り、gold passageまたはrelevance labelを付けます。
Recall@k
relevant passage集合を R_q、top-k検索結果を D_q^k とします。
1質問に正解passageが1件だけなら、「top-kに正解が入ったquestionの割合」として読めます。DPRのtop-k retrieval accuracyはanswer spanを含むpassageが少なくとも1件あるかを見る定義です。自社評価では、同じ内容を説明する複数passageをrelevantとして認めるかを先に決めます。
順位も見る
| metric | 見るもの | 向く条件 |
|---|---|---|
| Recall@k / Hit@k | relevant文書を候補集合へ入れたか | 後段にrerankerやLLMがある |
| MRR | 最初のrelevant文書が何位か | 早い順位の1件が重要 |
| nDCG@k | 複数段階のrelevanceと順位 | 複数文書に重要度差がある |
| exact-match against Flat | ANNがexact top-kをどれだけ再現したか | index parameter調整 |
| p50/p95 latency | 典型・tail latency | service運用 |
| index size/build time | memoryと更新負荷 | 大規模・頻繁な更新 |
retrieval metricが上がっても、最終回答が必ず改善するとは限りません。後段が根拠を使えない、context budgetで落とす、gold passage自体が回答に不十分という失敗があるためです。RAGとは?LLMに外部知識を使わせる仕組みで扱ったend-to-end評価と分けて記録します。
query category別に見る
平均値だけでは弱点が隠れます。test queryへ次のlabelを付けます。
- 言い換え
- exact keyword
- 製品型番・エラーコード
- 数値・日付
- 否定を含む質問
- domain固有略語
- 複数条件
- corpusに答えがない質問
「意味検索だから意味を理解する」という抽象的な評価ではなく、どのquery categoryでどの方式がgold passageを拾うかを比較します。
最小のexact検索でcontractを確認する
最初からANN parameterを増やすより、正規化とscore順を確認できるexact baselineを作ると診断しやすくなります。次は学習済みencoderが返したvectorを比較するだけの独自例です。
from __future__ import annotations
import logging
import numpy as np
from numpy.typing import NDArray
LOGGER = logging.getLogger(__name__)
def cosine_top_k(
query: NDArray[np.float32],
documents: NDArray[np.float32],
k: int,
) -> tuple[NDArray[np.int64], NDArray[np.float32]]:
"""L2正規化したvectorの内積でtop-kを返す。
Args:
query: shape (dimension,) のquery vector。
documents: shape (count, dimension) のdocument vector。
k: 返す候補数。
Returns:
score降順のdocument indexとcosine score。
Raises:
ValueError: shape不一致、zero vector、不正なkの場合。
Example:
>>> docs = np.array([[1.0, 0.0], [0.8, 0.2]], dtype=np.float32)
>>> ids, scores = cosine_top_k(
... np.array([1.0, 0.0], dtype=np.float32), docs, 1
... )
>>> ids.tolist()
[0]
"""
if query.ndim != 1 or documents.ndim != 2:
raise ValueError("queryは1次元、documentsは2次元で指定してください")
if documents.shape[1] != query.shape[0]:
raise ValueError("vector次元が一致していません")
if not 1 <= k <= documents.shape[0]:
raise ValueError("kは1以上document数以下で指定してください")
query_norm = np.linalg.norm(query)
document_norms = np.linalg.norm(documents, axis=1)
if query_norm == 0 or np.any(document_norms == 0):
raise ValueError("zero vectorはcosine similarityを計算できません")
# 評価と本番indexで同じmetric contractを守るため、両側を正規化する。
normalized_query = query / query_norm
normalized_documents = documents / document_norms[:, None]
scores = normalized_documents @ normalized_query
top_ids = np.argsort(scores)[::-1][:k].astype(np.int64)
LOGGER.info("dense retrieval complete: documents=%d k=%d", len(documents), k)
return top_ids, scores[top_ids].astype(np.float32)
このbaselineでgold passageの順位を測り、その後にANN indexへ差し替えます。結果が悪化した場合、同じquery vector、document vector、filter、metricで比較すれば近似indexの影響を確認できます。
Dense Retrievalが失敗する原因
| 症状 | 分離して確認する箇所 | 代表的な原因 | 次の確認 |
|---|---|---|---|
| exactでもgoldが低順位 | encoder/data | domain shift、negative不足、label誤り | query category別評価、hard negative監査 |
| exactは正しいがANNで落ちる | vector index | 探索幅、cluster、quantization | ANN recall、latency、memory比較 |
| scoreは高いが内容が違う | training unit | false positive、topicだけ近い | fine-grained relevance label |
| 型番や数値を落とす | retrieval方式 | tokenizer、dense表現、exact match弱さ | BM25 baseline、次回のhybrid設計 |
| 更新文書が出ない | indexing | 再encode漏れ、index version混在 | source/vector/index lineage |
| 本番だけ順位が違う | contract | normalization・metric・filter不一致 | versionと前処理をtrace |
| 別tenant文書が候補に出る | filter/data | ACL metadata欠落、filter漏れ | 検索前filterと権限test |

domain shiftを「modelが悪い」で済ませない
DPRの結果はopen-domain QA向けです。社内規程、医療略語、商品catalog、source codeではqueryとrelevanceの関係が異なります。BEIRが示したように、あるdatasetで学習したretrieverは別domainで同じ順位性能を保つとは限りません。
少量でも実queryに近い評価setを作り、pretrained model、domain fine-tuning、BM25を同じ条件で比較します。fine-tuningする場合は、どのquery categoryが改善し、どこが悪化したかを残します。
導入時の最小チェックリスト
- 検索単位とrelevance labelを決める。
- 実queryからtest setを作り、query categoryを付ける。
- BM25とFlat exact denseをbaselineにする。
- encoder、pooling、normalization、metricをversion固定する。
- positive、negative、hard negativeのlabelを監査する。
- corpusをencodeし、source IDとvector IDを対応付ける。
- corpus規模とSLOに応じてANNを追加する。
- Flatとの差としてANN recallを測る。
- Recall@k、順位metric、latency、memory、build時間を記録する。
- encoder更新時は新indexを作り、評価後にversionを切り替える。
この順序なら、「Embedding model」「vector database」「ANN parameter」を一度に変えて原因が分からなくなる事態を避けられます。
よくある質問
Dense Retrievalとベクトル検索は同じですか?
文章検索の文脈では近い意味で使われます。Dense Retrievalは、queryと文書をdense vectorで表し、近さに基づいて関連文書を取得する情報検索の方式です。ベクトル検索はtext以外の画像・音声・商品特徴量にも使える、より広い実装概念です。
cosine similarityと内積はどちらを使うべきですか?
modelが想定する学習時のscoreと揃えます。両側をL2 normalizeするmodelなら、cosine similarityとinner productは同じ順位になります。未正規化vectorではnormの影響が異なるため、無条件に交換しません。
vector databaseは必須ですか?
必須ではありません。小規模ならFlat exact searchや行列積でも検証できます。更新、filter、永続化、分散、latency要件が増えるとvector indexやdatabaseが有用になります。
kはいくつにすべきですか?
万能な値はありません。retrieverの候補kと、後段へ渡す文書数を分け、Recall@k、latency、後段costを同じtest setで比較します。正解がtop-kへ入らない問題をrerankerで直すことはできません。
encoderを変更したら再indexが必要ですか?
passage encoderやpooling、normalization、vector次元を変えた場合は、原則としてcorpusを再encodeし、新しいindexを作ります。question encoderだけの変更でも、古いpassage vectorとの互換性を評価してから切り替えます。
まとめ
Embedding検索は、文章をvectorへ変換する処理と、そのvectorが検索順位に役立つよう学習する処理の組み合わせです。DPRはquestionとpassageを別々のencoderで表し、positiveの内積をnegativeより高くすることで、大規模corpusの候補検索を可能にしました。
実装では、encoderの精度とANNの近似精度を分けます。まずBM25とFlat exact denseを比較し、その後にANNを追加します。normalizationとmetric、encoder/index version、filterを固定し、query category別のRecall@kとlatencyを測れば、検索漏れを直せるstageへ落とし込めます。
次回は、retrievalへ渡す文書の切り方と、dense・sparse候補をどう組み合わせてrerankするかを、Chunking / Hybrid Search / Rerankerとして扱います。



コメント