Skip to content

Commit b2edcdf

Browse files
committed
perf: reduce rerank payload latency
1 parent e34e79b commit b2edcdf

3 files changed

Lines changed: 266 additions & 7 deletions

File tree

src/dory_core/rerank_orchestrator.py

Lines changed: 151 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from __future__ import annotations
22

33
import logging
4+
import math
5+
import re
6+
import time
47
from dataclasses import replace
58
from typing import TYPE_CHECKING
69

7-
from dory_core.llm_rerank import LLMReranker
10+
from dory_core.llm_rerank import LLMReranker, RerankCandidate
811

912
if TYPE_CHECKING:
1013
from dory_core.search import _ChunkRow
@@ -28,12 +31,15 @@ def rerank(
2831
return rows
2932
if len(rows) <= self.candidate_limit:
3033
return self._apply(rows, query, warnings=warnings)
34+
rerank_rows = _diverse_prefix(rows, self.candidate_limit, query=query)
35+
rerank_ids = {row.chunk_id for row in rerank_rows}
3136
warnings.append(
32-
f"Rerank considered the top {self.candidate_limit} candidates and kept the remaining base order."
37+
f"Rerank considered {len(rerank_rows)} diverse candidates from the top {len(rows)} "
38+
"and kept the remaining base order."
3339
)
3440
return [
35-
*self._apply(rows[: self.candidate_limit], query, warnings=warnings),
36-
*rows[self.candidate_limit :],
41+
*self._apply(rerank_rows, query, warnings=warnings),
42+
*(row for row in rows if row.chunk_id not in rerank_ids),
3743
]
3844

3945
def _apply(
@@ -45,13 +51,32 @@ def _apply(
4551
) -> list[_ChunkRow]:
4652
from dory_core.search import _rerank_candidate_from_row
4753

48-
candidates = [_rerank_candidate_from_row(row) for row in rows]
54+
original_candidates = [_rerank_candidate_from_row(row) for row in rows]
55+
candidates = [_focused_candidate(candidate, query=query) for candidate in original_candidates]
56+
before_chars = sum(len(candidate.snippet) for candidate in original_candidates)
57+
after_chars = sum(len(candidate.snippet) for candidate in candidates)
58+
_logger.info(
59+
"rerank payload prepared candidate_count=%s query_chars=%s snippet_chars_before=%s snippet_chars_after=%s",
60+
len(candidates),
61+
len(query),
62+
before_chars,
63+
after_chars,
64+
)
65+
started_at = time.perf_counter()
4966
try:
5067
result = self.reranker.rerank(query=query, candidates=candidates)
5168
except Exception:
52-
_logger.exception("rerank call failed; falling back to base hybrid ranking")
69+
elapsed_ms = round((time.perf_counter() - started_at) * 1000)
70+
_logger.exception("rerank call failed elapsed_ms=%s; falling back to base hybrid ranking", elapsed_ms)
5371
warnings.append("Rerank failed; kept the base hybrid ranking.")
5472
return rows
73+
elapsed_ms = round((time.perf_counter() - started_at) * 1000)
74+
_logger.info(
75+
"rerank call completed candidate_count=%s elapsed_ms=%s returned_count=%s",
76+
len(candidates),
77+
elapsed_ms,
78+
0 if result is None else len(result.ordered_chunk_ids),
79+
)
5580
if result is None:
5681
warnings.append("Rerank returned no usable ranking; kept the base hybrid ranking.")
5782
return rows
@@ -62,4 +87,124 @@ def _apply(
6287
if row is None:
6388
continue
6489
reranked.append(replace(row, score=result.scores.get(chunk_id, row.score)))
90+
for row in rows:
91+
if row.chunk_id not in result.ordered_chunk_ids:
92+
reranked.append(row)
6593
return reranked
94+
95+
96+
_TOKEN_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.@/-]*")
97+
_FOCUSED_SNIPPET_CHARS = 1800
98+
_FOCUSED_WINDOW_CHARS = 700
99+
_MIN_MMR_RELEVANCE = 0.15
100+
101+
102+
def _diverse_prefix(rows: list[_ChunkRow], limit: int, *, query: str) -> list[_ChunkRow]:
103+
if len(rows) <= limit:
104+
return rows
105+
query_tokens = set(_query_tokens(query))
106+
selected: list[_ChunkRow] = []
107+
remaining = list(rows)
108+
while remaining and len(selected) < limit:
109+
next_index = _next_diverse_index(remaining, selected, query_tokens)
110+
selected.append(remaining.pop(next_index))
111+
return selected
112+
113+
114+
def _next_diverse_index(
115+
remaining: list[_ChunkRow],
116+
selected: list[_ChunkRow],
117+
query_tokens: set[str],
118+
) -> int:
119+
if not selected:
120+
return 0
121+
best_index = 0
122+
best_score = -math.inf
123+
for index, row in enumerate(remaining):
124+
relevance = _relevance_score(row.content, query_tokens)
125+
if relevance < _MIN_MMR_RELEVANCE:
126+
relevance = max(_MIN_MMR_RELEVANCE, 1.0 / (index + 2))
127+
redundancy = max(_row_similarity(row, selected_row) for selected_row in selected)
128+
path_penalty = 0.20 if any(row.path == selected_row.path for selected_row in selected) else 0.0
129+
rank_bonus = 1.0 / (index + 2)
130+
mmr_score = (0.30 * relevance) + (0.55 * rank_bonus) - (0.35 * redundancy) - path_penalty
131+
if mmr_score > best_score:
132+
best_index = index
133+
best_score = mmr_score
134+
return best_index
135+
136+
137+
def _focused_candidate(candidate: RerankCandidate, *, query: str) -> RerankCandidate:
138+
snippet = _focused_snippet(candidate.snippet, query=query)
139+
if snippet == candidate.snippet:
140+
return candidate
141+
return replace(candidate, snippet=snippet)
142+
143+
144+
def _focused_snippet(text: str, *, query: str) -> str:
145+
if len(" ".join(text.split())) <= _FOCUSED_SNIPPET_CHARS:
146+
return " ".join(text.split())
147+
query_tokens = set(_query_tokens(query))
148+
if not query_tokens:
149+
return " ".join(text.split())[:_FOCUSED_SNIPPET_CHARS]
150+
best_window = _best_focus_window(text, query_tokens)
151+
if best_window is None:
152+
return " ".join(text.split())[:_FOCUSED_SNIPPET_CHARS]
153+
return " ".join(best_window.split())[:_FOCUSED_SNIPPET_CHARS].strip()
154+
155+
156+
def _best_focus_window(text: str, query_tokens: set[str]) -> str | None:
157+
paragraphs = [paragraph.strip() for paragraph in re.split(r"\n\s*\n", text) if paragraph.strip()]
158+
if not paragraphs:
159+
paragraphs = [text]
160+
best_paragraph = max(paragraphs, key=lambda paragraph: _focus_score(paragraph, query_tokens))
161+
if _focus_score(best_paragraph, query_tokens) <= 0:
162+
return None
163+
normalized = " ".join(best_paragraph.split())
164+
if len(normalized) <= _FOCUSED_SNIPPET_CHARS:
165+
return normalized
166+
lowered = normalized.lower()
167+
matches = [lowered.find(token) for token in query_tokens if lowered.find(token) >= 0]
168+
if not matches:
169+
return normalized[:_FOCUSED_SNIPPET_CHARS]
170+
center = round(sum(matches) / len(matches))
171+
start = max(0, center - _FOCUSED_WINDOW_CHARS)
172+
end = min(len(normalized), center + _FOCUSED_WINDOW_CHARS)
173+
if end - start < _FOCUSED_SNIPPET_CHARS:
174+
start = max(0, min(start, len(normalized) - _FOCUSED_SNIPPET_CHARS))
175+
end = min(len(normalized), start + _FOCUSED_SNIPPET_CHARS)
176+
return normalized[start:end]
177+
178+
179+
def _focus_score(text: str, query_tokens: set[str]) -> float:
180+
tokens = set(_query_tokens(text))
181+
if not tokens:
182+
return 0.0
183+
overlap = len(tokens & query_tokens)
184+
if overlap == 0:
185+
return 0.0
186+
coverage = overlap / len(query_tokens)
187+
density = overlap / max(len(tokens), 1)
188+
return coverage + density
189+
190+
191+
def _row_similarity(first: _ChunkRow, second: _ChunkRow) -> float:
192+
first_tokens = set(_query_tokens(first.content))
193+
second_tokens = set(_query_tokens(second.content))
194+
if not first_tokens or not second_tokens:
195+
return 0.0
196+
return len(first_tokens & second_tokens) / len(first_tokens | second_tokens)
197+
198+
199+
def _relevance_score(text: str, query_tokens: set[str]) -> float:
200+
if not query_tokens:
201+
return 1.0
202+
tokens = set(_query_tokens(text))
203+
if not tokens:
204+
return 0.0
205+
return len(tokens & query_tokens) / len(query_tokens)
206+
207+
208+
def _query_tokens(query: str) -> list[str]:
209+
tokens = [match.group(0).lower() for match in _TOKEN_RE.finditer(query)]
210+
return [token for token in tokens if len(token) > 2]
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
5+
from dory_core.llm_rerank import RerankCandidate, RerankResult
6+
from dory_core.rerank_orchestrator import RerankOrchestrator
7+
from dory_core.search import _ChunkRow
8+
9+
10+
@dataclass
11+
class RecordingReranker:
12+
candidates: list[RerankCandidate] | None = None
13+
14+
def rerank(self, *, query: str, candidates: list[RerankCandidate]) -> RerankResult:
15+
self.candidates = list(candidates)
16+
return RerankResult(
17+
ordered_chunk_ids=tuple(candidate.chunk_id for candidate in candidates),
18+
scores={candidate.chunk_id: 1.0 for candidate in candidates},
19+
)
20+
21+
22+
def _row(chunk_id: str, path: str, content: str, *, score: float = 1.0) -> _ChunkRow:
23+
return _ChunkRow(
24+
chunk_id=chunk_id,
25+
path=path,
26+
content=content,
27+
start_line=1,
28+
end_line=1,
29+
frontmatter_json='{"title":"Test","type":"note"}',
30+
score=score,
31+
)
32+
33+
34+
def test_rerank_uses_focused_query_window_instead_of_entire_chunk() -> None:
35+
reranker = RecordingReranker()
36+
orchestrator = RerankOrchestrator(reranker, candidate_limit=10)
37+
long_intro = "irrelevant filler " * 600
38+
long_outro = "more irrelevant filler " * 600
39+
row = _row("a", "notes/long.md", f"{long_intro}needle detail lives here with context.{long_outro}")
40+
41+
orchestrator.rerank([row, _row("b", "notes/other.md", "needle elsewhere")], query="needle detail", warnings=[])
42+
43+
assert reranker.candidates is not None
44+
focused = reranker.candidates[0].snippet
45+
assert "needle detail lives here" in focused
46+
assert len(focused) < 2500
47+
assert len(focused) < len(row.content) // 4
48+
49+
50+
def test_rerank_focus_scores_paragraphs_instead_of_first_token_match() -> None:
51+
reranker = RecordingReranker()
52+
orchestrator = RerankOrchestrator(reranker, candidate_limit=10)
53+
early_weak = "needle appears here but not the rest. " + ("filler " * 300)
54+
later_strong = "needle detail context all appear together in this paragraph with the answer."
55+
row = _row("a", "notes/paragraphs.md", f"{early_weak}\n\n{later_strong}\n\n" + ("tail filler " * 300))
56+
57+
orchestrator.rerank([row, _row("b", "notes/other.md", "needle detail context")], query="needle detail context", warnings=[])
58+
59+
assert reranker.candidates is not None
60+
focused = reranker.candidates[0].snippet
61+
assert later_strong in focused
62+
assert "needle appears here but not the rest" not in focused
63+
64+
65+
def test_rerank_diversifies_duplicate_path_candidates_before_calling_model() -> None:
66+
reranker = RecordingReranker()
67+
orchestrator = RerankOrchestrator(reranker, candidate_limit=3)
68+
rows = [
69+
_row("same-1", "projects/alpha/state.md", "alpha memory first", score=0.99),
70+
_row("same-2", "projects/alpha/state.md", "alpha memory duplicate", score=0.98),
71+
_row("same-3", "projects/alpha/state.md", "alpha memory duplicate again", score=0.97),
72+
_row("other", "projects/beta/state.md", "beta memory", score=0.96),
73+
]
74+
75+
result = orchestrator.rerank(rows, query="memory", warnings=[])
76+
77+
assert reranker.candidates is not None
78+
assert [candidate.chunk_id for candidate in reranker.candidates] == ["same-1", "other", "same-2"]
79+
assert [row.chunk_id for row in result] == ["same-1", "other", "same-2", "same-3"]
80+
81+
82+
def test_rerank_diversifies_semantically_redundant_candidates() -> None:
83+
reranker = RecordingReranker()
84+
orchestrator = RerankOrchestrator(reranker, candidate_limit=3)
85+
rows = [
86+
_row("a", "docs/a.md", "alpha beta gamma memory note", score=0.99),
87+
_row("b", "docs/b.md", "alpha beta gamma memory duplicate", score=0.98),
88+
_row("c", "docs/c.md", "delta epsilon zeta different note", score=0.97),
89+
_row("d", "docs/d.md", "alpha beta gamma another duplicate", score=0.96),
90+
]
91+
92+
orchestrator.rerank(rows, query="memory note", warnings=[])
93+
94+
assert reranker.candidates is not None
95+
assert [candidate.chunk_id for candidate in reranker.candidates] == ["a", "c", "b"]
96+
97+
98+
def test_rerank_telemetry_logs_safe_metrics_without_content(caplog) -> None:
99+
reranker = RecordingReranker()
100+
orchestrator = RerankOrchestrator(reranker, candidate_limit=10)
101+
sensitive_text = "sensitive fixture sentence should never be logged"
102+
row = _row("a", "notes/private.md", f"needle detail {sensitive_text}")
103+
104+
with caplog.at_level("INFO", logger="dory_core.rerank_orchestrator"):
105+
orchestrator.rerank([row, _row("b", "notes/other.md", "needle")], query="needle detail", warnings=[])
106+
107+
messages = "\n".join(record.getMessage() for record in caplog.records)
108+
assert "rerank payload prepared" in messages
109+
assert "candidate_count=2" in messages
110+
assert "query_chars=13" in messages
111+
assert "snippet_chars_before=" in messages
112+
assert "snippet_chars_after=" in messages
113+
assert sensitive_text not in messages
114+
assert "needle detail" not in messages

tests/unit/test_search_rerank.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,4 @@ def test_rerank_limited_only_sends_top_candidates(tmp_path: Path, fake_embedder)
189189

190190
assert [candidate.chunk_id for candidate in reranker.last_candidates] == ["a", "b"]
191191
assert [row.chunk_id for row in reranked] == ["b", "a", "c"]
192-
assert any("Rerank considered the top 2 candidates" in warning for warning in warnings)
192+
assert any("Rerank considered 2 diverse candidates" in warning for warning in warnings)

0 commit comments

Comments
 (0)