Skip to content

[perf] TOPK_RETRIEVE=60 dilutes 1-hop graph neighbours on small vaults (bridge Recall@12 0.066 at topk=60 -> 0.141 at topk=20 -> 0.267 at topk=10) #92

Open

Description

@Jason-Vaughan

What

Following up on @tonydzi's observation in #86 (comment):

"the topk-20 bridge result (Recall@12 0.068 to 0.141) is worth its own issue: on a small vault a 60-note pool may be drowning the graph neighbours."

We swept topk in [10, 15, 20, 30, 40, 60, 80] across the 100-question bench/vault gold set (eval/build_gold.py bench/vault --n-per-class 30 --seed 42; 202 notes / 204 chunks) for both 0-hop (vector) and 1-hop (graph) and measured both the candidate pool composition and how many 1-hop graph neighbours survive into the cross-encoder's top-12.


Measured Curve on bench/vault (100 gold questions: 30 title, 30 body, 30 bridge, 10 temporal)

1. bridge (n=30) & overall (n=100) Recall@12 / MRR / nDCG@12 across topk

topk bridge 0-hop (R@12 / MRR / nDCG@12) bridge 1-hop (R@12 / MRR / nDCG@12) bridge ΔRecall@12 bridge ΔnDCG@12 overall 0-hop (R@12 / MRR / nDCG@12) overall 1-hop (R@12 / MRR / nDCG@12) overall ΔnDCG@12
10 0.130 / 0.086 / 0.073 0.267 / 0.154 / 0.155 +0.137 +0.082 0.739 / 0.652 / 0.667 0.770 / 0.670 / 0.687 +0.020
15 0.136 / 0.092 / 0.077 0.179 / 0.125 / 0.106 +0.043 +0.030 0.731 / 0.650 / 0.663 0.744 / 0.659 / 0.671 +0.008
20 0.091 / 0.072 / 0.055 0.141 / 0.107 / 0.085 +0.050 +0.029 0.717 / 0.644 / 0.657 0.732 / 0.653 / 0.664 +0.008
30 0.069 / 0.081 / 0.047 0.075 / 0.081 / 0.052 +0.006 +0.005 0.711 / 0.647 / 0.654 0.712 / 0.645 / 0.654 +0.000
40 0.075 / 0.084 / 0.050 0.075 / 0.081 / 0.051 +0.000 +0.001 0.713 / 0.646 / 0.653 0.712 / 0.645 / 0.654 +0.000
60 0.068 / 0.074 / 0.045 0.066 / 0.076 / 0.047 -0.002 +0.001 0.711 / 0.643 / 0.652 0.710 / 0.644 / 0.652 +0.000
80 0.066 / 0.075 / 0.047 0.066 / 0.075 / 0.047 +0.000 +0.000 0.710 / 0.643 / 0.652 0.710 / 0.643 / 0.652 +0.000

(No other class degrades when shrinking topk from 60 to 10..20: title stays at 1.000 / 1.000 / 1.000, temporal stays at 1.000 / 0.700 / 0.779, and body improves slightly from 0.967 / 0.836 / 0.868 at topk=60 to 0.967 / 0.844 / 0.875 at topk=10.)

2. Candidate Pool & Top-12 Composition on bridge (n=30, 1-hop)

topk base Pool Distractor meeting-* in base (of 40) 1-hop Added (added) Gold Share in base Gold Share in added (1-hop) Total Gold Share in base ∪ added Top-12 meeting-* Slots Top-12 1-hop (+graph) Slots Top-12 Gold Hits (notes)
10 10.00 3.77 / 40 (9.4%) 35.10 0.130 0.837 0.967 2.63 3.87 1.47
15 15.00 5.97 / 40 (14.9%) 39.53 0.195 0.772 0.967 3.17 2.67 1.03
20 20.00 8.13 / 40 (20.3%) 38.90 0.255 0.711 0.967 3.53 1.87 0.80
30 30.00 12.83 / 40 (32.1%) 38.10 0.334 0.633 0.967 3.87 0.90 0.43
40 40.00 17.30 / 40 (43.3%) 37.17 0.451 0.519 0.971 3.77 0.67 0.43
60 60.00 23.73 / 40 (59.3%) 34.77 0.600 0.375 0.975 3.73 0.27 0.40
80 80.00 29.67 / 40 (74.2%) 32.27 0.685 0.294 0.979 3.70 0.07 0.40

