Spaces:
Running
Running
| """Cross-encoder reranking: step 2 of a two-stage retrieval. | |
| The retriever (vector / hybrid / graph) outputs a **large top-N**; the | |
| cross-encoder, which reads the (query, document) pair **together**, re-scores | |
| them finely and we keep the **top-k**. More accurate than a bi-encoder (which encodes the | |
| query and the document *separately*), but too slow for the whole corpus -- hence | |
| the two stages: we only run it on the already-filtered candidates. | |
| """ | |
| from functools import lru_cache | |
| def _model(name: str): | |
| """Load (and cache) the cross-encoder; lazy import.""" | |
| from sentence_transformers import CrossEncoder | |
| return CrossEncoder(name) | |
| class CrossEncoderReranker: | |
| """Re-score candidates with a cross-encoder and return the reordered top-k.""" | |
| def __init__(self, model_name: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"): | |
| self.model_name = model_name | |
| def rerank( | |
| self, query: str, candidates: list[dict], top_k: int, | |
| mode: str = "replace", rrf_k: int = 60, | |
| ) -> list[dict]: | |
| """Reorder `candidates` (dicts with a "text" key) and return the top-k. | |
| - `replace`: we follow the cross-encoder alone (it **replaces** the base ranking). | |
| - `fusion` : RRF between the base rank (input position) and the | |
| cross-encoder rank -- each ranking "votes", which **protects** an | |
| already-strong retriever from being dragged down. | |
| """ | |
| if not candidates: | |
| return [] | |
| scores = _model(self.model_name).predict([(query, c["text"]) for c in candidates]) | |
| ce_order = sorted(range(len(candidates)), key=lambda i: scores[i], reverse=True) | |
| if mode == "fusion": | |
| ce_rank = [0] * len(candidates) | |
| for rank, i in enumerate(ce_order): | |
| ce_rank[i] = rank | |
| order = sorted( | |
| range(len(candidates)), | |
| key=lambda i: 1.0 / (rrf_k + i) + 1.0 / (rrf_k + ce_rank[i]), | |
| reverse=True, | |
| ) | |
| else: | |
| order = ce_order | |
| return [candidates[i] for i in order[:top_k]] | |
| class RerankedRetriever: | |
| """Decorator: add a reranking stage to any retriever. | |
| Exposes the **same** `search(query, k)`: retrieves an enlarged top-N via the | |
| inner retriever, then the cross-encoder reorders and we return the top-k. | |
| Transparent for the RAG and the application -- we wrap the retriever, nothing | |
| else changes (single responsibility). Disabled by default in the pipeline. | |
| """ | |
| def __init__(self, inner, reranker: CrossEncoderReranker, | |
| mode: str = "replace", candidates: int = 30): | |
| self.inner = inner | |
| self.reranker = reranker | |
| self.mode = mode | |
| self.candidates = candidates | |
| def search(self, query: str, k: int = 5) -> list[dict]: | |
| results = self.inner.search(query, k=max(self.candidates, k)) | |
| return self.reranker.rerank(query, results, top_k=k, mode=self.mode) | |