Reranker
On this page
core.reranker.Reranker
Overview
Reranker re-scores a list of (chunk, score) results from a vector search using one of three strategies — heuristic (no LLM), LLM-assisted, or hybrid — plus an optional near-duplicate filter. It exists to correct for the fact that raw vector-similarity scores don't account for chunk length, result diversity, or (in LLM modes) actual semantic relevance as judged by a language model.
When to use it: enable it (RAGConfig(enable_reranking=True)) when retrieval quality matters more than raw latency, especially when your vector DB tends to return near-duplicate or oddly-sized chunks.
When not to use it: for latency-sensitive applications where the vector DB's native ranking is already good enough, or when you don't have an LLM available (in which case it silently falls back to heuristic mode anyway — see below).
Exact signature
class Reranker:
def __init__(self, config: Optional[RerankerConfig] = None, llm: Any = None): ...
Constructor
Parameters
Initialization flow
self.config = config or RerankerConfig().- If
config.modeisLLMorHYBRIDandllm is None: logs a warning and silently downgradesself.config.modetoRerankMode.HEURISTIC(this mutates the passed-in config object in place). - Logs an INFO summary (
mode,top_k,dedup_enabled).
[!WARNING] Silent mode downgrade mutates your config object If you construct a
RerankerConfig(mode=RerankMode.LLM)without an LLM and pass it toReranker, the sameRerankerConfiginstance's.modeattribute is mutated toHEURISTIC. If you reused that config object elsewhere expectingLLMmode, it will now readHEURISTIC.
rerank
def rerank(self, query: str, chunks: List[Tuple[Any, float]]) -> List[Tuple[Any, float]]
Overview
The single public entry point. Dispatches to one of three private strategies based on self.config.mode.
Parameters
Returns
List[Tuple[Any, float]] — re-ranked (chunk, composite_score) pairs, sorted descending by composite score, truncated to config.top_k if set.
Raises
None explicitly raised by rerank() itself; exceptions from LLM calls inside _batch_llm_scores are caught per-chunk (see below) and never propagate out of rerank().
Algorithm
chunks == []→ return[]immediately.len(chunks) == 1→ returnchunksunchanged (no reranking needed/possible).- If
config.dedup_enabled, call_remove_near_duplicates(chunks)first. - Dispatch on
config.mode:HEURISTIC→_rerank_heuristic(query, chunks)LLM→_rerank_llm(query, chunks)HYBRID(else branch) →_rerank_hybrid(query, chunks)
- If
config.top_kis set, truncate the ranked list to that length. - Log a DEBUG summary and return.
Thread safety
rerank() itself is not internally parallel except for LLM-scoring calls inside _batch_llm_scores, which use a ThreadPoolExecutor. The method is safe to call from multiple threads concurrently as long as self.llm.generate() is itself thread-safe (the shared lru_cache-wrapped _llm_relevance_score_cached uses Python's GIL-protected dict internally, but functools.lru_cache is documented as thread-safe for concurrent access).
Side effects
- May call
self.llm.generate()up tolen(chunks)times (inLLM/HYBRIDmodes), in parallel across up to 6 threads. - Populates the module-level
lru_cacheon_llm_relevance_score_cached(shared across allRerankerinstances' bound methods only in the sense thatlru_cachehere is applied to an instance method, so the cache key implicitly includesself— meaning eachRerankerinstance gets its own effective cache space, though this is a bound-method caching quirk worth knowing about; see Common Mistakes). - Logs at DEBUG level.
Complexity
- Heuristic mode: O(n · (t + w)) where n = number of chunks, t = term extraction cost, w = word count — plus an O(n log n) sort.
- LLM/Hybrid mode: same as heuristic, plus up to n LLM calls, parallelized across
min(n, 6)threads (bounded by_MAX_LLM_WORKERS = 6). Wall-clock time ≈ceil(n / 6) × (LLM latency)for uncached queries. - Dedup filter: O(n²) in the worst case (each kept chunk is compared against all previously kept chunks via Jaccard similarity).
Examples
Basic — heuristic mode (default):
from zerda.rag.core import Reranker
reranker = Reranker() # heuristic mode, no LLM needed
ranked = reranker.rerank("refund policy", chunks)
LLM mode:
from zerda.rag.core import Reranker, RerankerConfig, RerankMode
reranker = Reranker(config=RerankerConfig(mode=RerankMode.LLM), llm=my_llm)
ranked = reranker.rerank("refund policy", chunks)
Hybrid mode with custom weights:
cfg = RerankerConfig(
mode=RerankMode.HYBRID,
original_score_weight=0.3, llm_score_weight=0.5,
diversity_weight=0.1, length_weight=0.1,
llm_eval_language="en",
)
reranker = Reranker(config=cfg, llm=my_llm)
ranked = reranker.rerank("what is the SLA?", chunks)
Error handling — LLM scoring failures degrade gracefully:
# If llm.generate() raises for some chunks, those chunks get a neutral
# 0.5 relevance score instead of crashing rerank():
ranked = reranker.rerank(query, chunks) # never raises due to LLM errors
Production-ready usage — disabling dedup for already-deduplicated corpora:
cfg = RerankerConfig(dedup_enabled=False)
reranker = Reranker(config=cfg)
ranked = reranker.rerank(query, chunks)
Internal workflow
_rerank_heuristic
Composite score per chunk:
composite = w["orig"] * orig_score
+ w["len"] * length_bonus(word_count)
+ w["div"] * (0.5 * diversity_bonus + 0.5 * term_overlap)
where diversity_bonus is 1.0 the first time a doc_id is seen in this call and 0.0 for subsequent chunks from the same document, and term_overlap is the Jaccard-style ratio |query_terms ∩ chunk_terms| / max(|query_terms|, 1). Term extraction uses the regex [-ۿ]+|\w+, which matches either a run of Arabic-script characters or a run of standard word characters — supporting mixed Arabic/English content.
_rerank_llm
Composite score per chunk:
composite = w["orig"] * orig_score + w["llm"] * llm_score + w["len"] * length_bonus
No diversity term is applied in pure LLM mode (w["div"] is not used in _rerank_llm, even though effective_weights still computes it).
_rerank_hybrid
Composite score per chunk — combines all four components:
composite = w["orig"] * orig_score + w["llm"] * llm_score
+ w["len"] * length_bonus + w["div"] * (0.5 * diversity_bonus + 0.5 * term_overlap)
_batch_llm_scores
def _batch_llm_scores(self, query: str, chunks: List[Tuple[Any, float]]) -> List[float]
Submits one _llm_relevance_score_cached(query, text) call per chunk to a ThreadPoolExecutor(max_workers=6), collects results via as_completed, and returns scores in original chunk order (not completion order). If an individual future raises, that chunk's score defaults to 0.5 and a warning is logged — the whole batch does not fail.
_remove_near_duplicates
def _remove_near_duplicates(self, chunks: List[Tuple[Any, float]]) -> List[Tuple[Any, float]]
- Sort chunks by original score descending (so the higher-scoring copy of a near-duplicate pair is evaluated first and kept).
- For each chunk, compute its word set (
re.findall(r"\w+", text.lower())) and compare via_jaccard()against every previously kept chunk's word set. - If any comparison meets or exceeds
config.dedup_threshold(default0.85), the chunk is dropped as a near-duplicate. - Returns the kept
(chunk, score)pairs (order = original sort order, i.e. highest original score first — not re-sorted back to input order).
_jaccard (static)
@staticmethod
def _jaccard(a: set, b: set) -> float
Standard Jaccard index |a ∩ b| / |a ∪ b|; returns 0.0 if either set is empty.
_length_bonus
def _length_bonus(self, word_count: int) -> float
Piecewise linear bonus in [0.0, 1.0]:
ideal_min_words <= word_count <= ideal_max_words→1.0word_count < ideal_min_words→max(0.0, word_count / ideal_min_words)word_count > ideal_max_words→max(0.0, 1.0 - (word_count - ideal_max_words) / ideal_max_words)
_get_text / _get_doc_id
_get_text checks text, content, page_content attributes in that order, returning the first non-empty string found, or "" (with a warning log) if none are present. _get_doc_id checks doc_id then document_id, falling back to the literal string "unknown".
_truncate_to_words (static)
Truncates text to at most max_words words (splitting on whitespace) — used to bound the size of text sent to the LLM for relevance scoring (_LLM_TEXT_MAX_WORDS = 150).
_llm_relevance_score
Builds an Arabic or English prompt (based on config.llm_eval_language) asking the LLM to rate 0–10 relevance with no explanation, calls llm.generate(prompt, max_tokens=5), extracts the first numeric match via regex, and normalizes to [0.0, 1.0]. Returns 0.5 (neutral) if self.llm is None, if the LLM call raises, or if no numeric match is found.
_llm_relevance_score_cached
@lru_cache(maxsize=512)
def _llm_relevance_score_cached(self, query: str, text: str) -> float
A cached wrapper around _llm_relevance_score, keyed on (self, query, text) (since lru_cache is applied directly to an instance method). Prevents redundant LLM calls for repeated (query, text) pairs within the same Reranker instance's lifetime, up to 512 entries.
clear_cache
def clear_cache(self) -> None
Calls self._llm_relevance_score_cached.cache_clear() and logs LLM relevance cache cleared..
[!WARNING]
cache_clear()on anlru_cache-wrapped method clears it for all instances Because@lru_cacheis applied at the class level to an unbound method,self._llm_relevance_score_cached.cache_clear()clears the single shared cache used by everyRerankerinstance in the process, not justself's entries. Callingclear_cache()on oneRerankerinstance will also evict cached scores computed by otherRerankerinstances.
__repr__
Reranker(mode={mode.value}, top_k={top_k}, dedup={dedup_enabled})
Best practices
- If you don't have an LLM handy, don't bother setting
mode=LLM/HYBRIDexplicitly —Rerankerwill downgrade to heuristic anyway, but it will also mutate yourRerankerConfigobject as a side effect (see warning above), which can be surprising if you reuse that config elsewhere. - Tune
ideal_min_words/ideal_max_wordsto match your chunk size — the default30–300word range assumes moderate-length chunks. - Set
dedup_thresholdlower (e.g.0.6–0.7) if your vector DB tends to return chunks with heavy overlapping boilerplate.
Common mistakes
- Assuming
clear_cache()is per-instance — it clears the process-widelru_cache. - Assuming diversity bonuses apply in pure
LLMmode — they don't (_rerank_llmdoesn't usew["div"]). - Passing a
RerankerConfigwithmode=LLMand nollmand being surprised the object's.modeattribute changed after constructing aRerankerwith it.
Related APIs
RerankerConfig,RerankMode— configuration types.RAGSystem.retrieve— the primary caller ofrerank().