Module 1: The Complete RAG Pipeline (Architecture Overview)

Architecture Overview: Modules 2-8

Capsule overview

This guide has 8 modules. Module 1 (this one) gave you the baseline: the complete RAG pipeline, architecture decisions, metrics, real-world cases. Modules 2-8 teach you the advanced techniques that improve each component of that pipeline.

This capsule shows you the full roadmap: what you'll learn in each module, how they connect to each other, what performance gains to expect, and how the evolving project integrates every technique into a production-ready RAG system.

Without this overview, modules 2-8 would feel disconnected. With it, you'll understand that chunking (M2) optimizes Indexing, re-ranking (M4) optimizes Retrieval, metadata filtering (M6) optimizes the search space, and evaluation (M8) measures the whole system so you can spot gaps and apply improvements iteratively.


🗺️ The guide's roadmap

High-level view:

Module 1: The Complete RAG Pipeline (Baseline)
   ↓
Phase 1: Advanced Fundamentals
├─ Module 2: Chunking Strategies
├─ Module 3: Query Optimization
└─ Module 4: Re-ranking Techniques
   ↓
Phase 2: Retrieval Techniques
├─ Module 5: Hybrid Search (BM25 + Embeddings)
└─ Module 6: Metadata Filtering
   ↓
Phase 3: Production & Integration
├─ Module 7: Production Vector DBs (Pinecone)
└─ Module 8: RAG Evaluation & Testing (RAGAS)

📐 The complete RAG architecture

The baseline pipeline (Module 1):

┌─────────────────────────────────────────┐
│          INDEXING (Offline)             │
├─────────────────────────────────────────┤
│ 1. Chunking: Fixed-size (naive)        │
│ 2. Embeddings: OpenAI ada-002          │
│ 3. Storage: ChromaDB (local)           │
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│         RETRIEVAL (Query time)          │
├─────────────────────────────────────────┤
│ 1. Query: Direct (no optimization)     │
│ 2. Search: Semantic (embeddings)       │
│ 3. Ranking: Cosine similarity           │
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│        GENERATION (Query time)          │
├─────────────────────────────────────────┤
│ 1. Context: Top-K docs                 │
│ 2. LLM: GPT-3.5-turbo                  │
│ 3. Response: Text                       │
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│          EVALUATION (Manual)            │
├─────────────────────────────────────────┤
│ 1. Metrics: Latency, Precision (manual) │
│ 2. Testing: Ad-hoc queries              │
└─────────────────────────────────────────┘

Baseline performance:

  • Latency P95: 800ms
  • Precision@5: 68%
  • Recall@50: 52%
  • Faithfulness: N/A (no framework)

The advanced pipeline (Modules 2-8):

┌─────────────────────────────────────────┐
│          INDEXING (Offline)             │
├─────────────────────────────────────────┤
│ 1. Chunking: Recursive + Semantic (M2) │ ← +15% precision
│ 2. Embeddings: OpenAI ada-002          │
│ 3. Storage: Pinecone (production) (M7) │ ← Scalability
│ 4. Metadata: date, type, source (M6)   │ ← Filtering
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│         RETRIEVAL (Query time)          │
├─────────────────────────────────────────┤
│ 1. Query Optimization: Expansion (M3)  │ ← +25% recall
│ 2. Hybrid Search: BM25 + Semantic (M5) │ ← +18% precision
│ 3. Metadata Filter: Pre-filtering (M6) │ ← -90% search space
│ 4. Re-ranking: Cross-encoder (M4)      │ ← +20% precision
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│        GENERATION (Query time)          │
├─────────────────────────────────────────┤
│ 1. Context: Top-K re-ranked docs       │
│ 2. LLM: GPT-3.5-turbo                  │
│ 3. Response: Text + Sources             │
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│       EVALUATION (Automated) (M8)       │
├─────────────────────────────────────────┤
│ 1. RAGAS: faithfulness, relevancy      │ ← Automated metrics
│ 2. Golden Dataset: 50 test queries     │ ← Regression testing
│ 3. CI/CD: Auto-eval on every deploy    │ ← Continuous quality
└─────────────────────────────────────────┘

