Skip to content
Shafin Zaman

RAG & search

Hybrid-search RAG + reranking

Combine keyword search and vector search, then rerank the merged hits with a cross-encoder so the passage the model reads is actually the best one.

Intermediate About a weekFree stack

Copy or download the full plan and paste it into your AI coding agent to build it.

Why build it

Pure vector search is great at meaning but misses exact terms like product codes, names, and acronyms; pure keyword search nails those but is blind to paraphrase. Real retrieval quality comes from doing both and then reranking. Hybrid search runs BM25 (a classic keyword ranking) and vector search in parallel and fuses the two result lists; a reranker (a cross-encoder that reads the query and each candidate together) then reorders them far more accurately than either first-stage retriever. This two-stage retrieve-then-rerank pattern is what separates a demo RAG from one people trust, and it is a direct upgrade to the basic Chat-with-your-PDF pattern.

Who it's for: You have built one RAG app and noticed the retrieved chunks are sometimes off. This is the next step: measurably better retrieval.

What you'll build

Core (MVP)

  • Ingest documents and index them for both keyword and vector search
  • Run BM25 keyword search and vector search for every query
  • Fuse the two ranked lists into one candidate set with reciprocal rank fusion
  • Rerank the fused candidates with a cross-encoder reranker
  • Feed the top reranked chunks to the LLM with a grounded, cited prompt
  • Show a side-by-side of vector-only vs hybrid+rerank for the same query

Stretch

  • Add a tiny labeled query set and compute recall@k for each retrieval mode
  • Cache rerank scores so repeated queries are instant
  • Expose sliders for the fusion weight and top-k so you can tune live
  • Swap in a larger reranker and compare quality vs latency

Step-by-step build

  1. 1

    Set up dual indexes

    Stand up Postgres with the pgvector extension. Create a chunks table with the text, a vector column for embeddings, and a tsvector column with a GIN index for full-text keyword search. One table now serves both retrieval modes, which keeps fusion simple.

  2. 2

    Ingest and embed

    Chunk your documents into overlapping passages the same way as a basic RAG app. For each chunk, store the raw text, its embedding, and let Postgres compute the tsvector. Keep the source and page so the final answer can cite them.

  3. 3

    Implement the two retrievers

    Write a vector search that returns the top 20 chunks by cosine distance, and a keyword search that returns the top 20 by ts_rank using websearch_to_tsquery. Run them independently first and eyeball where each one wins and loses on a few real queries.

  4. 4

    Fuse the results

    Merge the two ranked lists with reciprocal rank fusion: each chunk's score is the sum of 1/(k + rank) across the lists it appears in, with k around 60. This rewards chunks that both retrievers like and needs no score normalization between the two very different scales.

  5. 5

    Add the reranker

    Take the top 20 fused candidates and pass each (query, chunk) pair through the cross-encoder reranker to get a relevance score. Unlike the embedding model, the cross-encoder reads query and chunk together, so it is much more accurate. Sort by that score and keep the top 5.

  6. 6

    Ground the answer

    Send the top 5 reranked chunks to Groq with a strict system prompt: answer only from the context, cite sources, and say you do not know if the answer is not present. This is the same grounding discipline as basic RAG, now fed much better inputs.

  7. 7

    Build the comparison view

    Add a debug panel that shows, for one query, the vector-only top 5 next to the hybrid+rerank top 5. Seeing the reordering makes the value of each stage obvious and is a great thing to screenshot for your README.

  8. 8

    Measure it

    Write 15 to 20 queries where you know the correct source chunk, then compute recall@5 for vector-only, hybrid, and hybrid+rerank. Reporting a real number, not a vibe, is what makes this project credible on a resume.

Done when

  • On a query full of exact codes or names, hybrid retrieves the right chunk where vector-only misses it.
  • The reranked top 5 is visibly more relevant than the fused top 5 before reranking on several queries.
  • Your labeled query set shows recall@5 improving from vector-only to hybrid to hybrid+rerank.
  • The final answer cites sources and refuses to answer when the retrieved context does not contain it.
  • The live service handles a new document set with no code changes.

Ship it

Dockerize the FastAPI service with the reranker model baked into the image, and deploy it to a free AWS or Hugging Face Spaces target with Postgres. Ship a live URL and a README that leads with your recall@5 numbers for each retrieval mode and a screenshot of the vector-only vs hybrid+rerank comparison. Numbers plus a picture make the upgrade undeniable.

What it proves: You can build production-grade retrieval, not just a demo: you understand BM25, dense vectors, rank fusion, and cross-encoder reranking, and you can prove the quality gain with a real recall metric.

Hand it to your AI agent

Paste this into Cursor, Claude, or ChatGPT and build it step by step.

You are my senior AI engineer pair. Help me build "Hybrid-search RAG + reranking" step by step. RAG means retrieval-augmented generation: fetch relevant passages and make the LLM answer only from them. Hybrid search runs BM25 keyword ranking and vector (embedding) search together; a reranker is a cross-encoder that reads the query and each candidate together to score relevance more accurately than first-stage retrieval. Stack: FastAPI, Postgres + pgvector (vector column plus a tsvector full-text index), all-MiniLM-L6-v2 embeddings, a bge-reranker-base cross-encoder, Groq (Llama 3.3 70B) for the grounded answer.

Requirements:
1. One chunks table serving both vector and keyword search.
2. Retrieve top 20 from each retriever, fuse with reciprocal rank fusion (k=60).
3. Rerank the top 20 fused candidates with the cross-encoder, keep top 5.
4. Answer only from those 5 with citations and an "I do not know" fallback.
5. A debug view comparing vector-only vs hybrid+rerank, plus recall@5 on a labeled query set.

Work in this order: set up the dual-index table, then ingestion, then the two retrievers, then fusion, then the reranker, then the grounded answer, then the comparison and metrics. Give me the commands and code for each step and STOP after each so I can test. Do not write the whole app at once.

More in RAG & search

Building this? I post a new AI project plan on LinkedIn most weeks. Follow along and share what you ship.