Skip to content

explore/query relevance: a generic token's exact name-match overboosts in peripheral dirs — follow-up to #746 #982

Description

@michaelHMK

Summary

Follow-up to #746 (and #720 / #745). Those landed the project-name token down-weighting. #746 also floated a more general idea:

discount a token that matches a large fraction of indexed paths/symbols (it carries ~no signal but currently dominates).

That corpus-frequency discount is still not implemented on main (1.1.0). As a result, a generic non-stopword query token that happens to exactly match a symbol name (e.g. a bare usage) wins a large nameMatchBonus and crowds out the on-target product code — both in codegraph query / codegraph_search (no dampening at all) and, to a lesser but still user-visible degree, in codegraph_explore (where an existing dampening fires but is insufficient).

What actually happens today (corrected root cause)

There are two scoring paths, and they behave differently — the original write-up conflated them:

  1. codegraph query / codegraph_search (db/queries.ts searchNodes): score = bm25 + kindBonus + scorePathRelevance + nameMatchBonus. The only low-signal-token handling is dropping the project name (scorePathRelevance, via deriveProjectNameTokens). There is no corpus-frequency / generic-token discount, so a bare usage symbol ranks at the very top.

  2. codegraph_explore (context/index.ts findRelevantContext): this path does already demote generic exact-name matches — isDistinctiveIdentifier(token) is false for a plain lowercase word like usage, so a common-word exact match with no 2nd corroborating term gets score *= 0.3 (and a generic single-term match gets *= 0.6). This demotion fires for the reported case — and is still not enough, because nameMatchBonus hands a bare exact-name match a score so high that 0.3× of it still beats product symbols that don't literally contain the token.

So the accurate statement is not "generic-token downweighting isn't done" — it's "the existing demotion is a binary heuristic (distinctive-vs-common + 2-term corroboration) that can't counter a large nameMatchBonus, and the plain query/search path has no such demotion at all." The #746 general corpus-frequency discount is what's missing.

The lever (nameMatchBonus, src/search/query-utils.ts)

For a query token that exactly equals a symbol name:

  • whole query (whitespace-stripped) === name80
  • in a multi-word query, a token === name60 ← this is the case here (the earlier draft said "80")

STOP_WORDS deliberately doesn't filter common symbol names (get/set/usage/update/status), so these tokens keep full weight even when they match thousands of symbols. matchesNonProductionDir's de-prioritized set is hardcoded (integration/sample/example/fixture/benchmark/demo) and not extensible, so a peripheral tree like optional-skills/.../scripts/ gets no de-prioritization.

Minimal reproduction (public, no private repo needed)

Layout — product code with no symbol literally named usage, plus two standalone helpers that each have a module-level def usage():

apps/desktop/statusbar/StatusBar.ts            class DesktopStatusBar { render, refresh, mount }
apps/desktop/statusbar/StatusBarController.ts  class StatusBarController
apps/desktop/context/ContextWindowMeter.ts     class ContextWindowMeter { read, recompute }, estimateTokens
apps/desktop/context/format.ts                 formatTokens
gateway/server/server.ts, packages/core/util/strings.ts   (filler)
optional-skills/bodyfat/scripts/body_calc.py   def usage(): print("usage: ...")
optional-skills/nutrition/scripts/macro_calc.py def usage(): print("usage: ...")

codegraph init + codegraph index, then query desktop status bar context window usage:

Run Entry Where the usage() helpers land
A. codegraph query (no dampening) searchNodes ranks 1 and 2 (≈7626% vs product's top ≈5104%)
B. codegraph explore (0.3× demotion active) findRelevantContext top two Source Code blocks — push StatusBarController.ts + format.ts out of the section
C. control: same query with usagetokens codegraph explore helpers don't appear; 4 product files fill the section
D. causal probe: rename usage()print_usage(), re-index codegraph explore helpers vanish entirely; product code reclaims every slot

D isolates the cause to a single signal: removing the exact token match (so the 60-point nameMatchBonus no longer applies) removes the symptom. The existing explore-path demotion was active the whole time and didn't prevent B.

Why the existing user-side levers don't help

Proposal

Primary (the #746 follow-up — the part that's genuinely missing):

  • Add a corpus-frequency / IDF-style discount so a query token matching a large fraction of indexed symbols/paths contributes proportionally less to nameMatchBonus (and scorePathRelevance). This lowers the base an exact-name match starts from, which the explore path's existing 0.3× demotion then multiplies — fixing the "0.3× of a huge number is still huge" gap, and also fixing the query/search path that has no demotion at all.

Complementary / independently shippable, lower risk — keep these two distinct (they have different semantics):

  • Deprioritize / scope (a ranking lever): make the non-production-dir de-prioritization user-extensible via codegraph.json (e.g. deprioritize: ["optional-skills/", "scripts/"]), and/or add a --within <glob> / --exclude <glob> scope to explore / query + the MCP input schema.
  • Exclude (an index/recall lever) is a bigger product decision — name it separately if pursued; it's not required to fix the ranking symptom above.

Validation caveat

This touches the scorer for every query, so per codegraph/CLAUDE.md and the note in #746 it must be A/B'd with scripts/agent-eval/run-all.sh across the README repos plus a control before merge. Edge case to preserve: a query where the "generic" token genuinely IS the discriminating term (an actual query about a usage() reporter) — IDF should discount, not erase, the signal.

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