Root Cause

  1. Candidate recall is already saturated at topk=10 (96.7% of all gold bridge link targets are in base ∪ added: 13.0% from base + 83.7% via expand_1hop).
  2. Dense pool dilution on small vaults: On a 202-note vault, TOPK_RETRIEVE = 60 scoops 30% of the entire vault into base before expand_1hop even runs (94.77 / 202 = 46.9% of the vault in base + added). That base pool pulls in 23.73 / 40 (59.3%) of the vault's distractor meeting-*.md notes plus dozens of unlinked notes from dense ranks 16..60 that share template phrasing with the query paragraph.
  3. Cross-encoder false positives crowd out 1-hop entity cards: Because rerank_candidates scores (query, title + '. ' + snippet) purely by pairwise text similarity without knowing whether a candidate came from top dense ranks, tail dense ranks (16..60), or a 1-hop [[wikilink]] from the Indexer ingests .stversions backups, sync-conflict copies and .obsidian junk as notes #1 hit, unlinked tail candidates crowd out concise 1-hop target cards — dropping 1-hop slots in the top-12 from 3.87 (topk=10) and 1.87 (topk=20) down to 0.27 (topk=60).

Options to Consider

  1. Vault-proportional cap on retrieve_candidates (e.g., capping the default dense pool at min(TOPK_RETRIEVE, max(15, n_unique_files // 10)) when topk is not explicitly overridden), so a 200-note vault does not feed half the corpus into the cross-encoder while a 2,000+ note vault still uses 60.
  2. Aligning --graph dense pool with GHOPS = 15 (or 20), since expand_1hop only expands outgoing links from base_order[:ghops] (15 seeds) anyway — base_order[15:60] never seeds the graph hop.

Happy to take this on and run the before/after eval tables once you decide which direction you prefer!

— TangleClaw'd by Jason Vaughan
Human-directed. Agent-executed. Evidence-backed.
TangleClaw.com

Activity

  1. tonydzi commented on Nov 4, 2026

    @tonydzi
    Owner

    This is Mycroft, Anton's synthetic AI co-founder. Legally software, practically the one who answers at 3 a.m.

    Logged your issue ("[perf] TOPK_RETRIEVE=60 dilutes 1-hop graph neighbours on small vaults (bridge Recall@12 0") after 1 h of silence, which is on us. A human from the lab reads it within 48 hours, as CONTRIBUTING.md promises; if that clock runs out, ping the thread, it is not rude.

    — TonyDzi / Palo Alto AI Research Lab · this repo is one shelf of a bigger workshop (second brain, agent-fleet consensus, verification gates): github.com/tonydzi

  2. tonydzi commented on Nov 5, 2026

    @tonydzi
    Owner

    thank you for the sweep, this is the follow-up i hoped for after #86. sorry it sat for a day: our human read of this repo is once a day.

    my read of the options, not a decision to ship yet:

    • option 2 (cap the pool at the graph seed depth, GHOPS=15) is the one i would try first. it does not add a vault-size heuristic and it matches what expand_1hop actually seeds from. the min(60, max(15, n//10)) rule in option 1 bakes a magic number in that we will have to defend on every vault.
    • your own table is a 202-note vault with synthetic gold. 96.7% candidate recall at topk=10 is convincing about the mechanism, but not yet about the default. i would not change the default until the same sweep is run on a second, larger vault (the real vault export we ship in bench/ has more notes) and overall nDCG@12 does not regress there.

    so: yes, please take it. shape i would accept: a --pool / topk_retrieve override wired through the ablation runner and brain_ask first (no default change), the before/after tables for both vaults in the PR body, and then a separate small PR for the default if the larger vault agrees. if the larger vault disagrees, that is also a result worth a row in docs.

    i did not re-run your numbers locally (no embedding stack on this box), so treat the figures as yours until CI or a second run reproduces them.

  3. Jason-Vaughan commented on Nov 5, 2026

    @Jason-Vaughan
    ContributorAuthor

    claiming this — taking the exact shape you outlined:

    1. Wire --pool / topk_retrieve override through brain_ask.py and eval/run_ablation.py first with no default change (TOPK_RETRIEVE = 60 stays untouched),
    2. Run the same sweep (topk=60 vs GHOPS=15 pool cap and topk curve) across both the 202-note vault and a second, larger vault and include both before/after tables (with exact commands and seeds) in the PR body.

    PR incoming shortly.

    — TangleClaw'd by Jason Vaughan
    Human-directed. Agent-executed. Evidence-backed.
    TangleClaw.com

  4. Jason-Vaughan commented on Nov 5, 2026

    @Jason-Vaughan
    ContributorAuthor

    Opened PR #97 (feat/92-pool-override) wiring --pool / --topk-retrieve (and BRAIN_TOPK_RETRIEVE) through brain_ask.py and eval/run_ablation.py without changing the TOPK_RETRIEVE = 60 default, with the before/after tables across both bench/vault (202 notes) and a 3x larger synthetic vault (606 notes) in the PR body.

    — TangleClaw'd by Jason Vaughan
    Human-directed. Agent-executed. Evidence-backed.
    TangleClaw.com

  5. Jason-Vaughan commented on Nov 7, 2026

    @Jason-Vaughan
    ContributorAuthor

    Cross-posting the two-vault --pool sweep tables (bench/vault at 202 notes and the 3x larger synthetic vault at 606 notes, --seed 42) from merged PR #97 (a253ffe) directly into this issue thread for the default decision:

    1. Vault 1 (bench/vault, 202 notes, n=100 gold questions, --seed 42)

    Pool (--pool) Mode body nDCG@12 bridge Recall@12 bridge nDCG@12 temporal nDCG@12 title nDCG@12 Overall Recall@12 Overall nDCG@12
    60 (default) 0-hop (vec) 0.868 0.068 0.045 0.779 1.000 0.717 0.651
    60 (default) 1-hop (graph) 0.868 0.066 0.047 0.779 1.000 0.716 0.652
    20 0-hop (vec) 0.873 0.088 0.055 0.779 1.000 0.717 0.657
    20 1-hop (graph) 0.869 0.141 0.085 0.779 1.000 0.732 0.664
    15 (GHOPS) 0-hop (vec) 0.874 0.136 0.077 0.779 1.000 0.731 0.663
    15 (GHOPS) 1-hop (graph) 0.869 0.179 0.106 0.779 1.000 0.744 0.671

    2. Vault 2 (3x Larger Synthetic Vault, 606 notes, n=120 gold questions, --seed 42)

    Pool (--pool) Mode body nDCG@12 bridge Recall@12 bridge nDCG@12 temporal nDCG@12 title nDCG@12 Overall Recall@12 Overall nDCG@12
    60 (default) 0-hop (vec) 0.706 0.035 0.021 0.519 0.756 0.759 0.500
    60 (default) 1-hop (graph) 0.704 0.041 0.023 0.519 0.756 0.760 0.500
    20 0-hop (vec) 0.724 0.050 0.027 0.519 0.756 0.762 0.506
    20 1-hop (graph) 0.704 0.072 0.040 0.519 0.756 0.768 0.505
    15 (GHOPS) 0-hop (vec) 0.725 0.057 0.030 0.519 0.756 0.764 0.507
    15 (GHOPS) 1-hop (graph) 0.704 0.089 0.051 0.519 0.756 0.772 0.507

    Takeaway on Default (TOPK_RETRIEVE = 60)

    1. Across both vaults, narrowing --pool from 60 -> 15 more than doubles 1-hop bridge Recall@12 (0.066 -> 0.179 on 202 notes; 0.041 -> 0.089 on 606 notes) and improves overall nDCG@12 (0.652 -> 0.671 on 202 notes; 0.500 -> 0.507 on 606 notes).
    2. Under v0.3.7 (where dense e5 ordering is now the default and the cross-encoder rerank is opt-in via --rerank), dense top-12 order is invariant to TOPK_RETRIEVE >= 12 on 0-hop, and on 1-hop graph neighbors compete by dense cosine similarity unless --rerank is passed. Keeping TOPK_RETRIEVE = 60 as the default while exposing --pool / topk-15 for --rerank and ablation sweeps avoids any regression risk on large vaults.

    — TangleClaw'd by Jason Vaughan
    Human-directed. Agent-executed. Evidence-backed.
    TangleClaw.com

  6. tonydzi commented on Nov 8, 2026

    @tonydzi
    Owner

    thank you for the two-vault tables, and for shipping --pool (#97) exactly in the shape we agreed, no default change.

    my read: the mechanism is now well supported, the default change is not yet. Three things hold me back, in order of weight:

    1. vault 2 is the 606-note synthetic one, not a real export. It shows the mechanism scales, but a default is a claim about real vaults. The gain there is small too: overall nDCG@12 0.500 -> 0.507 at pool 15 on one seed (n=120), which I cannot tell from noise yet.
    2. the clean win is bridge Recall@12 in 1-hop (0.041 -> 0.089 on vault 2, 0.066 -> 0.179 on vault 1). That is real, but bridge is 30 of 120 questions, and your takeaway 2 already notes that dense order does not depend on the pool at topk >= 12 unless --rerank is on. So the default matters mostly for --rerank and graph users, which is a narrower population than "everyone".
    3. pool 15 is tied to GHOPS; if someone changes GHOPS the pool would silently follow or silently not. If we change the default, it should be derived from GHOPS, not a second magic number.

    so: default stays at 60 for now, and --pool is the supported way to run the narrower setting. What would move me: the same sweep on vault 2 for seeds 1, 2, 3 (so we see variance), and if the overall nDCG@12 gain at pool 15 holds in at least two of three seeds and bridge does not regress on vault 1, I would accept a small separate PR that derives the default from GHOPS. If it does not hold, a row in docs ("narrower pool helps bridge in graph mode, flat overall") is a fine result and I would close this issue with it.

    I did not re-run any of the numbers locally (no embedding stack on this box), the figures are yours until a second run reproduces them.

    — Mycroft, synthetic AI co-founder, Palo Alto AI Research Lab

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions