teachyou.ai academy
← All posts
RAG

Reranking Models Compared: Cohere vs BGE vs Jina for RAG

Ira Menon · May 5, 2026 · 14 min read

If your RAG pipeline retrieves the right chunk but ranks it eighth, your LLM never sees it. That's the entire problem reranking solves, and it's the single highest-leverage upgrade I make to almost every retrieval pipeline I touch. Vector search gets you a candidate pool fast — cosine similarity over embeddings is cheap and approximate. But "approximate" is doing a lot of work in that sentence. Bi-encoder embeddings compress a whole passage into one vector, and that compression loses the fine-grained interaction between a specific query and a specific document. A reranker fixes this by scoring query-document pairs jointly, at the cost of latency. The question every team eventually asks is which reranker to actually use in production. I've shipped RAG systems with all three of the major options — Cohere's hosted rerank API, BGE (BAAI General Embedding) reranker models running locally, and Jina's reranker family — and this article is the comparison I wish existed when I started. This is not a benchmark leaderboard regurgitation; it's what changes in your infra, your latency budget, and your ops burden depending on which one you pick.

Why Reranking Exists as a Separate Step

Before comparing models, it's worth being precise about why a second-stage reranker outperforms a bigger embedding model. A bi-encoder (the model behind your vector store) encodes the query and the document independently. It never lets the query "look at" the document during encoding — the interaction happens only at the final dot-product or cosine-similarity step. That's what makes vector search fast: you can pre-compute document embeddings once and store them in an index like pgvector or Pinecone, then just embed the query at search time.

A cross-encoder reranker, which is what Cohere, BGE, and Jina rerankers all are, works differently. It takes the query and a candidate document concatenated together as a single input, runs them through a transformer jointly, and outputs a relevance score. This lets the model attend across query tokens and document tokens simultaneously — it can notice that "python" in the query and "Python" in a code snippet are the same concept, or that a document mentions the exact entity the query is asking about, in a way that pooled embeddings often blur out.

The tradeoff is unavoidable: cross-encoders can't be precomputed. You must run inference at query time for every candidate document, which means reranking only ever operates on a shortlist — typically the top 20 to 100 results your vector search already returned — not your entire corpus. Retrieval narrows the funnel; reranking reorders the narrow part with a smarter model.

# Typical two-stage retrieval shape, model-agnostic
def retrieve_and_rerank(query, vector_store, reranker, top_k_retrieve=50, top_k_final=8):
    candidates = vector_store.similarity_search(query, k=top_k_retrieve)
    documents = [c.page_content for c in candidates]
    scores = reranker.score(query, documents)
    ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
    return [doc for doc, score in ranked[:top_k_final]]

Every option below plugs into that reranker.score() call differently — some are an HTTP request, some are a local model.predict() call. That distinction matters more than people expect once you're past the prototype stage.

Cohere Rerank: The Hosted, Zero-Ops Option

Cohere's rerank endpoint is the one most teams reach for first, and for good reason: it's a single API call, it's consistently strong across domains, and you never think about GPU provisioning. You send a query and a list of documents, and it returns a ranked list of relevance scores.

import cohere

co = cohere.Client("YOUR_COHERE_API_KEY")

response = co.rerank(
    model="rerank-english-v3.0",
    query="how do I handle rate limiting in a FastAPI app?",
    documents=[
        "FastAPI middleware can be used to enforce rate limits per client IP.",
        "Rate limiting protects APIs from abuse by capping request frequency.",
        "FastAPI supports dependency injection for shared logic across routes.",
        "You can use slowapi, a FastAPI-compatible wrapper around limits, for rate limiting.",
    ],
    top_n=2,
)

for result in response.results:
    print(result.relevance_score, result.index)

