RAG・検索基盤

フィルタ付き検索とは|上位10件を取ってから絞ったら、200回すべてで件数が足りなかった

フィルタ付き検索とは何が難しいのか後で絞ると何が起きるのかどう設計すればよいのか

検索結果を「営業部の文書だけ」に絞りたい場面があります。素朴に組むと、上位10件を取ってから条件で絞る形になります。

20,000件で試したところ、この方式では200回の問い合わせすべてで10件に足りませんでした。平均2.5件しか残っていません。

この記事の要点

  • 上位10件を取ってから絞ると、200回すべてで件数不足(平均2.5件)
  • 200件取れば緩い条件はそろうが、厳しい条件は33/200のまま
  • 先に絞れば200回すべてで10件そろう
  • しかも厳しい条件ほど走査件数は少なくなる(715件)

上位10件を取ってから絞ったら、200回すべてで件数が足りなかった

文書20,000件で実際に測りました。後で絞る方式は200回すべてで10件に届かず、先に絞れば必ずそろいます。

フィルタ付き検索を素朴に組むと何が起きるのかを、実際に作って測りました。用意したのは部署と作成年を持つ文書20,000件です。

検索も絞り込みも本当に実行しています。上位10件がそろった問い合わせの数と、走査した件数を数えました

javascript
// 後で当てる: まず上位 fetch 件を取り、そのあと条件で絞る
function postFilter(q, cond, fetch) {
  const scored = docs.map((d, i) => ({ i, s: dist2(q, d.vec) }));
  scored.sort((a, b) => a.s - b.s);
  const kept = scored.slice(0, fetch).filter(({ i }) => cond(docs[i]));
  return { got: kept.slice(0, K).length, scanned: fetch };
}

// 先に当てる: 条件に合うものだけを対象にして上位を取る
function preFilter(q, cond) {
  const cand = [];
  for (let i = 0; i < N; i++) if (cond(docs[i])) cand.push({ i, s: dist2(q, docs[i].vec) });
  cand.sort((a, b) => a.s - b.s);
  return { got: cand.slice(0, K).length, scanned: cand.length };
}
text
後で当てる(先に上位10件を取ってから絞る)

条件                      該当割合   上位10件がそろった問い合わせ  平均取得件数
部署が営業                      25.0%                 0 / 200           2.5件
部署が営業 かつ 2024年              3.6%                 0 / 200           0.4件
2026年のみ                    14.3%                 0 / 200           1.4件

後で当てる(先に上位200件を取ってから絞る)

部署が営業                      25.0%               200 / 200          10.0件
部署が営業 かつ 2024年              3.6%                33 / 200           7.1件
2026年のみ                    14.3%               200 / 200          10.0件

先に当てる(条件に合うものだけを対象にする)

条件                      該当割合   上位10件がそろった問い合わせ  走査件数
部署が営業                      25.0%               200 / 200     5,000件
部署が営業 かつ 2024年              3.6%               200 / 200       715件
2026年のみ                    14.3%               200 / 200     2,857件

最初の表がすべて0です。10件取って25%が残れば2.5件ですから、当然の結果とも言えます。

多めに取れば済むのか

200件取ると、該当割合25%の条件は200回すべてそろいました。ところが該当割合3.6%の条件は33回にとどまります

条件が厳しいほど、必要な取得件数は増えます。しかもどれだけ取れば足りるかは、条件ごとに違います。一律に決められません。

先に絞れば逆転する

先に絞る方式では、3つの条件すべてで200回そろいました。条件に合うものの中から上位を取るので、足りなくなりようがありません

走査件数も面白い形です。該当割合3.6%の条件では715件しか走査していません。条件が厳しいほど対象が減るので、むしろ速くなります。

近似の索引では後で当たる

ここで実際の製品の話になります。pgvectorでは、近似の索引を使うと絞り込みは索引を走査した後に当てられるため、結果が少なくなりうると明記されています。

つまり索引を足すと、意図せず前の表の状態になります。近似の索引がもたらす別の副作用はANN再現率の記事で扱っています。

条件が厳しいほど、後で絞る方式は足りなくなる。

後で絞る・10件取得10件そろった足りなかった200200回後で絞る・200件取得(該当3.6%)33167200回先に絞る200200回200回の問い合わせでの実測。先に絞る方式は、該当割合3.6%の条件でも走査715件で済んだ。
図1 ── 上位10件がそろった問い合わせの数
出典pgvector/pgvector README2026-08-18 確認
With approximate indexes, queries with filtering can return less results since filtering is applied after the index is scanned.
原文pgvector/pgvector README この内容の有効期限2027-02-18

フィルタ付き検索とは何が難しいのか

似ているものを探すことと、条件で絞ることを同時にやる点です。どちらを先にするかで結果が変わります。

フィルタ付き検索は、似ているものを探しつつ、属性の条件でも絞る検索です。「営業部の、2024年の文書のうち、この質問に近いもの」といった問い合わせになります。

順番が結果を変える

難しさは順番にあります。似ているものを先に選ぶか、条件に合うものを先に選ぶかで、返る件数も中身も変わります。

