Skip to content

Report unmatched repo filter in search response - #220

Merged
liplus-lin-lay merged 1 commit into
mainfrom
219-search-returns-a-silent-zero-when-the-repo-filter-matches-nothing-indistinguishable-from-no-hits
Aug 6, 2026
Merged

Report unmatched repo filter in search response#220
liplus-lin-lay merged 1 commit into
mainfrom
219-search-returns-a-silent-zero-when-the-repo-filter-matches-nothing-indistinguishable-from-no-hits

Conversation

@liplus-lin-lay

Copy link
Copy Markdown
Member

Closes #219

目的

searchrepo フィルタが1件もマッチしなかった場合を、結果ゼロと区別できる形でレスポンスに明示する。検索ロジックは一切変更せず、観測性のみを足す。

何が問題だったか

repo はフルスラッグ (owner/repo) の完全一致である。dense 側は Vectorize metadata の $eq、sparse 側は d.repo = ?。短いリポジトリ名を渡すと両方の候補集合が空になり、本当にヒットが無かった場合とまったく同じ形のレスポンスが返る。

単発検索ならゼロは行き止まりとして目に付くが、エージェンティックな多段検索ではゼロは「この角度には何も無かった」という正常な中間結果として消費される。フィルタを疑う契機がループに吸収され、偽陰性のままクエリ予算だけを1回分焼く。

実装

  • src/fts.ts
    • repoHasIndexedRowsSELECT 1 FROM search_docs WHERE repo = ? LIMIT 1repo は既存の index (idx_search_docs_repo) が効き、FTS5 は経由しない。
    • detectUnmatchedFilters — 判定本体。候補集合が空のときだけプローブする (候補が1件でもあればフィルタ成立の証拠なので、追加の読みが hot path に乗らない)。プローブ自体が失敗したら「観測していない不成立」は主張せず空を返す。
  • src/mcp.ts — search モードのレスポンスに filters_unmatched を常時付与 ([] は全フィルタ成立 = count: 0 が真のヒットゼロ)。tool description と repo の description に完全一致であることを明記。
  • mcp-server/server/tools.js — 静的スキーマのミラーを同期。クライアントが実際に読む description はこちらなので、完全一致の注意はここに載らないと届かない。
  • docs / README (ja/en)filters_unmatched の意味、プローブ条件、非スコープを記載。

プローブ対象は repo のみ。もっともらしく見える誤値 (フルスラッグに対する短いリポジトリ名) が存在するのはこのフィルタで、state / type は enum 制約があり、milestone / assignee に同種の near-miss 形はない。

テスト

「フィルタ不成立」と「ヒットゼロ」が別のレスポンスになることを二面で検証:

  • src/fts.test.ts (node) — 判定ロジック。誤スラッグ→["repo"] / 実在 repo のヒットゼロ→[] / 候補ありならプローブしない / repo フィルタ無しならプローブしない / プローブ失敗時は何も報告しない。
  • src/fts.workers.test.ts (実 D1) — SQL。同じ0件でも誤スラッグと無関係クエリはプローブで分かれること、および完全一致 (前方・後方一致しない) の確認。
  • mcp-server/test/search-tool-schema.test.js — proxy スキーマの repo description が完全一致要件と filters_unmatched に言及していること。

ローカルで npm test (node 213 + workers 60)、npx tsc --noEmitnode scripts/check-schema-drift.mjsnpx wrangler deploy --dry-run すべて green。

非スコープ

  • 短いスラッグからフルスラッグへの自動解決はしない (複数リポジトリにマッチする名前の曖昧解決が必要で一段重い)。
  • fusion / rerank / 候補数には触れていない。
  • scan mode は対象外。scan は Durable Object の recency endpoint から集約しており search_docs とは母集合が別なので、同じプローブでは答えられない。

リリース種別

patch。既存フィールドの意味を変えず追加のみで、検索結果そのものは変わらない。

🤖 Generated with Claude Code

