- AI ⚙️
RAGの評価|検索をRecall@k・MRR・nDCGで測り、件数のぶれを実測
結論
RAG の検索部分は、「質問」と「その質問の正解の文書(と関連度)」を組にした評価セットを作り、検索結果の上位 k 件に対して Recall@k・MRR・nDCG などを計算して評価します。RJC の技術記事 3,193 節・1,856 問で測ると、50 問の評価セットでは MRR の 95%信頼区間の幅が約 0.2 あり、差が 0.01 程度の改善は数百問ないと見分けられませんでした。
この記事でわかること
- Hit Rate・Recall@k・Precision@k・MRR・MAP・nDCG の定義と、手計算・自作の Python・ir_measures の3つが一致する実例
- 評価セット(質問・正解の文書・関連度)の作り方と、正解ラベルの付け方で検索方式の順位が入れ替わる実例
- 評価セットの件数ごとの信頼区間(ブートストラップ)と、対応のある比較で「本当に良くなったか」を判断する手順
この記事で実際に動かしたもの:RJC の技術記事 247 本を見出し(H2)ごとに分けた 3,193 節をコーパスにし、各記事の冒頭の早見表から 1,856 問の評価セットを作って、文字 n-gram の BM25・TF-IDF・LSA の6つの検索設定を、自作の評価関数と ir_measures(pytrec_eval)で評価し、ブートストラップと並べ替え検定でぶれを測りました。
RAG の回答が外れたとき、原因は「正しい資料を探せなかった(検索)」か「資料はあったのに答えを作り損ねた(生成)」のどちらかです。検索の評価は、LLM を呼ばずに、正解の文書の ID と検索結果の ID を突き合わせるだけで計算できます。速く、毎回同じ値が出るので、検索の設定を変えるたびに回せます。
flowchart TD
A[文書を分割して索引を作る] --> B[評価セット<br>質問・正解の文書・関連度]
B --> C["検索を実行<br>質問ごとに上位k件"]
C --> D["指標を計算<br>Recall@k・MRR・nDCG"]
D --> E[ぶれを見る<br>信頼区間・対応のある比較]
E -->|設定を変える| A
E -->|外れた質問を読む| B
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| RAG の評価は何から始めるか | 生成と切り分け、まず検索だけを正解の文書 ID で評価する | RAGの検索評価とは |
| RAG の検索の評価指標 | Hit Rate@k・Recall@k・MRR・nDCG@k の4つで足りることが多い | 評価指標の一覧 |
| Recall@k・MRR・nDCG の計算方法 | 上位 k 件に正解が何割入るか・最初の正解の順位の逆数・順位で割り引いた関連度の和を理想で割る | 手計算とPythonで一致させる |
| RAG の評価データセットの作り方 | 質問・正解の文書 ID・関連度(0〜2)を JSONL か TREC 形式で持つ | 評価データセットの作り方 |
| 評価セットは何問必要か | 50問で MRR の信頼区間の幅が約0.2。0.01〜0.02 の差を見るなら数百問以上 | 件数でどれだけぶれるか |
| 改善したかどうかの判断 | 同じ質問での差を、ブートストラップの信頼区間か並べ替え検定で見る | 本当に良くなったかを判断する |
| k はいくつにするか | LLM に渡す件数に合わせる。リランキングの前段は大きめの k の Recall を見る | kの決め方 |
| LLM で評価する方法(LLM-as-a-judge・Ragas) | 正解ラベルが無いときの代わり。人のラベルと突き合わせてから使う | LLMで評価する方法 |
確認環境:Ubuntu 24.04.5 LTS(CPU 2コア・メモリ 8GB、ほかの作業と同時に動いている共有のマシン)で、Python 3.13.18・numpy 2.5.3(OpenBLAS のスレッド数1)・scipy 1.18.1・scikit-learn 1.9.1 を使い、作業用の venv に ir_measures 0.4.3(pytrec_eval-terrier 0.5.11)を入れました。コーパスは RJC の技術記事 247 本の本文(HTML のタグとコードブロックを除き、よくある質問・まとめ・参考の節を除いた H2 単位の 3,193 節、1節の中央値 1,021 文字)を読み取り専用で使いました。学習済みの埋め込みモデルには接続できないため、密ベクトルの検索は TF-IDF と SVD で作った LSA を「埋め込みの代わり」に使っています。実際の埋め込みモデルを使うと、この記事の指標の値は変わります。この記事で示すのは、指標の計算方法と、評価セットの作り方・件数で値がどれだけ動くかです。Ragas と LLM-as-a-judge は Ragas の公式ドキュメントの範囲で書き、LLM は呼んでいません。検定の考え方は Smucker ほか(CIKM 2007)、件数の影響は Voorhees と Buckley(SIGIR 2002)、nDCG は Järvelin と Kekäläinen(ACM TOIS 2002)で確かめました。図は mermaid 11 で描画を確かめています(確認日 2026年11月4日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「RAGの評価|検索をRecall@k・MRR・nDCGで測り、件数のぶれを実測」
RAGの検索評価とは(生成の評価と分ける理由)
この節では、RAG の評価をなぜ検索と生成に分けるのか、検索の評価で何を測るのかが分かります。
RAG の検索評価は、「質問に答えるのに必要な文書(チャンク)を、検索が上位に持ってこられたか」を数字にすることです。正解の文書を先に決めておき、検索結果の並びと照らし合わせます。
検索の評価
入力は質問・検索結果の ID の並び・正解の ID
LLM を呼ばないので速く、毎回同じ値になる
分割・索引・検索方式・k の変更をすぐ比べられる
生成の評価
入力は質問・渡した資料・回答・参考の答え
文章の良し悪しを測るので、人か LLM の判定が要る
判定のぶれと費用がある
検索で正解が上位に来ていなければ、生成をいくら直しても回答は良くなりません。逆に、検索の指標が高いのに回答が悪ければ、プロンプトや資料の渡し方を疑えます。切り分けの順番として、まず検索を測ります。回答文そのものの評価(完全一致・ルーブリック・モデルによる採点)は関連記事「LLMの評価方法と評価指標一覧|テストケース表のテンプレート付き」で扱っています。この記事は、その前段の検索だけを扱います。
検索の評価は、情報検索(IR)の分野で長く使われてきた方法がそのまま使えます。TREC などの評価会議では、質問(トピック)・文書集合・関連度の判定(qrels)の3つをそろえ、検索システムの出力(run)を指標で比べてきました。RAG の検索も、この3つを自分のデータで用意すれば同じ枠組みで測れます。
RAG検索の評価指標の一覧(Hit Rate・Recall@k・MRR・nDCG)
この節では、RAG の検索でよく使う6つの指標の意味と、どの指標が何に強いかが分かります。
以下、k は「上位何件まで見るか」、関連文書は「正解として登録した文書」です。どの指標も、質問ごとに計算してから全質問で平均します。
| 指標 | 質問1つでの計算 | 順位を見るか | 向いている問い |
|---|---|---|---|
| Hit Rate@k(Success@k) | 上位 k 件に関連文書が1つでもあれば1、無ければ0 | 見ない | LLM に渡す k 件に答えが入るか |
| Recall@k | 上位 k 件に入った関連文書の数 ÷ 関連文書の総数 | 見ない | 必要な資料を取りこぼしていないか |
| Precision@k | 上位 k 件に入った関連文書の数 ÷ k | 見ない | 渡す資料にノイズがどれだけ混じるか |
| MRR@k(RR の平均) | 最初の関連文書の順位 r の逆数 1/r。k 件に無ければ0 | 最初の1件だけ | 正解が1つの質問で、どれだけ上に来るか |
| MAP@k(AP の平均) | 関連文書が出るたびの Precision の和 ÷ 関連文書の総数 | 全部の関連文書 | 正解が複数あり、全部を上に集めたいか |
| nDCG@k | 関連度を順位で割り引いて足した DCG ÷ 理想の並びの DCG | 全部・関連度の段階つき | 「とても関連」と「少し関連」を区別したいか |
正解の文書が質問ごとに1つだけなら、Recall@k は0か1になり Hit Rate@k と同じ値になります。MAP も MRR と同じ値になります。RAG の評価セットは正解が1つの形で作ることが多いため、実際に区別が要るのは Hit Rate@k(=Recall@k)・MRR・nDCG の3つになりがちです。
1Recall@k は「渡せたか」
LLM に上位5件を渡すなら Recall@5。順位は見ないので、1位でも5位でも同じ1点になる。
2MRR は「どれだけ上か」
1位なら1、2位なら0.5、3位なら0.333。上位の差に敏感で、下位の差はほとんど値に出ない。
3nDCG は「段階つきの並び」
関連度を0・1・2のように段階で付けたときに使う。理想の並びで割るので0〜1になる。
nDCG の DCG は、順位 i の文書の利得(gain)を log2(i+1) で割って足したものです(Järvelin と Kekäläinen が提案)。利得には関連度そのもの(線形)を使う流儀と、2 の関連度乗から1を引いた値(指数)を使う流儀があります。pytrec_eval(trec_eval と同じ計算)は線形で、ir_measures では gains で利得の対応を指定できます。論文やツールと比べるときは、どちらの利得かを必ずそろえます。
Recall@k・MRR・nDCGを手計算とPythonで一致させる
この節では、小さな例で各指標を手で計算し、自作の Python と ir_measures が同じ値を出すことが分かります。
例として、ある質問の検索結果が d3, d7, d1, d9, d4 の順で返り、正解(qrels)が d1(関連度2)・d4(関連度1)・d8(関連度1)だったとします。d8 は検索結果に出ていません。
Hit Rate@5 と Recall@5
上位5件に d1 と d4 が入るので Hit は1。関連文書は3つなので Recall@5 は 2÷3=0.6667。Recall@3 は d1 だけなので 1÷3=0.3333。
MRR@5
最初の関連文書 d1 は3位なので 1÷3=0.3333。d4 が5位にあることは値に影響しない。
AP@5
関連文書が出た3位で Precision は 1÷3、5位で 2÷5。和 0.7333 を関連文書の総数3で割って 0.2444。
nDCG@5(線形の利得)
DCG は 2÷log2(4)+1÷log2(6)=1+0.3869=1.3869。理想の並び(2,1,1)の DCG は 2+1÷log2(3)+1÷log2(4)=3.1309。比は 0.4430。
同じ計算を Python で書いたものです。どの関数も、検索結果の ID のリストと、ID から関連度への辞書を受け取ります。
import math
def hit_at_k(run, qrels, k):
return 1.0 if any(qrels.get(d, 0) > 0 for d in run[:k]) else 0.0
def recall_at_k(run, qrels, k):
rel = [d for d in qrels if qrels[d] > 0]
return sum(1 for d in run[:k] if qrels.get(d, 0) > 0) / len(rel)
def rr_at_k(run, qrels, k):
for i, d in enumerate(run[:k], 1):
if qrels.get(d, 0) > 0:
return 1.0 / i
return 0.0
def ap_at_k(run, qrels, k):
nrel = sum(1 for g in qrels.values() if g > 0)
hits = s = 0
for i, d in enumerate(run[:k], 1):
if qrels.get(d, 0) > 0:
hits += 1
s += hits / i
return s / nrel
def dcg(gains):
return sum(g / math.log2(i + 1) for i, g in enumerate(gains, 1))
def ndcg_at_k(run, qrels, k, exp=False):
f = (lambda g: 2 ** g - 1) if exp else (lambda g: g)
gains = [f(qrels.get(d, 0)) for d in run[:k]]
ideal = sorted((f(g) for g in qrels.values()), reverse=True)[:k] # 理想は qrels 全体から
return dcg(gains) / dcg(ideal) if ideal and dcg(ideal) > 0 else 0.0
実行結果と、ir_measures(中で pytrec_eval を使う)で同じ例を計算した結果です。
k=1 Hit=0.0000 Recall=0.0000 P=0.0000 RR=0.0000 AP=0.0000 nDCG=0.0000 nDCG(exp)=0.0000
k=3 Hit=1.0000 Recall=0.3333 P=0.3333 RR=0.3333 AP=0.1111 nDCG=0.3194 nDCG(exp)=0.3631
k=5 Hit=1.0000 Recall=0.6667 P=0.4000 RR=0.3333 AP=0.2444 nDCG=0.4430 nDCG(exp)=0.4568
# ir_measures 0.4.3
P@5 0.4
nDCG(gains={2:3})@5 0.4568
Success@5 1.0
RR@5 0.3333
AP@5 0.2444
R@5 0.6667
nDCG@5 0.443
手計算・自作・ir_measures の3つが一致しました。指数の利得(関連度2を利得3として扱う)にすると nDCG@5 は 0.4430 から 0.4568 に変わります。さらに、後で使う 1,856 問・上位100件の評価全体でも、自作の関数と ir_measures の nDCG@10・RR@10・R@10・Success@1・AP@100 が小数4桁まで一致することを確かめました(ir_measures の計算時間は 1.42 秒)。
{'RR(rel=2)@10': 0.709, 'AP(rel=2)@100': 0.7117, 'Success(rel=2)@1': 0.6088, 'R(rel=2)@10': 0.8971, 'nDCG@10': 0.6235} 1.42 s
self nDCG@10 0.6235 RR 0.709 R@10 0.8971 S@1 0.6088 AP@100 0.7117
自作の評価関数を使う場合は、このように一度、定評のある実装と同じデータで突き合わせておくと、後で値がおかしいときに「計算の誤り」を候補から外せます。
評価指標の実装でよくある間違い(値がどれだけ変わるかの実測)
この節では、自作の評価関数で起きやすい6つの間違いと、それぞれで値がどれだけ変わるかが分かります。
次の節以降で使う評価セット(1,856問)と BM25 の検索結果で、正しい計算と間違った計算を並べました。
| 間違い | 正しい値 | 間違えた値 | 起きること |
|---|---|---|---|
| nDCG の理想の並びを、検索結果の中だけで作る | 0.6235 | 0.8379 | 取りこぼした関連文書が分母から消え、大きく見える |
| Recall@k の分母を min(k, 関連文書数) にする | 0.4197 | 0.5423 | 関連文書が k より多いと別の指標になる(記事単位の正解で測定) |
| 同じ文書(同じ記事の重複チャンク)を何度も数える | 0.5423 | 0.6422 | Precision@10 が水増しされる(正解を3回重ねた場合) |
| 正解が無い質問を0点として平均に入れる | 0.7090 | 0.6473 | ラベル付けに失敗した177問が、検索の失敗に見える |
| MRR のカットオフを書かない | MRR@10 0.7090 | MRR@100 0.7117 | 差は小さいが、比べる相手と k がそろわない |
| 質問 ID の型・表記が qrels と run でずれる | ― | 0.0(エラーにならない) | ir_measures は一致しない ID を黙って0点にする |
nDCG の正規化
手元では 0.6235 が 0.8379 に見えた。取りこぼしが多い検索ほど水増しが大きい
重複の問題は RAG で特に起きやすいところです。1つの記事を複数のチャンクに分けると、同じ記事の別チャンクが上位を占めます。「文書単位で正解を決めたのか、チャンク単位で決めたのか」をそろえ、文書単位で評価するなら検索結果も文書 ID で重複を除いてから数えます。チャンクの切り方そのものの比較は、公開予定の関連記事「RAG のチャンク分割の決め方を実測する」で扱います。
RAGの評価データセットの作り方(質問・正解文書・関連度)
この節では、RAG の検索の評価データセットに何を入れ、どこから質問を集め、どう正解を付けるかが分かります。
評価データセットは、情報検索の言い方では「質問(queries)」と「関連度の判定(qrels)」の組です。最低限、次の4つを持ちます。
質問を集める
いちばん良いのは実際の問い合わせやチャットのログ。無ければ FAQ・目次・見出しから作る。LLM で生成する方法もある(後の節)。どの方法でも、出どころを列に残す。
正解の単位を決める
文書単位か、チャンク単位か、節単位か。チャンクの切り方を後で変える予定なら、チャンク ID ではなく「文書 ID+位置」や「答えの文字列」で持つと付け直しが少ない。
関連度の段階を決める
2=答えがそのまま書いてある、1=話題は同じだが答えは無い、0=無関係、のように文章で基準を書いてから付ける。段階が無いなら1と0だけでよい。
外れたものを読み、付け直す
検索の上位に出たのに正解でない文書を読み、本当は関連していないかを確かめる。ラベルの抜けを足していくと、評価セットが育つ。
持ち方は、1行1問の JSONL か、TREC 形式(「質問ID 0 文書ID 関連度」の空白区切り)が扱いやすいです。TREC 形式は ir_measures・pytrec_eval・trec_eval がそのまま読めます。今回作った評価セットの実物は次のとおりです。
{"qid": "q308", "query": "RDPのポート番号", "relevant": {"t128#2": 2}, "same_article": 12, "source": "早見表:ポート番号3389", "match": "contains"}
{"qid": "q1097", "query": "Bcc が相手に見えない理由", "relevant": {"t223#7": 2}, "same_article": 12, "source": "早見表:To・Cc・Bcc", "match": "contains"}
{"qid": "q1693", "query": "You may need an appropriate loader", "relevant": {"t77#11": 2}, "same_article": 12, "source": "早見表:よくあるエラー", "match": "contains"}
# TREC 形式の qrels(q308 の先頭4行)
q308 0 t128#0 1
q308 0 t128#1 1
q308 0 t128#2 2
q308 0 t128#3 1
正解のラベルを付けきれない、という問題は避けられません。TREC では、複数のシステムの上位を集めて(プーリング)その範囲だけを人が判定し、判定していない文書は無関係として扱う方法がとられてきました。RAG の評価セットでも、判定していない文書は「無関係」ではなく「分からない」と意識しておくと、後で見る「別の記事が1位に来た」ケースの読み方を誤りません。
RJCの技術記事で評価セットを作った手順と中身
この節では、今回の評価セットをどう作ったか、自動でラベルを付けたときにどこで失敗したかが分かります。
RJC の技術記事は、冒頭に「知りたいこと/答え/詳しい節」の早見表があります。「知りたいこと」を質問に、「詳しい節」が指す H2 の節を正解(関連度2)にしました。書き手自身が「この質問の答えはこの節」と書いた対応なので、ラベルの根拠がはっきりしています。同じ記事のほかの節は関連度1(話題は同じ)、ほかの記事の節は0としました。
「詳しい節」の文字列は、実際の見出しと完全には一致しません(例:「ポート番号3389」→「RDPのポート番号3389(TCPとUDP)と ss での確かめ方」)。そこで、見出しに文字列が含まれるかを先に見て、無ければ文字列の類似度(difflib の比率0.5以上)で一番近い見出しを選びました。
| 対応の取り方 | 件数 | 例 |
|---|---|---|
| 見出しに1つだけ含まれる | 1,559 | 「Cargoとは」→「Cargoとは(Rustのビルドツール・パッケージマネージャー)」 |
| 見出しに複数含まれる | 20 | 「回帰分析とは」が「重回帰分析とは(…)」にも含まれ、誤った節が選ばれた例あり |
| 類似度0.5以上で最も近い見出し | 277 | 「Test-NetConnectionと確認サイト」→「WindowsのTest-NetConnectionとポート開放確認サイト」 |
| 対応が取れない(除外) | 177 | 「料金と比較」に近い見出しが無い、など |
自動で付けたラベルが正しいかを、無作為に選んだ40問で人が読んで確かめました。38問は妥当で、2問に問題がありました。1つは「モーダルウィンドウとは」に「ダイアログ・ポップアップとの違い」の節が付いていたもので、「とは」の節の方が答えに近い可能性があります。もう1つは「HTTP リクエストとは」で、詳しい節が「/」で2つ書かれていたのに1つしか正解にしていませんでした。全体を調べると「/」で複数の節を指す質問が10問あり、2つ目以降も正解に足すと、その10問の MRR@10 は 0.268 から 0.448 に上がりました。ラベルの抜けは、検索の失敗として数字に出ます。
この評価セットには偏りもあります。質問を書いたのが記事の書き手なので、質問と正解の節が同じ言葉を使いやすく、語の一致で探す BM25 に有利です。実際の利用者の質問は、もっと短く、言い換えが多くなります。今回の数値は「この評価セットでの値」として読み、方式の優劣の一般論にはしません。
6つの検索設定を評価した結果(Recall@k・MRR・nDCG)
この節では、同じ評価セットで6つの検索設定を測った結果と、指標ごとに順位がどう違うかが分かります。
検索は文字 n-gram で作りました。日本語の分かち書きを使わず、正規化(NFKC・小文字化)した文字列の2文字・3文字の並びを語として扱います。BM25 は自作(k1=1.2、b=0.75)、TF-IDF は scikit-learn の TfidfVectorizer(sublinear_tf)のコサイン類似度、LSA は TF-IDF(2文字、3万語)を TruncatedSVD で128次元・256次元にしたベクトルのコサイン類似度です。どの設定も上位100件を返します。検索方式の組み合わせ(BM25 とベクトル検索の融合)は、公開予定の関連記事「ハイブリッド検索(BM25+ベクトル検索)と RRF で RAG の検索精度を上げる」で扱います。この記事では、評価の仕方を見るための材料として使います。
== strict(正解は詳しい節1つだけ)
bm25_2 Hit@1=0.5884 Hit@5=0.8475 Recall@10=0.8917 MRR@10=0.6954 nDCG@10=0.7435
bm25_23 Hit@1=0.6088 Hit@5=0.8432 Recall@10=0.8971 MRR@10=0.7090 nDCG@10=0.7548
bm25_2_b03 Hit@1=0.5900 Hit@5=0.8443 Recall@10=0.8928 MRR@10=0.6965 nDCG@10=0.7445
tfidf_23 Hit@1=0.5145 Hit@5=0.8055 Recall@10=0.8831 MRR@10=0.6362 nDCG@10=0.6960
lsa_128 Hit@1=0.2538 Hit@5=0.5943 Recall@10=0.7478 MRR@10=0.3934 nDCG@10=0.4773
lsa_256 Hit@1=0.3405 Hit@5=0.6934 Recall@10=0.8173 MRR@10=0.4853 nDCG@10=0.5647
| 設定 | Hit@1 | Hit@5 | MRR@10 | 索引+全問の検索 |
|---|---|---|---|---|
| BM25(2文字) | 0.588 | 0.848 | 0.695 | 4.7秒 |
| BM25(2〜3文字) | 0.609 | 0.843 | 0.709 | 20.9秒 |
| BM25(2文字、b=0.3) | 0.590 | 0.844 | 0.697 | 6.9秒 |
| TF-IDF(2〜3文字) | 0.515 | 0.806 | 0.636 | 18.7秒 |
| LSA 256次元 | 0.341 | 0.693 | 0.485 | 20.7秒 |
読み取れることは3つです。
- 指標によって1位が変わります。MRR@10 と Hit@1 では BM25(2〜3文字)が1位ですが、Hit@5 では BM25(2文字)がわずかに上です。3文字を足すと正解が1位に上がる質問が増える一方、5位以内に入る質問はほとんど増えていません。
- LSA は次元を増やすと大きく伸びます(MRR@10 が 0.393 から 0.485)。ただし、ここでの LSA は学習済みの埋め込みの代わりで、実際の埋め込みモデルの値は別物です。
- MRR の値の中身は、BM25(2文字)で「1位が1,092問、2位が242問、10位以内に無いものが201問」でした。平均値の裏にある分布を見ると、改善の余地が「1位にできていない」のか「そもそも見つからない」のかが分かります。
正解ラベルの付け方の揺れで指標と順位が変わる
この節では、同じ検索結果でも、正解ラベルの付け方を変えるだけで指標の値と方式の順位が入れ替わることが分かります。
同じ検索結果を、3通りのラベルで評価しました。
| ラベルの付け方 | BM25(2〜3文字) | TF-IDF(2〜3文字) | LSA 256次元 |
|---|---|---|---|
| 節だけ正解(MRR@10) | 0.7090 | 0.6362 | 0.4853 |
| 記事のどの節でも正解(Hit@1) | 0.8109 | 0.8017 | 0.7247 |
| 段階つき 2/1/0(nDCG@10、線形) | 0.6235 | 0.6279 | 0.6007 |
| 段階つき 2/1/0(nDCG@10、指数) | 0.6436 | 0.6383 | 0.5952 |
「節だけ正解」では BM25 が TF-IDF より 0.07 高いのに、「同じ記事の節を関連度1」にした nDCG では TF-IDF が上になりました。TF-IDF は正解の節を1位にする力は弱いものの、同じ記事のほかの節を上位にまとめて並べる傾向があり、段階つきのラベルではそれが点になります。さらに、利得を線形から指数に変えると、関連度2の重みが増えて BM25 が再び上になります。LSA と TF-IDF の差も、ラベルの付け方で 0.15 から 0.03 まで縮みます。
どれが正しいというより、「RAG で何を正解と見なすか」を先に決めないと、方式の選択が評価の作り方に左右されるということです。LLM に渡したいのが「答えの書いてある節」なら節だけ正解の MRR や Recall@k、「同じ話題の資料が広く集まればよい」なら記事単位の Hit、両方を見たいなら段階つきの nDCG(利得の式を明記)を使います。
質問の書き方でも値は動きます。各質問の頭に記事の主題(記事タイトルの「|」より前)を付けて、同じ BM25(2〜3文字)で検索し直すと、次のようになりました。
example: 400 bad requestとは http error 400 とは
bm25_23 raw MRR@10=0.7090 R@10=0.8971
bm25_23 prefixed MRR@10=0.6414 R@10=0.9375
主題を足すと、正しい記事に届く質問が増えて Recall@10 は上がる一方、主題の語で同じ記事の「〇〇とは」の節が上に来やすくなり、MRR は下がりました。「置くか置かないか」「どちらを選ぶか」のように、記事の外に出すと何の話か決まらない質問が評価セットに混じっていたためです。BM25 の1位が別の記事だった351問から15問を読むと、6問はこの「主語の無い質問」で、3問は別の記事の1位も質問への答えとして妥当でした(例:「C ドライブへの移動」に対する Windows のディレクトリの節)。評価セットの質問は、記事や文脈から切り離しても意味が通る形に書き直しておきます。
評価セットの件数で指標はどれだけぶれるか(ブートストラップで実測)
この節では、評価セットの問題数を変えたときに、指標の値と信頼区間がどれだけ揺れるかが分かります。
指標の平均値は、どの質問を評価セットに選んだかで変わります。その揺れの大きさを見積もるのがブートストラップです。評価セットの質問を、同じ数だけ重複を許して引き直し、そのたびに平均を計算します。これを2,000回繰り返し、平均の分布の 2.5%点と 97.5%点を95%信頼区間とします。
import numpy as np
rng = np.random.default_rng(0)
def bootstrap_ci(per_query, B=2000):
x = np.asarray(per_query) # 質問ごとの値(RR や Recall@10)
n = len(x)
means = x[rng.integers(0, n, (B, n))].mean(axis=1)
return x.mean(), np.percentile(means, 2.5), np.percentile(means, 97.5)
1,856問から n 問を無作為に選び、その n 問だけでブートストラップした結果です(BM25 2〜3文字)。
n= 20 MRR=0.649 [0.471,0.808] w=0.337 | R@10=0.850 [0.700,1.000] w=0.300
n= 30 MRR=0.696 [0.549,0.836] w=0.288 | R@10=0.867 [0.733,0.967] w=0.233
n= 50 MRR=0.755 [0.650,0.856] w=0.206 | R@10=0.920 [0.840,0.980] w=0.140
n= 100 MRR=0.673 [0.592,0.753] w=0.160 | R@10=0.860 [0.790,0.920] w=0.130
n= 200 MRR=0.748 [0.697,0.796] w=0.099 | R@10=0.930 [0.895,0.965] w=0.070
n= 500 MRR=0.715 [0.680,0.747] w=0.066 | R@10=0.902 [0.878,0.928] w=0.050
n= 1856 MRR=0.709 [0.691,0.726] w=0.035 | R@10=0.897 [0.883,0.911] w=0.028
幅はおよそ問題数の平方根に反比例して縮みます。4倍の問題数で幅が半分です。別の見方として、1,856問から n 問の部分集合を1,000通り作り、それぞれの MRR を計算すると、全体の値 0.709 に対して次の範囲に散らばりました。
| 問題数 | 最小 | 5%点〜95%点 | 最大 |
|---|---|---|---|
| 30問 | 0.493 | 0.590〜0.815 | 0.905 |
| 50問 | 0.521 | 0.623〜0.798 | 0.850 |
| 100問 | 0.592 | 0.642〜0.765 | 0.819 |
| 300問 | 0.635 | 0.676〜0.743 | 0.770 |
50問の評価セットでは、同じ検索でも「0.62」とも「0.80」とも出ます。関連記事「RAGの作り方|Pythonで動かす最小構成と精度の上げ方」で扱っている20〜50問の質問セットは、大きな失敗を見つけて直す段階には向いています。設定の細かな差を比べる段階に入ったら、問題数を増やし、信頼区間を添えて報告します。
もう1つ注意があります。今回の質問は1記事から中央値で8問作っているため、同じ記事の質問どうしは似た結果になりやすく、互いに独立ではありません。質問ではなく記事単位で引き直す(クラスタブートストラップ)と、MRR の信頼区間は 0.6906〜0.7257 から 0.6892〜0.7282 へ少し広がりました。質問を同じ文書やテンプレートからまとめて作った場合は、まとまり単位で引き直す方が控えめな見積もりになります。
検索の改善が「本当に良くなったか」を判断する方法
この節では、2つの検索設定の差が偶然の揺れか本当の差かを、対応のある比較で判断する方法が分かります。
2つの設定は同じ質問で評価しているので、平均どうしを比べるより、質問ごとの差(新しい設定の値−前の設定の値)を見る方が揺れを小さくできます。その差の平均について、ブートストラップの信頼区間と、並べ替え検定(質問ごとに2つの設定の値を入れ替えてもよいと仮定して、差の符号を無作為に反転させる)の p 値を計算しました。Smucker ほか(CIKM 2007)は TREC のデータで5つの検定を比べ、並べ替え(randomization)検定・ブートストラップ・t 検定はよく一致し、Wilcoxon の符号付き順位検定と符号検定は使わない方がよいと結論しています。
def paired(x, y, B=2000, R=10000):
d = np.asarray(y) - np.asarray(x) # 質問ごとの差
n = len(d)
boot = d[rng.integers(0, n, (B, n))].mean(axis=1)
signs = rng.choice([-1, 1], (R, n))
perm = (signs * d).mean(axis=1)
p = (np.sum(np.abs(perm) >= abs(d.mean())) + 1) / (R + 1)
return d.mean(), np.percentile(boot, [2.5, 97.5]), p
MRR bm25_2->bm25_23: diff=+0.0136 CI95=[+0.0044,+0.0228] perm_p=0.0044 win/tie/loss=252/1398/206
MRR bm25_2->bm25_2_b03: diff=+0.0011 CI95=[-0.0054,+0.0083] perm_p=0.7480 win/tie/loss=175/1502/179
MRR tfidf_23->bm25_23: diff=+0.0728 CI95=[+0.0626,+0.0832] perm_p=0.0001 win/tie/loss=444/1279/133
MRR lsa_128->lsa_256: diff=+0.0919 CI95=[+0.0811,+0.1031] perm_p=0.0001 win/tie/loss=702/949/205
R@10 bm25_2->bm25_23: diff=+0.0054 CI95=[-0.0016,+0.0124] perm_p=0.1521 win/tie/loss=25/1816/15
nDCG(graded) tfidf_23->bm25_23: diff=-0.0044 CI95=[-0.0080,-0.0008] perm_p=0.0150 win/tie/loss=713/476/667
3文字の n-gram を足した改善(MRR +0.0136)は、1,856問なら信頼区間が0をまたがず、p 値も0.0044で、偶然とは言いにくい差です。一方、BM25 の b を 0.75 から 0.3 に変えた差(+0.0011)は、信頼区間が0をまたぎ、勝ち負けもほぼ同数で、差があるとは言えません。同じ「3文字を足す」でも、Recall@10 で見ると改善は信頼区間が0をまたぎます。改善したかどうかは、指標ごとに確かめます。
最後の行は注意が要る例です。段階つきの nDCG では、平均の差は TF-IDF の方が良い(BM25 が −0.0044)のに、勝ち負けの数では BM25 の方が多く勝っています(713勝667敗)。BM25 は小さく勝つ質問が多く、TF-IDF は大きく勝つ質問がある、という形です。平均と勝ち負けの向きが食い違うときは、大きく差が付いた質問を読むと理由が分かります。
では、問題数が少ないとどうなるか。BM25(2文字)と(2〜3文字)の比較を、無作為に選んだ n 問で1,000回行い、「本当は劣る2文字の方が良く見えた」割合を数えました。
| 問題数 | MRR+0.0136 の改善が逆に見えた割合 | MRR+0.0011(差なし)が改悪に見えた割合 |
|---|---|---|
| 30問 | 41.0% | 48.6% |
| 50問 | 34.4% | 48.6% |
| 100問 | 26.7% | 48.0% |
| 300問 | 8.5% | 43.4% |
| 1,000問 | 0.0% | 37.3% |
50問の評価セットで「3文字を足したら良くなった」と判断すると、3回に1回は逆の結論になります。差の無い変更は、何問にしても半分近くが「良くなった」か「悪くなった」に見えます(1,000問の行は母集団1,856問の半分以上を使うため、部分集合どうしが重なっています)。Voorhees と Buckley(SIGIR 2002)も、TREC の結果から、トピック数が少ないと別のトピック集合で結論が入れ替わる確率が想定より高いと報告しています。
RAGの検索評価の判断表
状況ごとに、どの指標とどの確かめ方を使うかをまとめます。
| 状況 | 主に見る指標 | 確かめ方 | この記事の実測 |
|---|---|---|---|
| 上位 k 件を LLM に渡す RAG | Recall@k(=Hit Rate@k) | k は渡す件数にそろえる | BM25 の Recall@5 は 0.84〜0.85 |
| 上位1〜2件しか使わない | MRR@10・Hit@1 | 1位の失敗の質問を読む | 1位は1,092問、圏外は201問 |
| 関連度を段階で付けた | nDCG@10(利得の式を明記) | 線形と指数の両方で順位が同じか | 線形と指数で1位が入れ替わった |
| リランキングの前段の検索 | Recall@50〜100 | 後段に渡す件数での取りこぼし | 6設定の Recall@100 は 0.93〜0.96 |
| 評価セットが50問以下 | どれでも | 大きな失敗探しに使い、小さな差の判断に使わない | MRR の幅が約0.2 |
| 設定の細かな差を比べる | 決めておいた1つの指標 | 対応のある比較の信頼区間・並べ替え検定 | +0.0136 は1,856問で有意、+0.0011 は差なし |
kの決め方と、LLMに渡す件数
この節では、Recall@k の k をどう決めるか、k を変えると Recall がどう伸びるかが分かります。
k は、検索の結果を実際にいくつ使うかに合わせます。LLM のプロンプトに上位5件を入れるなら Recall@5 が「LLM に答えが届いた割合」です。前段で100件を取り、リランキングで5件に絞る構成なら、前段は Recall@100、後段は Recall@5 を見ます。k を大きくするほど Recall は上がりますが、LLM に渡す文字数(費用と遅延)も増えます。
bm25_2 @1=0.588 @3=0.780 @5=0.848 @10=0.892 @20=0.917 @50=0.941 @100=0.955
bm25_23 @1=0.609 @3=0.787 @5=0.843 @10=0.897 @20=0.925 @50=0.946 @100=0.959
tfidf_23 @1=0.515 @3=0.726 @5=0.805 @10=0.883 @20=0.923 @50=0.945 @100=0.961
lsa_256 @1=0.341 @3=0.571 @5=0.693 @10=0.817 @20=0.891 @50=0.941 @100=0.962
k=1 では BM25 と LSA の差が 0.27 ありますが、k=100 ではほぼ同じ(0.955〜0.962)になり、LSA が最も高くなります。「どちらの検索が良いか」は k によって答えが変わります。前段で多めに拾い、後段で並べ直す構成なら、前段には Recall@100 の高い方式を選ぶ理由があります。逆に、上位数件をそのまま LLM に渡すなら Recall@5 で比べます。
手元の評価セットでは、BM25 は k=5 から 10 で約0.05、10 から 20 で 0.02〜0.03 しか伸びません。この伸び方を見ると、k を増やすより、10位以内に入らない約1割の質問(主語の無い質問・ラベルの抜け・語が一致しない言い換え)を読む方が改善につながりやすいと分かります。
LLMで評価する方法(LLM-as-a-judge・Ragas)
この節では、正解ラベルが無いときに LLM で検索を評価する方法と、Ragas の指標が今回の指標とどう対応するかが分かります。ここは Ragas の公式ドキュメントで確かめた範囲で、LLM は呼んでいません。
LLM-as-a-judge は、人の代わりに LLM に「この文書はこの質問に関連するか」「この資料で参考の答えを裏付けられるか」を判定させる方法です。正解の文書 ID が無くても、質問と参考の答えがあれば評価できます。代わりに、判定に費用と時間がかかり、プロンプトやモデルで結果が変わります。
| Ragas の指標(公式ドキュメント) | 必要な入力 | LLM | この記事の指標との関係 |
|---|---|---|---|
| IDBasedContextPrecision | 検索した ID と参考の ID | 使わない | ID の一致で関連を判定する Context Precision(下の式) |
| IDBasedContextRecall | 検索した ID と参考の ID | 使わない | Recall@k(k は渡した件数) |
| ContextPrecision | 質問・参考の答え・検索した文書 | 使う | 関連の判定を LLM が行う |
| ContextRecall | 質問・参考の答え・検索した文書 | 使う | 参考の答えの主張のうち、文書で裏付けられる割合 |
Ragas の Context Precision@K は、公式ドキュメントでは「各順位の Precision@k に、その順位が関連なら1を掛けて足し、上位 K 件の中の関連文書の数で割る」と定義されています。AP と似ていますが、分母が「上位 K 件に入った関連文書の数」で、取りこぼした関連文書を数えません。手計算の例(d3, d7, d1, d9, d4、関連は d1・d4・d8)で式どおりに計算すると、Context Precision@5 は (1/3+2/5)÷2=0.3667、AP@5 は 0.2444 です。名前が似ていても値の意味が違うので、比べるときは定義をそろえます。
Ragas には、文書から評価用の質問を作る TestsetGenerator もあります(公式ドキュメントでは LLM と埋め込みモデルを渡して生成する例)。LLM で作った質問は件数をそろえやすい一方、この記事で見た「主語の無い質問」や、文書の言葉をそのまま使った質問が混じります。生成した質問も、一部を人が読んで確かめてから使います。Ragas の使い方と4つの主要な指標のコードは、関連記事「RagasでRAGを評価する|指標の意味とPythonコード」で扱っています。
LLM の判定の使い方
今回の自動ラベルも、40問の人の確認で2問の問題が見つかった
よくあるエラー・つまずき(原文つき)
この節では、評価のコードを書いたときに出たエラーの原文と、黙って値が変わるつまずきの直し方が分かります。
| 症状・原文 | 原因 | 直し方 |
|---|---|---|
ZeroDivisionError: division by zero(Recall・AP の関数) | 関連文書が0件の質問がある | 関連文書の無い質問は評価から外し、外した数を報告に書く |
RuntimeWarning: Mean of empty slice、結果が nan | 絞り込んだ結果、0問の平均を取った | 平均の前に件数を出力する |
TypeError: Expected relevance to be integer. | ir_measures の Qrel に 1.5 などの小数の関連度を渡した | 関連度は整数(0・1・2)で持つ |
TypeError: Expected string as key. | 質問 ID を整数で渡した | 質問 ID・文書 ID は文字列にそろえる |
| エラーが出ず nDCG@10 が 0.0 | qrels の質問 ID と run の質問 ID が一致していない | 評価の前に、両方の ID の集合が同じかを確かめる |
ValueError: problem parsing measure | nDCG@ten など指標名の書式の誤り | nDCG@10 のように数字で書く |
| 同じ設定なのに前回と値が違う | 評価セットやコーパスが途中で変わった | 評価セット・コーパス・索引の版を結果と一緒に保存する |
| 新しい設定の方が良いはずなのに下がった | ラベルの抜け(本当は関連する文書が0扱い) | 上位に出た「不正解」を読み、関連なら qrels に足す |
よくある質問
この節では、RAG の検索の評価でよく出る疑問への答えが分かります。
Q. Recall@k と Hit Rate@k は同じものですか。
正解が質問ごとに1つなら同じ値です。正解が複数あると、Hit Rate は1つでも入れば1、Recall は入った割合になります。ir_measures では Hit Rate を Success@k と呼びます。
Q. MRR と nDCG はどちらを使えばよいですか。
正解が1つで、関連度の段階が無いなら MRR が向いています。関連度を0・1・2のように段階で付けたなら nDCG を使い、利得が線形か指数かを明記します。手元では利得の式を変えるだけで1位の方式が入れ替わりました。
Q. 評価セットは何問あればよいですか。
見たい差の大きさで決まります。手元では50問で MRR の信頼区間の幅が約0.2、200問で約0.1、1,856問で約0.035でした。大きな失敗を見つけるなら数十問、0.01〜0.02 の差を比べるなら数百〜千問以上が目安です。
Q. 正解ラベルは誰が付けるべきですか。
文書の内容が分かる人が、文章で書いた基準に沿って付けます。自動や LLM で付ける場合も、一部を人が読んで一致率を確かめます。今回の自動ラベルは40問中38問が妥当でした。
Q. 検索結果の上位に正解ではない文書が来たら、検索の失敗ですか。
そうとは限りません。判定していない文書は「無関係」ではなく「分からない」です。手元では、別の記事が1位だった15問のうち3問は、その文書も答えとして妥当でした。読んで関連なら qrels に足します。
Q. 埋め込みモデルを変えたとき、この記事の数値は使えますか。
使えません。この記事の密ベクトルは TF-IDF と SVD で作った LSA で、学習済みの埋め込みとは別物です。使えるのは評価の手順と、件数・ラベルで値がどれだけ動くかの目安です。埋め込みの基本は関連記事「エンベディングとは?ベクトル検索とコサイン類似度を計算例で解説」で扱っています。
Q. 統計の検定は t 検定でもよいですか。
Smucker ほかの比較では、平均の差については t 検定・ブートストラップ・並べ替え検定がよく一致しました。並べ替え検定はどの指標にも使えるので、迷ったら並べ替え検定にします。Wilcoxon と符号検定は勧められていません。
Q. 評価は CI で毎回回せますか。
検索の評価は LLM を呼ばないので回せます。手元では1,856問の nDCG@10 の計算が0.05秒、ir_measures で5指標が1.42秒、ブートストラップと並べ替え検定の一式が6.9秒でした。時間がかかるのは索引の作成と検索の方です。
Q. ベクトル検索の実装から試したいときは。
PostgreSQL で試すなら関連記事「pgvectorの使い方|PostgreSQLでベクトル検索をコピペで構築」で扱っています。作った検索の結果を、この記事の評価関数に ID の並びとして渡せば同じ方法で測れます。
まとめ
この節では、記事全体の要点が分かります。
- RAG の評価は、まず検索だけを正解の文書 ID で測る。Hit Rate@k(=正解1つの Recall@k)・MRR・nDCG で多くの場合は足り、LLM を呼ばずに毎回同じ値が出る。
- 指標の実装は、手計算と定評のある実装で突き合わせる。理想の並びを検索結果から作ると nDCG が 0.62 から 0.84 に水増しされるなど、誤りは黙って値を変える。
- 評価セットは質問・正解・関連度・出どころを持ち、文脈から切り離しても意味が通る質問にする。ラベルの抜けや主語の無い質問は、検索の失敗として数字に出る。
- ラベルの付け方・利得の式・k で、方式の順位は入れ替わる。何を正解と見なし、どの k を使うかを、比べる前に決める。
- 50問では MRR の信頼区間の幅が約0.2。改善の判断は、質問ごとの差の信頼区間と並べ替え検定で行い、0.01 程度の差には数百問以上を用意する。
参考・出典
この節では、この記事で根拠にした論文・公式ドキュメントが分かります(確認日 2026年11月4日)。
- Kalervo Järvelin, Jaana Kekäläinen「Cumulated gain-based evaluation of IR techniques」ACM Transactions on Information Systems 20(4), 2002:https://doi.org/10.1145/582415.582418
- Mark D. Smucker, James Allan, Ben Carterette「A Comparison of Statistical Significance Tests for Information Retrieval Evaluation」CIKM 2007:https://ciir-publications.cs.umass.edu/getpdf.php?id=744
- Ellen M. Voorhees, Chris Buckley「The effect of topic set size on retrieval experiment error」SIGIR 2002:https://www.nist.gov/node/707561
- ir_measures 公式ドキュメント「Measures」:https://ir-measur.es/en/latest/measures.html
- Ragas 公式ドキュメント「Context Precision」:https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/context_precision/
- Ragas 公式ドキュメント「Context Recall」:https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/context_recall/
- Ragas 公式ドキュメント「Testset Generation for RAG」:https://docs.ragas.io/en/stable/getstarted/rag_testset_generation/
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「RAGの評価|検索をRecall@k・MRR・nDCGで測り、件数のぶれを実測」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。