普通の検索なら順番は問題になりません。条件を満たすものを全部見つけて並べればよいためです。ところが似ている順の上位K件という取り方をすると、順番が効いてきます

取りうる作り

  1. 条件に合うものだけを対象に、似ている順で取る。件数は必ずそろう
  2. 似ている順で多めに取り、後で絞る。足りなくなることがある
  3. 条件ごとに索引を分けておく。値の種類が少ない場合に有効
  4. 条件の値でデータを分割しておく。値の種類が多い場合

pgvectorでも、条件付きの最近傍検索に索引を効かせる方法はいくつかあるとされ、絞り込む列に索引を作る、条件を含んだ索引を作る、値で分割する、といった選択肢が挙げられています。

選び方の目安

3番目は、条件の値が数種類しかない場合に向きます。部署が4つなら、部署ごとに索引を持てます。値の種類が多い場合は、分割のほうが現実的です。

余談 該当割合を先に数える

測ってみて分かったのは、設計を決める前に条件の該当割合を数えておくべきだということでした。25%なら多めに取る方式でも間に合いますが、3.6%だと間に合いません。この数字は、既存のデータを1回集計すれば出ます。

出典pgvector/pgvector README2026-08-18 確認
There are a few ways to index nearest neighbor queries with a WHERE clause.
原文pgvector/pgvector README この内容の有効期限2027-02-18

件数が足りない状態にどう気づくか

返ってきた件数を毎回数えます。足りない場合に索引をさらに走査する設定も用意されています。

フィルタ付き検索の怖いところは、件数が足りなくてもエラーにならない点です。2件しか返らなくても、検索としては成功しています。

件数を数えるだけで分かる

気づき方は単純です。要求したK件に対して、実際に何件返ったかを記録する。それだけで足ります。

前の節の実測では、10件要求して平均2.5件でした。この差は、記録さえしていれば1日で気づけます。

足りなければ追加で走査する

製品側の対処も用意されています。pgvectorでは、行が足りない場合に索引をさらに走査する繰り返しの走査を有効にできるとされています。

十分な件数が集まるまで走査を続ける、という仕組みです。ただし走査が増えれば遅くなるので、足りない状態が常態化していないかは別に見る必要があります。

条件が固定なら索引を分ける

絞り込みの条件が決まっているなら、その条件を含んだ索引を作る手があります。値の種類が少ない場合に有効だと案内されています。

たとえば利用者ごとに見える範囲が違う構成では、この形が効きます。権限を引き継がないと何が起きるかはCopilot Studioの記事で扱っています。

件数が足りなくてもエラーにならない。数えなければ気づけない。

充足 2 / 4条件の該当割合を数えてある25%なら多めに取る方式でも間に合うが、3.6%だと間に合わない返ってきた件数を毎回記録している10件要求して平均2.5件という状態は、記録があれば1日で気づける上位K件を取ってから絞っている200回すべてで10件に届かなかった。条件が厳しいほど悪化する件数不足をエラーとして扱っている検索としては成功なのでエラーにならない。件数を見るしかない文書20,000件・200回の問い合わせでの実測にもとづく。先に絞れば200回すべてそろった。
図2 ── フィルタ付き検索の点検項目
出典pgvector/pgvector README2026-08-18 確認
For more rows, enable iterative index scans , which will automatically scan more of the index when needed.
原文pgvector/pgvector README この内容の有効期限2027-02-18

よくある質問

なぜ後で絞ると足りなくなるのですか
上位10件のうち条件に合うものだけが残るためです。該当割合が25%なら、平均2.5件しか残りません。
多めに取れば解決しませんか
緩い条件なら解決します。この計測では200件取れば該当割合25%の条件は200回すべてそろいました。ただし該当割合3.6%の条件は33回にとどまります。
先に絞るほうが常に良いのですか
件数がそろう点では確実です。ただし近似の索引を使っている場合、条件は索引を引いた後に当てられる作りになっていることがあります。
実際の製品ではどうなっていますか
pgvectorでは、近似の索引を使うと絞り込みは索引を走査した後に当てられ、結果が少なくなりうると明記されています。足りない場合に索引をさらに走査する設定も用意されています。

まとめ

  • 後で絞る方式は200回すべてで10件に届かなかった
  • 取得件数を増やすと緩い条件はそろうが、厳しい条件は残る
  • 先に絞れば必ずそろい、走査件数も少ない
  • 近似の索引では後で当たる作りになっていることがある

今日から始められること

  1. 検索に付けている絞り込み条件の、該当割合を数える
  2. 上位K件を取ってから絞る作りになっていないか確かめる
  3. なっているなら、返ってきた件数がKに満たない回を数える
  4. 使っている製品が、条件をいつ当てるかを確認する

実務で組んだフィルタ付き検索のワークフローには、値段が付きます

同じ課題を持つ会社にとって、動いている設定は「作る時間」を買えるということです。ServiceDockは自作のワークフローやテンプレートを出品できるマーケットプレイスです。手数料や出品の流れは出品者向けページにまとまっています。

出品の仕組みを見る