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
1 change: 1 addition & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,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以内が必須。 |
| `doc_context` | `prefix` / `matched` | 既定 prefix。matched は非空 query と doc/wiki_doc が必須、scan/vector_ids はエラー。準備済み全文を検索し、rerank:false でも検証済みの有界 matched_context を返す。coverage は語一致前の scope を数え、未準備の後半は検索できない。live include_content は別欄。[契約](docs/7-matched-context.ja.md)。 |
| `state` | `"open"` / `"closed"` / `"all"` | state で絞り込み (既定 `all`)。 |
| `labels` | string[] | label 名で AND 絞り込み。 |
| `milestone` | string | milestone title で絞り込み。 |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,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`. Pass it whenever the question is about a known repository — without it, a near-duplicate from another repository can rank first; leave it unset only when the question spans repositories. |
| `path_prefix` | string | Filter docs by repository-relative directory prefix. Requires `type: "doc"`, a trailing `/`, and at most 64 UTF-8 bytes. |
| `doc_context` | `prefix` / `matched` | Default `prefix`. Explicit `matched` requires nonempty query and `type: doc` or `wiki_doc`, rejects scan/vector_ids, and searches prepared indexed full sources. Returns bounded verified `matched_context` even with `rerank: false`. Coverage counts the filtered indexed scope before term matching; unprepared tails remain unsearchable. Live `include_content` stays separate. |
| `state` | `"open"` \| `"closed"` \| `"all"` | Filter by state (default `all`). |
| `labels` | string[] | Filter by label names (AND). |
| `milestone` | string | Filter by milestone title. |
Expand Down
9 changes: 9 additions & 0 deletions docs/0-requirements.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -690,3 +690,12 @@ source の `docs/_Sidebar.md` は既存 Feedback Memory EN/JA ページを参照
#284 は既定 off の保存・operator 基盤を追加する。[全文 sparse source 契約](6-full-sparse-sources.ja.md)に従い prefix corpus、既定検索、graph、vector 数を維持する。matched 検索は #281 の別作業。

sparse operator の 2 経路は既存 Worker secret と `GITHUB_TOKEN` ヘッダの完全一致で認証し、secret が未設定・空なら401。専用 token と Authorization bearer は受け入れない。GitHub source 取得は従来の `GITHUB_TOKEN` を使う。既定 off の更新を含め、prefix revision CAS により版修復と owner 保存を同一 D1 batch に結ぶ。
# 明示的な文書一致コンテキスト (#281)

legacy code/trigram prefix 証拠は、未準備と準備済み owner の両方に共通 scope を LIMIT 前に適用して保持する。独立した全文 code storage/index は追加しない。code strict arm を他 arm と同じ順位定数で統合し、一資料一票を維持する。全文 nat の位置がない code-only prefix 候補は `code_only_prefix` 理由で原文抜粋をせず既存本文を返す。coverage は先行 SELECT の観測値であり、後続候補 batch と同一 snapshot を保証しない。

coverage の total=0 は metadata scope に対象なし。full=total による status=complete でも全 repo の準備完了を意味しない。total>0 で候補0件なら対象資料はあるが検索語に一致しない状態であり、両者は件数から区別する。

`doc_context: matched` は非空 query と doc/wiki_doc に限定する。省略および prefix は従来契約を維持する。検索は search_docs の共通 scope 内で、有効な full owner の全文 FTS と、それ以外の prefix FTS を LIMIT 前に分割する。strict を relaxed より先にし、各母集団内の順位を同じ RRF 定数で合成する。同一 vector は一票だけとする。coverage は語との一致前の対象全体を数え、未準備の後半は検索できない。全文読取り失敗時には unavailable を明示し、準備済みを除外しない従来 prefix 候補へ戻る。

抜粋は postfilter と base 本文供給、非空判定後の最大50件に限定する。狭い snippet/版/bytes を先に読み、keyword 順位と vector_id 順で raw 合計1MiB、資料 raw256KiB/segmented512KiB/owner1MiB を割り当てる。raw は最大5件ずつ取得し、位置 map は1件ずつ検証する。原文 path+2改行+body の連続1400 UTF16 units 以下を、版・hash・保存 segmentation 完全一致・一意 fragment・surrogate 境界検証後にモデルと matched_context の両方へ渡す。不成立は資料を残して prefix と理由を返す。indexed 抜粋と live include_content は別欄とする。予算は工学的初期値であり Cloud CPU/heap 限界の証明ではない。取り込みフラグ、保存/backfill、graph と frozen corpus は変更しない。
9 changes: 9 additions & 0 deletions docs/0-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -695,3 +695,12 @@ The source `docs/_Sidebar.md` must link the existing Feedback Memory EN/JA pages
Issue #284 adds a default-off storage/operator foundation; its contract is [Full sparse sources](6-full-sparse-sources.ja.md). It preserves the prefix corpus, default retrieval, graph and vector count. Matched retrieval remains #281.

The two sparse operator endpoints require an exact match between the `GITHUB_TOKEN` header and the existing Worker `GITHUB_TOKEN` secret, and return 401 when the secret is unset or empty. Dedicated tokens and Authorization bearer credentials are not accepted; GitHub source fetching retains `GITHUB_TOKEN`. Version repair and owner writes share a D1 batch guarded by the prefix revision, including default-off updates.
# Explicit matched document context (#281)

Legacy code/trigram prefix evidence is retained for both unprepared and prepared owners, with the same canonical metadata scope before LIMIT. No full code storage/index is added. The strict code arm uses the same rank constant and deduplicates to one vote per vector. Code-only prefix hits without a full natural-language position keep their base text with `code_only_prefix` rather than claiming a matched excerpt. Coverage is an earlier SELECT observation, not a guaranteed shared snapshot with the subsequent candidate batch.

Coverage total=0 means the metadata scope is empty; its status=complete (full=total) does not imply repository-wide preparation. A nonempty scope with no candidates instead means no term matches. Counts distinguish these cases.

`doc_context: matched` requires a nonempty query and doc/wiki_doc type; prefix and omission preserve existing retrieval. The canonical metadata scope separates current full owners from prefix-only owners before LIMIT. Strict precedes relaxed; independent corpus ranks use the same RRF constant and each vector contributes once. Coverage counts the scope before term matching. Unprepared tails cannot be searched; unavailable full retrieval falls back to every legacy prefix owner.

After postfilters, base text supply and nonempty checks, at most 50 candidates may resolve excerpts. Narrow snippets/version/bytes precede raw reads, with a 1 MiB raw request budget, 256 KiB raw / 512 KiB segmented / 1 MiB owner per source, batches of five and one position map at a time. Verified excerpts are contiguous original slices of at most 1400 UTF16 units, with exact path, source version, body/source/raw/excerpt hashes, saved segmentation equality, unique fragments and safe surrogate boundaries. The model and matched_context receive the same text; live include_content remains separate. Failed validation retains the prefix candidate and supplies a reason. Budgets are initial engineering limits, not measured Cloud CPU/heap guarantees. Storage, backfill, ingest enablement, graph and frozen corpora remain unchanged.
2 changes: 2 additions & 0 deletions docs/6-full-sparse-sources.ja.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Doc/wiki 全文 sparse source 基盤

明示的な `doc_context: matched` 読取りは [一致コンテキスト](7-matched-context.ja.md) が所有する。保存フラグ false でも準備済み source を読める。省略/prefix の既定検索は以下の旧経路を維持する。

Issue #284 の保存・運用契約。通常検索への接続は #281 の別作業。

`DOC_SPARSE_CONTEXT_ENABLED` は省略時 false。false では ingest は全文の生成・取得・追加 D1 呼出しを行わない。8000 UTF-16 unit の prefix、通常 nat FTS corpus/IDF、1 資料 1 vector、fetch、graph は維持する。既定 off の ingest と検索は migration 0009 の新 schema にアクセスしない。migration 0009 は operator 経路と enable より先に適用する。検索だけの旧経路は全文テーブルを読まない。
Expand Down
19 changes: 19 additions & 0 deletions docs/7-matched-context.ja.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 文書の一致コンテキスト

`doc_context: matched` は `query` と `type: doc|wiki_doc` を明示する。`rerank: false` や単一候補でも一致抜粋を返す。省略/`prefix` は従来の検索経路を使う。既存の全文保存フラグは取り込みの制御であり、この読取りの条件にはしない。準備済み少数 source でも利用できる。

`doc_context_coverage` の total/full/prefix_only/unprepared/out_of_range/stale は query 語との一致前の metadata scope 件数。status は complete/partial/unprepared/unavailable。全文索引読取り失敗時は unavailable とし、準備済みも含む従来 prefix 候補を返す。未準備資料の後半は検索できない。

coverage は候補 batch の直前に別 SELECT で読むため、同時更新時には候補と件数の観測時点が少し異なる。候補の全文/prefix 分割は同一 batch で読み、raw を再読した版は再検証する。

total=0 は metadata scope の対象なし。status=complete は full=total の条件なので、空 scope でも complete になるが全 repo 準備完了の意味ではない。total>0 で結果0件なら対象資料は存在し、語との一致がない状態。件数で区別する。

legacy code/trigram prefix arm は、未準備/準備済み owner のどちらも検索する補助証拠。共通 scope を LIMIT 前に適用し、strict の順位として統合して一資料一票にする。全文 code index は追加しない。全文 nat の位置を得られない code-only prefix hit は `code_only_prefix` と既存本文を返し、matched_context を生成しない。

metadata 正本 search_docs の repo/type/state/milestone/path_prefix/labels/assignee/since/until を共通 SQL で適用する。full owner は版・取得時点・revision・index_version の一致が必要。それ以外を prefix 系統に含める。strict/relaxed 各 tier 内の順位を 1/(60+rank) とし、異なる corpus の BM25 生点数を比較せず、同点は vector_id、同一 vector は一票。全 sparse を internalTopK 最大50へ絞り、既存 dense/sparse 融合へ渡す。

`matched_context.text` は indexed 原文 path+2改行+body の連続 slice。start/end はその UTF16 座標、最大1400 units、source_version と body/source/raw/excerpt hash を伴う。同じ text がモデル入力になる。`include_content` の最新 GitHub 本文とは別欄。graph_results、stored fetch は従来の所有を維持する。

候補の postfilter/base 本文/非空判定後に最大50件を選び、keyword 順位で狭い source 情報を検査する。raw 合計1MiB、資料 raw256KiB/segmented512KiB/owner1MiB、raw batch 最大5、位置 map 同時1。大きい候補は skip し後続を扱う。予算は初期工学的制限であり、実 heap/GC/Cloud CPU 適合の証明ではない。

版や hash、path、byte 数、保存 segmentation と再計算が完全一致し、一意 fragment が安全な surrogate 境界に写像される場合だけ抜粋を返す。曖昧fragment・更新/削除競合・未準備・範囲外・dense-only/位置なし・予算・読取り失敗等では matched_context_fallback と既存本文を維持し、資料を捨てない。位置 map は保存しない。取り込みフラグ変更、全 corpus backfill、secret、production 操作をこの検索機能へ含めない。
9 changes: 9 additions & 0 deletions mcp-server/server/search-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"doc_context": {
"default": "prefix",
"description": "Document context. Default prefix preserves existing search. matched requires a nonempty query and type=doc or wiki_doc; scan and vector_ids are rejected. Searches prepared indexed full sources and returns a verified bounded matched_context separately from live include_content, even with rerank=false. doc_context_coverage reports the filtered indexed population before term matching; total=0 means an empty metadata scope, distinct from no term matches. Coverage is read before the candidate batch and may differ under concurrent updates. Unprepared tails cannot be searched. Legacy code-only prefix evidence is retained with code_only_prefix fallback when no full natural-language position exists. Failures retain prefix candidates with explicit fallback reasons.",
"type": "string",
"enum": [
"prefix",
"matched"
]
},
"query": {
"description": "Natural language search query. When omitted or empty, the tool switches to metadata-only scan mode and results are ordered by the timestamp implied by sort (default sort=\"updated_desc\" for empty query).",
"type": "string",
Expand Down
61 changes: 36 additions & 25 deletions src/fts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,41 @@ export interface FtsFilter {
state?: "open" | "closed" | "published" | "active";
milestone?: string;
pathPrefix?: string;
labels?: string[];
assignee?: string;
since?: string;
until?: string;
}

/** Shared canonical metadata scope; matched retrieval also pushes postfilters before LIMIT. */
export function ftsScope(filter?: FtsFilter): { sql: string; params: (string | number)[] } {
const clauses: string[] = [];
const params: (string | number)[] = [];
for (const [key, column] of [['repo', 'repo'], ['type', 'type'], ['state', 'state'], ['milestone', 'milestone']] as const) {
if (filter?.[key]) { clauses.push(`d.${column} = ?`); params.push(filter[key]!); }
}
if (filter?.pathPrefix) {
const { lower, upper } = pathPrefixRange(filter.pathPrefix);
clauses.push('d.doc_path >= ? AND d.doc_path < ?'); params.push(lower, upper);
}
for (const label of filter?.labels ?? []) {
clauses.push(csvContains('labels'));
params.push(label);
}
if (filter?.assignee) {
clauses.push(csvContains('assignees'));
params.push(filter.assignee);
}
if (filter?.since) { clauses.push('d.updated_at >= ?'); params.push(filter.since); }
if (filter?.until) { clauses.push('d.updated_at < ?'); params.push(filter.until); }
return { sql: clauses.length ? ` AND ${clauses.join(' AND ')}` : '', params };
}

function csvContains(column: 'labels' | 'assignees'): string {
return `EXISTS (WITH RECURSIVE parts(value,rest) AS (
SELECT '',d.${column} || ',' UNION ALL
SELECT substr(rest,1,instr(rest,',')-1),substr(rest,instr(rest,',')+1) FROM parts WHERE rest<>''
) SELECT 1 FROM parts WHERE trim(value)=?)`;
}

/**
Expand Down Expand Up @@ -421,31 +456,7 @@ export async function queryFts(

// Build dynamic WHERE clause for metadata filters. Shared by both branches; only
// the leading MATCH parameter differs, so it is bound per branch below.
const whereClauses: string[] = [];
const params: (string | number)[] = [];
if (filter?.repo) {
whereClauses.push("d.repo = ?");
params.push(filter.repo);
}
if (filter?.type) {
whereClauses.push("d.type = ?");
params.push(filter.type);
}
if (filter?.state) {
whereClauses.push("d.state = ?");
params.push(filter.state);
}
if (filter?.milestone) {
whereClauses.push("d.milestone = ?");
params.push(filter.milestone);
}
if (filter?.pathPrefix) {
const { lower, upper } = pathPrefixRange(filter.pathPrefix);
whereClauses.push("d.doc_path >= ? AND d.doc_path < ?");
params.push(lower, upper);
}
const whereSql =
whereClauses.length > 0 ? ` AND ${whereClauses.join(" AND ")}` : "";
const { sql: whereSql, params } = ftsScope(filter);

// UNION ALL arms so each tokenizer contributes hits. The nat arms read the v3 index
// created by migration 0006 (external content = the segmented `content_fts` column);
Expand Down
Loading
Loading