A hands-on learning project that lets you compare four RAG strategies side-by-side using your own PDFs and locally-running Ollama models โ no API keys required.
I built this to deeply understand how RAG works in practice โ not just read about it. The app lets you upload any PDFs, then query them using four increasingly sophisticated RAG pipelines while seeing exactly what each strategy does differently.
What you can experiment with:
- Switch between 4 RAG strategies on the same documents
- Choose from multiple Ollama LLMs and embedding models
- Control chunking strategy, text splitter, chunk size, overlap, and top-K
- Pull missing Ollama models directly from the UI
- See agent reasoning traces (Agentic RAG) and rewritten queries (Advanced RAG)
RAG (Retrieval-Augmented Generation) is a technique that grounds an LLM's answers in external documents rather than relying solely on its training data.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Your PDF โโโโโโโบ โ 1. CHUNK โ 2. EMBED โ 3. STORE โ (Indexing)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
Your Question โโโบ 4. EMBED QUERY โ
5. SIMILARITY SEARCH โโโโโโโโโโโโ
6. RETRIEVE top-K chunks
7. LLM generates answer from chunks
Without RAG, an LLM can only answer from its training data (potentially outdated, and it can hallucinate). With RAG, the LLM answers from your documents.
The baseline. Simple and fast.
Query โ Embed โ Cosine Similarity Search โ Top-K Chunks โ LLM โ Answer
When to use: Quick prototypes, simple Q&A, when your documents are clean and well-structured.
Limitation: No query understanding, retrieves whatever is most similar even if redundant.
Three enhancements on top of Naive RAG:
Query
โ
โผ
[Query Rewriting] โ LLM rewrites the query to be retrieval-friendly
โ
โผ
[MMR Retrieval] โ Maximal Marginal Relevance: diverse, non-redundant chunks
โ
โผ
[Contextual Compression] โ LLM strips irrelevant sentences from each chunk
โ
โผ
LLM โ Answer
MMR explained: Instead of picking the 4 most similar chunks (which might all say the same thing), MMR picks chunks that are both relevant and different from each other.
When to use: Production systems, when answer quality matters more than speed.
Builds a knowledge graph during indexing. Enables multi-hop reasoning.
INDEXING PHASE:
Each chunk โ LLM extracts entities + relationships โ NetworkX graph
"Paris" โโ[capital of]โโโบ "France"
"France" โโ[in]โโโบ "Europe"
QUERY PHASE:
Query โ Extract query entities โ Graph traversal (1-2 hops) โ Relevant chunks
Why this matters: Classic RAG can answer "What is the capital of France?" but struggles with "What continent contains the capital of France?" โ a 2-hop question. GraphRAG can follow entity links to answer it.
Limitation: Slow indexing (one LLM call per chunk to extract entities). Quality depends on how well the LLM extracts structured data.
An LLM agent that decides when, how many times, and what to retrieve.
User Question
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ AGENT (ReAct loop) โ
โ โ
โ Think: "I need to find X" โ
โ โ โ
โ โผ โ
โ [search_documents tool] โโโโบ retrieve chunks
โ โ โ
โ โผ โ
โ Think: "I also need Y" โ
โ โ โ
โ โผ โ
โ [search_documents tool] โโโโบ retrieve more chunks
โ โ โ
โ โผ โ
โ "I have enough context now" โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
Final Answer (with Agent Trace shown in UI)
When to use: Complex multi-part questions, when the question structure isn't known in advance, research-style queries.
| Component | Library | Purpose |
|---|---|---|
| UI | Streamlit 1.58 | Web interface |
| LLM + Embeddings | Ollama + LangChain-Ollama | Local inference |
| Document Loading | LangChain Community (PyPDF) | PDF parsing |
| Vector Store | FAISS | Similarity search |
| Text Splitting | LangChain Text Splitters | Chunking |
| Graph | NetworkX | Knowledge graph (GraphRAG) |
| Agent | LangChain Agents | ReAct agent (Agentic RAG) |
| Requirement | Version | Install |
|---|---|---|
| Python | 3.11+ | python.org |
| Ollama | latest | ollama.com |
| Git | any | git-scm.com |
macOS:
brew install python ollamaLinux: Follow ollama.com/download Windows: Use WSL2 + the Linux instructions
git clone https://github.com/harshb/rag-learning.git
cd rag-learningpython -m venv .venv
# Activate it:
source .venv/bin/activate # macOS / Linux
.venv\Scripts\activate # Windowspip install -r requirements.txtollama serve
# Leave this running in a separate terminalYou need at least one LLM and one embedding model.
Recommended minimum setup:
# Embedding model (required to index PDFs)
ollama pull nomic-embed-text
# LLM (choose one)
ollama pull llama3.2 # fast, good quality (~2 GB)
ollama pull qwen2.5-coder:7b # great at structured reasoning (~4 GB)
ollama pull mistral # well-rounded (~4 GB)Or pull from the UI: The app shows โ / โ next to each model dropdown and has a โฌ Pull button that downloads the model with a live progress bar.
streamlit run app.pyOpen http://localhost:8501 in your browser.
- Upload PDFs using the sidebar file uploader
- Select a RAG strategy using the segmented control at the top
- Configure models โ choose your LLM and embedding model (check they show โ )
- Tune chunking โ adjust splitter, strategy, chunk size, and overlap
- Click โ Process PDFs โ this indexes your documents
- Ask questions in the chat box at the bottom
Tips:
- Start with Naive RAG to establish a baseline, then switch strategies to compare
- For GraphRAG, indexing is slow (LLM call per chunk) โ use smaller PDFs first
- Agentic RAG shows an Agent Trace so you can see exactly what it searched for
- Advanced RAG shows the rewritten query it used for retrieval
| Setting | What it does |
|---|---|
| LLM Model | The model used for answering questions (and query rewriting / entity extraction) |
| Embedding Model | Converts text to vectors for similarity search โ must match what you used when indexing |
| Text Splitter | Algorithm for splitting text: Recursive (default), Character, or Token-based |
| Chunking Strategy | Fixed size, Sliding Window (high overlap), Sentence-aware, or Paragraph-aware |
| Chunk Size | Max characters per chunk. Smaller = more precise retrieval. Larger = more context per chunk. |
| Overlap | How much consecutive chunks share. Prevents facts from being split across chunk boundaries. |
| Top K | How many chunks to retrieve. More = more context, but also more noise. |
Fixed Size: [chunk1][chunk2][chunk3] no overlap, clean boundaries
Sliding Window: [chunk1 ][chunk2 ] each chunk overlaps the previous by โฅ50%
Sentence-aware: [sentence. sentence.][sentence.] splits at sentence endings
Paragraph-aware: [paragraph] [paragraph] splits at double newlines
rag-learning/
โโโ app.py # Main Streamlit application
โโโ requirements.txt # Python dependencies
โโโ README.md # This file
โโโ data/ # (optional) Put sample PDFs here
Connection refused when processing PDFs
โ Ollama isn't running. Run ollama serve in a separate terminal.
model "xyz" not found
โ Pull the model first: ollama pull xyz or use the โฌ Pull button in the UI.
GraphRAG takes too long
โ It runs one LLM call per chunk (capped at 40 chunks). Use smaller PDFs or a faster model like phi3.
Agentic RAG gives no answer
โ Not all Ollama models support tool calling. Use llama3.2, qwen2.5-coder:7b, or mistral.
The app is slow โ LLMs run on CPU by default. If you have an Apple Silicon Mac, Ollama automatically uses the Metal GPU. For NVIDIA GPUs on Linux, follow Ollama GPU setup.
Chunking Strategy, Embedding Model, Retrieval Strategy, and Metadata Strategy usually impact answer quality more than the LLM itself.
Swapping GPT-4 for GPT-3.5 on a well-tuned pipeline often hurts less than using the wrong chunk size. Here's what I learned by experimenting with real documents like the Indian Constitution.
The problem with too-small chunks (chunk_size = 100):
Article 21 Right to Life โ Chunk 1
and Personal Liberty... โ Chunk 2 (retriever might only get this)
No person shall be deprived... โ Chunk 3
Context breaks. The retriever may return Chunk 2 without Chunk 1, producing an incomplete answer.
The problem with too-large chunks (chunk_size = 3000):
Article 20 + Article 21 + Article 22 + Article 23 โ all in one chunk
Retrieval becomes imprecise โ the chunk is so broad it matches everything and nothing.
Sweet spot by document type:
| Document Type | Recommended Chunk Size |
|---|---|
| FAQ / Short answers | 300 โ 500 |
| General PDFs | 500 โ 1000 |
| Legal documents | 800 โ 1500 |
| Books / Long narrative | 1000 โ 2000 |
| Source code | 300 โ 800 |
| Indian Constitution | 800 โ 1200 |
No overlap (overlap = 0) โ information at chunk boundaries gets lost:
Chunk 1: chars 0โ1000 โ Article 21 starts here...
Chunk 2: chars 1000โ2000 โ Article 21 continues... โ connection severed
With overlap (chunk_size = 1000, overlap = 200):
Chunk 1: chars 0โ1000 โ
Chunk 2: chars 800โ1800 โค last 200 chars of Chunk 1 repeated at the start of Chunk 2
Chunk 3: chars 1600โ2600 โ
The boundary context is preserved. The cost is slightly more chunks, more storage, and more embeddings to compute.
Typical overlap ratios:
| Chunk Size | Recommended Overlap |
|---|---|
| 500 | 50 โ 100 |
| 1000 | 100 โ 200 |
| 2000 | 200 โ 400 |
How many chunks to hand to the LLM after retrieval.
Too low (top_k = 1): Fast and cheap, but a single chunk rarely contains the full answer. For "Can police arrest me without reason?", you need at least Article 21, Article 22, and the detention clauses โ none of which sit in the same chunk.
Too high (top_k = 20): Good recall, but you flood the LLM context with noise. The model starts averaging over irrelevant chunks.
Top-K by use case:
| Use Case | Top K |
|---|---|
| FAQ bot | 3 |
| PDF chat | 4 โ 6 |
| Legal documents | 5 โ 10 |
| Research / Agentic RAG | 10 โ 20 |
For the Constitution: start at 5, increase to 8โ10 for complex multi-article questions.
| Splitter | How it works | Good for |
|---|---|---|
| CharacterTextSplitter | Splits every N characters, ignores structure | Quick tests only |
| RecursiveCharacterTextSplitter | Tries \n\n โ \n โ โ "" in order, preserving semantic units |
General purpose (default) |
| TokenTextSplitter | Counts LLM tokens, not characters โ more accurate for model context limits | Any LLM-specific pipeline |
| MarkdownHeaderTextSplitter | Splits on #, ##, ### headings |
Documentation, wikis |
| HTMLHeaderTextSplitter | Splits on <h1>, <h2> tags |
Web content |
| SemanticChunker | Embeds sentences, splits where meaning changes (cosine distance) | Advanced โ expensive but powerful |
Best splitter for the Indian Constitution:
Not Recursive, not Character. An article-aware splitter that treats each Article as its own chunk gives dramatically better results:
โ RecursiveCharacter: "...and personal liberty. No person shall be deprived of his li"
"fe or personal liberty except according to procedure established..."
โ
Article-based: "Article 21: Right to Life and Personal Liberty.
No person shall be deprived of his life or personal liberty
except according to procedure established by law."
The RecursiveCharacterTextSplitter with separators=["Article ", "\n\n", "\n", " "] gets close to this.
The embedding model converts text into a vector. Better models = more semantically accurate similarity search = better chunks retrieved.
| Model | Dimensions | Speed | Quality | Best for |
|---|---|---|---|---|
all-MiniLM-L6-v2 |
384 | โกโกโก | โญโญ | Learning, prototypes |
nomic-embed-text |
768 | โกโก | โญโญโญ | Local Ollama (great balance) |
mxbai-embed-large |
1024 | โก | โญโญโญโญ | Higher quality local |
bge-base-en-v1.5 |
768 | โกโก | โญโญโญโญ | Production retrieval |
bge-large-en-v1.5 |
1024 | โก | โญโญโญโญโญ | Best local quality |
text-embedding-3-small |
1536 | API | โญโญโญโญ | OpenAI (production) |
voyage-3 |
1024 | API | โญโญโญโญโญ | State of the art retrieval |
Why it matters in practice:
Question: "What is Article 21?"
Model: all-MiniLM-L6-v2 โ similarity score = 0.71 โ Article 22 retrieved (wrong)
Model: nomic-embed-text โ similarity score = 0.86 โ Article 21 retrieved (correct)
The LLM is identical. The answer quality is completely different.
Rule: Never change the embedding model after indexing. If you switch models, you must re-process all documents โ the old vectors become meaningless.
If I were building a production Constitution AI today:
PDFs
โ
โผ
Article-aware splitter
chunk_size: 1200
chunk_overlap: 150
โ
โผ
Embedding: nomic-embed-text (or bge-base-en-v1.5 for higher quality)
โ
โผ
Vector Store: FAISS (local) / OpenSearch (production)
โ
โผ
Retrieval: MMR, top_k = 5, fetch_k = 15
โ
โผ
Reranking: Cross-encoder reranker (cuts noise from top 15 โ best 5)
โ
โผ
Context Compression: LLMChainExtractor (strips irrelevant sentences)
โ
โผ
LLM: llama3.1 / mistral (local) or GPT-4o (production)
โ
โผ
Answer + Sources
Chunking = How information is stored
Embeddings = How meaning is represented
Retriever = How information is found
Prompt = How information is presented to the LLM
LLM = How information is explained to the user
Optimise in that order. A bad chunking strategy cannot be fixed by a better LLM.
Chunking beats strategy switching.
A well-tuned chunk size often beats switching from Naive โ Advanced RAG. Tune your chunking first.
Embedding model โ LLM.
They do entirely different jobs. You can pair a tiny, fast embedding model with a large, slow LLM โ they don't need to match.
MMR is quietly one of the best improvements.
Maximum Marginal Relevance (Advanced RAG) silently fixes redundant retrieval โ getting 4 chunks that all say the same thing. Force diversity.
Agents are powerful but unpredictable.
Agentic RAG can loop and self-correct, but the same question may take different retrieval paths on each run. Don't use it where determinism matters.
GraphRAG has a narrow sweet spot.
Only worth the slow indexing for highly interconnected knowledge bases โ legal documents, scientific literature, or wikis. Overkill for most PDF Q&A.
Pull requests are welcome! If you want to extend this:
- Add a new RAG strategy โ add it to
RAG_TYPES,RAG_INFO, and implement*_retrieve()+ wire it intorun_rag() - Add a new Ollama model โ append to
LLM_MODELSorEMBEDDING_MODELS - Improve GraphRAG โ better entity extraction prompt, smarter graph traversal
- Add evaluation โ implement RAGAS metrics to quantitatively compare strategies
MIT โ use it, fork it, learn from it.
Built as a learning project to deeply understand RAG systems by building them from scratch.