Skip to content
SDB
Generative AI

Chapter 06 · intermediate · 30 min

Retrieval-Augmented Generation (RAG)

Grounding LLM responses in external knowledge with vector search and retrieval

Subhendu Datta BhowmikAI Tutorials

The Problem RAG Solves

LLMs have three fundamental knowledge limitations:

  1. Knowledge cutoff: training data has a fixed date
  2. Context window: can't load entire knowledge bases at inference time
  3. Hallucination: models confidently confabulate when they don't know

RAG (Lewis et al., 2020) solves all three by retrieving relevant documents at inference time and injecting them into the prompt.

The RAG Pipeline

INDEXING (offline):
Documents → Chunking → Embedding → Vector Store

RETRIEVAL (online):
Query → Embed Query → Nearest Neighbors → Top-K Chunks

GENERATION (online):
Prompt = System + Retrieved Chunks + User Query → LLM → Answer

Step 1: Chunking

Raw documents must be split into chunks that fit within the context window and represent coherent units of information.

Chunking Strategies

Fixed-size chunking

  • Split every N characters/tokens with M overlap
  • Simple but may cut mid-sentence

Recursive character splitting (most common)

  • Split by paragraph, then sentence, then word until chunks are small enough
  • Preserves semantic units

Semantic chunking

  • Use embedding similarity to split at topic boundaries
  • Higher quality, more expensive

Document-aware chunking

  • Markdown: split by headers
  • Code: split by functions/classes
  • PDFs: split by page or section

Key Parameters

  • Chunk size: 256–1024 tokens (smaller = more precise retrieval; larger = more context)
  • Overlap: 10–20% of chunk size to avoid cutting relevant sentences at boundaries

Step 2: Embedding

Convert each chunk into a dense vector that captures its semantic meaning. Similar chunks → similar vectors → close in vector space.

Popular Embedding Models

ModelDimensionsContextNotes
text-embedding-3-large30728191 tokensBest quality (OpenAI)
text-embedding-3-small15368191 tokensCost-efficient (OpenAI)
voyage-3-large102432K tokensBest for RAG (Voyage AI)
nomic-embed-text7688192 tokensOpen-source, fast
mxbai-embed-large1024512 tokensStrong on MTEB

Cosine Similarity

Embeddings are compared using cosine similarity (angle between vectors): sim(a,b)=abab\text{sim}(a, b) = \frac{a \cdot b}{\|a\| \|b\|}

Values range from -1 (opposite) to 1 (identical). In practice, relevant chunks score 0.7–0.95.

Building a RAG Pipeline from Scratchpython
import anthropic
import numpy as np
from typing import List

client = anthropic.Anthropic()

# ─── Indexing ───────────────────────────────────────────────

def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> List[str]:
    """Simple sliding-window chunking."""
    words = text.split()
    chunks = []
    for i in range(0, len(words), chunk_size - overlap):
        chunk = " ".join(words[i:i + chunk_size])
        if chunk:
            chunks.append(chunk)
    return chunks

def embed(texts: List[str]) -> np.ndarray:
    """Embed texts using Voyage AI via Anthropic."""
    # In practice, use a dedicated embedding API
    # Here we simulate with a placeholder
    import random
    return np.array([[random.random() for _ in range(1024)] for _ in texts])

def cosine_similarity(a: np.ndarray, b: np.ndarray) -> np.ndarray:
    a_norm = a / np.linalg.norm(a, axis=1, keepdims=True)
    b_norm = b / np.linalg.norm(b, axis=1, keepdims=True)
    return a_norm @ b_norm.T

# Build the index
documents = [
    "Machine learning is a subset of artificial intelligence...",
    "Transformers use self-attention mechanisms to process sequences...",
    "Large language models are trained on massive text corpora...",
    # ... more documents
]

all_chunks = []
for doc in documents:
    all_chunks.extend(chunk_text(doc))

chunk_embeddings = embed(all_chunks)  # shape: (n_chunks, 1024)

# ─── Retrieval + Generation ──────────────────────────────────

