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
3 changes: 2 additions & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ GitHub の issue / pull request / release / documentation / **GitHub Wiki page**
3. **doc 本文取得** — `include_content: true` を指定すると、`type="doc"` 結果の本文が GitHub contents API 経由で取得され、該当行の `content` フィールドに inline されます。API fan-out を抑えるため先頭の数件に絞られます。従来の `get_doc_content` を置き換えます。
4. **保存済み本文の取得** — `vector_ids`(先行する結果が持つ `vector_id`)を渡します。索引済みの全 type がその行の本文を返します——doc だけでなく issue / PR / comment / review / release / diff も対象です。`search` であたりを付けたあと本文を読むための `gh` / grep の一往復が不要になります。D1 から返すので GitHub API は呼びません。返る文字列が何であって何でないかは下記「保存済み本文の取得」を参照してください。

structured filter (`repo` / `state` / `labels` / `milestone` / `assignee` / `type`) は、保存済み本文の取得を除くすべてのモードで有効です。保存済み本文の取得では行をサーバが選ぶのではなく呼び出し側が名指しするため、filter は適用しません。
structured filter (`repo` / `path_prefix` / `state` / `labels` / `milestone` / `assignee` / `type`) は、保存済み本文の取得を除くすべてのモードで有効です。`path_prefix` だけは意図的に狭く、`type: "doc"` と組み合わせて repository-relative directory を ranking 前に選びます。保存済み本文の取得では行をサーバが選ぶのではなく呼び出し側が名指しするため、filter は適用しません。

search モードは「1件もマッチしなかったフィルタ」を `filters_unmatched` に載せます (常に存在し、すべて成立していれば `[]`)。`repo` はフルスラッグ `owner/repo` の完全一致なので、短いリポジトリ名を渡すと母集合が空になり、本当にヒットゼロだった場合と同じ形のレスポンスが返ります。このフィールドがその2つを区別します。効くのは多段のエージェンティック検索で、ゼロが正常な中間結果として読まれてしまい、フィルタ不成立が表に出ないまま終わる場面です。

