2.02 – RAG Deep Dive | AI Course

📚 2.02 – RAG Deep Dive

📑 On this page

🏗️ RAG Architecture Overview

📌 Complete RAG Pipeline
     ┌─────────────────────────────────────────────────────────────┐
     │                    OFFLINE (Indexing)                       │
     │  Documents → Chunking → Embeddings → Vector Database        │
     └─────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
     ┌─────────────────────────────────────────────────────────────┐
     │                    ONLINE (Query)                           │
     │  User Query → Embeddings → Similarity Search → Top-K Chunks │
     │                                                             │
     │  Prompt: Context + Query → LLM → Response                   │
     └─────────────────────────────────────────────────────────────┘
💡 Key Insight

RAG solves the "knowledge cutoff" problem. The LLM doesn't need to know your data – it just needs to read relevant chunks at query time.

✂️ Chunking Strategies

📌 Why Chunking Matters

Documents are too long to fit in context windows. Chunking determines what information is retrieved. Bad chunking = irrelevant retrieval = bad answers.

StrategyDescriptionBest forPros/Cons
Fixed-size chunking Split every N characters/tokens Simple documents, consistent format ✅ Simple
❌ Might break sentences
Semantic chunking Split at sentence boundaries, paragraphs, or semantic meaning Prose, articles, books ✅ Respects natural boundaries
❌ More complex
Recursive chunking Try paragraph → sentence → character if too long Mixed content (markdown, code, prose) ✅ Adaptive
❌ Slower
Document-specific Use document structure (headers, lists, tables) HTML, Markdown, PDF with structure ✅ Preserves context
❌ Format-specific
Chunking Example (Python with LangChain):
from langchain.text_splitter import RecursiveCharacterTextSplitter

# Recommended for most documents
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,        # characters per chunk
    chunk_overlap=200,      # overlap between chunks (preserves context)
    separators=["\n\n", "\n", ".", "!", "?", ",", " ", ""]
)

chunks = text_splitter.split_text(document)
💡 Chunk Size Guidelines:

🔢 Embedding Models

📌 Popular Embedding Models Comparison
ModelDimensionsContext LengthMTEB ScoreCost per 1M tokens
text-embedding-3-small1536819262.3$0.02
text-embedding-3-large3072819264.6$0.13
BAAI/bge-large-en102451263.7Free (local)
intfloat/e5-mistral-7b40963276866.0Needs GPU
Creating Embeddings (OpenAI):
from openai import OpenAI
client = OpenAI()

response = client.embeddings.create(
    model="text-embedding-3-small",
    input="Your text to embed",
    dimensions=1536  # Can reduce dimensionality to save cost
)

embedding = response.data[0].embedding

🎯 Retrieval Strategies

📌 Beyond Simple Similarity Search
Hybrid Search Implementation (Weaviate):
# Combined vector + keyword search
response = client.query.get(
    "Document", ["text", "source"]
).with_hybrid(
    query="What is the refund policy?",
    alpha=0.5  # 0 = pure keyword, 1 = pure vector
).with_limit(10).do()
Multi-Query Retrieval:
from langchain.retrievers.multi_query import MultiQueryRetriever

retriever = MultiQueryRetriever.from_llm(
    retriever=vectorstore.as_retriever(),
    llm=ChatOpenAI(model="gpt-4"),
    prompt="Generate 3 different versions of this question..."
)

# Automatically generates multiple queries and combines results
docs = retriever.get_relevant_documents(query)

📊 Reranking

📌 Why Rerank?

Initial retrieval (embedding similarity) is fast but can be noisy. Reranking uses a more expensive but more accurate model to reorder results.

Cross-Encoder Reranking:
from sentence_transformers import CrossEncoder

# Load cross-encoder model (more accurate but slower)
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

# Initial retrieval (fast)
initial_results = vectorstore.similarity_search(query, k=20)

# Rerank pairs (query, document)
pairs = [(query, doc.page_content) for doc in initial_results]
scores = reranker.predict(pairs)

# Sort by reranking score and get top k
top_docs = sorted(zip(initial_results, scores), key=lambda x: x[1], reverse=True)[:5]
MethodSpeedAccuracyCost
Vector similarityVery fastGoodLow ($)
Cross-encoder rerankSlowExcellentHigher (but only on top results)
LLM-as-rerankerVery slowBestHigh ($$$)

📈 RAG Evaluation

📌 Key Metrics for RAG Systems
Evaluation with RAGAS:
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_relevancy

# Prepare dataset with questions, answers, contexts, ground_truth
dataset = Dataset.from_dict({
    "question": questions,
    "answer": generated_answers,
    "contexts": retrieved_contexts,
    "ground_truth": ground_truths
})

result = evaluate(dataset, metrics=[faithfulness, answer_relevancy])

print(result)  # Scores between 0-1
💡 RAG Optimization Checklist

✍️ Exercises

Exercise 2.02.1 – Chunking experiment

Take a long document (e.g., a news article). Try chunk sizes of 256, 512, and 1024 tokens. Ask the same question to each version. Which chunk size gave the best answer?

There's no single correct answer – it depends on your document and question. Generally:
- Smaller chunks (256) → higher precision but may miss context
- Larger chunks (1024) → more context but may include noise
- 512 with overlap is a good starting point
Exercise 2.02.2 – Embedding choice

You're building a RAG system for 1 million short support tickets (average 200 words each). Which embedding model would you choose and why?

Recommendation: text-embedding-3-small
Reasons: 1M tickets × ~200 words = ~200M tokens. At $0.02/1M tokens = $4 total embedding cost. Small is 2x cheaper than large, only 5% less accurate. Perfect for this scale.
Exercise 2.02.3 – Build a simple RAG system

Using LangChain, Chroma, and OpenAI, build a RAG system that answers questions from a PDF of your choice.

from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI

# Load PDF
loader = PyPDFLoader("your_document.pdf")
documents = loader.load()

# Split
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
docs = text_splitter.split_documents(documents)

# Embed and store
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(docs, embeddings)

# Create QA chain
qa_chain = RetrievalQA.from_chain_type(
    llm=ChatOpenAI(model="gpt-4"),
    retriever=vectorstore.as_retriever(search_kwargs={"k": 4})
)

# Ask
answer = qa_chain.run("What is the main topic of this document?")
print(answer)
📌 Key Takeaways
📘 Next Module: 2.03 – LangChain & LlamaIndex →