search の repo フィルタが1件もマッチしなかった場合を、レスポンスの
filters_unmatched フィールドで明示する。repo はフルスラッグ (owner/repo) の
完全一致なので、短いリポジトリ名を渡すと候補集合が空のまま「該当なし」と
同じ形のレスポンスが返り、呼び出し側からフィルタ不成立とヒットゼロが
区別できなかった。多段のエージェンティック検索ではゼロが正常な中間結果と
して消費されるため、この silent zero は偽陰性のまま流れてクエリ予算だけを
焼く。

変更点:
- fts.ts: repoHasIndexedRows (search_docs への LIMIT 1 存在確認) と
  detectUnmatchedFilters (候補ゼロのときだけプローブ/プローブ失敗時は
  観測していない不成立を主張しない) を追加
- mcp.ts: search モードのレスポンスに filters_unmatched を常時付与し、
  tool schema の repo description に完全一致であることを明記
- mcp-server/server/tools.js: 静的スキーマのミラーを同期
- テスト: 「フィルタ不成立」と「ヒットゼロ」が別レスポンスになることを
  node 側 (判定ロジック) と workers 側 (実 D1 の SQL) の両面で検証
- docs / README (ja/en): filters_unmatched の意味と非スコープを記載

検索ロジック (fusion / rerank / 候補数) は一切変更していない。観測性のみ。
短いスラッグからフルスラッグへの自動解決は非スコープ。

Refs #219

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 a6e1305 Aug 06 2026, 04:37 AM

@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.

セルフレビュー結果: pass

受入基準チェック

issue #219 の要求 実装 判定
フィルタ不成立を結果ゼロと区別できる形で返す filters_unmatched を search モードのレスポンスに常設、[] が「全フィルタ成立」 満たす
検索ロジックは変更しない fusion / rerank / 候補数に変更なし。追加はプローブ1本とレスポンスの1フィールド 満たす
最小スコープは repo 1本 repo のみプローブ。理由(もっともらしい誤値が存在する唯一のフィルタ)をコードコメントに明記 満たす
短縮名→フルスラッグの自動解決はしない 非スコープとして維持、コメントで理由を明示 満たす
テスト1本(不成立とヒットゼロが別レスポンス) 3層(決定ロジック node / 実 D1 workers / proxy スキーマ)で実装 満たす

設計の検証

  • プローブが hot path に乗らない: candidateCount === 0 のときだけ実行。候補が1件でもあればフィルタ成立の証拠なので追加の読みが発生しない。非対称の根拠がコメントに書かれている。
  • プローブ失敗を所見にしない: catch で握って空リストを返す。観測していない不成立を主張しない安全側。
  • 候補数の取り方: dense(Vectorize metadata $eq)と sparse(d.repo = ?)の両方が同じ完全一致なので、repo 誤値は両側を同時に空にする。合計を条件にするのが正しい。
  • proxy ミラーの同期: mcp-server/server/tools.js はクライアントが tools/list で実際に読む記述面。ここを同期しないと警告が届かない。同期済みで、スキーマテストも追加されている。

スコープ逸脱

なし。対象ファイル外への波及は proxy ミラーとドキュメント4本のみで、いずれも issue の対象ファイル列挙に含まれる面。

持ち越し

scan モード(query 省略)に同じ silent zero が残る。マージ前に実測で確認し、#221 として起票済み。

repo mode count filters_unmatched
liplus-language(短縮) scan 0 フィールド自体が無い
Liplus-Project/liplus-language scan 3 フィールド自体が無い

PR #220 がこれを除外した判断は妥当。scan は Durable Object の recency endpoint から集約するため母集合が異なり、search_docs へのプローブでは答えられない。抜けではなく別作業。

次の期待

auto モードにつき人間ゲートなし。リリース種別 patch(既存フィールドの意味を変えず追加のみ)。self-review pass によりマージへ進む。

@liplus-lin-lay
liplus-lin-lay merged commit 0f90dda into main Aug 6, 2026
3 checks passed
@liplus-lin-lay
liplus-lin-lay deleted the 219-search-returns-a-silent-zero-when-the-repo-filter-matches-nothing-indistinguishable-from-no-hits branch August 6, 2026 04:40
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.

search returns a silent zero when the repo filter matches nothing, indistinguishable from no hits

1 participant