Building RAG with LlamaIndex
LlamaIndexRAGTutorialRetrieval
Complete guide to building production-ready RAG systems with LlamaIndex, from data ingestion to advanced retrieval strategies.
Building RAG with LlamaIndex
Overview
This tutorial walks you through building a production-ready Retrieval-Augmented Generation (RAG) system using LlamaIndex. You'll learn how to ingest data from multiple sources, build an index, implement advanced retrieval strategies, and deploy a working RAG application.
What is RAG?
Retrieval-Augmented Generation (RAG) is a pattern that combines:
- Retrieval: Finding relevant information from your data
- Generation: Using an LLM to generate answers based on retrieved context
RAG enables AI systems to answer questions about your specific data without retraining the model.
Prerequisites
pip install llama-index llama-index-llms-openai llama-index-embeddings-openai
pip install llama-index-readers-file
Step 1: Setting Up Your Environment
import os
from llama_index.core import Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
# Configure LlamaIndex
os.environ["OPENAI_API_KEY"] = "your-api-key"
Settings.llm = OpenAI(model="gpt-4o")
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Step 2: Loading Your Data
From Files
from llama_index.core import SimpleDirectoryReader
# Load all documents from a directory
documents = SimpleDirectoryReader("./data").load_data()
print(f"Loaded {len(documents)} documents")
for doc in documents[:3]:
print(f"- {doc.metadata['file_name']}")
From Multiple Sources
from llama_index.readers.file import PDFReader, DocxReader
from llama_index.readers.web import SimpleWebPageReader
from llama_index.readers.discord import DiscordReader
# PDF documents
pdf_documents = PDFReader().load_data("./reports/*.pdf")
# Web pages
web_documents = SimpleWebPageReader(html_to_text=True).load_data(
["https://example.com/docs/page1", "https://example.com/docs/page2"]
)
# Combine all documents
all_documents = documents + pdf_documents + web_documents
From Databases
from llama_index.readers.database import DatabaseReader
# Connect to PostgreSQL
db_reader = DatabaseReader(
scheme="postgresql",
host="localhost",
port=5432,
user="postgres",
password="password",
database="mydb"
)
# Query data
documents = db_reader.load_data(
query="SELECT title, content FROM articles WHERE published = true"
)
Step 3: Building the Index
Basic Vector Index
from llama_index.core import VectorStoreIndex
# Build index from documents
index = VectorStoreIndex.from_documents(documents)
# Create query engine
query_engine = index.as_query_engine()
# Ask a question
response = query_engine.query("What is the return policy?")
print(response)
Advanced Index Types
Summary Index
from llama_index.core import SummaryIndex
# Good for document-level queries
summary_index = SummaryIndex.from_documents(documents)
summary_query_engine = summary_index.as_query_engine()
Keyword Table Index
from llama_index.core import KeywordTableIndex
# Good for keyword-based retrieval
keyword_index = KeywordTableIndex.from_documents(documents)
Tree Index
from llama_index.core import TreeIndex
# Good for hierarchical queries
tree_index = TreeIndex.from_documents(documents)
Step 4: Advanced Retrieval Strategies
Hybrid Search
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.vector_stores.chroma import ChromaVectorStore
import chromadb
# Create Chroma client
chroma_client = chromadb.PersistentClient(path="./chroma_db")
chroma_collection = chroma_client.create_collection("my_collection")
# Create vector store
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
# Build index with hybrid search
index = VectorStoreIndex.from_documents(
documents,
storage_context=storage_context,
show_progress=True
)
# Configure hybrid search
query_engine = index.as_query_engine(
similarity_top_k=5,
vector_store_kwargs={"vector_store": vector_store}
)
Sentence Window Retrieval
from llama_index.core.node_parser import SentenceWindowNodeParser
from llama_index.core import StorageContext, VectorStoreIndex
from llama_index.postprocessor.metadata_retriever import MetadataReplacementPostProcessor
# Parse documents with window context
node_parser = SentenceWindowNodeParser(
window_size=3,
window_metadata_key="window",
original_text_metadata_key="original_text"
)
# Build index
nodes = node_parser.get_nodes_from_documents(documents)
storage_context = StorageContext.from_defaults()
storage_context.docstore.add_documents(nodes)
index = VectorStoreIndex(nodes, storage_context=storage_context)
# Query with metadata replacement
query_engine = index.as_query_engine(
node_postprocessors=[
MetadataReplacementPostProcessor(target_metadata_key="window")
]
)
Auto-merging Retrieval
from llama_index.core import StorageContext, VectorStoreIndex
from llama_index.core.node_parser import HierarchicalNodeParser
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.retrievers import RouterRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
# Create hierarchical nodes
node_parser = HierarchicalNodeParser.from_defaults(
chunk_sizes=[2048, 512, 128]
)
nodes = node_parser.get_nodes_from_documents(documents)
# Store all levels
storage_context = StorageContext.from_defaults()
storage_context.docstore.add_documents(nodes)
# Get leaf nodes for retrieval
leaf_nodes = [n for n in nodes if n.level == 0]
# Create retrievers
vector_retriever = index.as_retriever(similarity_top_k=2)
bm25_retriever = BM25Retriever.from_defaults(
nodes=leaf_nodes,
similarity_top_k=2
)
# Router retriever
retriever = RouterRetriever.from_lists(
retrievers=[vector_retriever, bm25_retriever],
query_engine_tools=[],
llm=Settings.llm
)
query_engine = RetrieverQueryEngine.from_args(retriever)
Step 5: Creating a Chat Engine
Context Chat Engine
from llama_index.core import Settings
from llama_index.core.chat_engine import ContextChatEngine
# Create chat engine
chat_engine = index.as_chat_engine(
chat_mode="context",
system_prompt="""
You are a helpful assistant that answers questions based on the provided context.
Always cite the source of your information.
If the answer is not in the context, say so.
""",
similarity_top_k=5
)
# Chat
response = chat_engine.chat("What is the pricing model?")
print(response)
# Continue conversation
response = chat_engine.chat("How does it compare to competitors?")
print(response)
Conversation Chat Engine
from llama_index.core.chat_engine import ConversationChatEngine
chat_engine = index.as_chat_engine(
chat_mode="conversation",
verbose=True
)
response = chat_engine.chat("What are the main features?")
Step 6: Adding Metadata Filtering
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.vector_stores.chroma import ChromaVectorStore
import chromadb
# Initialize Chroma with metadata
chroma_client = chromadb.PersistentClient(path="./chroma_db")
chroma_collection = chroma_client.create_collection(
"documents",
metadata={"hnsw:space": "cosine"}
)
# Add metadata to documents
for doc in documents:
doc.metadata["category"] = "documentation"
doc.metadata["year"] = "2024"
# Build index
index = VectorStoreIndex.from_documents(
documents,
storage_context=StorageContext.from_defaults(
vector_store=ChromaVectorStore(chroma_collection)
)
)
# Query with filters
from llama_index.core.vector_stores.types import MetadataFilters, MetadataFilter
filters = MetadataFilters(
filters=[
MetadataFilter(key="category", value="documentation"),
MetadataFilter(key="year", value="2024", operator="=="),
]
)
query_engine = index.as_query_engine(
filters=filters,
similarity_top_k=5
)
Step 7: Building a Complete RAG Application
FastAPI Backend
# main.py
from fastapi import FastAPI
from pydantic import BaseModel
from llama_index.core import VectorStoreIndex
app = FastAPI()
# Load and index data once
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
class QueryRequest(BaseModel):
query: str
top_k: int = 5
class QueryResponse(BaseModel):
answer: str
sources: list[str]
confidence: float
@app.post("/query", response_model=QueryResponse)
async def query_endpoint(request: QueryRequest):
response = query_engine.query(request.query)
return QueryResponse(
answer=str(response),
sources=[n.node.text[:100] for n in response.source_nodes[:3]],
confidence=response.metadata.get("confidence", 0.0)
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Simple Frontend
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>RAG Chat</title>
<style>
body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 20px; }
.message { margin: 10px 0; padding: 10px; border-radius: 8px; }
.user { background: #e0f2fe; }
.assistant { background: #f0fdf4; }
.sources { font-size: 0.9em; color: #666; margin-top: 8px; }
</style>
</head>
<body>
<h1>RAG Chat</h1>
<div id="chat"></div>
<input type="text" id="query" placeholder="Ask a question..."
onkeypress="if(event.key==='Enter')sendQuery()">
<button onclick="sendQuery()">Send</button>
<script>
async function sendQuery() {
const input = document.getElementById('query');
const query = input.value;
if (!query) return;
// Add user message
addMessage(query, 'user');
input.value = '';
// Get response
const res = await fetch('/query', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query})
});
const data = await res.json();
// Add assistant message
addMessage(data.answer, 'assistant', data.sources);
}
function addMessage(text, type, sources = []) {
const div = document.createElement('div');
div.className = `message ${type}`;
div.innerHTML = `<p>${text}</p>`;
if (sources.length) {
div.innerHTML += `<div class="sources">Sources: ${sources.join(' | ')}</div>`;
}
document.getElementById('chat').appendChild(div);
}
</script>
</body>
</html>
Step 8: Evaluating Your RAG System
Using LlamaIndex Evaluation
from llama_index.core.evaluation import DatasetGenerator, FaithfulnessEvaluator, ResponseEvaluator
# Generate evaluation dataset
eval_documents = documents[:20]
data_generator = DatasetGenerator.from_documents(eval_documents)
eval_questions = data_generator.generate_questions_from_nodes()
# Evaluate faithfulness
faithfulness_evaluator = FaithfulnessEvaluator()
for q in eval_questions[:3]:
response = query_engine.query(q)
result = faithfulness_evaluator.evaluate_response(response)
print(f"Question: {q}")
print(f"Faithfulness: {result.passing}")
print(f"Score: {result.score}")
print()
Common Issues and Solutions
Issue: Low Retrieval Accuracy
Solutions:
- Increase
similarity_top_k - Use hybrid search (vector + keyword)
- Improve chunking strategy
- Add metadata filters
- Use re-ranking
Issue: Slow Response Time
Solutions:
- Use smaller embeddings model
- Cache frequent queries
- Use async queries
- Optimize chunk sizes
- Use a faster vector store (Qdrant, Weaviate)
Issue: Hallucinated Answers
Solutions:
- Use faithfulness evaluation
- Add "I don't know" handling
- Use stricter similarity thresholds
- Implement answer verification
- Use citation requirements
Best Practices
- Chunk Size: Start with 512-1024 tokens, adjust based on your data
- Embeddings: Use
text-embedding-3-smallfor cost efficiency,text-embedding-3-largefor accuracy - Hybrid Search: Always consider hybrid search for better recall
- Evaluation: Always evaluate before deploying
- Monitoring: Track query performance and user feedback
