A retrieval-augmented generation chatbot for querying a corpus of PDF documents, built to correct a specific, observed failure mode in its predecessor.
The earlier Vectorless RAG Chatbot proved that a government document repository could be made conversationally searchable with keyword retrieval alone. Evaluating it also showed exactly where that approach breaks: queries phrased differently from the source text, indirect references, and non-English content.
This system addresses those gaps directly — semantic vector search and keyword search run in parallel and are fused into a single ranking, with OCR for scanned pages and multilingual embeddings so one index serves cross-lingual search. Every answer carries citations back to its source passages.
Keyword retrieval and semantic retrieval fail in opposite directions.
Keyword (lexical) retrieval is precise on exact terms — policy numbers, regulation codes, defined terminology — and is cheap to deploy and easy to audit. It has no understanding of meaning, so a question phrased differently from the source text may retrieve nothing useful.
Semantic (vector) retrieval represents query and passage as embeddings and matches on meaning, handling paraphrase and cross-lingual similarity well. It requires additional infrastructure, and can occasionally under-rank a passage that is an exact but non-obvious match.
Choosing one means accepting the other's weakness. Fusing both rankings, rather than choosing between them, was the central design decision here — and it was driven by an observed failure pattern in a working baseline, not by a generic preference for more advanced retrieval.
Five layers — the vectorless system's four, plus a dedicated embedding layer, with retrieval becoming a dual path.
| Component | Implementation |
|---|---|
Ingestion (ingestion/) |
Loads PDFs (loader.py), OCRs scanned pages with Tesseract (ocr.py), extracts tables with pdfplumber, chunks text (chunker.py) |
Embedding (ingestion/embedder.py) |
intfloat/multilingual-e5-base embeddings written to ChromaDB and a FAISS index (vector_store.py) |
Retrieval (retrieval/search.py) |
Hybrid search combining FAISS/Chroma vector similarity with BM25 keyword search, plus lightweight query spell-correction |
Generation (generation/llm.py) |
Local LLM via Ollama (gemma2:2b-instruct-q4_K_M by default), prompted with the retrieved context |
API (api/main.py) |
FastAPI server — query, file upload/ingestion, health, and index-stats endpoints |
| Frontends | react_frontend/ (Vite + React + Tailwind) and streamlit/app.py |
CLI (chatbot/chat.py) |
Minimal terminal chat loop |
Configuration lives in config/settings.py (embedding model, LLM model, chunk size/overlap).
Documents are OCR'd and cleaned, then chunked. Embeddings and a keyword index are built in parallel over the same chunks. At query time semantic and keyword search run against their respective indexes independently, and their results are merged through rank fusion before context construction. The fused context builds the prompt, and the response is paired with generated citations before it reaches the interface.
Conversational interface — indexed query templates, active guardrails, and engine state
Citation trace — every retrieved chunk with its source document and relevance score
- Hybrid retrieval — FAISS/Chroma vector similarity fused with BM25 keyword ranking, so neither paraphrase nor exact-term precision is sacrificed.
- Multilingual embeddings —
intfloat/multilingual-e5-baselets a single index serve cross-lingual queries instead of one store per language. - OCR ingestion — Tesseract (English + Gujarati) brings scanned pages into the same index as machine-readable documents.
- Table extraction —
pdfplumberpreserves tabular content that naive text extraction flattens. - Citation-backed answers — citations are a first-class pipeline output, not a post-hoc addition.
- Retrieval transparency — the interface shows which passages contributed, via which retrieval path, with scores.
- Query spell-correction — lightweight correction ahead of retrieval, recovering matches lost to typos and OCR noise.
- On-premise by default — the LLM runs locally through Ollama; document content never leaves the host.
- Two frontends — a React chat UI and a Streamlit app over the same FastAPI backend.
Python · FastAPI · React · Vite · Tailwind CSS · Streamlit · ChromaDB · FAISS · BM25 · multilingual-e5-base · Ollama · Tesseract OCR · pdfplumber
- Python 3.10+
- Node.js 18+ (for the React frontend)
- Ollama running locally with the target LLM pulled:
ollama pull gemma2:2b-instruct-q4_K_M
- Tesseract OCR (English + Gujarati packs) for scanned PDFs:
brew install tesseract tesseract-lang
- Poppler (for
pdf2image):brew install poppler
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txtAdd your PDF source documents to data/.
Build the index — this populates chroma_db/ and faiss_index/:
python -c "from ingestion.vector_store import run_ingestion; run_ingestion()"Run the API:
uvicorn api.main:app --reloadRun a frontend — React:
cd react_frontend
npm install
npm run devStreamlit:
streamlit run streamlit/app.pyTerminal chat:
python -m chatbot.chatingestion/store.pyis an older MongoDB-backed ingestion path, unused by the current API (which usesvector_store.pywith Chroma + FAISS). Kept for reference.- Downloaded embedding weights (
models/), the vector stores (chroma_db/,faiss_index/) and source PDFs (data/) are git-ignored — regenerate or re-add them locally.
- Quantitative retrieval evaluation (NDCG over a labelled query set) to measure the fusion gain rather than infer it.
- Tunable fusion weighting between the lexical and semantic rankings per query class.
- Incremental re-indexing on document upload instead of full rebuilds.
- Broader OCR language coverage beyond English and Gujarati.
Developed during an AI engineering internship at Ernst & Young LLP (Technology Consulting, CNS — Technology Strategy & Transformation). Per enterprise confidentiality, this repository contains no client data, prompt text or internal configuration values. Architecture and workflow diagrams are redrawn from my Project Semester Report.