def rag_query(user_query: str, top_k: int = 3) -> str:
    # 1. Embed the query
    query_embedding = embed([user_query])  # (1, 1024)

    # 2. Compute similarities
    similarities = cosine_similarity(query_embedding, chunk_embeddings)[0]

    # 3. Retrieve top-k chunks
    top_indices = np.argsort(similarities)[::-1][:top_k]
    retrieved_chunks = [all_chunks[i] for i in top_indices]
    top_scores = [similarities[i] for i in top_indices]

    # 4. Build prompt with retrieved context
    context = "\n\n".join([
        f"[Source {i+1} (relevance: {score:.2f})]\n{chunk}"
        for i, (chunk, score) in enumerate(zip(retrieved_chunks, top_scores))
    ])

    # 5. Generate with grounded context
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system="""Answer the user's question based ONLY on the provided context.
If the context doesn't contain the answer, say "I don't have information about that in my knowledge base."
Always cite which source(s) you used.""",
        messages=[{
            "role": "user",
            "content": f"Context:\n{context}\n\nQuestion: {user_query}"
        }],
    )
    return response.content[0].text

answer = rag_query("How do transformers work?")
print(answer)

Vector Databases

Production RAG systems use dedicated vector databases for:

  • Billion-scale nearest neighbor search
  • Metadata filtering (filter by date, category, author before ranking)
  • Hybrid search (combine vector similarity with BM25 keyword search)
  • Persistent storage and updates

Popular Vector DBs

DatabaseNotes
ChromaEasy to start, in-memory or SQLite
PineconeFully managed, production-ready
QdrantOpen-source, rich filtering, fast
WeaviateGraphQL API, multi-modal
pgvectorPostgreSQL extension, no new infra
MilvusBillion-scale, cloud-native
RAG with ChromaDBpython
import chromadb
import anthropic

chroma = chromadb.Client()
collection = chroma.create_collection("knowledge_base")
client = anthropic.Anthropic()

# Index documents
docs = ["AI learns from data...", "Transformers use attention...", "RAG retrieves context..."]
ids = [f"doc_{i}" for i in range(len(docs))]
metadatas = [{"source": "textbook", "chapter": i+1} for i in range(len(docs))]

collection.add(documents=docs, ids=ids, metadatas=metadatas)

def rag(query: str, n_results: int = 3) -> str:
    # Retrieve with optional metadata filter
    results = collection.query(
        query_texts=[query],
        n_results=n_results,
        where={"source": "textbook"},  # optional filter
    )
    context = "\n\n".join(results["documents"][0])

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=512,
        messages=[{
            "role": "user",
            "content": f"Answer using only this context:\n{context}\n\nQuestion: {query}"
        }],
    )
    return response.content[0].text

print(rag("How does attention work in AI?"))

Advanced RAG Techniques

Reranking

Initial retrieval recalls many candidates; a cross-encoder reranker scores each (query, chunk) pair together for higher precision:

from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
scores = reranker.predict([(query, chunk) for chunk in candidates])
top_chunks = [candidates[i] for i in np.argsort(scores)[::-1][:3]]

HyDE (Hypothetical Document Embeddings)

Generate a hypothetical answer first, then use its embedding for retrieval:

# 1. Generate a hypothetical answer (even if wrong)
hypothetical = llm("Write a detailed answer about: " + query)
# 2. Embed the hypothetical answer (not the query)
embedding = embed(hypothetical)
# 3. Retrieve using the hypothetical embedding

Hybrid Search

Combine dense vector search with BM25 keyword search, then fuse rankings:

  • Dense: catches semantic similarity ("automobile" ↔ "car")
  • BM25: catches exact matches (rare terms, product codes)
  • Fusion: Reciprocal Rank Fusion (RRF) merges both lists

Knowledge check

Why is document chunking necessary for RAG?

Summary

  • RAG grounds LLM responses in external knowledge, reducing hallucinations and overcoming the knowledge cutoff
  • Chunking splits documents into retrievable units — strategy matters (fixed, recursive, semantic)
  • Embeddings convert text to dense vectors; similar meaning → nearby vectors
  • Vector databases enable fast nearest-neighbor search at scale with metadata filtering
  • Advanced techniques: reranking improves precision; HyDE improves recall; hybrid search combines dense and sparse
  • Monitor retrieval and generation quality independently

Next: Multimodal AI — extending these concepts to images, audio, and video.

Generative AI