Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/0-requirements.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,14 @@ Responsibilities:
- vector と metadata を Vectorize に upsert する
- 同じ content を D1 FTS5 の `search_docs` table にも upsert する(sparse 側同期、tokenizer_kind は type に応じて `nat` / `code` を自動選択)
- embedding 失敗時も次回 retry できる状態を保つ
- D1 FTS5 upsert 失敗は Vectorize upsert を無効化しない(次回 reindex で reconcile)
- embed 経路では D1 FTS5 upsert 失敗は Vectorize upsert を無効化しない。保存した bodyHash が次回の試行を駆動し、次の reindex で sparse 側が reconcile される
- metadata のみの経路(body は変わらず state / labels / milestone / assignees が変わった場合)では、mirror 書き込みの失敗を best-effort 扱いに**しない**。差分検出の基準を進めずに保持し、次の poll / webhook 配信で再試行させる。基準は IssueStore の record そのものなので、失敗した mirror を追い越して基準を進めると取り残しが恒久化する — state だけの変更は、embed 経路が待っている body 変更を二度と連れてこない(issue #209)
- この経路の dense / sparse mirror は互いに独立して書く。vector が欠けている行(issue #210)でも sparse 側の state は更新される
- commit diff は 1 commit 分の file リストを batch embed(Workers AI の `text: string[]` 対応を利用)し、1 回の Vectorize upsert で N vector を書き込む
- batch size は `MAX_EMBEDDING_BATCH_SIZE`(既定 20)で上限。これを超える commit は複数 batch call に分割する

**取り残した state の修復.** 上の順序欠陥が残した行は、生きている項目として検索に出続ける。通常の poll では到達できない — 差分検出の基準がすでに GitHub と一致しているため。`POST /admin/backfill-issue-state?repo=owner/repo`(installation guide 参照)が repository 単位でこれを揃える。ページングした `state=open` 一覧を正とし、そこに無い索引済み `open` 行を dense / sparse 両側で `closed` にする。再 embed は伴わない(dense 側は既存の値をそのまま再 upsert し、`state` だけ差し替える)。修復は一方向(`open` → `closed`)で、欠陥が生んだ方向に一致する。open 一覧が打ち切られる場合は何もせず中断する — 「一覧に無いこと」が close の根拠だから。

### 5. Vector Store (Dense)

Vectorize は hybrid retrieval の dense 側を担う。次の metadata を伴う semantic embedding を保持する。
Expand Down
6 changes: 5 additions & 1 deletion docs/0-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,14 @@ Responsibilities:
- upsert vectors with metadata into Vectorize
- mirror the same content into the D1 FTS5 `search_docs` table for the sparse (BM25) side, choosing tokenizer_kind `nat` or `code` by surface type
- keep retryable failures detectable on the next run
- D1 FTS5 upsert failures do not invalidate a successful Vectorize upsert; the next reindex reconciles the sparse side
- D1 FTS5 upsert failures do not invalidate a successful Vectorize upsert on the embed path; the stored bodyHash drives the next attempt and the next reindex reconciles the sparse side
- on the metadata-only path (state / labels / milestone / assignees changed, body did not) a failed mirror write is **not** best-effort: the diff baseline is held so the next poll or webhook delivery retries. The baseline is the IssueStore record itself, so advancing it past a failed mirror makes the miss permanent — a state-only change never brings the body change the embed path waits for (issue #209)
- the dense and sparse mirrors on that path are written independently: a row with no vector (issue #210) still gets its sparse state updated
- for commit diffs: batch-embed a commit's file list in a single Workers AI call (`text: string[]`) and upsert the resulting N vectors in one `VECTORIZE.upsert` call
- batch size is capped by `MAX_EMBEDDING_BATCH_SIZE` (default 20); commits exceeding it are split across multiple batch calls

**Stale-state repair.** Rows left behind by the ordering defect above keep answering searches as live items, and no ordinary poll reaches them: their diff baseline already matches GitHub. `POST /admin/backfill-issue-state?repo=owner/repo` (see the installation guide) reconciles them per repository — one paginated `state=open` listing supplies the truth, indexed `open` rows absent from it are set to `closed` on both sides, and nothing is re-embedded (the dense side re-upserts the existing values with only `state` replaced). The repair is one-way (`open` → `closed`), which is the direction the defect produced; it aborts rather than act on a truncated open listing, since absence from that listing is what marks a row closed.

### 5. Vector Store (Dense)

Vectorize is the dense side of hybrid retrieval. It stores semantic embeddings and metadata for:
Expand Down
41 changes: 41 additions & 0 deletions docs/installation.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,47 @@ POST /admin/purge-legacy-vectors?repo=owner/repo
- 残差は増えない — 旧形式が書き込まれた期間は移行時点で閉じている。したがってこの endpoint は repository ごとに一回性で、定期実行するものではない
- 確認は二重索引されていたファイルを検索し、移行前のコピー(古い内容・dense のみ)が出なくなることを見る

## 14. close 済みなのに `open` のまま残っている索引行を揃える

issue / PR が open の状態で索引された行は `state: "open"` を持つ。その後の state 変更を索引へ反映するのは metadata のみを更新する経路だが、この経路は自分が守っている mirror 書き込みより**先に**差分検出の基準を進めていた。そのため mirror が失敗しても二度と再試行されず、行は「まだ生きている項目」として検索に出続けた(issue #209)。順序は発生源側で修正済み。この endpoint は旧順序が残した行を修復する。

再 embed は伴わない。実際の state は repo ごとに 1 回の `state=open` 一覧から取り、sparse 側は `UPDATE`、dense 側は既存の vector 値をそのまま再 upsert して metadata の `state` だけ差し替える。`/admin/reset-hashes` はこの用途には使えない — repository 全体の再 embed を起こす。

Admin endpoint:

```text
POST /admin/backfill-issue-state?repo=owner/repo
```

パラメータ:

- `repo` — `owner/repo`
- `dry_run` — `true` で件数だけ返し、D1 にも Vectorize にも書かない
- `limit` — 1 回の呼び出しで見る行数、`1..1000`(既定 `200`)
- `cursor` — 再開位置の issue 番号。前回のレスポンスの `nextCursor` をそのまま渡す

認証:

- `GITHUB_TOKEN` ヘッダに worker secret と同じ値を送る

レスポンス:

```json
{ "repo": "owner/repo", "dryRun": false, "openOnGitHub": 33, "cursor": 0, "limit": 200,
"scanned": 165, "stale": 132, "ftsUpdated": 132, "vectorsUpdated": 130,
"vectorsMissing": 2, "nextCursor": null, "done": true }
```

運用上の注意:

- `done` が `true` になるまで `nextCursor` を渡して繰り返し呼ぶ
- 何度実行しても安全。GitHub 側でまだ open な行は書き込みなしでスキップされるので、完了済みの修復を再実行すると `stale: 0` が返る
- 方向は一方向(`open` → `closed`)。欠陥が生んだ方向であり、検索を害する方向(閉じた判断が生きた検討事項として再供給される)でもある。走査対象が索引全体でなく open 集合の大きさに比例する点も、この方向に限る理由
- open 一覧が 50 ページを超える場合、何も閉じずにエラーで中断する。「一覧に無いこと」が close の根拠なので、部分的な一覧を使ってはならない
- `vectorsMissing` は対応する vector が無い stale 行の数。これは索引欠落側(issue #210)の面で本 endpoint の範囲外。sparse 側は存在するので、そちらは修復する
- dense 側の書き込みが失敗した場合、D1 に触れる前に呼び出し全体が失敗する。中途半端な修復を残さないための設計なので、同じ `cursor` で再実行する
- 確認は close 済みと分かっている項目を `state: "closed"` で検索するか、`SELECT COUNT(*) FROM search_docs WHERE repo = ? AND type IN ('issue','pull_request') AND state = 'open'` が実際の open 数と一致することを見る

## Troubleshooting

### `GITHUB_TOKEN not configured`
Expand Down
41 changes: 41 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,47 @@ Operational notes:
- the residue is closed, not growing — the legacy scheme stopped writing at the migration — so this endpoint is a one-off per repository, not a recurring job
- verify by searching for a file that was double-indexed: the pre-migration copy (old content, dense-only) should stop appearing

## 14. Close indexed rows that stayed `open` after the item was closed

A row indexed while its issue or PR was open carries `state: "open"`. The state change is mirrored onto the index by the metadata-only path, and that path used to advance its own diff baseline before the mirror writes it guards — so a mirror write that failed was never retried, and the row kept answering searches as a live item (issue #209). The ordering is fixed at the source; this endpoint repairs the rows the old ordering left behind.

Nothing is re-embedded: the true state comes from one `state=open` listing per repo, the sparse side is an `UPDATE`, and the dense side re-upserts the existing vector values with only `state` replaced. `/admin/reset-hashes` is the wrong tool here — it triggers a full re-embedding of the repository.

Admin endpoint:

```text
POST /admin/backfill-issue-state?repo=owner/repo
```

Parameters:

- `repo` — `owner/repo`
- `dry_run` — `true` reports the counts and writes to neither D1 nor Vectorize
- `limit` — rows examined per call, `1..1000` (default `200`)
- `cursor` — issue number to resume after; pass back the `nextCursor` of the previous response

Authentication:

- send the same `GITHUB_TOKEN` value in the `GITHUB_TOKEN` header

Response:

```json
{ "repo": "owner/repo", "dryRun": false, "openOnGitHub": 33, "cursor": 0, "limit": 200,
"scanned": 165, "stale": 132, "ftsUpdated": 132, "vectorsUpdated": 130,
"vectorsMissing": 2, "nextCursor": null, "done": true }
```

Operational notes:

- call it repeatedly, feeding `nextCursor` back in, until `done` is `true`
- safe to repeat: a row GitHub still lists as open is skipped without a write, so a re-run of a finished repair reports `stale: 0`
- the direction is one-way (`open` → `closed`). That is the direction the defect produced and the one that harms retrieval — a closed decision resurfacing as a live one. It also keeps the scan proportional to the open set rather than to the whole index
- the run aborts with an error rather than closing anything if the open-item listing would exceed 50 pages. Absence from that listing is what marks a row closed, so a partial listing must never be used
- `vectorsMissing` counts stale rows with no vector to refresh. Those are the missing-index-entry surface (issue #210) and are out of scope here; their sparse half still exists and is still repaired
- if the dense write fails, the whole call fails before D1 is touched, leaving the window unrepaired rather than half-repaired. Retry with the same `cursor`
- verify with a `state: "closed"` search for an item you know was closed, or by counting `search_docs` rows: `SELECT COUNT(*) FROM search_docs WHERE repo = ? AND type IN ('issue','pull_request') AND state = 'open'` should match the repository's real open count

## Troubleshooting

### `GITHUB_TOKEN not configured`
Expand Down
Loading
Loading