mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-27 16:06:11 +00:00
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>
This commit is contained in:
committed by
safishamsi
co-authored by
Claude Opus 4.8
parent
e2d47535c3
commit
2f9deae8d5
+33
-3
@@ -285,7 +285,11 @@ def _trigram_candidates(G: nx.Graph, needles: list[str], *, guard_frac: float =
|
||||
|
||||
def _score_nodes(G: nx.Graph, terms: list[str]) -> list[tuple[float, str]]:
|
||||
scored = []
|
||||
norm_terms = [tok for t in terms for tok in _search_tokens(t)]
|
||||
# Dedupe tokens, order-preserving (as _pick_seeds already does): a repeated
|
||||
# query word must not double-count every tier, and with coverage scaling
|
||||
# below it would also inflate the matched-term ratio (#1602).
|
||||
norm_terms = list(dict.fromkeys(tok for t in terms for tok in _search_tokens(t)))
|
||||
n_terms = len(norm_terms)
|
||||
idf = _compute_idf(G, norm_terms)
|
||||
# Whole-query string for full-label matching (mirrors _find_node's `term`).
|
||||
joined = " ".join(norm_terms)
|
||||
@@ -328,18 +332,38 @@ def _score_nodes(G: nx.Graph, terms: list[str]) -> list[tuple[float, str]]:
|
||||
or label_tokens.startswith(joined)
|
||||
):
|
||||
score += _PREFIX_MATCH_BONUS * 10 * joined_w
|
||||
# Term coverage (#1602): scale the per-term exact/prefix tiers by the
|
||||
# squared fraction of query terms the node's LABEL matches, so a lone
|
||||
# generic word that happens to equal a short label (query term "home"
|
||||
# vs. a home() leaf) cannot bury nodes that match several of the
|
||||
# query's terms. Squaring matters because the exact tier is 10x the
|
||||
# prefix tier: at linear coverage a 1-of-10-terms exact match still
|
||||
# outscores a 3-of-10 prefix+substring match. Single-term and
|
||||
# full-coverage queries are unchanged (coverage == 1), so identifier
|
||||
# lookups keep exact-match dominance. Source-file hits score but do
|
||||
# not count as coverage: a colliding leaf whose directory shares
|
||||
# tokens with the query (common near the intended target) must not
|
||||
# win back its exact tier via path fragments. The substring/source
|
||||
# bonuses and the full-query tier above stay unscaled.
|
||||
matched = 0
|
||||
tiered = 0.0
|
||||
for t in norm_terms:
|
||||
w = idf.get(t, 1.0)
|
||||
# Three-tier precedence: exact > prefix > substring (take the
|
||||
# strongest tier per term so a single term cannot double-count).
|
||||
if t == norm_label or t == bare_label:
|
||||
score += _EXACT_MATCH_BONUS * w
|
||||
tiered += _EXACT_MATCH_BONUS * w
|
||||
matched += 1
|
||||
elif norm_label.startswith(t) or bare_label.startswith(t):
|
||||
score += _PREFIX_MATCH_BONUS * w
|
||||
tiered += _PREFIX_MATCH_BONUS * w
|
||||
matched += 1
|
||||
elif t in norm_label:
|
||||
score += _SUBSTRING_MATCH_BONUS * w
|
||||
matched += 1
|
||||
if t in source:
|
||||
score += _SOURCE_MATCH_BONUS * w
|
||||
if tiered:
|
||||
score += tiered * (matched / n_terms) ** 2
|
||||
if score > 0:
|
||||
scored.append((score, nid))
|
||||
# Sort by score desc; break ties toward the shorter label so a concise exact
|
||||
@@ -378,6 +402,12 @@ def _pick_seeds(
|
||||
collision cannot starve out the others. Ties within a term are broken by
|
||||
graph degree (structural centrality), so an isolated incidental match
|
||||
doesn't out-rank a real, well-connected hub for that term.
|
||||
|
||||
Coverage scaling in _score_nodes (#1602) now dampens a lone collision's
|
||||
exact tier on multi-term queries, which brings label-matching relevant
|
||||
nodes back inside the gap window; this per-term guarantee remains
|
||||
load-bearing for relevant nodes matched only via substrings, whose flat
|
||||
scores a dampened collision can still exceed.
|
||||
"""
|
||||
if not scored:
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user