2段階検索が機能する理由
単一段階検索における再現率と適合率のトレードオフを理解し、高速で大まかな検索器の後に低速でも正確な再ランキング器を続けることで、双方の利点を得る方法を学びます。
「2段階検索が機能する理由」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。
検索における再現率と適合率のトレードオフ
すべての検索システムは、根本的なトレードオフに直面します。再現率は、関連文書をどれだけ多く見つけられたか(見落としはないか)を測定します。一方、適合率は、上位結果がどれだけ正確か(取得した文書のうち、実際に関連しているものが何件あるか)を測定します。両方を同時に最大化するには、計算コストがかかります。高速な検索器は再現率を優先して適合率を犠牲にし、精度の高いランカーは正確さを優先して速度を犠牲にします。
Bi-EncoderとCross-Encoder:中心的な違い
2段階検索の中心となる2種類のモデルは、クエリと文書をどのように捉えるかが異なります。bi-encoderはクエリと各文書を独立してエンコードし、それぞれのベクトル間の類似度を測定します。高速ですが、独立したエンコードによる制約があります。cross-encoderはクエリと文書を1つに連結した入力として処理するため、両者の深い相互作用を捉えられます。その一方で、候補集合に対してO(n)の計算量が必要です。
# Bi-encoder: compute query embedding ONCE, compare to all doc embeddings
# O(1) query encoding + O(n) dot products via ANN index = fast
query_vec = embed(query) # done once
results = vector_index.search(query_vec, top_k=100) # fast ANN search
# Cross-encoder: re-scores (query, doc) pairs jointly
# O(k) forward passes for k candidate documents = slow but accurate
for doc in results[:100]:
score = cross_encoder.score(query, doc.text) # joint scoring第1段階:高速な粗い検索
第1段階は高速な検索器です。通常は、近似最近傍インデックスを使うbi-encoderやBM25インデックスによって、再現率を高く保ちながら適度な適合率で、多数の候補(50~200件)を取得します。目的は正確さではなく、関連文書を見落とさないことです。広い範囲から候補を拾い、誤検出を一部許容します。第2段階でそれらを整理できるためです。
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
# Stage 1: retrieve 100 candidates (high recall, modest precision)
vectorstore = FAISS.from_documents(documents, OpenAIEmbeddings())
coarse_retriever = vectorstore.as_retriever(
search_kwargs={'k': 100} # large candidate set
)
candidates = coarse_retriever.invoke(query)
print(f'Stage 1: retrieved {len(candidates)} candidate documents')第2段階:正確なCross-Encoder再ランキング
第2段階では、第1段階の候補集合を受け取り、cross-encoderを使って各(クエリ、文書)ペアを再スコアリングします。全文書集合ではなく50~200件の候補だけを処理するため、コストの高い同時エンコードが可能です。連結された入力に対するcross-encoderの深いアテンションにより、bi-encoderよりも真の関連性をはるかに正確に推定できます。
from sentence_transformers import CrossEncoder
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
def rerank(query: str, candidates: list[str], top_k: int = 5) -> list[str]:
# Score each (query, document) pair jointly
pairs = [[query, doc] for doc in candidates]
scores = reranker.predict(pairs)
# Sort by score descending
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
return [doc for doc, _ in ranked[:top_k]]
candidate_texts = [doc.page_content for doc in candidates]
final_docs = rerank(query, candidate_texts, top_k=5)
print(f'Stage 2: selected top {len(final_docs)} documents after re-ranking')この組み合わせが機能する理由
2段階設計は、重要な非対称性を活用しています。第1段階の高速なANN検索は数百万件の文書にもミリ秒単位でスケールする一方、第2段階の正確なcross-encoderは小規模な候補集合だけを処理します。これにより、近似検索のスケーラビリティと、厳密な同時スコアリングの精度を両立できます。パイプライン全体が高速かつ高精度になり、どちらか一方の段階だけでは実現できない性能が得られます。
2段階検索のレイテンシプロファイル
一般的な2段階パイプラインでは、第1段階(100万件の文書に対するベクトルANN検索)に5~20ミリ秒、第2段階(100件の候補に対するcross-encoder処理)に、文書の長さとハードウェアに応じて100~500ミリ秒かかります。合計のレイテンシは150~600ミリ秒で、ほとんどのアプリケーションで許容できる範囲です。第2段階でGPUを使えば、短い文書の再ランキングを30ミリ秒未満に短縮でき、レイテンシに敏感なアプリケーションでも単一段階検索に匹敵する性能になります。
import time
def two_stage_search(query, coarse_retriever, reranker, top_k=5):
t0 = time.perf_counter()
candidates = coarse_retriever.invoke(query) # stage 1
t1 = time.perf_counter()
candidate_texts = [c.page_content for c in candidates]
final_docs = rerank(query, candidate_texts, top_k) # stage 2
t2 = time.perf_counter()
print(f'Stage 1 (retrieval): {(t1-t0)*1000:.1f}ms')
print(f'Stage 2 (re-ranking): {(t2-t1)*1000:.1f}ms')
print(f'Total: {(t2-t0)*1000:.1f}ms')
return final_docs適切な候補集合のサイズを選ぶ
第1段階の候補集合のサイズは、重要なハイパーパラメーターです。小さすぎる場合(たとえば10件)、再ランキングが始まる前に関連文書を見落とす可能性があります。大きすぎる場合(たとえば500件)、第2段階のレイテンシが急増します。Nにおける再現率の曲線、つまりNの値ごとに関連文書をいくつ取得できるかを示す曲線が、この選択の指針になります。一般的な最適範囲は候補50~150件で、再現率がほぼ飽和しつつ、レイテンシも管理可能な水準に収まります。
def recall_at_n(coarse_retriever, test_queries, golden_relevant, n_values):
for n in n_values:
recalls = []
for query, relevant in zip(test_queries, golden_relevant):
# Temporarily set k to n
coarse_retriever.search_kwargs['k'] = n
results = coarse_retriever.invoke(query)
retrieved_ids = {r.metadata.get('id') for r in results}
relevant_found = len(set(relevant) & retrieved_ids)
recalls.append(relevant_found / len(relevant))
avg = sum(recalls) / len(recalls)
print(f'N={n}: recall={avg:.3f}')第1段階にハイブリッド、第2段階にCross-Encoderを使う
最も強力な2段階構成は、第1段階にハイブリッド検索器(密ベクトル+BM25)、第2段階にcross-encoderを組み合わせる構成です。ハイブリッド検索は意味検索とキーワード検索を組み合わせて第1段階の再現率を最大化し、cross-encoderは統合された候補集合から最も関連性の高い文書を正確に選びます。この構成は、ベンチマークで最先端の検索品質を一貫して達成しています。
from langchain.retrievers import EnsembleRetriever
# Stage 1: hybrid retrieval for maximum recall
hybrid_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6],
)
# Stage 2: cross-encoder re-ranking for high precision
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
cross_encoder_model = HuggingFaceCrossEncoder(model_name='cross-encoder/ms-marco-MiniLM-L-6-v2')
compressor = CrossEncoderReranker(model=cross_encoder_model, top_n=5)
from langchain.retrievers import ContextualCompressionRetriever
two_stage = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=hybrid_retriever,
)商用の再ランキングAPI
自分でモデルを管理せずにcross-encoderの精度を得たい場合は、Cohere RerankとJina AI Rerankerの両方が、クラウドホスト型の再ランキングAPIを提供しています。クエリと文書テキストのリストを送信すると、関連性スコアが返されます。これらのAPIでは、大規模なcross-encoderモデル(多くの場合5億以上のパラメーター)が使われており、セルフホストの小規模なcross-encoderを上回ります。ただし、追加のAPIレイテンシ(50~300ミリ秒)と、再ランキングする文書ごとの料金が発生します。
import cohere
co = cohere.Client('YOUR_API_KEY')
def cohere_rerank(query: str, documents: list[str], top_k: int = 5):
response = co.rerank(
model='rerank-english-v3.0',
query=query,
documents=documents,
top_n=top_k,
)
return [
{'text': documents[r.index], 'score': r.relevance_score}
for r in response.results
]
final = cohere_rerank(query, candidate_texts, top_k=5)
for doc in final:
print(f'Score {doc["score"]:.3f}: {doc["text"][:80]}')2段階検索が過剰になる場合
2段階検索は、単一段階検索と比べて複雑さとレイテンシが増します。常に必要とは限りません。1万件未満の小規模なコーパスであれば、全文書集合に対して単一のcross-encoderを実行しても十分に高速な場合があります。100ミリ秒未満のレイテンシが重要で、精度の向上が小さいアプリケーションでは、単一段階の密ベクトル検索の方が適していることもあります。大規模なコーパス、高い精度要件、そして200~500ミリ秒の検索レイテンシを許容できる場合に、2段階検索を使用してください。
極めて大規模な検索のための3段階検索
数千万件の文書を扱うコーパスでは、3段階パイプラインが使われることがあります。第1段階でANNにより10,000件の候補を取得し、第2段階で高速な小規模cross-encoderを使って100件に再ランキングし、第3段階で大規模かつ高性能なcross-encoderを使って5件に再ランキングします。各段階では、より高コストで精度の高いモデルを、より少ない候補集合に適用します。このアーキテクチャは、大規模検索エンジンや文書Q&Aシステムで使われています。
理解度チェック
このレッスンで学んだ、2段階検索が機能する理由を確認しましょう。
レッスンのまとめ
このレッスンでは、bi-encoderは高速ですがクエリと文書を独立してエンコードする方式に限られること、cross-encoderは同時エンコードによって高い精度を実現する一方、コーパス全体の検索には遅すぎること、そして2段階検索はこの2つを組み合わせ、高い再現率を実現する高速な第1段階と、高い適合率を実現する精度の高い第2段階で構成されることを学びました。第1段階では、関連文書を取りこぼさないよう、必要数よりもはるかに多くの候補を取得します。次は、CohereとBGEを使ったcross-encoderの再ランキングを実装します。
よくある質問
「2段階検索が機能する理由」レッスンは無料ですか?
はい。「2段階検索が機能する理由」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。
「2段階検索が機能する理由」で何を学びますか?
単一段階検索における再現率と適合率のトレードオフを理解し、高速で大まかな検索器の後に低速でも正確な再ランキング器を続けることで、双方の利点を得る方法を学びます。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
AI Engineering Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。
「2段階検索が機能する理由」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このAI Engineering Academyレッスンでコードを書いて実行できますか?
はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。