feat(llm): recency-blended re-ranking of retrieval hits

This commit is contained in:
kert
2026-09-03 10:32:14 -04:00
parent a94024d5d5
commit 73e2665145
2 changed files with 151 additions and 0 deletions

66
src/llm/rerank.py Normal file
View File

@@ -0,0 +1,66 @@
"""Similarity × recency re-ranking over merged retrieval hits (pure).
pgvector returns cosine *distance* (0 = identical). We map it to a
similarity in [0, 1], blend with an exponential recency decay, and keep
the best chunk per item so one long comment can't fill the context.
"""
from __future__ import annotations
from dataclasses import dataclass, replace
from datetime import date
@dataclass(frozen=True)
class Hit:
text: str
metadata: dict[str, str]
distance: float
score: float = 0.0
def recency(when: str, *, now: date, half_life_days: float) -> float:
"""1.0 for today (or future), halving every ``half_life_days``; 0.0
when ``when`` is empty or unparsable."""
try:
d = date.fromisoformat(when[:10])
except (ValueError, TypeError):
return 0.0
age = (now - d).days
if age <= 0:
return 1.0
return 0.5 ** (age / half_life_days)
def _similarity(distance: float) -> float:
return min(1.0, max(0.0, 1.0 - distance))
def blend(
hits: list[Hit],
*,
weight: float,
half_life_days: float,
now: date,
top_n: int,
) -> list[Hit]:
"""Score, dedupe per ``item_key`` (best chunk wins), sort desc, cut."""
best: dict[str, Hit] = {}
for h in hits:
score = (1.0 - weight) * _similarity(h.distance) + weight * recency(
h.metadata.get("date", ""), now=now, half_life_days=half_life_days
)
scored = replace(h, score=score)
key = h.metadata.get("item_key", "") or str(id(h))
if key not in best or scored.score > best[key].score:
best[key] = scored
ranked = sorted(best.values(), key=lambda h: h.score, reverse=True)
return ranked[:top_n]
def filter_since(hits: list[Hit], since: str) -> list[Hit]:
"""Keep hits dated on/after ``since`` (ISO date); undated hits are
dropped when a cutoff is active. Empty ``since`` is a no-op."""
if not since:
return hits
return [h for h in hits if h.metadata.get("date", "")[:10] >= since[:10]]

85
tests/llm/test_rerank.py Normal file
View File

@@ -0,0 +1,85 @@
"""llm.rerank — similarity × recency blend, pure."""
from datetime import date
import pytest
from llm.rerank import Hit, blend, filter_since, recency
NOW = date(2026, 9, 3)
def _hit(key, distance, when, kind="comment"):
return Hit(
text=f"text {key}",
metadata={"item_key": key, "date": when, "kind": kind},
distance=distance,
)
class TestRecency:
def test_today_is_one(self):
assert recency("2026-09-03", now=NOW, half_life_days=365) == pytest.approx(1.0)
def test_one_half_life_is_half(self):
assert recency("2025-09-03", now=NOW, half_life_days=365) == pytest.approx(0.5)
def test_future_dates_clamp_to_one(self):
assert recency("2027-01-01", now=NOW, half_life_days=365) == 1.0
def test_undated_or_garbage_is_zero(self):
assert recency("", now=NOW, half_life_days=365) == 0.0
assert recency("not a date", now=NOW, half_life_days=365) == 0.0
def test_datetime_prefix_accepted(self):
assert recency("2026-09-03T12:00:00Z", now=NOW, half_life_days=365) == 1.0
class TestBlend:
def test_recent_beats_slightly_closer_old_hit(self):
old = _hit("OLD", distance=0.20, when="2019-01-01")
new = _hit("NEW", distance=0.25, when="2026-08-19")
out = blend([old, new], weight=0.3, half_life_days=365, now=NOW, top_n=8)
assert [h.metadata["item_key"] for h in out] == ["NEW", "OLD"]
assert out[0].score > out[1].score
def test_weight_zero_is_pure_similarity(self):
old = _hit("OLD", distance=0.20, when="2019-01-01")
new = _hit("NEW", distance=0.25, when="2026-08-19")
out = blend([old, new], weight=0.0, half_life_days=365, now=NOW, top_n=8)
assert [h.metadata["item_key"] for h in out] == ["OLD", "NEW"]
def test_dedupes_per_item_keeping_best_chunk(self):
a1 = _hit("A", distance=0.30, when="2026-01-01")
a2 = _hit("A", distance=0.10, when="2026-01-01")
b = _hit("B", distance=0.20, when="2026-01-01")
out = blend([a1, a2, b], weight=0.3, half_life_days=365, now=NOW, top_n=8)
assert [(h.metadata["item_key"], h.distance) for h in out] == [
("A", 0.10),
("B", 0.20),
]
def test_top_n_truncates(self):
hits = [_hit(f"K{i}", 0.1 * i, "2026-01-01") for i in range(5)]
assert len(blend(hits, weight=0.3, half_life_days=365, now=NOW, top_n=2)) == 2
def test_distance_clamped_into_unit_range(self):
far = _hit("F", distance=1.7, when="")
(out,) = blend([far], weight=0.0, half_life_days=365, now=NOW, top_n=1)
assert out.score == 0.0
class TestFilterSince:
def test_keeps_on_or_after_and_drops_undated(self):
hits = [
_hit("A", 0.1, "2025-12-31"),
_hit("B", 0.1, "2026-01-01"),
_hit("C", 0.1, ""),
]
assert [h.metadata["item_key"] for h in filter_since(hits, "2026-01-01")] == [
"B"
]
def test_empty_since_is_noop(self):
hits = [_hit("A", 0.1, ""), _hit("B", 0.1, "2020-01-01")]
assert filter_since(hits, "") == hits