fix(index): purge pre-migration doc vectors the reap cannot name [index, pipeline, poller, docs, tests] - #208
Conversation
…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>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
github-rag-mcp | 541f231 | Aug 02 2026, 12:33 PM |
liplus-lin-lay
left a comment
There was a problem hiding this comment.
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.ts を pipeline.js barrel から export しない。旧 scheme が通常の ingest 経路から到達可能になると、今回消しているのと同じ層を将来作りうる。呼び出し側がパス直接 import になることで、legacy 経路であることが call site で明示される。良い隔離。
fetchRepoTree が truncated を捨てていたのを通すように修正。Trees API は listing を打ち切るので、巨大 repo では候補集合が黙って部分的になる。treeTruncated としてレスポンスに出る。issue の想定変更箇所には無かったが、候補集合の完全性は本 endpoint の正しさに直結するので in-scope と判断する。
cursor は body 一貫性を要求する(順序付きリストの先頭が明示 paths のため、複数回呼ぶなら同じ body を送り直す必要がある)。制約として docs と module header の両方に書かれている。
scope
surface は doc 以外を 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 本を取る。
Closes #204
何をしたか
移行前(
215e2e2/ PR #84 以前)の平文 id 体系で書かれた doc ベクトルを掃除する admin endpoint を追加した。削除経路(poller の doc reap / webhook の removed 処理)はいずれも現行 id を計算するため、旧世代は構造上到達できない。旧 id は path から決定的に定まるので、Vectorize を列挙せずに孤児集合を組み直して消す。再 embed は伴わない。
主な設計判断
pathsを先、次にツリー由来。上限に当たる run でも、確認済みの孤児(すでに削除済みの path)に先に到達する。順序が安定していることがcursorの再開可能性を支えているskippedOversizeで件数を返すsrc/pipeline/legacy-vector-id.tsに隔離し、pipeline.jsの barrel には載せていない。通常の ingest 経路から旧体系に手が届かないようにするためcursor/nextCursor/remaining/done。既存のbackfill-fts-segmentsと同じ呼び出し作法に揃えたcursorから再開できるtreeTruncatedを返す。Trees API が一覧を打ち切った場合、候補のツリー由来側が部分的になることを呼び出し側から見えるようにした現行世代が巻き込まれる余地は無い。2 つの id 形式は交わらず(
{repo}#doc-…/d:…)、削除呼び出しに渡すのはlegacyDocVectorIdが組んだ id だけ。範囲
issue の限定どおり doc のみ。
surfaceはdoc以外を 400 で弾く。移行は全 surface の id を変えているが、updated_atの意味が type ごとに違うため他 surface の孤児は実測できておらず、対象外のままにした。テスト
src/purge-legacy.test.ts(14 本):dry_runが件数を返し、Vectorize を一切呼ばないこと(受け入れ条件の固定)targetedと実 run のdeletedが一致すること.mdblob だけが候補になり、ディレクトリ entry や非.mdは入らないことpathsがツリーより先に処理されること / 重複が畳まれることremaining/nextCursorで呼び出し側に残りが見えることnextCursorを渡し続けると重複も取りこぼしも無く drain することnpm test= 139 (node) + 53 (workers) all pass、tsc --noEmitclean。残る作業(マージ後の実測)
受け入れ条件の実測 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