doc-ingestion / src /core /reranker.py
Vamshi Pokala
chore(ci): fix Ruff lint and Mypy for GitHub Actions
40db081
Raw
History Blame Contribute Delete
2.65 kB
"""Cross-encoder reranking for hybrid retrieval results."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, List, Sequence, Tuple, cast
from sentence_transformers import CrossEncoder
from src.core.retrieval_result import RetrievalResult
@dataclass
class RankedResult:
"""Retrieval chunk with cross-encoder score and final rerank position."""
result: RetrievalResult
cross_encoder_score: float
rerank_position: int
class CrossEncoderReranker:
"""Batch cross-encoder scoring and top-k reranking with optional score threshold."""
def __init__(
self,
model_name: str = "cross-encoder/ms-marco-MiniLM-L-6-v2",
batch_size: int = 32,
score_threshold: float = 0.1,
) -> None:
self.model_name = model_name
self.batch_size = batch_size
self.score_threshold = score_threshold
self._model: CrossEncoder | None = None
@property
def model(self) -> CrossEncoder:
if self._model is None:
self._model = CrossEncoder(self.model_name)
return self._model
def batch_score(self, pairs: List[Tuple[str, str]]) -> List[float]:
"""Score query-document pairs in batches."""
if not pairs:
return []
scores: List[float] = []
for start in range(0, len(pairs), self.batch_size):
batch = pairs[start : start + self.batch_size]
raw = self.model.predict(cast(Any, batch))
# predict may return ndarray or list of floats
for s in raw: # type: ignore[union-attr]
scores.append(float(s))
return scores
def rerank(
self,
query: str,
documents: Sequence[RetrievalResult],
top_k: int,
) -> List[RankedResult]:
"""Rerank retrieval hits by cross-encoder relevance; filter by score_threshold."""
if not documents:
return []
pairs: List[Tuple[str, str]] = [(query, d.text) for d in documents]
ce_scores = self.batch_score(pairs)
ranked: List[Tuple[RetrievalResult, float]] = [
(doc, score) for doc, score in zip(documents, ce_scores) if score >= self.score_threshold
]
if not ranked:
# keep best-effort: if everything filtered, take top by raw CE anyway
ranked = list(zip(documents, ce_scores))
ranked.sort(key=lambda x: x[1], reverse=True)
ranked = ranked[:top_k]
return [
RankedResult(result=doc, cross_encoder_score=score, rerank_position=i)
for i, (doc, score) in enumerate(ranked)
]