Reranking & retrieval
Retrieval in CiteNexus is a fixed, auditable pipeline: several retrievers each produce a ranked list, the lists are merged with Reciprocal Rank Fusion, and the fused head is optionally reranked by a cross-encoder you inject. Fusion lives in the core, so no plugin can bypass it — every candidate that reaches the grounding gate went through the same deterministic path.
The pipeline
Section titled “The pipeline”- Retrieve. Each enabled retriever runs against the query and returns a ranked candidate list.
- Fuse. All lists are combined with RRF (
k = 60): a candidate at zero-basedrankcontributes1 / (k + rank + 1), merged by Evidence-Unit id. - Rerank. The fused head is reordered by the injected reranker against the original query (a no-op passthrough if you inject none).
- Cut. The top
top_ksurvive to grounding.
Retrievers and the signals that enable them
Section titled “Retrievers and the signals that enable them”Which retrievers run is gated by the signals you declare on the client. Omit
signals and all of them are built and queried.
| Retriever | Enabled by signal | What it does |
|---|---|---|
| Vector (dense) | embedding (and an embedder is set) |
nearest-neighbour over the vector store |
| Lexical (BM25) | text |
BM25 / full-text search (Postgres tsvector or the in-core BM25) |
| Structure | structure |
matches query terms to structure-node labels |
| Graph | graph / community |
matches query terms to entity-node labels and returns that node’s Evidence Units (no edge walking — see graph) |
| Wiki | wiki |
distilled-wiki navigation (slow path) |
from citenexus import CiteNexus
# Lexical + vector only — no graph/wiki/structure built or queried.rag = CiteNexus("./citenexus-data", embedder=..., generator=..., signals=["embedding", "text"])Injecting a reranker
Section titled “Injecting a reranker”By default CiteNexus applies an identity passthrough (fusion order is kept).
Inject OpenAICompatibleReranker to reorder the fused head with a cross-encoder.
It POSTs {model, query, documents} to {base_url}/rerank and reads the
Cohere/Jina-shape results[] (relevance_score + index).
from citenexus import CiteNexusfrom citenexus.retrieve import OpenAICompatibleReranker
reranker = OpenAICompatibleReranker( base_url="http://localhost:11434/v1", # any endpoint exposing POST {base_url}/rerank model="bge-reranker-v2-m3",)
rag = CiteNexus( "./citenexus-data", embedder=..., generator=..., reranker=reranker, # replaces the default identity passthrough top_k=5,)Controlling top_k
Section titled “Controlling top_k”top_k is how many candidates survive to grounding. Set a default on the client,
or override it per call:
rag = CiteNexus("./citenexus-data", embedder=..., generator=..., top_k=5) # default
response = rag.ask("What notice does termination require?", k=8) # override for this calldocs = rag.retrieve("termination notice", k=20) # retrieval only, no generationretrieve() returns the ranked candidates without generating an answer — the
engine under ask(), and the escape hatch for wiring your own small model.
RRF, directly — in all three ports
Section titled “RRF, directly — in all three ports”The fusion function is public and pure, if you fuse ranked lists yourself. It is
one of the algorithms pinned byte-for-byte across the ports
(conformance/cases/rrf.json), so the fused order is identical in each:
from citenexus.retrieve import rrf_fuse
fused = rrf_fuse([list_a, list_b], k=60) # lists of Candidate objectsimport "github.com/muthuishere/citenexus/golang/rrf"
fused := rrf.Fuse([][]string{listA, listB}, rrf.DefaultK) // DefaultK == 60// rrf.Fuse([][]string{{"a", "b"}, {"b", "c"}}, 60) → [b a c]import { rrfFuse } from "@muthuishere/citenexus";
const fused = rrfFuse([listA, listB], 60);// rrfFuse([["a", "b"], ["b", "c"]], 60) → ["b", "a", "c"]