再ランキングの効果を測定する
単一段階検索と再ランキングを行う2段階検索を前後で比較するベンチマークを実施し、NDCG、MRR、エンドツーエンドの回答品質を測定します。
「再ランキングの効果を測定する」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。
再ランキングの影響を測定する理由
再ランキングをパイプラインに追加すると、レイテンシとコストが増加します。測定しなければ、その複雑さを追加する価値があるかどうかを判断できません。ベンチマークによって検索品質とエンドツーエンドの回答品質の改善を定量化できるため、情報に基づいた判断が可能になります。また、どの種類のクエリが最も恩恵を受けるかも明らかになり、すべてのリクエストに再ランキングを適用するのではなく、必要な場合だけ適用できるようになります。
ゴールデンテストセットの構築
信頼性の高いベンチマークには、ゴールデンテストセットが必要です。これは、関連性が既知のドキュメントIDとクエリを組み合わせたコレクションです。アプリケーションログから実際のユーザークエリをサンプリングし、関連するドキュメントを手作業または専門家によるアノテーションで特定して、構造化された形式に整理します。ほとんどのRAG評価では、50〜200件のクエリで十分です。
golden_test_set = [
{
'query': 'How does pgvector HNSW indexing improve search speed?',
'relevant_doc_ids': ['doc_042', 'doc_107'],
},
{
'query': 'What is the difference between BM25 and dense retrieval?',
'relevant_doc_ids': ['doc_015'],
},
{
'query': 'How to implement reciprocal rank fusion in Python?',
'relevant_doc_ids': ['doc_093', 'doc_094'],
},
# ... 47 more entries
]
print(f'Test set size: {len(golden_test_set)} queries')
print(f'Avg relevant docs per query: {sum(len(e["relevant_doc_ids"]) for e in golden_test_set) / len(golden_test_set):.1f}')検索指標:NDCG、MRR、ヒット率
検索品質の評価には、相互に補完する3つの指標を使用します。Kにおけるヒット率は、上位K件の結果に関連するドキュメントが少なくとも1件含まれているかを測定します。MRR(Mean Reciprocal Rank)は、最初に現れる関連ドキュメントの順位の逆数の平均を測定します。KにおけるNDCG(Normalized Discounted Cumulative Gain)は、下位の順位よりも上位の順位を重く評価して、ランキング品質を測定します。
def compute_retrieval_metrics(results: list[str], relevant_ids: set, k: int = 5):
results_at_k = results[:k]
relevant_found = [r for r in results_at_k if r in relevant_ids]
# Hit rate
hit = 1 if relevant_found else 0
# MRR
rr = 0
for i, doc_id in enumerate(results_at_k, start=1):
if doc_id in relevant_ids:
rr = 1.0 / i
break
# NDCG (binary relevance)
import math
dcg = sum(
1.0 / math.log2(i + 1)
for i, doc_id in enumerate(results_at_k, start=1)
if doc_id in relevant_ids
)
ideal = sum(1.0 / math.log2(i + 1) for i in range(1, min(len(relevant_ids), k) + 1))
ndcg = dcg / ideal if ideal > 0 else 0
return {'hit': hit, 'rr': rr, 'ndcg': ndcg}ベースライン:単一段階のDense Retrieval
再ランキングの影響を測定する前に、単一段階のDense Retrievalを使ってベースラインを設定します。テストセット内のすべてのクエリをbi-encoderのRetrieverで処理し、順位付けされたドキュメントIDを収集して、NDCG、MRR、ヒット率の平均を計算します。このベースラインによって、どの程度の改善余地があるかが分かります。たとえば、ベースラインがすでにNDCG@5で0.95なら、再ランキングによる改善の余地はほとんどありません。
def evaluate_pipeline(retriever_fn, test_set: list[dict], k: int = 5) -> dict:
all_metrics = []
for entry in test_set:
query = entry['query']
relevant = set(entry['relevant_doc_ids'])
results = retriever_fn(query, top_k=k)
result_ids = [r['id'] for r in results]
metrics = compute_retrieval_metrics(result_ids, relevant, k)
all_metrics.append(metrics)
n = len(all_metrics)
return {
f'hit_rate@{k}': sum(m['hit'] for m in all_metrics) / n,
f'mrr@{k}': sum(m['rr'] for m in all_metrics) / n,
f'ndcg@{k}': sum(m['ndcg'] for m in all_metrics) / n,
}変更前後のベンチマークを実行する
単一段階のRetrieverと、再ランキングを行う2段階のRetrieverの両方に対して、同じ評価関数を実行します。結果を横並びで出力すると、改善したかどうかがすぐに分かります。品質指標とともにクエリごとのレイテンシも記録してください。検索品質が5%向上しても、アプリケーションのSLA要件によっては、レイテンシが400ミリ秒増加することに見合わない場合があります。
import time
def evaluate_with_latency(retriever_fn, test_set, k=5):
metrics_list = []
latencies = []
for entry in test_set:
t0 = time.perf_counter()
results = retriever_fn(entry['query'], top_k=k)
latencies.append((time.perf_counter() - t0) * 1000)
result_ids = [r['id'] for r in results]
metrics_list.append(compute_retrieval_metrics(
result_ids, set(entry['relevant_doc_ids']), k
))
n = len(metrics_list)
return {
f'hit_rate@{k}': sum(m['hit'] for m in metrics_list) / n,
f'ndcg@{k}': sum(m['ndcg'] for m in metrics_list) / n,
'p50_latency_ms': sorted(latencies)[n // 2],
'p99_latency_ms': sorted(latencies)[int(n * 0.99)],
}
baseline = evaluate_with_latency(dense_retrieval_fn, golden_test_set)
two_stage = evaluate_with_latency(two_stage_fn, golden_test_set)
print('Baseline:', baseline)
print('Two-stage:', two_stage)NDCGの改善を解釈する
Dense Retrievalにcross-encoderによる再ランキングを追加した場合、一般的な改善幅はNDCG@5で絶対値0.05〜0.15で、およそ5〜15パーセントポイントに相当します。改善幅が大きくなるのは、次のような場合です。(1) クエリが多様で、多くの言い換えを含む場合、(2) コーパスに関連性の近いチャンクが多数ある場合、(3) 第1段階のRetrieverが弱い場合です。NDCGの改善が0.02未満であれば、追加される複雑さに見合うメリットがない可能性があります。
# Interpreting benchmark results
example_results = {
'baseline': {'hit_rate@5': 0.78, 'ndcg@5': 0.64, 'p99_latency_ms': 35},
'two_stage': {'hit_rate@5': 0.89, 'ndcg@5': 0.77, 'p99_latency_ms': 287},
}
delta_ndcg = example_results['two_stage']['ndcg@5'] - example_results['baseline']['ndcg@5']
delta_latency = example_results['two_stage']['p99_latency_ms'] - example_results['baseline']['p99_latency_ms']
print(f'NDCG improvement: +{delta_ndcg:.2f} (+{delta_ndcg/example_results["baseline"]["ndcg@5"]*100:.0f}%)')
print(f'Latency increase: +{delta_latency}ms')
# NDCG improvement: +0.13 (+20%) — clearly worth the 252ms latency costエンドツーエンドの回答品質を測定する
検索指標は適切なドキュメントが取得されたかを測定しますが、最終的な評価基準はエンドツーエンドの回答品質です。LLMを評価者として使用し、再ランキングされたコンテキストから生成した回答が、単一段階のコンテキストから生成した回答よりも正確で忠実かどうかを評価します。正確性、忠実性、関連性を1〜5の尺度で採点し、テストセット全体の平均を計算します。
from openai import OpenAI
client = OpenAI()
JUDGE_PROMPT = '''
Rate the following answer on a scale of 1-5 for correctness and faithfulness to the context.
Question: {question}
Context: {context}
Answer: {answer}
Ground truth: {ground_truth}
Return a JSON with fields: {"correctness": int, "faithfulness": int, "explanation": str}
'''
def judge_answer(question, context, answer, ground_truth):
prompt = JUDGE_PROMPT.format(
question=question, context=context,
answer=answer, ground_truth=ground_truth,
)
response = client.chat.completions.create(
model='gpt-4o',
messages=[{'role': 'user', 'content': prompt}],
response_format={'type': 'json_object'},
)
import json
return json.loads(response.choices[0].message.content)クエリタイプ別の層別分析
平均指標では、クエリタイプごとの重要な違いが隠れてしまいます。テストセットを、事実検索(誰、何、いつ)、手順に関するクエリ(どのように行うか)、概念に関するクエリ(なぜ、説明)、技術的なクエリ(エラーコード、API名)に分類し、グループごとに指標を計算します。再ランキングは、キーワードマッチングよりも意味理解が重要な概念クエリや手順クエリで、特に効果を発揮することが多くあります。
def stratified_eval(retriever_fn, test_set, k=5):
groups = {'factual': [], 'procedural': [], 'conceptual': [], 'technical': []}
for entry in test_set:
q = entry['query'].lower()
if any(w in q for w in ['how to', 'how do', 'steps to']):
groups['procedural'].append(entry)
elif any(w in q for w in ['why', 'explain', 'what is the reason']):
groups['conceptual'].append(entry)
elif any(c.isupper() for c in q.split()) or 'error' in q:
groups['technical'].append(entry)
else:
groups['factual'].append(entry)
for group_name, group_entries in groups.items():
if group_entries:
metrics = evaluate_pipeline(retriever_fn, group_entries, k)
print(f'{group_name} ({len(group_entries)} queries): ndcg@{k}={metrics[f"ndcg@{k}"]:.3f}')CI統合によるリグレッションテスト
検索ベンチマークをCIのリグレッションテストとして実行します。NDCG@5、MRR、ヒット率について、許容できる最低限のしきい値を設定します。パイプラインの変更によって指標がしきい値を下回るとCIビルドが失敗し、検索品質のリグレッションが本番環境に反映されるのを防げます。これは、チャンクサイズ、埋め込みモデル、再ランキングモデルを変更した後に特に重要です。
# pytest integration for retrieval quality gates
import pytest
MIN_NDCG_5 = 0.70
MIN_HIT_RATE_5 = 0.85
def test_retrieval_quality_meets_threshold():
metrics = evaluate_pipeline(production_retriever_fn, golden_test_set, k=5)
assert metrics['ndcg@5'] >= MIN_NDCG_5, (
f'NDCG@5 {metrics["ndcg@5"]:.3f} below threshold {MIN_NDCG_5}'
)
assert metrics['hit_rate@5'] >= MIN_HIT_RATE_5, (
f'Hit rate {metrics["hit_rate@5"]:.3f} below threshold {MIN_HIT_RATE_5}'
)
# Run with: pytest tests/test_retrieval.py -v検索指標を可視化する
複数の実験結果を生の数値だけで比較するのは困難です。ベースライン、ハイブリッドのみ、ハイブリッド+再ランキングの各パイプラインについて、NDCG、MRR、ヒット率、レイテンシを横並びで示すシンプルな比較表または棒グラフを作成します。改善を行うたびにこれらの指標を追跡すると、検索改善の履歴が蓄積され、今後の最適化の判断に役立ちます。
def print_comparison_table(results: dict[str, dict]):
headers = ['Pipeline', 'NDCG@5', 'MRR@5', 'Hit@5', 'P99 ms']
print('|'.join(f'{h:20}' for h in headers))
print('-' * (len(headers) * 21))
for pipeline_name, metrics in results.items():
row = [
pipeline_name,
f'{metrics.get("ndcg@5", 0):.3f}',
f'{metrics.get("mrr@5", 0):.3f}',
f'{metrics.get("hit_rate@5", 0):.3f}',
f'{metrics.get("p99_latency_ms", 0):.0f}',
]
print('|'.join(f'{v:20}' for v in row))
results = {
'Dense only': {'ndcg@5': 0.64, 'mrr@5': 0.68, 'hit_rate@5': 0.78, 'p99_latency_ms': 35},
'Hybrid RRF': {'ndcg@5': 0.71, 'mrr@5': 0.74, 'hit_rate@5': 0.84, 'p99_latency_ms': 55},
'Hybrid + Rerank': {'ndcg@5': 0.77, 'mrr@5': 0.81, 'hit_rate@5': 0.89, 'p99_latency_ms': 287},
}
print_comparison_table(results)ベンチマーク結果を活用する
ベンチマークを実行した後は、結果に基づいて具体的な判断を行います。再ランキングによるNDCGの改善が0.03未満なら、再ランキングを使わず、第1段階の改善に注力します。ヒット率が低い場合は、第1段階で関連ドキュメントを取りこぼしているため、候補セットのサイズを増やすか、ハイブリッド検索に切り替えます。検索の改善が控えめでもエンドツーエンドの回答品質が大きく向上するなら、再ランキングによって、順位が変わらなくてもLLMが効果的に利用できる非常に関連性の高い文が上位に表示されている可能性があります。
クイックチェック
このレッスンで学んだ検索と再ランキングの影響の測定について、理解度を確認しましょう。
レッスンのまとめ
このレッスンでは、検索品質を測定する前に、関連するドキュメントが既知のゴールデンテストセットを構築することが不可欠であること、NDCG、MRR、ヒット率がランキング品質を総合的に測定する3つの主要な検索指標であること、そしてLLMを評価者として用いるエンドツーエンドの回答品質が、パイプライン改善の最終的な評価基準になることを学びました。検索ベンチマークは必ずCIのリグレッションテストとして実行してください。次は、生成中のトークンを表示するLLMストリーミングについて学びます。
よくある質問
「再ランキングの効果を測定する」レッスンは無料ですか?
はい。「再ランキングの効果を測定する」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。
「再ランキングの効果を測定する」で何を学びますか?
単一段階検索と再ランキングを行う2段階検索を前後で比較するベンチマークを実施し、NDCG、MRR、エンドツーエンドの回答品質を測定します。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
AI Engineering Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。
「再ランキングの効果を測定する」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このAI Engineering Academyレッスンでコードを書いて実行できますか?
はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。