Retrieval evaluation
InsureAssist: A Measured RAG Benchmark
Answers questions about flood insurance policies, with sources.
- Role
- Project author and engineer
- Classification
- Reference implementation
Problem
Near-identical policy forms fool a topic search.
How it was solved
I tuned chunk size and combined keyword and vector search.
- Chunk
- Hybrid search
- Cite
Result
0.167 → 0.556
questions matched to the right form
- Starting point16.7%
- Final system55.6%
Built with
- Qdrant
- BM25
- BGE embeddings
- FastAPI
- Docker
Reference implementation / Retrieval
Three policy forms differ by a few words. The nearest passage can be the correct clause taken from the wrong document, and read exactly like the answer.
Form discrimination0.556 top-document accuracy
Two retrievers, one fusion rule, and a citation that resolves
This diagram in words
- NFIP corpus source — Licensed federal flood-insurance text, committed to the repository.
- Chunk and identify process — Offsets kept. IDs derived from document, offset and text, so re-ingest is idempotent.
- Dense index store — Cosine similarity over embedded chunks.Qdrant · BGE embeddings
- Lexical index store — Okapi BM25, written out rather than imported.BM25
- Rank fusion gate — Reciprocal rank, k=60. Positions only - the two scales are never added.
- Prompt and answer process — Contexts are numbered in the prompt so the answer can refer to them.Ollama
- Citations artifact — Chunk ID, document, CFR citation and character offsets. Never generated.
- Query service sink — Answer and citations returned together, never separately.FastAPI
1.00
hit@5 on the 14 questions it was chosen on
0.5556
hit@5 on the 18 it was not
Selected on development questions, reported on held-out ones. The first number is what selection produces; the second is the one that means anything.
| Chunk size | Overlap | Depth | Chunks | hit@5 | MRR |
|---|---|---|---|---|---|
| 400 | 80 | 10 | 664 | 0.4286 | 0.3824 |
| 400 | 80 | 20 | 664 | 0.5000 | 0.4167 |
| 600 | 100 | 10 | 426 | 0.6429 | 0.4923 |
| 600 | 100 | 20 | 426 | 0.7857 | 0.5126 |
| 600 | 150 | 10 | 474 | 0.7143 | 0.5185 |
| 600 | 150 | 20 | 474 | 0.6429 | 0.5211 |
| 800 | 120 | 10 | 314 | 1.0000 | 0.6214 |
| 800 | 120 | 20 | 314 | 0.9286 | 0.6071 |
| 1000 | 150 | 10 | 251 | 0.7143 | 0.6015 |
| 1000 | 150 | 20 | 251 | 0.7143 | 0.6066 |
Read the full background
Problem
Why this work exists
The three NFIP policy forms are near-duplicates by design: they share a skeleton and much verbatim wording but differ in substance. A retriever matching on topic finds the right provision in the wrong document, and nothing in a demo reveals that.
Contribution
What I can claim
Built a licensed corpus with hash-verified provenance, deterministic ingestion with content-derived chunk IDs, a 40-question labelled benchmark with a held-out split, a lexical baseline to compare against, and a machine-readable reference run that the repository's own CI checks its documentation against.
Method
Why retrieval quality and answer quality are two different results
A retrieval-augmented system fails in two distinct ways, and reporting one number hides both.
Ingest
Source documents are split into passages.
Represent
Each passage takes a position in a space where distance is meant to stand for relevance.
Retrieve
A question arrives and the nearest passages are selected.
Ground
The answer is tied back to the passages it was built from.
Verify
Retrieval is scored separately from generation, so a fluent answer built on the wrong passage is visible as the failure it is.
What this does not show
Retrieval scores in the evidence table are measured on a fixed question set over public policy text. They say nothing about performance on other corpora, other question styles, or questions whose answer is not present at all.
How it was built
System
Workflow and decisions
Retrieval is dense BGE vectors fused with an in-process BM25 index by reciprocal rank fusion. The architecture was selected on a development split and frozen before the held-out split was run once.
- 1Licensed NFIP corpus
- 2Deterministic chunking and IDs
- 3Dense and lexical retrieval
- 4Rank fusion
- 5Traceable citations
- FastAPI
- Qdrant
- BGE embeddings
- BM25
- Ollama
- Docker
- Kubernetes
- GitHub Actions
Evidence
What is actually versioned
Up from 0.167 at the starting point: the share of questions whose best hit comes from the correct policy form, on the held-out split.
32 answerable and 18 unanswerable, each naming exact chunk IDs and character offsets across 314 chunks.
Every citation's offsets reproduce its quoted text from the committed corpus, so no provenance is fabricated.
Inspection points
Go directly to the evidence
How the work is checked
Quality controls
How the work is checked
- The retrieval architecture was chosen on a development split and frozen in a committed config before the held-out split was run once
- Chunk IDs derive from document, offset and text, so a relevance label still points at its evidence after re-ingestion
- Ground-truth labels resolve from text anchors, so a chunking change regenerates them instead of invalidating the benchmark
- A validator rejects broken references, spans outside a document, and unanswerable questions carrying evidence
- 447 offline tests run with no model, no database and no network
- CI reproduces the reference run against a real vector database and fails if the repository's documentation drifts from it
Limitations
Where the evidence stops
- The lexical baseline alone still retrieves more relevant chunks in the top five than the selected hybrid; the hybrid was chosen for ranking and form discrimination, not raw recall
- Development results did not generalise: the selected architecture scored far higher on the split it was chosen on than on the held-out split
- Unanswerable questions are not detected. No similarity threshold was defensible on this data, so none is claimed
- Answer quality is not measured; only one local model is available and grading its own output would be circular
- One jurisdiction, one peril, three documents. Results do not transfer to insurance documents generally
- The Kubernetes manifests have been applied only to a local kind cluster, and the Azure Container Apps deployment hosts the API alone, with its readiness probe reporting the vector store absent by design; no full stack runs in the cloud, and the Google Cloud guide has never been run
- A fine-tuning notebook exists but is archived: its training data was the earlier evaluation set, so no tuned-model result could be honest
What this changed in my practice
What this changed in my practice
A benchmark is only worth what its hardest cases are worth. Near-duplicate documents exposed a failure that topic-level retrieval metrics would have hidden entirely.