Skip to content

fix(retrieval): collapse rows of the same entity so top_k counts entities [mcp, docs, tests] - #201

Merged
liplus-lin-lay merged 1 commit into
mainfrom
189-specretrieval-top-k-is-crowded-by-multiple-rows-of-the-same-underlying-entity
Aug 2, 2026
Merged

fix(retrieval): collapse rows of the same entity so top_k counts entities [mcp, docs, tests]#201
liplus-lin-lay merged 1 commit into
mainfrom
189-specretrieval-top-k-is-crowded-by-multiple-rows-of-the-same-underlying-entity

Conversation

@liplus-lin-lay

@liplus-lin-lay liplus-lin-lay commented Aug 2, 2026

Copy link
Copy Markdown
Member

Closes #189

何を変えたか

融合・rerank・time sort の後、filtered.slice(0, requestedTopK) の手前に集約段を追加した(src/mcp.ts)。判定ロジックは src/aggregate.ts に切り出してある。

  • entity keydoc 行と同一ファイルの diff 行は file:{repo}:{doc_path ?? file_path}、issue / PR とその comment / review は thread:{repo}:{number}。それ以外(wiki_doc / release / key となるフィールドが欠けた行)は row:{vector_id} で畳まれない。
  • 代表 — group の最上位行。最新版は固定しない。
  • 返却 — 代表 item に same_entity { count, others[] } を付す。count は代表を含むので必ず 2 以上、1 件以上畳んだ場合のみ出る。フィールド追加のみで既存フィールドは変えていない。
  • overfetch — 条件付き(post-filter / time filter / rerank 有効時)だったのを無条件にした。集約がどの経路でも複数行を 1 件に畳むので、rerank: false でも候補プールが top_k を上回っていないと要求件数を返せない。

畳む軸を referent に限った理由

同じ対象の複数の版を畳んでも対象の数は減らないので、集約が独立した情報を隠すことはない。一方「作業」で畳むと、1 つの commit が触った別々のファイルが 1 枠になり、実際に独立した対象が隠れる。したがって key は commit SHA を持たない。issue #1317 と PR #1318 も 2 実体のまま残す(両者を結ぶ Closes #N は索引に無く、入れるには索引側の変更が要る=制約違反)。

代表に最新版を固定しないのは「いつ変わったか」を問うクエリへの答えを消さないため。そのクエリでは該当する古い diff が最上位に来るので、それが代表として残る。

