ベクトル走査窓の「到達範囲」と「新しさの優遇」¶
Status: 2.5.x 系で出荷済、既定は off。計測済みで、計測は通らなかった — 結果と 2.6 系の対応は
REACH_AND_RECENCY_PLAN.md を参照。本ページは機構の参照として残る。SCHEMA_VERSION は 13 のまま、新しいランタイム依存は
増えず、新設定が既定値のあいだは現行の走査が返す答えとビット単位で一致する
(類似度が等しい行の並びを含む)。
1. ひとつの数字が、ふたつの仕事をしている¶
ローカルのベクトル検索は最新 CPERSONA_MAX_MEMORIES 行 (既定 10,000。memories と
episodes をそれぞれこの幅で走査する) をコサイン類似度で順位付けする。この設定は
費用の上限として文書化されてきた。recall 1 回が読む行数を抑え、連続配置索引の大きさを
決める。六桁のコーパスではコーパスの大半をベクトル腕から見えなくもしていて、
素直な直し方は値を上げることに見える。
実際の recall 経路で測ると、上げることは緩和ではなかった
(benchmarks/measurements/results-scan-window-default-ab.md)。保存 237,654 文書で、
窓を 10,000 から 200,000 にすると:
| 層 | 意味 | Δ NDCG@10 |
|---|---|---|
| far — 答えが 10,000 行の窓より下にある | 広い窓のベクトル腕だけが見える | +4.93 ± 0.90 |
| near — 答えが 10,000 行の窓の中にある | どちらの窓でも見える | −20.19 ± 1.70 |
切り捨ては起きていない。どの腕のどの呼び出しもちょうど 10 行返し、品質ゲートの
fallback は一度も発火しなかった。near 層の損失は reciprocal-rank fusion の中での順位の
押し出しである。ベクトル腕が融合に渡すのは上位 limit 行なので、10,000 候補の中で 3 位
だった最近の答えが 200,000 候補の中で 30 位になると、それはベクトルのリストの下のほうに
いるのではなく、リストから消える。reciprocal-rank の票が無くなる。字句の腕は
まだその行を運ぶが、ベクトルの票を失った行は、より良く融合した行に負ける。
つまり窓はふたつの仕事を同時にしていた:
- 到達範囲 (reach) — ベクトル腕がどこまで遡って見るか。設定名が指していた費用の上限。
- 新しさの優遇 (recency prior) — 最新の行だけを残すことで、最近の記憶はどれも コーパス全体ではなく 10,000 行の候補場で競えばよくなる。答えが最近にあるクエリに とってこれは無償の精度で、この計器では 20 点ぶんの価値があった。名前も設定も無く、 費用上限の副作用として存在している。
窓を広げると、到達範囲を伸ばすその動作で優遇が消える。ひとつの数字が両方を制御して いるあいだ、どちらも単独では動かせない。本書はそれぞれに固有のノブを与える。
同じ測定の探索的な掃引が、答えが取ってはならない形も示している。窓 50,000 は 200,000 より多くの far 層の品質を買った (+12.87 対 +6.06)。far の答えは深さ 20,000〜29,500 に あり、広い窓はそれらを 150,000 行多く競わせただけだった。答えより先へ到達しても、 費用だけがかかって何も買えない。到達範囲をいくつにするにせよ、near の場は小さいままで なければならない。
2. 採らない形: 段階つきの窓¶
最初の案は幅がふたつある窓である。内側の幅の中の行は今日どおりに採点し、内側と外側の あいだの行は低い重みで受け入れる。ふたつの幅が等しければ今日の走査とビット一致する。
これは採らない。重みはどこかから来なければならないからである。内側の幅を越えた行は、 コサインをそのまま持つか — その場合内側の幅は何もしておらず、単一の広い窓である — コサインに年齢の関数を掛けるか、のどちらかである。その関数は時間で重み付けした スコアであり、時間で重み付けしたスコアとは 2.6 系に予定されている recency-weighted search そのものであって、そちらには固有の未決事項がある (confidence の再ソートとどう 相互作用するか、時間的に一様でないクエリ分布で時間項に何の価値があるか)。窓の設定に 畳み込めば、それらを偶然で決めてしまう。本設計が変えるのはどの行が候補になるかで あって、候補の採点ではない。時間項も重みも持たない。
3. 設計: ベクトルのリストをもう 1 本¶
設定はふたつ、うちひとつは既存:
| 設定 | 意味 | 既定 |
|---|---|---|
CPERSONA_MAX_MEMORIES |
near 窓: ベクトル腕が今日どおりに順位付けする最新 N 行 | 10,000 (変更なし) |
CPERSONA_VECTOR_REACH |
到達範囲: ベクトル腕が合計で見てよい最新行数。走査位置 [N, REACH) の行が 2 本目のリストになる |
0 = 窓と同じ。far リストは存在しない |
REACH > N のとき、ベクトル retriever は 1 本ではなく 2 本の順位付きリストを作る:
- near リスト — 位置
[0, N)の中でコサイン上位limit行。今日作っているリストその ものである。同じ行、同じ閾値、同じ安定な tie-break、同じ順序。 - far リスト — 位置
[N, REACH)の中でコサイン上位limit行。同じ切り方。
位置は走査自身の順序 created_at DESC, id ASC で数える。連続配置索引ファイルが書かれる
順序でもある。memories と episodes はそれぞれ同じ位置で分ける (今日の走査のされ方を
写す)。
融合は far リストを「順位付きリストがもう 1 本」として受け取る。reciprocal-rank fusion (出荷モード) では変更はそれで全部である — 既存の 3 つと同一のループが 4 つ目に増える。 既存のリストはどれも触られないので、既存の行はどれも今日とまったく同じ票を保つ。far の 行は追加しかできない。新しさの優遇はこれで名前を持つものになった — near リストが 幅 N で存在すること、それ自体である — そして到達範囲は、もはやそれを奪わない別の数字に なった。
3.1 near リストが「似ている」ではなく「保存される」理由¶
融合が今日見る 3 本のリストは、ベクトルのリスト、episode FTS のリスト、memory keyword の
リストである。near リストは構成上、今日のベクトルのリストそのものである: 走査位置
[0, N) は今日の走査が読む行であり、閾値と limit の切り方は同じコードで、tie-break は
前後とも走査順である。行の reciprocal-rank の寄与はその行の自分のリスト内の順位の関数
なので、別のリストが追加されても変わらない。far リストと near リストは位置で互いに素
なので、両方に載る行は無く、票が二重に数えられる行も無い。
この互いに素であることが、品質ゲートのスケールも正しく保つ。旧来のゲートは閾値を
「3 つの retriever が 1 行に与えうる最大スコア」3 / (K + 1) で伸縮する。far リストが
あっても 1 行が到達できる最大は依然 3 票である — near または far に加えて字句 2 本 —
ので、この定数は行あたりの最大値のままである。較正済みゲートは生の融合スコアを比較
するが、同じ理由で影響を受けない: 今日存在する行のスコア分布は動かない。
3.2 relative-score fusion¶
CPERSONA_RECALL_MODE=rsf では各チャネルの生スコアをクエリごとに min-max 正規化し、
合計を有効チャネル数で割る。far リストを 4 本目のチャネルとして融合すると near の行の
正規化値は保たれる (min と max は自分のリスト内で計算される) が、すべての行で除数が
3 から 4 になり、コサインスケールのゲートに対して全融合スコアが下がる。代わりに far の
行をベクトルチャネルへ併合すると、near の行の min と max が変わる。far リストが存在する
時点でどちらもビット保存ではなく、下の測定は出荷モード rrf にのみ登録されている。
rsf では far リストは 4 本目のチャネルとして融合し、測定されるまで何も主張しない。
3.3 ふたつの供給元¶
ベクトル走査には同じ契約 — 走査順の (ids, similarities) — に対する供給元がふたつあり、
どちらも far リストを同じ方法で作る:
- 連続配置索引:
select()はファイル順の位置を返す。near リストは今日どおりpositions[:N]、far リストはpositions[N:REACH]を取る。索引の watermark より上の行 (最後の build 以降に保存された行) は最新の行であり near リストに属する — 今日の走査に merge されるのとまったく同じに。 - SQLite 走査 (索引なし、または索引が辞退したとき): 同じ文に far 領域向けの
LIMIT ? OFFSET ?を付け、chunk で読む。ピークメモリは到達範囲でなく chunk で抑えられる。OFFSETは飛ばす索引順を歩く。それは索引なし経路が窓に対して既に払っている値段であり、 到達範囲を大きくして遅くなりすぎるなら、それは索引を建てる理由であって、この経路が 近似する理由ではない。
どちらの供給元も far リストを作れなければならない。索引があるときだけ動く分離では、
索引の build が答えを変えることになり、索引の設計はそれを禁じている
(CONTIGUOUS_INDEX_DESIGN.md §2)。
4. この設定の費用¶
REACH > N のとき、recall 1 回は今日より REACH − N 行多く embedding を読む。連続配置
索引があればその読み出しは索引で測った速い経路 (SQLite 読み出しの 7 倍速) であり、無ければ
chunk 走査が上の測定で広い窓が払ったのとほぼ同じ費用を払う — 237,654 行に到達範囲
200,000 で p50 がおよそ 2 倍、どちらの場合も床は字句の腕である。behavior-contracts.md
のこの設定の項は、窓と同じ言葉でこの費用を述べる。
メモリは走査経路では chunk サイズで、索引経路では索引ファイルで抑えられ、どちらも索引が 既に持つ量を越えて到達範囲とともに増えることはない。
5. この設計がしないこと¶
- 時間で重み付けしない。よく融合した far の行はそのコサインで返り、near の行もその コサインで返る。far リストの票が near リストの票より軽くあるべきかは採点の問題であり、 下の測定がそれを提起する証拠になる。
- 値を選ばない。既定は 10,000 と off のまま。far リストが意図どおりに働くかを事前登録した 測定が決め、値はその後に、固有の文書を持つ固有の変更として選ぶ。
- episode penalty、confidence の再ソート、autocut、品質ゲートに触れない。それぞれは 融合済みリストを従来どおり見る。よく融合した far の行のぶんだけ長くなっているだけである。
6. ゲート¶
設定の状態ごとにゲートがひとつずつ:
- 既定 (off): behaviour golden (
tests/golden/behaviour_252.json) と索引の等価性テスト が差分ゼロを示さなければならない。設定が既定のあいだ far リストは「走るコード」として 存在してはならない — 空を返す走査ではなく、guard であること。 - on:
benchmarks/measurements/prereg-scan-window-reach-ab.md。どの腕も走らせる前に 登録し、広い窓を退けたのと同じ計器・同じ判定規則で測る — far 層は NDCG@10 で +5.0 以上 得て、near 層は 1.0 より多く失ってはならない。分離が通れば、到達範囲を上げることは値段を 付けて文書化できる変更になる。near 層で落ちたなら、等しい重みでは far リストの票が強すぎる ということで、次の候補はリストごとの重みになる — それはこの設計が意図的に recency-weighted の線へ残した採点の問題である。