Advanced performance (the Module 8 target):

  • Latency P95: 1,200ms (+400ms from the optimizations)
  • Precision@5: 88% (+20% vs baseline)
  • Recall@50: 77% (+25% vs baseline)
  • Faithfulness: 0.92 (automated with RAGAS)

The trade-off we accept: +400ms of latency for +20-25% accuracy (worth it in most cases).


📦 Module 2: Chunking Strategies

The goal:

Optimize how you split long documents into chunks to maximize retrieval quality.

The baseline's problem:

# Fixed-size chunking (naive)
chunks = [document[i:i+500] for i in range(0, len(document), 500)]

# Problem 1: it cuts in the middle of a sentence
# "FastAPI is a web framework. It was crea..." ← Sentence cut off

# Problem 2: it loses semantic context
# Chunk 1: "Python is..."
# Chunk 2: "...an interpreted language"
# The problem: "Python" and "language" ended up in different chunks

The techniques you'll learn:

1. Recursive chunking (LangChain):

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,  # The overlap preserves context
    separators=["\n\n", "\n", ". ", " ", ""]
)

chunks = splitter.split_text(document)

# The benefit: it respects paragraphs and sentences

Expected improvement: +10-15% precision (more coherent chunks)


2. Semantic chunking (embeddings-based):

from langchain.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

splitter = SemanticChunker(OpenAIEmbeddings())
chunks = splitter.split_text(document)

# The benefit: it splits by topic (semantic coherence)

Expected improvement: +15-20% precision (best quality)
Trade-off: +embedding cost, +indexing latency


3. Structural chunking (code, HTML, Markdown):

# For code: chunk by function
import ast

def chunk_by_functions(python_code):
    tree = ast.parse(python_code)
    return [ast.get_source_segment(python_code, node) 
            for node in ast.walk(tree) 
            if isinstance(node, ast.FunctionDef)]

# For HTML: chunk by section
from bs4 import BeautifulSoup

def chunk_by_sections(html):
    soup = BeautifulSoup(html, 'html.parser')
    return [section.get_text() for section in soup.find_all('section')]

Expected improvement: +20-25% precision for code/HTML (it respects the structure)


The Module 2 project:

Implement 3 chunking strategies in the baseline RAG and compare their performance:

# Chunking comparison
baseline_fixed = benchmark_rag(chunking="fixed")
# Precision@5: 68%

recursive = benchmark_rag(chunking="recursive")
# Precision@5: 78% (+10%)

semantic = benchmark_rag(chunking="semantic")
# Precision@5: 83% (+15%)

🔍 Module 3: Query Optimization

The goal:

Optimize the user's query before searching, to raise both recall and precision.

The baseline's problem:

# A direct query, no optimization
user_query = "What is FastAPI?"

# The problem: the query can be ambiguous, incomplete, or badly phrased
# "FastAPI" → is the user asking what it is, how to install it, or for examples?

The techniques you'll learn:

1. Query expansion (raising recall):

# Generate several similar queries
expanded_queries = [
    "What is FastAPI?",
    "FastAPI framework explained",
    "FastAPI features",
    "FastAPI vs Flask"
]

# Search with all of them and merge the results

Expected improvement: +20-30% recall (you find more relevant docs)


2. Query rewriting (clarity):

# Reformulate the query for clarity
user_query = "fastapi auth"  # Ambiguous

rewritten_query = llm.invoke(f"""
Reformulate this query for a documentation search:
Query: {user_query}
Reformulated:
""")

# Output: "How to implement authentication in FastAPI using OAuth2"

Expected improvement: +10-15% precision (clearer queries)


3. Query decomposition (multi-hop):

# A complex query → simple sub-queries
user_query = "Compare FastAPI and Flask authentication approaches"

decomposed = [
    "FastAPI authentication methods",
    "Flask authentication methods",
    "Compare FastAPI vs Flask"
]

# Search each sub-query and aggregate the results

Expected improvement: +15-20% precision on complex queries


4. HyDE (Hypothetical Document Embeddings):

# Generate a hypothetical document that would answer the query
hypothetical_doc = llm.invoke(f"""
Write a document that would answer: {user_query}
""")

