xavier-ramirez.com
← Your own AI Overview: Chunking

Your own AI Overview · 03

Your own AI Overview: Retrieve & rerank

Dense recall finds candidates; reranking decides the answer.

A cheap vector search casts a wide net — the top dozen chunks that look related. A reranker then reads the question and each chunk together and reorders them, so the passage that actually answers you lands in the top few you can afford to send the model. This post opens up the two middle nodes of the pipeline: retrieve and rerank.

01 · Spine02 · Chunking03 · Retrieve & rerank04 · Access control05 · Grounding & citations06 · Evals & guardrails

↳ Post 01 was the whole spine, thin. This is node 03 — the two-stage funnel that turns a wide net of candidates into the few passages the model reads.

Worked exampleThis series uses property & casualty (P&C) insurance as its running example — policies, claims and underwriting notes. The pipeline itself is domain-agnostic; the data just happens to be an insurer’s.

The two-stage funnel

Retrieve wide, rerank sharp

One search can’t be both fast over millions of chunks and precise about which one answers the question — so we do two passes.

Why two passes, not one?

A reranker is accurate but expensive — you can’t run it over a million chunks per query. Vector search is the opposite: cheap enough to score everything, but only “roughly” right about order. So the net catches a wide dozen fast, and the reranker spends its precision on just those twelve. Recall first, precision second.

retrieve → rerank

↳ The reranker reorders all 12; the budget decides how many survive to the model.

Budget 5: the underwriting note (vector rank #9) sits comfortably inside the budget — the reranker put it at #1.

Vector search is a librarian who fetches every book with the right words on the spine. The reranker is the one who opens each book and checks which page actually answers you. You need the first pass to be fast; you need the second to be right.

That funnel isn’t a diagram of the code — it is the code. The retrieve node embeds each sub-query, searches with a permission filter, and merges the hits, keeping each chunk’s best score:

pipeline/nodes/retrieve.py
def run(state, settings, *, embedder: Embedder, store: VectorStore) -> dict:
    identity = state["identity"]
    # The filter is derived only from the signed identity — never the prompt.
    # Fail-closed: no roles => no results. (That RBAC deep-dive is Post 4.)
    flt = build_filter(identity)

    queries = state.get("sub_queries") or [state.get("question", "")]
    queries = [q for q in queries if q and q.strip()]

    best: dict[str, Retrieved] = {}
    for q in queries:
        vec = embedder.embed_query(q)
        for hit in store.search(vec, flt, k=settings.retrieve_k):   # k = 12
            cid = hit.chunk.chunk_id
            existing = best.get(cid)
            # Keep the strongest score seen for a chunk across sub-queries.
            if existing is None or hit.score > existing.score:
                best[cid] = hit

    candidates = sorted(best.values(), key=lambda r: r.score, reverse=True)
    return {"candidates": candidates, ...}

The rerank node is deliberately tiny — reorder by true relevance, trim to the budget, hand the sharper set on to grounding:

pipeline/nodes/rerank.py
def run(state, settings, *, reranker: Reranker) -> dict:
    question = state.get("question", "")
    candidates = state.get("candidates", [])
    # Reorder by true relevance, then trim to the context budget.
    reranked = reranker.rerank(question, candidates, k=settings.rerank_k)  # k = 5
    return {"reranked": reranked, ...}

Where vectors fumble

When meaning isn’t enough

Embeddings are great at “what is this about” and surprisingly bad at “does it contain exactly POL-55012”.

Ask a librarian who only understands topics to find the file numbered 55012 and they’ll hand you five files about the same policy. Add a clerk who reads numbers and the exact file comes first. Production RAG usually wants both.

What ships in v3-retrieval

Be honest about the code: the default query today is dense k-NN + the permission filter + a reranker. True hybrid — a keyword (BM25) pass merged with the vectors — is the documented next step, not something already wired into the default query. It’s a small reach, though: the search index already stores the chunk text as a full-text field, so a keyword pass is one query away. This section shows why you’d add it — an honest next commit, not a claim that it runs.

The reranker is the one place this stage can call an outside service, so it’s built to fail soft — a service error degrades to plain retrieval order rather than breaking the answer. Local mode uses a no-op passthrough, so the graph runs with zero cloud:

retrieval/rerank.py
class BedrockReranker:
    """Reranker backed by a managed Rerank API, with a graceful fallback."""

    def rerank(self, query, candidates, k):
        candidates = list(candidates)
        if not candidates:
            return []
        try:
            return self._rerank_via_service(query, candidates, k)
        except Exception as exc:            # rerank is best-effort, never fatal
            logger.warning("Rerank failed (%s); falling back to retrieval order.", exc)
            return candidates[:k]           # degrade to dense order, don't break


class NoopReranker:
    """Local / reranker=none passthrough — keep retrieval order, trim to k."""

    def rerank(self, query, candidates, k):
        return list(candidates[:k])
Run it yourself · own-overview @ v3-retrieval

This whole post is one repo at one tag. Clone it, seed synthetic P&C insurance data, and watch the two-stage funnel run on your laptop — dense recall, then a reranker that reorders and trims to the budget.

terminal
git clone https://github.com/xaviramirezcom/open-ai-overview
cd open-ai-overview
git checkout v3-retrieval

uv sync --extra local
cp .env.example .env         # set the *_PROVIDER / VECTOR_STORE knobs to "local"

own-overview seed            # synthetic P&C insurance data + ingest
own-overview query "Why did the premium on POL-55012 go up?" --role adjuster

You’ll see the answer with numbered citations back to the source chunks — the top-5 reranked passages the model was allowed to read. Both vector-store backends implement the same search(query_vector, flt, k) contract and apply the permission filter before scoring: local mode is numpy cosine, the default is a k-NN index with the filter compiled into the query.

Teaching-grade reference implementation, not a production insurance product. It reproduces the ideas; bring your own data and keys. MIT-licensed.

Explain it back

Your vector search returns 12 chunks and the one that actually answers the question is sitting at rank #9. You send the top 5 to the model. Two things are now true — name the problem, and name the fix.

Reveal a model answer

The problem: dense similarity ranked the answer #9, so a naive “top-5 by vector score” cuts it — the model never sees the passage it needs and either guesses or abstains. The fix: rerank before you trim. A reranker reads the question and each candidate together, scores true relevance, and can lift that #9 chunk to #1 — so it’s safely inside the top-5 budget the model receives. (And if the exact identifier matters, add a keyword/hybrid pass so the chunk that literally names POL-55012 can’t be missed in the first place.)

Next in the series · 04
Access control at retrieval

You saw build_filter(identity) ride along on every search here — quietly deciding which chunks were even candidates. In a regulated insurer that filter is the whole game: an adjuster and an underwriter ask the same question and get different answers, because restricted chunks are excluded before the model sees them. Next: how the signed token becomes a query filter, why fail-closed matters, and what the audit log records.

Continue to Access control →