Skip to content

fix(index): purge pre-migration doc vectors the reap cannot name [index, pipeline, poller, docs, tests] - #208

Merged
liplus-lin-lay merged 1 commit into
mainfrom
204-bugindex-pre-d1-doc-vectors-linger-in-vectorize-and-the-reap-cannot-reach-them
Aug 2, 2026
Merged

fix(index): purge pre-migration doc vectors the reap cannot name [index, pipeline, poller, docs, tests]#208
liplus-lin-lay merged 1 commit into
mainfrom
204-bugindex-pre-d1-doc-vectors-linger-in-vectorize-and-the-reap-cannot-reach-them

Conversation

@liplus-lin-lay

Copy link
Copy Markdown
Member

Closes #204

何をしたか

移行前(215e2e2 / PR #84 以前)の平文 id 体系で書かれた doc ベクトルを掃除する admin endpoint を追加した。

POST /admin/purge-legacy-vectors?repo=owner/repo[&dry_run=true][&surface=doc][&limit=N][&cursor=N]
Body(任意): {"paths": [".claude/CLAUDE.md", ...]}

削除経路(poller の doc reap / webhook の removed 処理)はいずれも現行 id を計算するため、旧世代は構造上到達できない。旧 id は path から決定的に定まるので、Vectorize を列挙せずに孤児集合を組み直して消す。再 embed は伴わない。

主な設計判断

  • 候補の並び順 = 明示 paths を先、次にツリー由来。上限に当たる run でも、確認済みの孤児(すでに削除済みの path)に先に到達する。順序が安定していることが cursor の再開可能性を支えている
  • 64 byte 超の旧 id は送らずに落とす。その溢れこそが hash 化移行の理由であり、上限を超える id は upsert 時点で弾かれている=対応するベクトルが存在しない。skippedOversize で件数を返す
  • 旧 id の再構成は src/pipeline/legacy-vector-id.ts に隔離し、pipeline.js の barrel には載せていない。通常の ingest 経路から旧体系に手が届かないようにするため
  • per-run 上限(既定 500 / 最大 2000)と cursor / nextCursor / remaining / done。既存の backfill-fts-segments と同じ呼び出し作法に揃えた
  • 削除は 500 件ずつバッチ。存在しない id の削除は no-op なので、途中で失敗しても同じ cursor から再開できる
  • treeTruncated を返す。Trees API が一覧を打ち切った場合、候補のツリー由来側が部分的になることを呼び出し側から見えるようにした

現行世代が巻き込まれる余地は無い。2 つの id 形式は交わらず({repo}#doc-… / d:…)、削除呼び出しに渡すのは legacyDocVectorId が組んだ id だけ。

範囲

issue の限定どおり doc のみsurfacedoc 以外を 400 で弾く。移行は全 surface の id を変えているが、updated_at の意味が type ごとに違うため他 surface の孤児は実測できておらず、対象外のままにした。

テスト

src/purge-legacy.test.ts(14 本):

  • dry_run が件数を返し、Vectorize を一切呼ばないこと(受け入れ条件の固定)
  • dry run の targeted と実 run の deleted が一致すること
  • ツリーの .md blob だけが候補になり、ディレクトリ entry や非 .md は入らないこと
  • 明示 paths がツリーより先に処理されること / 重複が畳まれること
  • 64 byte 超の id が送られないこと
  • 上限で打ち切り、remaining / nextCursor で呼び出し側に残りが見えること
  • nextCursor を渡し続けると重複も取りこぼしも無く drain すること
  • 500 件ずつバッチされること
  • 旧 id が現行 id と衝突しないこと

npm test = 139 (node) + 53 (workers) all pass、tsc --noEmit clean。

残る作業(マージ後の実測)

受け入れ条件の実測 2 項目は deploy 後に確認する:

  • README.md / docs/installation.md / docs/0-requirements.md の doc 行が 1 本ずつになる
  • .claude/CLAUDE.md が検索から消える(.claude/ 65 本は明示 paths で一回流す)

ドキュメント

docs/installation.md / .ja.md に第 13 節、docs/0-requirements.md / .ja.md の Vector Store に「vector ID 体系」「移行前世代」を追加。

🤖 Generated with Claude Code

…ex, pipeline, poller, docs, tests]

Add POST /admin/purge-legacy-vectors, a one-off cleanup for doc vectors written
under the plain-text ID scheme that 215e2e2 (PR #84) replaced with the hashed
one. Every delete path computes the current ID, so those vectors are unreachable
by construction: they answer dense queries with pre-migration content and take a
candidate slot from the live row for the same file.

The legacy ID is deterministic from the path, so the orphan set is rebuilt from
the repository tree plus an explicit `paths` body for files already deleted (the
residue nothing left in the worker can enumerate), and deleted in capped batches.
No re-embedding. IDs over the 64-byte Vectorize cap are dropped rather than sent,
since the cap rejected them at upsert time and no vector is keyed to them.

- src/pipeline/legacy-vector-id.ts: legacy ID reconstruction, kept out of the
  pipeline barrel so ingest code cannot reach the old scheme
- src/purge-legacy.ts: candidate ordering (explicit paths first), per-run cap
  with a resumable cursor, batched deleteByIds
- src/poller.ts: export listRepoDocPaths and pass `truncated` through
  fetchRepoTree so a partial tree listing is visible to the caller

旧世代のベクトルが削除経路から到達不能になっていた問題への対処。到達不能性は
既存データ側の性質でコード側の非対称ではないため、reap の修正ではなく一回性の
掃除として admin endpoint に置いた。旧 id と現行 id は形式が交わらないので、
組み直した旧 id 以外は削除呼び出しに渡らない。

Refs #204

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
github-rag-mcp 541f231 Aug 02 2026, 12:33 PM

@liplus-lin-lay liplus-lin-lay self-assigned this Aug 2, 2026

@liplus-lin-lay liplus-lin-lay left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review (auto mode, parent agent)

旧 id 体系の再構成が正しいこと

legacyDocVectorId = ${repo}#doc-${path}。親側で 215e2e2~1:src/pipeline.ts を実読して確認した実装とリテラル一致する。

export function docVectorId(repo: string, path: string): string {
  return `${repo}#doc-${path}`;
}

現行 id は d:{base64url} 形式で #/ も含まないため、旧式 id と現行 id は構文上衝突しない。制約「現行世代のベクトルを消さないこと」は形式の分離によって構造的に満たされている(実行時判定に依存しない)。

受け入れ条件

  • dry_run=true で件数のみ返し Vectorize に変更が無い — 満たす(テストで固定)
  • per-run 上限があり超過分が呼び出し側に分かる — 満たす。limit 既定 500 / 上限 2000、cursor / nextCursor / remaining / done で再開可能
  • 実測 2 本(doc 行が 1 本ずつになる / .claude/CLAUDE.md が検索から消える)未達。deploy 後に endpoint を実行してからでないと測れない。merge 後に本 session 内で実施する

判断が要った点 — いずれも受理

64 バイト超の旧 id を送らないskippedOversize として計上)。旧式 id が cap を超えた場合、そもそも upsert 時に弾かれている——その溢れこそが 215e2e2 の動機——ので、その id に紐づくベクトルは存在し得ない。存在しない id を混ぜて delete batch 全体を落とすリスクだけが残る。論拠が移行の動機そのものに接続していて妥当。

legacy-vector-id.tspipeline.js barrel から export しない。旧 scheme が通常の ingest 経路から到達可能になると、今回消しているのと同じ層を将来作りうる。呼び出し側がパス直接 import になることで、legacy 経路であることが call site で明示される。良い隔離。

fetchRepoTreetruncated を捨てていたのを通すように修正。Trees API は listing を打ち切るので、巨大 repo では候補集合が黙って部分的になる。treeTruncated としてレスポンスに出る。issue の想定変更箇所には無かったが、候補集合の完全性は本 endpoint の正しさに直結するので in-scope と判断する。

cursor は body 一貫性を要求する(順序付きリストの先頭が明示 paths のため、複数回呼ぶなら同じ body を送り直す必要がある)。制約として docs と module header の両方に書かれている。

scope

surfacedoc 以外を 400 で拒否。issue の「未確認の surface へ範囲を広げない」がコード側の制約として実装されている。未測定を未測定のまま扱う形が保たれている点を評価する。

認証は他の admin endpoint と同じ GITHUB_TOKEN ヘッダ。dry_run の既定が false である点は issue が明示的に許容した範囲で、既存 admin endpoint(reset-hashes / diff-watermark)とも整合する。

brake

user repo の PR で Li+ source を触っていないため brake 1 / brake 2 の対象外。

次のステップ

auto mode につき human gate なし。self-review pass → merge → deploy 後に dry_run=true で確認してから本実行、受け入れ条件の実測 2 本を取る。

@liplus-lin-lay
liplus-lin-lay merged commit e35f50a into main Aug 2, 2026
3 checks passed
@liplus-lin-lay
liplus-lin-lay deleted the 204-bugindex-pre-d1-doc-vectors-linger-in-vectorize-and-the-reap-cannot-reach-them branch August 2, 2026 12:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(index): pre-D1 doc vectors linger in Vectorize and the reap cannot reach them

1 participant