検索は当たっているのか ― RAG に「物差し」を作る: Golden セットと hit@k

検索は当たっているのか ― RAG に「物差し」を作る: Golden セットと hit@k AI

RAG を作ると、必ず「で、ちゃんと当たっているの?」と聞かれる。自分で数問試して「当たっている気がする」では、チャンクの大きさを変えたときに良くなったのか悪くなったのかが分からない。paperlake(前回まで)に、変更のたびに同じ質問で測れる物差しを作った話を書く。結論から言うと、40 問の Golden セットを作るのに約 $0.2、測るのは 1 回 20 秒で費用ほぼゼロ、そして最初に測ったら「直すべき場所」が思っていたのと違った。

物差しの形

「質問」と「その答えが書いてある資料とページ」の組を用意し、検索の上位 k 件にその場所が入っているかを数える。これだけである。指標は次の 3 つ。

  • hit@k: 上位 k 件に正解が入っている質問の割合(k = 1, 3, 5)
  • MRR: 正解が何位に出たかの逆数の平均。1 位なら 1、3 位なら 1/3、外れなら 0
  • 判定は「資料名が一致し、期待するページ(前後 1 ページまで)の本文と、返ってきたチャンクの内容が重なる」こと

Golden セットを作って測るまでの流れ

質問をどう作るか

40 問を手で書くのは面倒だし、書く人の癖が入る。そこで各資料(縦書きの小説 2 点、横書きの技術書 5 点)から本文の多いページを無作為に 3 つ選び、Bedrock の Claude にそのページを見せて「このページの内容だけで答えられる質問を 2 問。資料名やページ番号を含めず、利用者が実際に聞きそうな言い回しで。1 問は用語の定義、1 問は理由や手順を問うもの」と頼んだ。7 資料 × 3 ページ × 2 問 = 42 問、費用は約 $0.2。

生成した質問は目視で選別した。落としたのは「『防衛拠点』という表現はここでどういう意味か」「このスクリプトが返す終了コードは」のような、ページを見ていないと意味が定まらない 2 問。残り 40 問が Golden セットになった。質問と正解は資料の内容を含むので、リポジトリには入れず手元に置いている。

判定の落とし穴

最初の実装は「返ってきたチャンクに、期待ページの [p.N] マーカーが含まれているか」で判定した。OCR パイプラインが各ページの先頭に埋めているマーカーだ。ところがこれだと hit@3 が 0.69 で、外れの中に「資料は合っているのにページが違う」が目立った。

原因はチャンクの切れ方だった。索引のチャンクは 1,500 トークンの固定長で、ページの途中から始まることがある。そのチャンクには期待ページの本文が入っているのに、マーカーは前のチャンクに置いていかれている。つまり判定が厳しすぎて偽陰性を出していた。

判定を「チャンクの 24 文字の断片が、期待ページ(±1)の本文に含まれるか」という内容の重なりに変えたところ、hit@3 は 0.775 になった。索引は何も変えていない。物差し自体の欠陥で 8 ポイント損していたわけで、Golden セットは作って終わりではなく、最初の数回は物差しのほうを疑う必要がある。

基準値

条件 n hit@1 hit@3 hit@5 MRR
現状(階層チャンク 1500/300、Titan Text Embeddings v2、図・表あり) 40 0.425 0.775 0.850 0.594

上位 5 件に正解が入るのが 85%、1 位に来るのは 43%。Claude Code は複数のチャンクを読んで答えるので、体感の正答率は hit@5 に近い。hit@1 が低いのは生成側の負担になっているが、致命的ではない。

外れた 6 問の内訳が大事で、4 問は「資料は合っていてページが違う」、2 問は「別の資料が 1 位」だった。前者には「梃子の効果とは」「プロファイリングとは」のように、その資料の複数箇所で説明されている概念が含まれる。無作為に選んだページが唯一の正解とは限らないので、これは索引の欠点というより Golden セットの限界だ。相対比較に使う分には問題ない。

「ジャンルで整理すれば当たるのでは」を測る

資料が増えたらジャンル別に分ければ精度が上がるのでは、という案が出た。感覚的にはそう思える。そこで評価器に「正解のジャンルの資料だけに絞って検索する」オプションを足し、ジャンルを完璧に知っている場合の上限値を測った。

条件 hit@1 hit@3 hit@5 MRR
現状(絞り込みなし) 0.425 0.775 0.850 0.594
正解のジャンルに絞る(上限値) 0.450 0.775 0.850 0.609
正解の資料そのものに絞る(上限値) 0.450 0.775 0.875 0.620

ジャンルを完璧に知っていても hit@1 は 1 問分しか上がらない。資料そのものを知っていても hit@5 が 1 問分。つまり外れの主因は「別の資料やジャンルが混ざる」ことではなく、同じ資料の中で違うページが返ることだった。ジャンル整理は管理のしやすさには効くが、検索精度には効かない。物差しがなければ、たぶんジャンル分けを実装して「良くなった気がする」で終わっていた。

代わりに効きそうなのは、資料の中の構造だ。固定長チャンクは章の途中で切れ、質問の文脈と章題が結びつかない。OCR 側で章題(柱)が信頼できるようになったので、章・節単位でチャンクを切って「資料名 › 章題」を前置する実験を、同じ 40 問で測る予定である。

使い方のまとめ

  1. 資料の本文からページを無作為に選び、Bedrock に質問を作らせる(1 回、約 $0.2)
  2. 文脈依存の質問を目視で落とす
  3. Retrieve だけで測る(1 回 20 秒、費用ほぼゼロ)。結果は日時付きで保存し、前回との差分を出す
  4. 索引を変えたら同じ質問で測り直し、表に行を足す
  5. 最初の数回は、索引ではなく物差し(判定方法)のほうを疑う

「上限値を測る」オプションは、思いつきの改善案を実装する前に潰すのに便利だった。ジャンル分けのように直感に合う案ほど、数字で見ると効かないことがある。

コメント

タイトルとURLをコピーしました