Skip to content

Commit 2f9deae

Browse files
fkhawajaghclaude
authored andcommitted
fix(serve): scale per-term exact/prefix score tiers by squared term coverage (#1602)
In a multi-term query, a lone generic term that exactly equals a short leaf label (`home` vs a `home()` action, `list` vs a `list()` function) received the full exact-tier bonus and outscored nodes matching several of the query's terms by ~10x, so `_pick_seeds`' gap cutoff discarded the genuinely relevant candidates and `path`-style consumers of `scored[0]` had no backstop. `_score_nodes` now scales the per-term exact/prefix tier contributions by the squared fraction of query terms the node's label matches (`tiered * (matched / n_terms) ** 2`). Squared because the exact tier is 10x the prefix tier; label hits only (source-path hits score but don't count as coverage, so a colliding leaf in the target's own directory can't win its exact tier back via path fragments); query tokens deduped order-preserving so a repeated word can't quadratically restore the collision. Single-term and full-coverage queries are unchanged (coverage == 1), keeping identifier exact-match dominance. Guarded against zero-term division. Dropped the unrelated README badge changes from the PR; kept the scoring fix and its two regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e2d4753 commit 2f9deae

2 files changed

Lines changed: 95 additions & 3 deletions

File tree

graphify/serve.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,11 @@ def _trigram_candidates(G: nx.Graph, needles: list[str], *, guard_frac: float =
285285

286286
def _score_nodes(G: nx.Graph, terms: list[str]) -> list[tuple[float, str]]:
287287
scored = []
288-
norm_terms = [tok for t in terms for tok in _search_tokens(t)]
288+
# Dedupe tokens, order-preserving (as _pick_seeds already does): a repeated
289+
# query word must not double-count every tier, and with coverage scaling
290+
# below it would also inflate the matched-term ratio (#1602).
291+
norm_terms = list(dict.fromkeys(tok for t in terms for tok in _search_tokens(t)))
292+
n_terms = len(norm_terms)
289293
idf = _compute_idf(G, norm_terms)
290294
# Whole-query string for full-label matching (mirrors _find_node's `term`).
291295
joined = " ".join(norm_terms)
@@ -328,18 +332,38 @@ def _score_nodes(G: nx.Graph, terms: list[str]) -> list[tuple[float, str]]:
328332
or label_tokens.startswith(joined)
329333
):
330334
score += _PREFIX_MATCH_BONUS * 10 * joined_w
335+
# Term coverage (#1602): scale the per-term exact/prefix tiers by the
336+
# squared fraction of query terms the node's LABEL matches, so a lone
337+
# generic word that happens to equal a short label (query term "home"
338+
# vs. a home() leaf) cannot bury nodes that match several of the
339+
# query's terms. Squaring matters because the exact tier is 10x the
340+
# prefix tier: at linear coverage a 1-of-10-terms exact match still
341+
# outscores a 3-of-10 prefix+substring match. Single-term and
342+
# full-coverage queries are unchanged (coverage == 1), so identifier
343+
# lookups keep exact-match dominance. Source-file hits score but do
344+
# not count as coverage: a colliding leaf whose directory shares
345+
# tokens with the query (common near the intended target) must not
346+
# win back its exact tier via path fragments. The substring/source
347+
# bonuses and the full-query tier above stay unscaled.
348+
matched = 0
349+
tiered = 0.0
331350
for t in norm_terms:
332351
w = idf.get(t, 1.0)
333352
# Three-tier precedence: exact > prefix > substring (take the
334353
# strongest tier per term so a single term cannot double-count).
335354
if t == norm_label or t == bare_label:
336-
score += _EXACT_MATCH_BONUS * w
355+
tiered += _EXACT_MATCH_BONUS * w
356+
matched += 1
337357
elif norm_label.startswith(t) or bare_label.startswith(t):
338-
score += _PREFIX_MATCH_BONUS * w
358+
tiered += _PREFIX_MATCH_BONUS * w
359+
matched += 1
339360
elif t in norm_label:
340361
score += _SUBSTRING_MATCH_BONUS * w
362+
matched += 1
341363
if t in source:
342364
score += _SOURCE_MATCH_BONUS * w
365+
if tiered:
366+
score += tiered * (matched / n_terms) ** 2
343367
if score > 0:
344368
scored.append((score, nid))
345369
# Sort by score desc; break ties toward the shorter label so a concise exact
@@ -378,6 +402,12 @@ def _pick_seeds(
378402
collision cannot starve out the others. Ties within a term are broken by
379403
graph degree (structural centrality), so an isolated incidental match
380404
doesn't out-rank a real, well-connected hub for that term.
405+
406+
Coverage scaling in _score_nodes (#1602) now dampens a lone collision's
407+
exact tier on multi-term queries, which brings label-matching relevant
408+
nodes back inside the gap window; this per-term guarantee remains
409+
load-bearing for relevant nodes matched only via substrings, whose flat
410+
scores a dampened collision can still exceed.
381411
"""
382412
if not scored:
383413
return []

tests/test_serve.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
_communities_from_graph,
99
_score_nodes,
1010
_compute_idf,
11+
_EXACT_MATCH_BONUS,
12+
_SOURCE_MATCH_BONUS,
1113
_pick_seeds,
1214
_bfs,
1315
_dfs,
@@ -123,6 +125,66 @@ def _add(nid, label, src):
123125
assert scored[0][0] > scored[1][0], "exact label must strictly outrank superset/token-bag matches"
124126

125127

128+
def test_score_nodes_coverage_lone_generic_exact_hit_loses_to_multi_term_match():
129+
"""A lone generic-word exact match must not bury a multi-term match.
130+
131+
Reproduces #1602: in a multi-term query, a single generic term that
132+
exactly equals a short leaf label (query term "list" vs a list() function
133+
node) received the full exact-tier bonus and outranked every node matching
134+
several of the query's terms, even when the query contained the target's
135+
literal identifier. The per-term exact/prefix tiers are now scaled by
136+
squared term coverage, so a 1-of-5-terms collision drops below a
137+
multi-term match. The leaves live in the same directory as the target
138+
(the realistic case) to pin that source-path hits do not count as
139+
coverage and hand the collision its exact tier back.
140+
"""
141+
G = nx.Graph()
142+
143+
def _add(nid, label, src):
144+
G.add_node(nid, label=label, norm_label=label.lower(),
145+
source_file=src, community=0)
146+
147+
_add("target", "ClientLive.Index", "lib/clients_live/index.ex")
148+
_add("form", "ClientLive.Form", "lib/clients_live/form.ex")
149+
_add("show", "ClientLive.Show", "lib/clients_live/show.ex")
150+
# Same-named tiny leaf functions: "list" == bare label fires the exact
151+
# tier. Placed in the target's own directory so their source paths also
152+
# substring-match the query term "clients": a path hit must not inflate
153+
# the coverage that multiplies the exact tier.
154+
for i in range(3):
155+
_add(f"leaf{i}", "list()", f"lib/clients_live/helpers{i}.ex")
156+
# Filler making "list" a common (low-IDF) token, as in a real graph where
157+
# list()/get()/new() style names are ubiquitous.
158+
for i in range(24):
159+
_add(f"filler{i}", f"shopping list {i}", f"lib/filler{i}.ex")
160+
161+
# The user pastes the real identifier plus context words; tokenization
162+
# yields 5 terms: clientlive, index, clients, list, columns.
163+
scored = _score_nodes(G, [t.lower() for t in "ClientLive.Index clients list columns".split()])
164+
by_id = {nid: s for s, nid in scored}
165+
166+
assert scored[0][1] == "target"
167+
assert by_id["target"] > by_id["leaf0"], (
168+
"a 1-of-5-terms exact collision must not outrank the node matching 3 of 5 terms"
169+
)
170+
171+
172+
def test_score_nodes_coverage_full_coverage_query_is_unchanged():
173+
"""Coverage scaling must not touch full-coverage queries (coverage == 1).
174+
175+
A single-term identifier lookup keeps the exact tier's full magnitude, so
176+
`query "FooBarService"` behavior is byte-identical to before #1602.
177+
"""
178+
G = _make_graph()
179+
scored = _score_nodes(G, ["extract"])
180+
w = _compute_idf(G, ["extract"])["extract"]
181+
assert scored[0][1] == "n1"
182+
# Full-query exact tier (10x) + per-term exact tier + source hit
183+
# ("extract" in "extract.py"), all undampened.
184+
expected = (_EXACT_MATCH_BONUS * 10 + _EXACT_MATCH_BONUS + _SOURCE_MATCH_BONUS) * w
185+
assert scored[0][0] == pytest.approx(expected)
186+
187+
126188
def test_find_node_ignores_trailing_punctuation():
127189
G = _make_graph()
128190
assert _find_node(G, "extract?") == ["n1"]

0 commit comments

Comments
 (0)