Spaces:
Sleeping
title: RAG Document Q&A
emoji: π
colorFrom: indigo
colorTo: blue
sdk: docker
app_port: 7860
pinned: false
RAG Document Q&A System
Upload PDF documents and get cited, grounded answers β with hybrid retrieval, cross-encoder re-ranking, and confidence-aware generation.
Overview
This project implements a production-grade Retrieval-Augmented Generation pipeline that goes beyond the typical "embed + nearest-neighbor + prompt" tutorial. It combines semantic and keyword search with cross-encoder re-ranking, applies Anthropic's contextual retrieval pattern to enrich embeddings with document metadata, and gates LLM calls on retrieval confidence to avoid hallucinated answers. The system ships with both a streaming Gradio UI and a FastAPI backend with Server-Sent Events, plus an evaluation harness that benchmarks chunking strategies and scores end-to-end answer quality using an LLM-as-judge.
Architecture
βββββββββββββββββββββββββββββββββββββββββββ
β INGESTION β
β β
PDF βββΊ pymupdf4llm βββΊ β Chunking (3 strategies) β
(pdfplumber β ββ fixed_size sliding window β
fallback) β ββ recursive_char LangChain splits β
β ββ semantic embedding sim β
β β β
β βΌ β
β Contextual Enrichment β
β "filename | Page N | Section\ntext" β
β β β
β βΌ β
β all-MiniLM-L6-v2 (384-dim, L2-norm) β
β β β
β βββββββ΄βββββββ β
β βΌ βΌ β
β FAISS BM25Okapi β
β IndexFlatIP rank-bm25 β
βββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββ
β QUERY β
β β
Question βββββββββββββββΊβ Adaptive Expansion (Groq) β
β only if top_score < 0.45 β
β β β
β βΌ β
β Hybrid Search β
β score = 0.7Β·semantic + 0.3Β·bm25 β
β (top 20 candidates) β
β β β
β βΌ β
β Cross-Encoder Re-ranking β
β ms-marco-MiniLM-L-6-v2 β
β (20 β top k) β
β β β
β βΌ β
β Confidence Gating β
β cosine sim < 0.3 β skip LLM β
β β β
β βΌ β
β Groq (Llama 3.3 70B) streaming β
β β β
β βΌ β
β Cited answer [Source N] notation β
βββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββ
β EVALUATION β
β β
β Retrieval: Acc@1/3/5, MRR β
β End-to-end: faithfulness + relevance β
β (LLM-as-judge, temp=0.0) β
β Config sweep: chunking strategy grid β
βββββββββββββββββββββββββββββββββββββββββββ
Key Features
Cross-encoder re-ranking β bi-encoder retrieval (FAISS) produces 20 candidates; a cross-encoder (
ms-marco-MiniLM-L-6-v2) scores each(query, chunk)pair jointly and re-orders them. Cross-encoders are too slow for full-corpus search but dramatically improve precision over the shortlist.Hybrid search β FAISS inner-product search and BM25 run in parallel. Both score sets are normalized to
[0, 1]then fused (0.7Β·semantic + 0.3Β·keyword), catching exact-match terms that dense embeddings tend to smooth over.Adaptive query expansion β the pipeline runs an initial search first. Only if the top cosine similarity falls below 0.45 does it call the LLM to generate 3 rephrased variants, search each, and merge results. High-confidence queries pay zero expansion cost.
Contextual chunk enrichment β each chunk is embedded as
"<filename> | Page N | <section>\n<text>"rather than raw text, following Anthropic's contextual retrieval pattern. The embedding captures document location and section context, not just lexical content.Confidence-based response gating β
max_scoreis taken from cosine similarity before re-ranking (cross-encoder scores on a β10 to +10 scale would break the threshold). Ifmax_score < 0.3, the LLM is skipped entirely and the user receives an honest "insufficient context" message. Scores between 0.3β0.5 append a low-confidence warning.Streaming responses β Gradio UI streams tokens via a generator; the FastAPI backend streams as Server-Sent Events (
text/event-stream) with{"text": chunk}events and a terminal{"done": true}.Layout-aware PDF parsing β
pymupdf4llmextracts structured markdown with headings and tables preserved.pdfplumberserves as a fallback for PDFs thatpymupdf4llmcannot parse cleanly.End-to-end evaluation β the eval harness measures retrieval hit rate (Acc@K, MRR), then pipes results through the LLM-as-judge to score faithfulness (are claims grounded in context?) and relevance (does the answer address the question?) at temperature 0 for determinism.
Tech Stack
| Component | Technology |
|---|---|
| Embedding model | all-MiniLM-L6-v2 (sentence-transformers, 384-dim) |
| Vector index | FAISS IndexFlatIP (exact inner-product search) |
| Keyword index | BM25 (rank-bm25, BM25Okapi) |
| Re-ranker | cross-encoder/ms-marco-MiniLM-L-6-v2 |
| LLM | Llama 3.3 70B via Groq API |
| PDF parsing | pymupdf4llm + pdfplumber fallback |
| Text splitting | LangChain RecursiveCharacterTextSplitter |
| Backend | FastAPI + Uvicorn |
| Frontend | Gradio (Blocks, streaming) |
| Containerization | Docker |
| Cloud deployment | HuggingFace Spaces |
Quick Start
git clone <repo-url>
cd rag-qa
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
Create a .env file:
GROQ_API_KEY=your_key_here
Get a free key at console.groq.com β the free tier allows 14,400 requests/day.
Launch the Gradio UI:
python app_gradio.py
# Opens at http://127.0.0.1:7860
Or run the FastAPI server:
uvicorn main:app --reload
# API docs at http://127.0.0.1:8000/docs
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Liveness check |
GET |
/stats |
Index size, chunks ingested, model info |
POST |
/ingest |
Upload a PDF; returns chunk count and IDs |
POST |
/query |
Ask a question; returns answer + sources + confidence |
POST |
/query/stream |
Same as /query but streams as SSE |
POST |
/debug/chunks |
Return raw retrieved chunks without calling the LLM |
All POST query endpoints accept {"question": "...", "top_k": 5}.
The /debug/chunks endpoint is useful for diagnosing retrieval issues β it shows each chunk's rank, cosine similarity score, source, page, section header, and a 300-character text preview.
Evaluation
The evaluation framework has three layers:
Retrieval metrics (evaluation/eval.py) β given a test set of (question, expected_source, expected_page) tuples, measures whether the correct chunk appears in the top-k results:
| Metric | Definition |
|---|---|
| Accuracy@K | Fraction of queries where correct chunk is in top K |
| MRR | Mean Reciprocal Rank of the first correct result |
End-to-end metrics (evaluation/eval_e2e.py) β runs the full pipeline and scores each answer:
| Metric | Scorer |
|---|---|
| Faithfulness | LLM judge: are all claims grounded in retrieved context? |
| Relevance | LLM judge: does the answer address the question? |
| Keyword coverage | Fraction of expected answer keywords found in output |
Chunking strategy comparison (evaluation/compare.py) β grid search over chunk sizes, overlap values, and re-ranking on/off. Results table (fill in with your benchmark numbers):
| Strategy | Chunk size | Overlap | Reranking | Acc@5 | MRR | Faithfulness | Relevance |
|---|---|---|---|---|---|---|---|
| recursive_char | 300 | 0 | No | β | β | β | β |
| recursive_char | 500 | 50 | No | β | β | β | β |
| recursive_char | 500 | 50 | Yes | β | β | β | β |
| recursive_char | 1000 | 100 | Yes | β | β | β | β |
| semantic | 500 | β | Yes | β | β | β | β |
Project Structure
rag-qa/
βββ main.py # FastAPI server
βββ app_gradio.py # Gradio web UI
βββ requirements.txt
βββ Dockerfile
βββ .env.example
β
βββ ingestion/
β βββ pdf_reader.py # pymupdf4llm + pdfplumber extraction
β βββ chunker.py # fixed_size, recursive_char, semantic strategies
β βββ embedder.py # SentenceTransformer + contextual enrichment
β βββ pipeline.py # Orchestrates extract β chunk β embed β index
β
βββ retrieval/
β βββ index.py # FAISS IndexFlatIP wrapper
β βββ bm25_index.py # BM25Okapi wrapper
β βββ reranker.py # Cross-encoder re-ranking
β βββ searcher.py # Hybrid search + expansion + confidence score
β
βββ generation/
β βββ generator.py # Groq client, confidence gating, streaming
β
βββ evaluation/
β βββ judge.py # LLM-as-judge (faithfulness + relevance)
β βββ eval.py # Retrieval metrics: Acc@K, MRR
β βββ eval_e2e.py # End-to-end pipeline evaluation
β βββ compare.py # Configuration grid comparison
β
βββ data/ # Sample PDFs for testing
How It Works
PDF Extraction β pymupdf4llm converts each page to structured markdown, preserving headings and table layout. If it fails, pdfplumber extracts flat text. Pages are processed independently so that page_num metadata stays accurate for citations.
Chunking β three strategies are available. fixed_size is a naive sliding window (fast baseline). recursive_character uses LangChain's splitter with markdown-aware separators (\n## , \n\n, . ) and extracts the nearest heading above each chunk as section_header metadata. semantic embeds individual sentences and splits at cosine similarity drops below a threshold, grouping topically coherent content together.
Embedding β before embedding, each chunk's text is prefixed with "<filename> | Page N [| Section]\n". This means the stored vector encodes not just the chunk's content but where in the document it came from, which improves retrieval precision for location-specific queries.
Retrieval β a query vector is searched against FAISS (semantic) and BM25 (keyword) simultaneously. Both result sets are min-max normalized to [0, 1] and fused by weighted sum. The top 20 fused candidates are then re-scored by the cross-encoder, which reads the full (query, chunk) string pair and produces a relevance score independent of embedding geometry.
Generation β max_score (cosine similarity of the top-ranked chunk before re-ranking) determines whether to call the LLM. Below 0.3, the call is skipped. Between 0.3 and 0.5, the answer is appended with a confidence warning. Above 0.5, the model receives a numbered context block and is prompted to cite every claim with [Source N] notation and synthesize across sources.
Deployment
Docker:
docker build -t rag-qa .
docker run -p 7860:7860 -e GROQ_API_KEY=your_key rag-qa
HuggingFace Spaces:
Set GROQ_API_KEY in your Space's Settings β Repository secrets, then push the repository. The Dockerfile is picked up automatically by Spaces.
Future Improvements
- OCR support β integrate
pytesseractorsurya-ocrfor scanned PDFs that contain no extractable text layer - Persistent vector store β replace the in-memory FAISS index with a disk-backed store (FAISS
write_indexwith reload on startup, or a dedicated vector DB) so the index survives server restarts - Multi-document cross-referencing β surface when the same fact is corroborated across multiple uploaded documents rather than citing only one source
- GPU-accelerated embedding β the sentence-transformers model runs on CPU by default; passing
device="cuda"cuts batch embedding time significantly for large document sets