Expand All @@ -120,6 +120,7 @@ bot (`sender.login` が `[bot]` で終わる) と trim 後 10 文字未満の bo
|------|----|------|
| `query` | string (省略可) | 自然言語クエリ。省略または空文字で scan モード。 |
| `repo` | string | repository で絞り込み。フルスラッグ (`owner/repo`) の完全一致。短いリポジトリ名は1件もマッチせず、search モードはそれをレスポンスの `filters_unmatched` に `"repo"` として報告します。 |
| `path_prefix` | string | repository-relative directory prefix で doc を絞り込み。`type: "doc"`、末尾 `/`、UTF-8で64 byte以内が必須。 |
| `state` | `"open"` / `"closed"` / `"all"` | state で絞り込み (既定 `all`)。 |
| `labels` | string[] | label 名で AND 絞り込み。 |
| `milestone` | string | milestone title で絞り込み。 |
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Four modes are selected by the parameter set:
3. **Doc / wiki content fetch** — set `include_content: true`. For result rows whose `type` is `"doc"`, the raw file content is fetched from the GitHub contents API; for `type: "wiki_doc"` rows, the raw markup is fetched from `raw.githubusercontent.com/wiki/`. Both are inlined as a `content` field. Capped at the first few rows of each type to bound API fan-out. This subsumes the previous `get_doc_content` tool.
4. **Stored-content fetch** — pass `vector_ids` (the `vector_id` values carried by earlier results). Every indexed type returns the body text the index already holds for that exact row — issues, PRs, comments, reviews, releases and diffs included, not just docs — so locating something with `search` and then reading it no longer costs a round trip through `gh` or grep. Served from D1: no GitHub API call is made. See [Stored-content fetch](#stored-content-fetch) below for what the returned text is and is not.

Structured filters (`repo`, `state`, `labels`, `milestone`, `assignee`, `type`) apply in every mode except stored-content fetch, where the rows are named rather than selected.
Structured filters (`repo`, `path_prefix`, `state`, `labels`, `milestone`, `assignee`, `type`) apply in every mode except stored-content fetch, where the rows are named rather than selected. `path_prefix` is intentionally narrower: it is valid only with `type: "doc"` and selects one repository-relative directory before ranking.

Search mode reports filters that matched nothing at all in `filters_unmatched` (always present, `[]` when every filter matched something). `repo` is an exact match on the full `owner/repo` slug, so a bare repository name selects an empty population and returns a response shaped exactly like a genuine zero-hit search — this field is what separates the two. It matters most in multi-step agentic search, where a zero reads as a normal intermediate result and the mis-specified filter would otherwise never surface.

Expand All @@ -120,6 +120,7 @@ Bot-authored comments (`sender.login` ending in `[bot]`) and comments shorter th
|------|------|-------------|
| `query` | string (optional) | Natural-language query. Omit or empty = scan mode. |
| `repo` | string | Filter by repository — full slug (`owner/repo`), exact match. A bare repository name matches nothing; search mode reports that as `"repo"` in the response's `filters_unmatched`. |
| `path_prefix` | string | Filter docs by repository-relative directory prefix. Requires `type: "doc"`, a trailing `/`, and at most 64 UTF-8 bytes. |
| `state` | `"open"` \| `"closed"` \| `"all"` | Filter by state (default `all`). |
| `labels` | string[] | Filter by label names (AND). |
| `milestone` | string | Filter by milestone title. |
Expand Down
16 changes: 10 additions & 6 deletions docs/0-requirements.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,10 @@ Vectorize は hybrid retrieval の dense 側を担う。次の metadata を伴

Metadata index(10/10 枠使用):

- Pre-filter 対応: repo, type, state, milestone
- 将来の pre-filter 用に格納: label_0, label_1, label_2, label_3, assignee_0, assignee_1
- Pre-filter 対応: repo, type, state, milestone, doc_path
- 将来の pre-filter 用に格納: label_0, label_1, label_2, label_3, assignee_0

`assignee_1` は vector metadata に保存し続け、現行 post-filter からも利用できるが、metadata index は持たない。platform の10枠上限に対する優先順位として、未使用だった将来用 `assignee_1` pre-filter 枠を `doc_path` へ振り替え、directory 単位の文書検索が ranking 前に dense 候補母集合を絞れるようにする。

Vectorize の metadata filter はフィールド間で AND のみサポートし、OR は非対応。`label_0 = "bug" OR label_1 = "bug"` のようなクエリは表現できない。そのため labels / assignees は overfetch + post-filter で recall を改善している。Vectorize が OR または `$in`-across-fields をサポートした時点で、個別フィールドは即座に pre-filter 化可能。

Expand Down Expand Up @@ -390,7 +392,7 @@ retrieval layer は hybrid search(dense + sparse)+ cross-encoder rerank + st
想定フロー:

1. query の embedding を Workers AI BGE-M3 で生成
2. structured params から Vectorize filter(dense 側)と D1 SQL WHERE(sparse 側)を同時構築(repo, state, type, milestone pre-filter)
2. structured params から Vectorize filter(dense 側)と D1 SQL WHERE(sparse 側)を同時構築(repo, state, type, milestone と文書 `path_prefix` は両側で pre-filter)
3. 内部 topK を常にオーバーフェッチ(requestedTopK × 5, max 50)。条件なしなのは、8 の entity 集約がどの経路でも複数行を 1 件に畳むため、rerank 無効時でも候補プールが top_k を上回っていなければ要求件数を満たせないからである。reranker は最大 50 件まで処理
4. dense (Vectorize.query) と sparse (D1 FTS5 MATCH + BM25) を並列実行
5. 両 ranker の結果を Reciprocal Rank Fusion(RRF、k=60)で合成
Expand Down Expand Up @@ -527,13 +529,15 @@ Returns:
- 同一実体の他の行を吸収した結果には `same_entity`(Entity Aggregation 参照)。`top_k` は行数ではなく実体数で数える
- top-level metadata: `fusion`、`dense_candidates`、`sparse_candidates`、`rerank_requested`、`rerank_applied`、`filters_unmatched`

**フィルタ不成立(`filters_unmatched`).** `repo` はフルスラッグ(`owner/repo`)の完全一致である——dense 側は Vectorize metadata の `$eq`、sparse 側は `d.repo = ?`。短いリポジトリ名を渡すと1行にもマッチせず、返るレスポンスは「本当にヒットが無かった」場合と同じ形になる。`filters_unmatched` がこの2つを分ける: search mode では常に存在し、`[]` は適用した全フィルタが空でない母集合を選べたこと(つまり `count: 0` は真にヒットゼロ)を意味し、名前が載っていればそのフィルタの母集合が空、すなわち誤っているのはクエリではなくフィルタの値である。
**フィルタ不成立(`filters_unmatched`).** `repo` はフルスラッグ(`owner/repo`)の完全一致である——dense 側は Vectorize metadata の `$eq`、sparse 側は `d.repo = ?`。`path_prefix` は repository-relative path が指定 directory prefix で始まる doc 行を選ぶ。短いリポジトリ名や、doc 行を1件も選ばない path prefix を渡すと、返るレスポンスは「本当にヒットが無かった」場合と同じ形になる。`filters_unmatched` がこの2つを分ける: search mode では常に存在し、`[]` は適用した全フィルタが空でない母集合を選べたこと(つまり `count: 0` は真にヒットゼロ)を意味し、名前が載っていればそのフィルタの母集合が空、すなわち誤っているのはクエリではなくフィルタの値である。

区別にフィールドを割く理由は、エージェンティックな多段検索が silent zero のコストを反転させるからである。単発検索ならゼロは呼び出し側が見に行く行き止まりだが、検索ループの中では「この角度には何も無かった」という正常な中間結果として消費されて次へ回る。フィルタ不成立が表に出ないまま、クエリ予算を1回分、偽陰性に使って終わる。

判定は存在確認クエリ(`SELECT 1 FROM search_docs WHERE repo = ? LIMIT 1`)で、候補集合が空のときだけ走る——候補が1件でもあればフィルタが成立した証拠なので、追加の読みが hot path に乗ることはない。プローブ自体が失敗した場合は「観測していない不成立」を主張せず、何も報告しない。プローブ対象は `repo` のみ: もっともらしく見える誤値(フルスラッグに対する短いリポジトリ名)が存在するのはこのフィルタだからである。短い名前からフルスラッグへの自動解決は意図的に非スコープ——複数リポジトリにマッチする名前の曖昧解決を設計する必要がある。
判定は候補集合が空のときだけ存在確認クエリを走らせる——候補が1件でもあればフィルタが成立した証拠なので、追加の読みが hot path に乗ることはない。まず repository を確認し、成立した場合だけその repository 内の doc path を確認する。誤った repository に対して、別の repository では妥当かもしれない path まで誤りと報告しないためである。プローブ自体が失敗した場合は「観測していない不成立」を主張せず、何も報告しない。短い名前からフルスラッグへの自動解決は意図的に非スコープ——複数リポジトリにマッチする名前の曖昧解決を設計する必要がある。

**scan mode(query 空).** Vectorize / FTS5 / reranker を経由せず、structured store の recency endpoint から集約する。`since` / `until` と文書 `path_prefix` は store 側へ push down されるので、窓に対象行があれば、その窓がどれだけ古くても、prefix 外に新しい文書が何件あっても返る。`since` 省略時の既定は `until` の 7 日前(`until` も省略時は現在の 7 日前)。`until` だけ指定した問い合わせが「下限が上限より新しい空窓」に潰れないための既定である。

**scan mode(query 空).** Vectorize / FTS5 / reranker を経由せず、structured store の recency endpoint から集約する。`since` / `until` は store 側へ push down されるので、窓に行があれば、その窓がどれだけ古くても返る。`since` 省略時の既定は `until` の 7 日前(`until` も省略時は現在の 7 日前)。`until` だけ指定した問い合わせが「下限が上限より新しい空窓」に潰れないための既定である
**文書 path prefix.** `path_prefix` は `type: "doc"` と組み合わせた場合だけ有効。repository-relative directory を表し、末尾 `/` が必須で、先頭 `/`、backslash、NUL、空 segment、`.` / `..` segment を含めず、Vectorize が string metadata の先頭64 byteだけを索引する制約に合わせUTF-8で64 byte以内とする。dense / sparse は `doc_path` に同じ半開 lexical range を適用し、scan は range を doc store query へ押し下げ、返った行を `startsWith` でも確認する。fetch mode は `vector_ids` が行を直接名指しするため、従来どおり無視する

scan mode は top-level に `truncated` を追加する。窓が応答に載せた以上の行を持つとき true になる(endpoint が cap 一杯まで返した、または merge 後の件数が `top_k` を超えた)。これが「該当なし」と「読み切れていない」を呼び出し側に区別させる: 返った最古の行の時刻を次の `until` にして遡ればよい。両者を区別できない欠損調査ツールは、存在しない欠損を報告し実在する取り込みを見落とす——#178 の再検証で 1 日に 2 度踏んだ誤りがこれである。

Expand Down
Loading
Loading