# Search with the hypothetical document's embedding (not the query's)
hyde_embedding = create_embedding(hypothetical_doc)
results = collection.query(query_embeddings=[hyde_embedding])

Expected improvement: +15-25% recall (doc embeddings > query embeddings)


The Module 3 project:

Implement query optimization in the baseline RAG:

baseline = benchmark_rag(query_optimization=None)
# Recall@50: 52%

expansion = benchmark_rag(query_optimization="expansion")
# Recall@50: 67% (+15%)

hyde = benchmark_rag(query_optimization="hyde")
# Recall@50: 72% (+20%)

🎯 Module 4: Re-ranking Techniques

The goal:

Improve retrieval precision by re-ordering the top-K documents with more sophisticated models.

The baseline's problem:

# Similarity search returns the top-5
results = collection.query(query_embeddings=[...], n_results=5)

# The problem: cosine similarity isn't perfect
# Some docs with a high similarity score are false positives
# Precision@5: 68% (32% are irrelevant)

The techniques you'll learn:

1. Cross-encoder re-ranking:

from sentence_transformers import CrossEncoder

# 1. Wide retrieval (top-20)
results = collection.query(query_embeddings=[...], n_results=20)

# 2. Re-ranking with a cross-encoder
model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
scores = model.predict([(user_query, doc) for doc in results['documents'][0]])

# 3. Select the re-ranked top-5
top_5 = [results['documents'][0][i] for i in np.argsort(scores)[::-1][:5]]

Expected improvement: +20-25% precision
Trade-off: +150-200ms latency


2. LLM-based re-ranking:

# Re-rank with an LLM
reranking_prompt = f"""
Query: {user_query}

Documents:
1. {doc1}
2. {doc2}
3. {doc3}

Rank documents by relevance (output: 2,1,3 for example):
"""

ranking = llm.invoke(reranking_prompt)
# Output: "2,1,3" (doc2 is the most relevant, then doc1, then doc3)

Expected improvement: +25-30% precision (best quality)
Trade-off: +500-800ms latency, +LLM cost


3. Cohere Rerank API:

import cohere

co = cohere.Client("api_key")

reranked = co.rerank(
    query=user_query,
    documents=[doc1, doc2, doc3],
    top_n=5,
    model="rerank-english-v2.0"
)

# Output: the documents re-ranked by Cohere

Expected improvement: +22-27% precision
Trade-off: +100-150ms latency, $2/1K rerank calls


The Module 4 project:

Implement re-ranking in the baseline RAG:

baseline = benchmark_rag(reranking=None)
# Precision@5: 68%

cross_encoder = benchmark_rag(reranking="cross_encoder")
# Precision@5: 88% (+20%)
# Latency: +180ms

cohere = benchmark_rag(reranking="cohere")
# Precision@5: 90% (+22%)
# Latency: +120ms

🔀 Module 5: Hybrid Search (BM25 + Embeddings)

The goal:

Combine keyword search (BM25) and semantic search (embeddings) for better coverage.

The baseline's problem:

# Semantic search alone
query = "FastAPI OAuth2PasswordBearer"

# Problem 1: it misses the exact match on "OAuth2PasswordBearer" (a class name)
# Semantic can confuse it with "OAuth2AuthorizationCodeBearer"

# Problem 2: it misses keyword-heavy queries ("API endpoint /users/{id}")

The techniques you'll learn:

1. BM25 + embeddings with RRF:

from rank_bm25 import BM25Okapi

# 1. BM25 keyword search
bm25_results = bm25.get_top_n(query.split(), corpus, n=50)

# 2. Semantic search
semantic_results = collection.query(query_embeddings=[...], n_results=50)

# 3. Reciprocal Rank Fusion
def rrf(bm25_results, semantic_results, k=60):
    scores = {}
    for rank, doc in enumerate(bm25_results, 1):
        scores[doc] = scores.get(doc, 0) + 1/(k + rank)
    for rank, doc in enumerate(semantic_results, 1):
        scores[doc] = scores.get(doc, 0) + 1/(k + rank)
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

merged = rrf(bm25_results, semantic_results)

