Spaces:
Running
Running
| """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 | |
| 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 | |
| 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) | |
| ] | |