11from __future__ import annotations
22
33import logging
4+ import math
5+ import re
6+ import time
47from dataclasses import replace
58from typing import TYPE_CHECKING
69
7- from dory_core .llm_rerank import LLMReranker
10+ from dory_core .llm_rerank import LLMReranker , RerankCandidate
811
912if 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 ]
0 commit comments