What I like about Cohere in practice: it handles multilingual queries well out of the box, it has a documented context window per document (so you don't silently truncate long chunks without knowing it), and the relevance scores are well-calibrated enough that you can set a hard threshold (say, drop anything below 0.15) to filter out genuinely irrelevant chunks rather than just reordering them. That threshold-based filtering is underrated — it turns your reranker into a relevance gate, not just a sorter, which matters a lot when your retrieval step sometimes returns zero good matches and you'd rather show "I don't have information on that" than force-feed the LLM the least-bad chunk.

The downsides are the ones you'd expect from any hosted API: per-document pricing that adds up if you're reranking large candidate sets on every query, a network round trip that adds 100-300ms depending on your region and document count, and a hard dependency on Cohere's uptime. If your product has strict data residency requirements — health records, financial documents, anything that legally can't leave your VPC — sending document text to a third-party API is a blocker regardless of model quality. That's the actual decision point for a lot of teams, not raw accuracy.

BGE Reranker: Open-Weight and Self-Hosted

BGE (from the Beijing Academy of Artificial Intelligence) publishes reranker checkpoints — bge-reranker-base, bge-reranker-large, and bge-reranker-v2-m3 — as open weights you can download and run yourself. This is the option for teams that need reranking inside their own network boundary, or that are running high query volumes where per-call API pricing stops making sense.

from FlagEmbedding import FlagReranker

reranker = FlagReranker("BAAI/bge-reranker-v2-m3", use_fp16=True)

query = "how do I handle rate limiting in a FastAPI app?"
documents = [
    "FastAPI middleware can be used to enforce rate limits per client IP.",
    "FastAPI supports dependency injection for shared logic across routes.",
    "You can use slowapi, a FastAPI-compatible wrapper around limits, for rate limiting.",
]

pairs = [[query, doc] for doc in documents]
scores = reranker.compute_score(pairs, normalize=True)

for doc, score in sorted(zip(documents, scores), key=lambda x: x[1], reverse=True):
    print(round(score, 4), doc)

The bge-reranker-v2-m3 checkpoint in particular is multilingual and pairs naturally with bge-m3 embeddings if you're already using BAAI's embedding model for retrieval — using matched embedding and reranker families from the same lab tends to reduce weird edge-case mismatches, though it's not a hard requirement.

Running BGE yourself means you own the inference stack. On a single mid-range GPU (something like an A10 or even a T4 for the base model), you can comfortably serve reranking for a small-to-medium production workload, and you can batch requests to keep GPU utilization reasonable. The catch is everything that comes with self-hosting: you need to manage model serving (I've deployed BGE rerankers behind both a plain FastAPI wrapper and a Text Embeddings Inference container from Hugging Face), you need to think about autoscaling under bursty traffic, and you're responsible for monitoring latency degradation when a GPU node gets noisy neighbors. If you don't already have ML infra experience on the team, this is a real cost, not just a checkbox.

One thing worth flagging from hands-on use: bge-reranker-base is fast and fine for straightforward keyword-adjacent relevance, but on queries that require more semantic nuance — distinguishing "how to prevent overfitting" from "how to detect overfitting," for instance — the larger v2-m3 checkpoint noticeably outperforms it. Don't default to the base model just because it's smaller; test both on your actual query distribution before deciding.

Jina Reranker: The Long-Context Specialist

Jina AI's reranker models (jina-reranker-v2-base-multilingual and the newer jina-reranker-v3) carve out a distinct niche: they're built with unusually long context windows and strong multilingual and code-aware behavior. If your documents are long — legal contracts, full support tickets with quoted email threads, long code files — Jina's rerankers handle that without you having to chunk as aggressively before reranking.

import requests

url = "https://api.jina.ai/v1/rerank"
headers = {
    "Authorization": "Bearer YOUR_JINA_API_KEY",
    "Content-Type": "application/json",
}
payload = {
    "model": "jina-reranker-v2-base-multilingual",
    "query": "how do I handle rate limiting in a FastAPI app?",
    "documents": [
        "FastAPI middleware can be used to enforce rate limits per client IP.",
        "FastAPI supports dependency injection for shared logic across routes.",
        "You can use slowapi, a FastAPI-compatible wrapper around limits, for rate limiting.",
    ],
    "top_n": 2,
}

response = requests.post(url, headers=headers, json=payload)
print(response.json())

Jina also ships open weights for several of its reranker checkpoints, so you get the same hosted-vs-self-hosted choice you get with BGE — you can call their API for convenience or pull the weights down and run them with sentence-transformers or their own inference library. That flexibility is genuinely useful if you want to prototype against the hosted API and then move to self-hosted once you understand your traffic pattern.

In practice, I reach for Jina specifically when a client's documents don't compress well into short chunks — think technical specifications with heavy cross-referencing, or codebases where a relevant function spans 150+ lines. Forcing those into 512-token chunks to fit a shorter-context reranker loses exactly the surrounding detail that makes the match relevant in the first place. Jina's longer effective context reduces how much pre-chunking gymnastics you need to do upstream.

A Side-by-Side Look at Practical Tradeoffs

Rather than quoting benchmark numbers that go stale within a quarter, here's how I actually reason about the choice for a new project:

  • Data residency and compliance: If documents cannot leave your infrastructure, self-hosted BGE (or self-hosted Jina weights) is the only real option. Cohere and Jina's hosted API both require sending document text over the network.
  • Team ML-ops maturity: If you don't have anyone comfortable managing GPU inference, model versioning, and autoscaling, a hosted API (Cohere or Jina's endpoint) removes an entire category of operational risk.
  • Query volume and cost curve: Hosted APIs charge per document reranked. At low-to-moderate volume this is cheaper than provisioning a GPU. Past a certain query-per-second threshold, self-hosting BGE on your own hardware becomes cheaper, sometimes dramatically so.
  • Document length: Long documents with important context spread across many tokens favor Jina's longer-context rerankers. Short, dense chunks (FAQ entries, short support articles) work fine with any of the three.
  • Multilingual requirements: Both Cohere's multilingual rerank model and BGE's v2-m3 and Jina's multilingual checkpoints handle non-English queries reasonably well. If you're English-only, this isn't a differentiator.
  • Latency budget: Self-hosted BGE on a local GPU, once warmed up, often beats a hosted API round trip because you skip the network hop entirely — but only if your GPU isn't already saturated by other workloads.

A pattern I use often in early client conversations: start with Cohere or Jina's hosted API to validate that reranking actually improves your specific pipeline's answer quality (it's a five-minute integration), and only invest in self-hosting BGE once you've confirmed the uplift is real and you understand your query volume well enough to justify the infra cost.

Measuring Whether Reranking Actually Helped

A mistake I see constantly: teams add a reranker, ship it, and never verify it's actually improving anything. Reranking is not free — it adds latency and, for hosted options, cost — so you should measure the lift, not assume it.

The simplest evaluation I run uses a small labeled set of query-to-relevant-chunk pairs, built from real user questions and a human (or careful LLM-assisted) judgment of which chunk actually answers them.

def evaluate_rerank_lift(eval_set, vector_store, reranker, k=5):
    """eval_set: list of dicts with 'query' and 'relevant_doc_id'"""
    hits_before, hits_after = 0, 0

    for item in eval_set:
        candidates = vector_store.similarity_search(item["query"], k=20)

        # Before: raw vector search order
        top_k_before = [c.metadata["id"] for c in candidates[:k]]
        if item["relevant_doc_id"] in top_k_before:
            hits_before += 1

        # After: reranked order
        docs = [c.page_content for c in candidates]
        scores = reranker.score(item["query"], docs)
        reranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
        top_k_after = [c.metadata["id"] for c in reranked[:k]]
        if item["relevant_doc_id"] in top_k_after:
            hits_after += 1

    print(f"Recall@{k} before rerank: {hits_before / len(eval_set):.2%}")
    print(f"Recall@{k} after rerank:  {hits_after / len(eval_set):.2%}")

Run this before you commit to a reranker in production, and again whenever you change chunking strategy, embedding model, or the reranker itself. I've seen cases — usually when the underlying embedding model is already quite strong and the corpus is small and homogeneous — where reranking barely moves recall@5, and the added latency isn't worth it. I've also seen cases, especially with noisy or long-tail corpora, where reranking takes recall@5 from mediocre to genuinely reliable. You want to know which situation you're in before you ship, not guess.

Where Reranking Fits in the Broader Pipeline

Reranking is a middle step, not a bookend. The full shape looks like: chunk your documents, embed and index them, retrieve a generous candidate set (I typically pull 30-50 candidates for reranking, even though I'll only pass 5-8 to the LLM), rerank that set, then optionally apply a final relevance threshold before constructing your prompt.

class RAGPipeline:
    def __init__(self, vector_store, reranker, llm, relevance_threshold=0.1):
        self.vector_store = vector_store
        self.reranker = reranker
        self.llm = llm
        self.relevance_threshold = relevance_threshold

    def answer(self, query, retrieve_k=40, final_k=6):
        candidates = self.vector_store.similarity_search(query, k=retrieve_k)
        docs = [c.page_content for c in candidates]
        scores = self.reranker.score(query, docs)

        ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
        filtered = [(doc, score) for doc, score in ranked if score >= self.relevance_threshold]

        if not filtered:
            return "I don't have enough information to answer that."

        context_chunks = [doc.page_content for doc, _ in filtered[:final_k]]
        context = "\n\n".join(context_chunks)
        prompt = f"Answer using only this context:\n{context}\n\nQuestion: {query}"
        return self.llm.generate(prompt)

Notice the relevance_threshold guard. It's easy to build a RAG system that always answers, even when nothing retrieved is actually relevant, because you never told it that "answer nothing" is a valid outcome. Whichever reranker you pick, spend time calibrating that threshold against your own eval set rather than copying a number from a blog post — the right cutoff depends on your reranker's score distribution, which differs meaningfully between Cohere, BGE, and Jina.

Common Mistakes I See Teams Make

A few patterns come up again and again when I'm called in to debug a RAG system that "isn't working":

  • Reranking too small a candidate set. If you only retrieve the top 5 from vector search and then rerank those same 5, you've gained almost nothing — the reranker can only reorder what's already there. Retrieve wide (20-50), rerank down to what you actually feed the LLM.
  • Ignoring the reranker's max input length. Every reranker, including all three covered here, has a token limit per document. If your chunks exceed it, silent truncation happens, and you'll rerank based on only the first portion of a chunk. Log actual token counts during testing.
  • Not re-testing after changing the embedding model. Retrieval and reranking are coupled through the candidate set. Swap your embedding model and your reranker's effective performance can shift, because the pool of candidates it's choosing from has changed.
  • Treating relevance score as a fixed universal number. Cohere's, BGE's, and Jina's scores are not on the same scale and aren't directly comparable. If you migrate rerankers, recalibrate your relevance threshold from scratch.
  • Skipping reranking for "obviously easy" domains. Even narrow, well-structured corpora benefit from reranking when queries are ambiguous or when multiple chunks are superficially similar. Don't assume it's unnecessary without measuring.

Bringing It Together

None of Cohere, BGE, or Jina is a universally "best" reranker — the right pick depends on constraints that have nothing to do with raw model quality: whether your documents can leave your network, whether your team can run GPU inference reliably, how long your typical document chunk is, and how many queries per second you're actually serving. My default recommendation for teams starting out is to prototype with a hosted API — Cohere or Jina — because the integration cost is minutes, not days, and it lets you validate that reranking helps before you invest in anything heavier. Once you understand your traffic and your compliance constraints, self-hosted BGE is a strong, well-supported option if you need full control or need to cut per-query cost at scale. And if your corpus leans toward long, context-heavy documents, give Jina's longer-context checkpoints a real look before defaulting to whatever's most popular.

If you're still building the retrieval fundamentals that reranking sits on top of — chunking strategy, embedding model choice, vector store selection, evaluation harnesses — that foundation matters more than which reranker you eventually pick, and it's exactly what we walk through step by step in Introduction to RAG. Get the retrieval layer right first, then layer reranking on top once you have a way to measure whether it's actually helping.