7月3日、Towards Data Scienceが「The Untaught Lessons of RAG Retrieval: Cosine Is Not the Foundation」と題した記事を公開した。この記事では、RAG検索における「埋め込み+コサイン類似度」を中心に据えた通説を6つの観点から批判的に検証し、構造化テーブルへのフィルタリングを軸にした代替アーキテクチャについて詳しく紹介されている。
RAGの実装において、「ドキュメントをチャンク分割し、埋め込みベクトルに変換し、コサイン類似度でtop-kを取得する」というパイプラインは、今や標準的なアプローチとして広く普及している。しかし本記事はその前提を正面から否定する。コサイン類似度は「最後の手段」であり、基盤ではないというのが一貫した主張だ。
本記事は「Enterprise Document Intelligence」シリーズの一部で、4つのブリック構成のうちブリック3(検索)に該当する。コンパニオンコードはGitHubのdoc-intel/notebooks-vol1で公開されている(リポジトリ名は元記事記載のものに準拠)。
最重要の転換:検索(Search)ではなくフィルタリング(Filter)
本記事が最も強調するのが「Lesson 1: 検索ではなくフィルタリング」だ。
通常のRAGは「類似スコアを全候補に付与し、top-kを返す」というSearchの発想で動く。Searchは常に何かを返してしまう。答えがドキュメントに存在しなくても、それらしいチャンクが返る。
対して、このシリーズが採用するのはFilterの発想だ。line_df(行データ)とtoc_df(目次データ)という構造化テーブルに対してSQL的な条件を適用する。条件に合致する行だけが返り、存在しなければゼロ件が返る。これが「答えが文書にない」ことを明示できる唯一の方法だ。
具体例として記事が挙げているのは次のケースだ:
- ユーザーが「この契約は地震損害をカバーするか?」と質問
- 対象は洪水専門のポリシー文書
- キーワード検索:
"earthquake"がゼロ件 →answer_found = Falseと確信を持って返せる - 埋め込みコサイン:「自然災害」関連のチャンクが5件ヒット → LLMが誤って「カバーされる」と推論する可能性がある
これがLesson 4の核心でもある:「キーワード検索はゼロ件で不在を証明できるが、埋め込みにはそれができない」。
3つのシグナルを並列で走らせる
このシリーズのアーキテクチャは、単一のコサイン類似度に代えて3つの検索シグナルを並列実行する。
- キーワード検索(
line_dfに対する文字列マッチ):常時実行。高速・決定論的 - TOC推論(目次テーブルへのLLM一発処理):文書の構造を検索シグナルとして活用
- 埋め込みコサイン:語彙ミスマッチが予想される場合のみ起動するフォールバック
最終的な順位付けはLLMによるアービター(仲裁者)が1回のパスで行う。
この構成の効率性は記事中の図(複数セクターへの適用例)で示されている。保険・法務・財務など5分野に適用した場合、医療分野の1行だけが埋め込みを必要とした。それは「tachycardia(頻脈)」という患者の語彙が、文書中の「rapid heart rate」と一致しなかったためだ。残り4行はキーワード+TOCだけで完結した。埋め込みモデルの呼び出しが1件に抑えられたことで、そのぶんのLLMトークンコストを丸ごとスキップできたという意味であり、TOC推論のLLM呼び出し自体は別途発生している点は注意が必要だ。
Anchor(アンカー)とContext(コンテキスト)を分離する
Lesson 2もエンジニアには刺さる論点だ。
通常のtop-kでは、チャンクサイズのジレンマが生じる:小さいと文脈が不足し、大きいと何がヒットしたのかが曖昧になる。
このシリーズはアンカーとスコープを型付きのペアとして返す設計を採用する。
- アンカー:
"the deductible is the amount the insured pays before coverage begins"という1行(精度) - スコープ:その周辺3文(LLMが回答を生成するのに必要な文脈)
アンカーだけでも文脈不足、スコープだけでは何が根拠かが不明確。両者を分離して持つことで精度と十分性を同時に確保する。
BM25への疑問:狭いコーパスでは共起検索が勝る
Lesson 5は見落としがちな観点だ。
BM25はIDF(逆文書頻度)に基づいてランキングする。しかし20文書程度の企業コーパスでは、すべての用語が「レア」になり、IDFの前提が崩れる。
例として:「控除額(deductible)はいくらか?」という質問に対し、BM25はglossaryセクションで12回登場する行を上位に出す。一方、"deductible"と金額の数字が共起する行を探す共起検索なら、"the deductible is $1000"という実際の回答行が正しく1位になる。
TOCを1回のLLM呼び出しで使い切る
Lesson 6は実装上の妙手だ。20〜100行程度の目次テーブル(toc_df)をそのままLLMに渡し、「どのセクションが質問に答えられるか」を1回で推論させる。
キーワードマッチが失敗するケースの典型例:「いつポリシーを早期解約できるか?」という質問に対し、"leave"のサブストリング検索は目次でゼロ件。しかし28行の目次をLLMに渡すと「Termination and Cancellation」セクションが正しく返る。この1回のキャッシュ済みLLM呼び出しが、以降の処理を決定論的にする。
まとめ
6つのレッスンを貫く論旨は一つだ:chunk-embed-cosineの反射的な使用をやめ、構造化テーブルへのフィルタリングとして検索を再定義せよ。
埋め込みを最後に回すことで、不要な呼び出しを削減しながら検索の説明可能性も確保できる。フィルタの条件は1行のコードとして残り、6ヶ月後も同じ動作を保証できる。埋め込みのランキングはモデルを再実行しなければ再現できない。この監査可能性の差は、エンタープライズ用途では決定的だ。ドキュメントの構造が比較的整っており、セクション・章立てが明確な企業文書を扱う場面では、特にこのアーキテクチャの優位性が発揮されやすいと言える。
詳細はThe Untaught Lessons of RAG Retrieval: Cosine Is Not the Foundationを参照していただきたい。