repo 横断の複製(軸 (c))は判定材料が索引に無いため対象外。別 issue に切り出した(#202)。

テスト

src/aggregate.test.ts(node pool、14 件)。

  • doc + 複数 commit の diff が 1 実体になる
  • 同一 commit が触った別々のファイルが畳まれない(referent / event の線の固定)
  • issue #1317 と PR #1318 が畳まれない
  • issue + issue_comment / PR + pr_review + pr_review_comment が 1 実体になる
  • wiki_doc が file namespace に入らない、key となるフィールドが欠けた行は畳まれない
  • 最上位が古い diff のとき、それが代表のまま残る(集約が情報を隠していないことの負の対照)
  • spec(retrieval): top-k is crowded by multiple rows of the same underlying entity #189 の実測 2 クエリの候補プールを再現した重複率の回帰テスト 2 本

想定される観測

top_k の意味が「行数」から「実体数」に変わる。呼び出し側が要求した件数は従来どおり返る。

検証

npm test(118 + 53 pass)、tsc --noEmitnode scripts/check-schema-drift.mjswrangler deploy --dry-runmcp-servernpm test(10 pass)をローカルで実行済み。

…ties [mcp, docs, tests]

Add an entity aggregation stage between the reorder stages and the trim in
src/mcp.ts. Rows are grouped by the referent they point at: a file's `doc`
row and its `diff` rows across commits become one result, an issue or PR and
its comments / reviews become one result. The representative is the
highest-ranked row of the group, and the rows it absorbed are attached as
`same_entity { count, others[] }` rather than dropped. The overfetch is now
unconditional so `rerank: false` also fills top_k after the collapse.

同一の実体が複数行として索引されるため、`top_k: 10` の実測で独立した情報が 6
前後しか返っていなかった(本番索引、2026-08-01、rrf + rerank)。diff を索引し
続けるのは意図的なので、畳むのは索引側ではなく表示段に置いた。

畳む軸は referent 側に限る。同じ対象の複数の版を畳んでも対象の数は減らないが、
「作業」で畳むと 1 つの commit が触った別々のファイルが 1 枠になり、実際に独立
した対象が隠れるため。したがって key は commit SHA を持たず、issue と、それを
閉じる PR も 2 実体のまま残す(両者を結ぶ `Closes #N` は索引に無い)。

代表に最新版を固定しないのは「いつ変わったか」を問うクエリへの答えを消さない
ため。そのクエリでは該当する古い diff が最上位に来るので、それが代表になる。

repo 横断の複製(軸 (c))は判定材料が索引に無いので対象外。

Closes #189
@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 6304a22 Aug 02 2026, 07:32 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.

Self-review (auto mode, parent agent)

受け入れ条件

  • 同一 commit が触った別々のファイルが畳まれない — 満たす。entityKeyfile:{repo}:{path} で commit SHA を key に含めない。テスト「keeps two files touched by ONE commit as two entities」で固定
  • 「いつ変わったか」クエリで該当 diff が代表として残る(負の対照) — 満たす。代表は最終順序の先頭行で、最新版を pin しない。テスト「does not pin the newest version」で固定
  • 重複率の回帰テスト — 2026-08-01 実測の候補プールを再構成した 2 本。既知の重複(同一ファイルの doc + diff / 別 commit の同一ファイル / PR とその review / issue とその comment)が畳まれ、既知の非重複(同一 commit の別ファイル / issue #1317 と PR #1318 / repo 横断の複製)が畳まれないことを両方向で固定
  • top_k に対し独立実体 9 以上 — unit test 側では閉じない条件。再構成プールでの挙動は固定されているが、本条件は実インデックスに対する実測。merge 後の deploy 完了を待って本番の 2 クエリで確認する(本 session 内で実施)

制約

  • 索引側は変更しない — ingest 経路に変更なし。集約は src/mcp.ts の融合後・trim 前のみ
  • top_k の意味を変えない — 要求件数ぶんの独立実体が返る。そのために overfetch を無条件化した(下記)
  • same_entity は追加フィールド — 既存フィールドの型・意味は不変。無視する client は #189 以前と同じ形を見る

判断が要った点

overfetch の無条件化 は仕様が明示していなかった必然の随伴変更。従来は post-filter / time filter / rerank 有効のいずれかでのみ 5 倍取りしていたが、集約が全経路で行を畳む以上、rerank: false 経路では要求 top_k を満たせなくなる。予算(top_k × 5 / 上限 50)自体は不変なので、既定経路(rerank 有効)の取得量は変わらない。増えるのは rerank: false のデバッグ経路のみ。

wiki_docfile: 名前空間に入れない 判断は #189 の定義と整合する。wiki page は referent あたり 1 行しか存在せず畳む対象が無い一方、名前空間を共有すると同名の repo doc と誤って畳む。畳まない側に倒すのが安全側。

same_entity.count が代表を含む(常に 2 以上)。issue の例が曖昧だったので実測データ(1 ファイルが 3 行)に照らして解釈し、明示的に文書化されている。妥当。

既知の境界(欠陥ではない)

graph_expand: true で追加される近傍行は集約を通らず、trim 後に追記される。これらは graph_hop / graph_from で明示的に区別された opt-in の拡張であり top_k の外側なので、top_k が実体を数えるという性質は損なわれない。近傍が既存代表と同一実体になる経路は理論上あるが、本 issue の対象は top_k プール内の枠の奪い合いであってここではない。

scope からの逸脱

軸 (c) の follow-up issue が二重に立った(#200 / #202)。原因は親側の伝達漏れで、委譲前に親が #200 を立てていたことを subagent に渡していなかった。#189 本文と本 PR が参照する #202 を残し、#200 は固有の検討材料 2 件を #202 へ移した上で close 済み。PR 側の修正は不要。

次のステップ

auto mode につき human gate なし。self-review pass → merge → deploy 後に実インデックスで受け入れ条件の残り 1 本を確認。

@liplus-lin-lay
liplus-lin-lay merged commit 70254ea into main Aug 2, 2026
3 checks passed
@liplus-lin-lay
liplus-lin-lay deleted the 189-specretrieval-top-k-is-crowded-by-multiple-rows-of-the-same-underlying-entity branch August 2, 2026 07:36
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.

spec(retrieval): top-k is crowded by multiple rows of the same underlying entity

1 participant