Expected improvement: +15-20% precision, +10-15% recall


2. Weighted hybrid (α blending):

# Combine the scores with a weight
alpha = 0.7  # 70% semantic, 30% BM25

hybrid_score = alpha * semantic_score + (1 - alpha) * bm25_score

Expected improvement: +12-18% precision (tunable with α)


The Module 5 project:

Implement hybrid search:

baseline_semantic = benchmark_rag(search="semantic")
# Precision@5: 68%, Recall@50: 52%

baseline_bm25 = benchmark_rag(search="bm25")
# Precision@5: 72%, Recall@50: 48%

hybrid_rrf = benchmark_rag(search="hybrid_rrf")
# Precision@5: 83% (+15%), Recall@50: 62% (+10%)

🏷️ Module 6: Metadata Filtering

The goal:

Shrink the search space with metadata (date, type, source) before the semantic search runs.

The baseline's problem:

# Search with no filtering
results = collection.query(query_embeddings=[...], n_results=5)

# The problem: it searches the ENTIRE corpus (100K docs)
# Many docs are contextually irrelevant (e.g. docs from 5 years ago)

The techniques you'll learn:

1. Pre-filtering (metadata WHERE):

# Filter BEFORE the semantic search
results = collection.query(
    query_embeddings=[...],
    n_results=5,
    where={
        "date": {"$gte": "2024-01-01"},  # Only 2024 docs
        "type": "tutorial",               # Only tutorials
        "source": "official_docs"         # Only official docs
    }
)

# The benefit: the search space shrinks 95% (100K → 5K docs)

Expected improvement: +10-15% precision, -90% search space


2. Post-filtering (filter afterwards):

# Retrieve first, filter afterwards
results = collection.query(query_embeddings=[...], n_results=50)

# Filter by metadata
filtered = [doc for doc in results['documents'][0] 
            if doc['metadata']['type'] == 'tutorial'][:5]

