Public API for the Step 2.7 two-stage reranker. Architectural notes
live in architecture/reranking.md;
the design anchor is ADR-0006.
The rag-reranker package ships three pieces:
| Symbol | Module | Purpose |
|---|---|---|
RerankPipeline |
rag_reranker.pipeline |
Async orchestrator running the two-stage cascade + optional MMR. |
MMRConfig |
rag_reranker.types |
Configuration for the MMR diversification pass. |
mmr_rerank |
rag_reranker.mmr |
Pure MMR primitive (sync, deterministic). |
Concrete reranker backends ship under rag_reranker.backends behind
optional extras:
| Class | Extra | Notes |
|---|---|---|
SentenceTransformersCrossEncoder |
sentence-transformers |
Local cross-encoder. Factories: bge_reranker_v2_m3() (ADR default), minilm_l6() (fast English). |
CohereReranker |
cohere |
Cohere Rerank v3 hosted API. |
JinaReranker |
jina |
Jina AI Reranker hosted API via aiohttp. |
from rag_core.spi.noop import NoopReranker
from rag_reranker import RerankPipeline
pipe = RerankPipeline(
reranker=NoopReranker(),
hydrator=my_hydrator, # async (ctx, refs) -> list[Chunk]
)
chunks = await pipe.rerank(ctx, "the query", refs, top_k=10)from rag_reranker import MMRConfig, RerankPipeline
from rag_reranker.backends.sentence_transformers import bge_reranker_v2_m3
pipe = RerankPipeline(
reranker=bge_reranker_v2_m3(),
hydrator=hybrid_backend.hydrate,
embedder=my_embedder, # used by MMR when chunks lack metadata['embedding']
mmr=MMRConfig(lambda_=0.7, top_k=10),
stage1_top_n=50,
stage2_top_n=20,
)
chunks = await pipe.rerank(ctx, "the query", refs, top_k=10)from rag_reranker.backends.cohere import CohereReranker
reranker = CohereReranker(api_key=os.environ["COHERE_API_KEY"], model="rerank-v3.5")
pipe = RerankPipeline(reranker=reranker, hydrator=hybrid_backend.hydrate)$ ragctl rerank "what is a cross-encoder?" --top-k 3
query: 'what is a cross-encoder?'
tenant: ragctl-local
backend: NoopReranker
mmr: off
top_k: 3
results: 3
#1 score=1.0000 id=chunk-0 Cross-encoder rerankers score (query, document) pairs jointly.
#2 score=1.0000 id=chunk-1 Reciprocal rank fusion combines multiple ranked lists.
#3 score=1.0000 id=chunk-2 MMR balances relevance against diversity in selection.
Flags:
--top-k / -k Final number of reranked chunks to print (default 3).
--stage1-top-n Stage-1 (fast_rerank) truncation depth (default 50).
--stage2-top-n Stage-2 (precise_rerank) candidate depth (default 10).
--mmr / --no-mmr Apply MMR diversification (default off).
--mmr-lambda MMR relevance/diversity trade-off in [0, 1] (default 0.7).
--backend noop | sentence-transformers | cohere | jina (default noop).
--model Model id (sentence-transformers / cohere / jina backends).
--tenant / -t Tenant id used to build the RequestContext.
--backend cohere and --backend jina require COHERE_API_KEY /
JINA_API_KEY in the environment.
- Stage 1.
Reranker.fast_rerank(ctx, query, candidates, top_n=stage1_top_n)narrows the candidate set. Default implementation truncates without re-scoring. - Early exit.
Reranker.should_early_exit(stage1_scores)— ifTrue, hydrate the toptop_kChunkRefs and return; stage 2 and MMR are skipped. Stage-1 scores are preserved on the way out. - Hydrate.
hydrator(ctx, stage1_refs)turnsChunkRef\s intoChunk\s (loading content + metadata). Any*RetrievalBackend.hydratemethod satisfies this protocol. - Stage 2.
Reranker.precise_rerank(ctx, query, chunks, top_n=stage2_top_n)runs the cross-encoder and writes scores onto each returned chunk'sChunk.scorefield. - MMR (optional). When
MMRConfigis supplied AND there's more than one candidate,mmr_rerank(...)re-orders stage-2 output for diversity. SeeMMRConfigfor theλknob.
Any backend or stage failure raises
RerankerError (a
RetrievalError subclass). The context dict carries a stage key
identifying which step failed:
stage |
Meaning |
|---|---|
"fast" |
fast_rerank raised. |
"early_exit" |
should_early_exit raised. |
"hydrate" |
The hydrator callable raised. |
"precise" |
precise_rerank raised. |
"mmr" |
MMR was requested but embeddings could not be resolved. |
Carbonell & Goldstein (1998). The selection rule is:
MMR(d) = λ · rel(d, q) − (1 − λ) · max_{s ∈ S} sim(d, s)
where S is the running set of selected documents, rel is the
relevance score (passed in by the caller), and sim is cosine
similarity over the supplied embeddings. The first pick uses
argmax(relevance) (no penalty term) so the algorithm is stable at
λ = 0.
| Argument | Required | Notes |
|---|---|---|
candidates |
yes | Treated as a set; MMR builds its own ordering. |
relevance_scores |
yes | Same length as candidates. |
embeddings |
yes | Same length as candidates; all rows same dimension. |
lambda_ |
no | Trade-off in [0, 1]. Default 0.7. 1.0 → pure relevance; 0.0 → first pick by relevance then pure diversity thereafter. |
top_k |
no | Cap at len(candidates). Default 10. |
The cosine-similarity cache is lazy — most picks only need similarity
to the selected set, not to every other candidate. Returned chunks
are unmodified (MMR does NOT write to Chunk.score; that's the
output of precise_rerank).
| Hook | What you provide | Where it goes |
|---|---|---|
Reranker subclass |
precise_rerank (required), optionally fast_rerank / should_early_exit. |
RerankPipeline(reranker=...). |
Hydrator callable |
async (ctx, refs) -> list[Chunk]. |
RerankPipeline(hydrator=...). |
Embedder |
Only when MMR is on and chunks lack metadata['embedding']. |
RerankPipeline(embedder=...). |
MMRConfig |
λ + top_k. |
RerankPipeline(mmr=...). |
To add a backend, see the backends folder — each existing backend is a stand-alone module under 200 lines.
architecture/reranking.md— why it works the way it does.adr/ADR-0006-two-stage-rerank.md— the design decision.reference/retrieval.md—HybridRetriever, what feeds stage 1.reference/rag-core.md—RequestContext,ChunkRef,Chunk,RerankerError.