検索結果を「営業部の文書だけ」に絞りたい場面があります。素朴に組むと、上位10件を取ってから条件で絞る形になります。
20,000件で試したところ、この方式では200回の問い合わせすべてで10件に足りませんでした。平均2.5件しか残っていません。
文書20,000件で実際に測りました。後で絞る方式は200回すべてで10件に届かず、先に絞れば必ずそろいます。
フィルタ付き検索を素朴に組むと何が起きるのかを、実際に作って測りました。用意したのは部署と作成年を持つ文書20,000件です。
検索も絞り込みも本当に実行しています。上位10件がそろった問い合わせの数と、走査した件数を数えました。
// 後で当てる: まず上位 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 };
}
後で当てる(先に上位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再現率の記事で扱っています。
条件が厳しいほど、後で絞る方式は足りなくなる。
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件という取り方をすると、順番が効いてきます。
pgvectorでも、条件付きの最近傍検索に索引を効かせる方法はいくつかあるとされ、絞り込む列に索引を作る、条件を含んだ索引を作る、値で分割する、といった選択肢が挙げられています。
3番目は、条件の値が数種類しかない場合に向きます。部署が4つなら、部署ごとに索引を持てます。値の種類が多い場合は、分割のほうが現実的です。
測ってみて分かったのは、設計を決める前に条件の該当割合を数えておくべきだということでした。25%なら多めに取る方式でも間に合いますが、3.6%だと間に合いません。この数字は、既存のデータを1回集計すれば出ます。
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の記事で扱っています。
件数が足りなくてもエラーにならない。数えなければ気づけない。
For more rows, enable iterative index scans , which will automatically scan more of the index when needed.原文pgvector/pgvector README この内容の有効期限2027-02-18
同じ課題を持つ会社にとって、動いている設定は「作る時間」を買えるということです。ServiceDockは自作のワークフローやテンプレートを出品できるマーケットプレイスです。手数料や出品の流れは出品者向けページにまとまっています。
出品の仕組みを見る