Skip to content

Commit 8c42458

Browse files
fix(search): keep higher-scoring rag hit when two share a topic (#23)
Spotted in code-review 2026-05-07. The merge loop overwrote ``acc[topic]`` unconditionally, so when two RAG hits resolved to the same topic key (typical for the author-layout where concept.md and reference.md sit in the same feature dir), the LATER hit silently won — even when the earlier one had a higher score. Net effect: precision loss whenever the corpus included multiple depths of the same topic. Compare scores before overwriting; keep the existing entry when its ``rag_score`` is at least as high as the incoming hit's normalized score. The result is order-independent: high-then-low and low-then- high both keep the high-scoring entry. Two new tests cover both orderings. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a7b69b4 commit 8c42458

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

sidecar/attune_gui/search.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,22 @@ def merge(
6363
for hit in rag_hits:
6464
# citation.hits contains CitedSource objects: .template_path, .score, .excerpt
6565
topic = _rag_topic(hit.template_path)
66+
normalized_score = hit.score / max_rag
67+
68+
existing = acc.get(topic)
69+
if existing is not None and existing["rag_score"] >= normalized_score:
70+
# Already seen this topic at an equal-or-better score; keep
71+
# the earlier hit. Without this, the loop silently overwrote
72+
# the higher-scoring entry whenever two RAG templates
73+
# resolved to the same topic (precision loss flagged in
74+
# code-review 2026-05-07).
75+
continue
76+
6677
acc[topic] = {
6778
"topic": topic,
6879
"path": hit.template_path,
6980
"excerpt": (hit.excerpt or "")[:200].strip(),
70-
"rag_score": hit.score / max_rag,
81+
"rag_score": normalized_score,
7182
"help_score": None,
7283
}
7384

sidecar/tests/test_unified_search.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,46 @@ def test_engine_failure_degrades_gracefully(client, monkeypatch):
232232
data = resp.json()
233233
assert len(data["results"]) == 1
234234
assert data["results"][0]["source"] == "rag"
235+
236+
237+
# ---------------------------------------------------------------------------
238+
# merge() dedup — duplicates resolving to the same topic must keep the best
239+
# ---------------------------------------------------------------------------
240+
241+
242+
def test_merge_keeps_higher_score_when_two_rag_hits_share_topic():
243+
"""Two RAG hits resolving to the same topic key must produce one
244+
result whose score reflects the higher hit, not the later one.
245+
246+
Before the fix, the second iteration of the loop unconditionally
247+
overwrote the first ``acc[topic]``, silently dropping the higher-
248+
scoring hit when the order happened to be high-then-low.
249+
"""
250+
# Same author-layout topic, different depths, both score above zero
251+
# but the concept is rated higher than the reference.
252+
rag_hits = [
253+
_rag_hit("auth/concept.md", 5.0, "high-rank content"),
254+
_rag_hit("auth/reference.md", 1.0, "low-rank content"),
255+
]
256+
results = merge([], rag_hits, limit=10)
257+
assert len(results) == 1
258+
r = results[0]
259+
assert r["topic"] == "auth"
260+
# Top RAG hit normalizes to 1.0 (max=5.0), weighted by _RAG_WEIGHT=0.6
261+
assert r["score"] == pytest.approx(0.6, abs=0.01)
262+
# Path should be the higher-scoring hit's path
263+
assert r["path"] == "auth/concept.md"
264+
265+
266+
def test_merge_keeps_higher_score_when_lower_comes_first():
267+
"""Order-independent: low-score-first should still keep the high-score hit."""
268+
rag_hits = [
269+
_rag_hit("auth/reference.md", 1.0, "low first"),
270+
_rag_hit("auth/concept.md", 5.0, "high second"),
271+
]
272+
results = merge([], rag_hits, limit=10)
273+
assert len(results) == 1
274+
r = results[0]
275+
assert r["topic"] == "auth"
276+
assert r["path"] == "auth/concept.md"
277+
assert r["score"] == pytest.approx(0.6, abs=0.01)

0 commit comments

Comments
 (0)