Trade-off: pre-filtering > post-filtering (it's more efficient)


3. Multi-tenant filtering (privacy):

# Every user has a workspace_id
results = collection.query(
    query_embeddings=[...],
    n_results=5,
    where={"workspace_id": user.workspace_id}  # Privacy-critical
)

# The benefit: zero cross-workspace leakage

The Module 6 project:

Implement metadata filtering:

baseline = benchmark_rag(filtering=None)
# Precision@5: 68%, Search space: 100K docs

pre_filtering = benchmark_rag(filtering="pre", metadata=["date", "type"])
# Precision@5: 81% (+13%), Search space: 8K docs (-92%)

🚀 Module 7: Production Vector DBs (Pinecone)

The goal:

Migrate from ChromaDB (dev, local) to Pinecone (production, managed) for scalability.

The baseline's problem:

# ChromaDB local
client = chromadb.Client()

# Problem 1: performance degrades beyond 100K docs
# 10K docs: 30ms query ✅
# 100K docs: 300ms query ⚠️
# 1M docs: 3,000ms query ❌

# Problem 2: single-machine (not distributed)
# Problem 3: not managed (you handle backups, updates, etc.)

The techniques you'll learn:

1. Migrating ChromaDB → Pinecone:

from pinecone import Pinecone

# Set up Pinecone
pc = Pinecone(api_key="...")
index = pc.create_index(name="my-index", dimension=1536, metric="cosine")

# Migrate the data from ChromaDB
chroma_docs = chroma_collection.get()

# Batch upsert into Pinecone
index.upsert(
    vectors=[
        (id, embedding, metadata)
        for id, embedding, metadata in zip(
            chroma_docs['ids'],
            chroma_docs['embeddings'],
            chroma_docs['metadatas']
        )
    ],
    batch_size=100
)

2. Pinecone features:

# Namespaces (multi-tenant)
index.upsert(vectors=[...], namespace="user_123")
index.query(vector=[...], namespace="user_123")

# Metadata filtering (pre-filtering)
index.query(
    vector=[...],
    filter={"type": {"$eq": "tutorial"}},
    top_k=5
)

# Sparse-dense vectors (native hybrid search)
index.upsert(
    vectors=[
        ("id1", dense_vector, sparse_vector, metadata)
    ]
)

Performance comparison:

Vector DB10K docs100K docs1M docsCost
ChromaDB30ms300ms3,000msFree
Pinecone40ms45ms50ms$70/mo

The decision: Pinecone if your dataset is >100K docs or you need a guaranteed <100ms.


The Module 7 project:

Migrate the baseline RAG to Pinecone:

# Before (ChromaDB, 100K docs)
latency_p95 = 850ms

# After (Pinecone, 100K docs)
latency_p95 = 520ms (-40%)

📊 Module 8: RAG Evaluation & Testing (RAGAS)

The goal:

Evaluate the RAG system's quality with automated metrics (faithfulness, relevancy).

The baseline's problem:

# Manual evaluation
# 1. Generate an answer
answer = rag_system.query("What is FastAPI?")

# 2. Read the answer and judge it by hand
# "Is it correct? Is it grounded in the docs? Does it answer the question?"

# The problem: it doesn't scale, it isn't consistent, it isn't automated

The techniques you'll learn:

1. The RAGAS framework (automated metrics):

from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_recall

# Evaluation dataset
dataset = {
    "question": ["What is FastAPI?"],
    "answer": ["FastAPI is a web framework..."],
    "contexts": [["FastAPI is a modern web framework..."]],
    "ground_truth": ["FastAPI is a Python web framework"]
}

# Evaluate
results = evaluate(
    dataset,
    metrics=[faithfulness, answer_relevancy, context_recall]
)

print(results)
# Output:
# {
#   'faithfulness': 0.92,      # Grounded in the context?
#   'answer_relevancy': 0.89,  # Does it answer the question?
#   'context_recall': 0.75     # Did we retrieve the relevant docs?
# }

2. Golden dataset (regression testing):

# Build a dataset of 50 test queries + ground truth
golden_dataset = [
    {
        "question": "What is FastAPI?",
        "ground_truth": "FastAPI is a web framework...",
        "relevant_docs": ["doc_123", "doc_456"]
    },
    # ... 49 more
]

# Evaluate on every deploy
def regression_test(rag_system):
    results = evaluate(rag_system, golden_dataset)
    assert results['faithfulness'] > 0.85, "Faithfulness degraded!"
    assert results['answer_relevancy'] > 0.80, "Relevancy degraded!"
    return results

3. CI/CD integration (continuous evaluation):

# Auto-eval on every PR/deploy
# .github/workflows/rag-eval.yml

steps:
  - name: Run RAG evaluation
    run: |
      python test_rag.py
      # If the metrics drop below the threshold, fail CI

The Module 8 project:

Implement the complete evaluation:

# Baseline (manual)
faithfulness = "Unknown"
answer_relevancy = "Unknown"

# With RAGAS (automated)
results = evaluate(rag_system, golden_dataset)
# faithfulness: 0.92
# answer_relevancy: 0.89
# context_recall: 0.75

# Identify the gaps and apply improvements
if results['context_recall'] < 0.80:
    apply_improvement("query_expansion")  # Module 3

🔗 How the modules connect

The evolving pipeline:

Module 1: Baseline RAG
   ↓ (Precision 68%, Recall 52%)
   
Module 2: + Optimized chunking
   ↓ (Precision 78%, Recall 52%) [+10% precision]
   
Module 3: + Query optimization
   ↓ (Precision 78%, Recall 67%) [+15% recall]
   
Module 4: + Re-ranking
   ↓ (Precision 88%, Recall 67%) [+10% precision]
   
Module 5: + Hybrid search
   ↓ (Precision 91%, Recall 77%) [+3% precision, +10% recall]
   
Module 6: + Metadata filtering
   ↓ (Precision 93%, Recall 77%) [+2% precision, -90% search space]
   
Module 7: + Pinecone migration
   ↓ (Latency -40%, Scalability ∞)
   
Module 8: + RAGAS evaluation
   ↓ (Automated quality gates, CI/CD integration)

Final performance:

  • Precision@5: 93% (baseline: 68%, gain: +25%)
  • Recall@50: 77% (baseline: 52%, gain: +25%)
  • Latency P95: 1,200ms (baseline: 800ms, trade-off: +400ms for quality)
  • Faithfulness: 0.92 (baseline: N/A)
  • Cost per query: $0.003 (acceptable for production)

🎯 The evolving project (Module 8)

The final goal:

A production-ready RAG system that integrates ALL the techniques:

# advanced_rag_system.py (the final Module 8 build)

class AdvancedRAGSystem:
    def __init__(self):
        # Module 2: Chunking
        self.chunker = RecursiveCharacterTextSplitter(...)
        
        # Module 3: Query optimization
        self.query_optimizer = QueryExpansionOptimizer(...)
        
        # Module 4: Re-ranking
        self.reranker = CrossEncoder('cross-encoder/...')
        
        # Module 5: Hybrid search
        self.bm25 = BM25Okapi(...)
        self.semantic_search = PineconeIndex(...)
        
        # Module 6: Metadata filtering
        self.metadata_filters = {...}
        
        # Module 7: Production vector DB
        self.vector_db = Pinecone(...)
        
        # Module 8: Evaluation
        self.evaluator = RAGASEvaluator(...)
    
    def query(self, user_query: str) -> dict:
        # 1. Query optimization (M3)
        optimized_queries = self.query_optimizer.expand(user_query)
        
        # 2. Hybrid retrieval (M5) with metadata filtering (M6)
        bm25_results = self.bm25.search(user_query)
        semantic_results = self.semantic_search.query(
            optimized_queries[0],
            filter=self.metadata_filters
        )
        merged = reciprocal_rank_fusion(bm25_results, semantic_results)
        
        # 3. Re-ranking (M4)
        reranked = self.reranker.predict([(user_query, doc) for doc in merged])
        top_k = select_top_k(reranked, k=5)
        
        # 4. Generation
        answer = self.llm.invoke(context=top_k, query=user_query)
        
        # 5. Evaluation (M8)
        metrics = self.evaluator.evaluate(
            question=user_query,
            answer=answer,
            contexts=top_k
        )
        
        return {
            "answer": answer,
            "sources": top_k,
            "metrics": metrics
        }

# The final benchmark
rag_system = AdvancedRAGSystem()
final_results = benchmark(rag_system, golden_dataset)

print(f"""
Advanced RAG System Results:
- Precision@5: {final_results['precision']:.2%}
- Recall@50: {final_results['recall']:.2%}
- Faithfulness: {final_results['faithfulness']:.2f}
- Latency P95: {final_results['latency_p95']:.0f}ms
- Cost per query: ${final_results['cost_per_query']:.4f}
""")

Expected output:

Advanced RAG System Results:
- Precision@5: 93%
- Recall@50: 77%
- Faithfulness: 0.92
- Latency P95: 1,200ms
- Cost per query: $0.0031

🎯 Summary

Key concepts:

  • Module 1: Baseline RAG (68% precision, 52% recall)
  • Module 2: Chunking strategies → +10-15% precision
  • Module 3: Query optimization → +15-25% recall
  • Module 4: Re-ranking → +20-25% precision
  • Module 5: Hybrid search → +15-18% precision, +10-15% recall
  • Module 6: Metadata filtering → +10-13% precision, -90% search space
  • Module 7: Pinecone migration → -40% latency, ∞ scalability
  • Module 8: RAGAS evaluation → automated quality gates
  • The evolving pipeline: baseline → advanced (68% → 93% precision)
  • The trade-off we accept: +400ms latency for +25% accuracy

What's next:

Capsule 08 gives you the Module 1 mini-project: implement the baseline RAG with ChromaDB + OpenAI, measure its performance, and document your decisions so you can compare them against the improvements in modules 2-8.


📚 Additional resources

  1. RAG Architecture Patterns - LangChain patterns
  2. Advanced RAG Techniques - Pinecone's guide
  3. Production RAG Best Practices - The LlamaIndex blog
  4. RAGAS Documentation - The evaluation framework
  5. Hybrid Search Deep Dive - Elasticsearch's guide
  6. Chunking Strategies Comparison - An analysis of chunk size

Created: February 6, 2026
Version: 1.0