Module 1: The Complete RAG Pipeline (Architecture Overview)

RAG Architecture Decisions

Capsule overview

Building advanced RAG isn't just wiring components together. It's making technical decisions at every layer: which chunking strategy to use, which embeddings to choose, which vector DB to select, when to apply re-ranking. Every decision has real trade-offs: performance vs quality, cost vs precision, simplicity vs features.

This capsule gives you a complete decision framework: comparison tables, selection criteria, typical use cases, and examples of real decisions. There are no one-size-fits-all answers ("always use X"), just the context you need to decide intelligently for your specific use case: dataset size, latency requirements, budget constraints, quality targets.

By the end of this capsule, you'll be able to justify your architecture decisions with data and clear trade-offs. You won't say "I use Pinecone because it's popular", you'll say "I use Pinecone because my dataset has 5M documents, I need <100ms latency, and I have a $200/month budget for a managed service".


🧩 Decision 1: Chunking strategy

The problem:

Long documents (5,000+ tokens) don't fit in a vector DB or an LLM context window. You need to split them into chunks.

Your options:

Option A: Fixed-size chunking

How it works:

def fixed_size_chunking(document: str, chunk_size: int = 500) -> list[str]:
    """Splits a document into fixed-size chunks"""
    return [document[i:i+chunk_size] for i in range(0, len(document), chunk_size)]

# Example
doc = "Python is a language. " * 100  # 2400 characters
chunks = fixed_size_chunking(doc, chunk_size=500)

print(f"Chunks: {len(chunks)}")  # 5 chunks
print(f"First chunk: {chunks[0]}")

Pros:

  • ✅ Simple to implement
  • ✅ Fast
  • ✅ Predictable chunk sizes

Cons:

  • ❌ Can cut in the middle of a sentence
  • ❌ Loses semantic context
  • ❌ Doesn't respect the document's structure

When to use it:

  • Structured documents (tables, lists)
  • Quick prototypes
  • Small datasets (<1,000 docs)

Option B: Semantic chunking

How it works:

from langchain.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

def semantic_chunking(document: str) -> list[str]:
    """Splits a document while preserving semantic coherence"""
    
    embeddings = OpenAIEmbeddings()
    splitter = SemanticChunker(embeddings)
    
    chunks = splitter.split_text(document)
    return chunks

# Example
doc = """
Python is an interpreted programming language.
It was created by Guido van Rossum in 1991.

FastAPI is a web framework for Python.
It was created by Sebastián Ramírez in 2018.
"""

chunks = semantic_chunking(doc)

# Output: 2 chunks (Python vs FastAPI)
# Chunk 1: "Python is... 1991."
# Chunk 2: "FastAPI is... 2018."

Pros:

  • ✅ Preserves semantic coherence
  • ✅ Doesn't cut at arbitrary points
  • ✅ Better retrieval quality

Cons:

  • ❌ Slower (it generates embeddings)
  • ❌ Variable chunk sizes
  • ❌ Requires API calls (cost)

When to use it:

  • Narrative documents (articles, books)
  • Quality > speed
  • The budget allows for extra embeddings

Option C: Recursive chunking

How it works:

from langchain.text_splitter import RecursiveCharacterTextSplitter

def recursive_chunking(
    document: str,
    chunk_size: int = 500,
    chunk_overlap: int = 50
) -> list[str]:
    """
    Splits a document recursively using separators.
    
    It tries to split by:
    1. Paragraphs (\n\n)
    2. Sentences (. )
    3. Words ( )
    4. Characters (if everything else fails)
    """
    
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        separators=["\n\n", "\n", ". ", " ", ""]
    )
    
    chunks = splitter.split_text(document)
    return chunks

# Example
chunks = recursive_chunking(doc, chunk_size=200, chunk_overlap=30)

# Output: chunks that respect paragraphs/sentences
# The 30-char overlap preserves context between chunks

Pros:

  • ✅ Balanced: respects structure while keeping size under control
  • ✅ Overlap preserves context between chunks
  • ✅ Doesn't need extra embeddings

Cons:

  • ❌ More complex than fixed-size
  • ❌ Custom separators need tuning
  • ❌ Overlap increases storage (duplication)

When to use it:

  • The default for production (best balance)
  • Mixed documents (narrative + code + lists)
  • You need to preserve context without semantic's cost

Chunking strategies compared:

StrategySpeedRetrieval qualityCostChunk sizeWhen to use
Fixed-sizeVery fastMediumZeroFixed (500)Prototype, structured docs
SemanticSlowHighHighVariable (200-800)Narrative, quality-critical
RecursiveFastMedium-HighZeroControlled (500±50)Production default

Recommended for the baseline (Module 1): Fixed-size (simple)
Recommended for production (Module 7): Recursive (balanced)
Going deeper: Module 2 covers chunking strategies in detail


🧠 Decision 2: Embedding model

The problem:

You need to turn text into vectors. Which model do you use?

Your options:

Option A: OpenAI text-embedding-ada-002

from openai import OpenAI

client = OpenAI()

embedding = client.embeddings.create(
    model="text-embedding-ada-002",
    input="Your text here"
).data[0].embedding

# Dimensions: 1536
# Cost: $0.0001 per 1K tokens

Pros:

  • ✅ High quality (state-of-the-art)
  • ✅ Managed (no deployment)
  • ✅ 1536 dimensions (good resolution)

Cons:

  • ❌ Cost per call
  • ❌ Requires an API key
  • ❌ Network latency (~50ms)

When to use it:

  • Production with a budget
  • Quality-critical work
  • English or multilingual

Option B: Local (Sentence-Transformers)

from sentence_transformers import SentenceTransformer

model = SentenceTransformer('all-MiniLM-L6-v2')  # 22MB model

embedding = model.encode("Your text here")

# Dimensions: 384
# Cost: Zero (local)
# Latency: ~20ms (local)

Pros:

  • ✅ Free (zero cost)
  • ✅ Total privacy (local)
  • ✅ Fast (~20ms, no network)

Cons:

  • ❌ Lower quality than OpenAI
  • ❌ 384 dimensions (less resolution)
  • ❌ Requires deploying the model

When to use it:

  • Zero budget
  • Privacy-critical work
  • A prototype with no external dependencies

Option C: Cohere embed-multilingual-v3

import cohere

co = cohere.Client("api_key")

embedding = co.embed(
    texts=["Your text here"],
    model="embed-multilingual-v3.0",
    input_type="search_document"  # Or "search_query"
).embeddings[0]

# Dimensions: 1024
# Cost: $0.0001 per 1K tokens

Pros:

  • ✅ Excellent multilingual support (100+ languages)
  • ✅ Different input types (document vs query)
  • ✅ High quality

Cons:

  • ❌ Cost similar to OpenAI
  • ❌ Less adoption than OpenAI
  • ❌ Requires a different API key

When to use it:

  • Multilingual is critical (Spanish, French, etc.)
  • You need to separate document from query embeddings

Embedding models compared:

ModelDimensionsCost (1M tokens)QualityMultilingualLatencyWhen to use
OpenAI ada-0021536$0.10⭐⭐⭐⭐⭐Good50msEnglish production
Cohere multilingual1024$0.10⭐⭐⭐⭐⭐Excellent60msMultilingual production
Local MiniLM384Free⭐⭐⭐Limited20msDev, prototype, privacy

Recommended decision:

  • Baseline (Module 1): OpenAI ada-002 (simple, good quality)
  • Production: OpenAI if English, Cohere if multilingual
  • Local dev: Sentence-Transformers (zero cost)

🗄️ Decision 3: Vector database

The problem:

You need to store millions of vectors and search them fast (<100ms).

Your options:

Option A: ChromaDB (local)

import chromadb

# Local client (in memory)
client = chromadb.Client()

# Persistent client (on disk)
client = chromadb.PersistentClient(path="./chroma_db")

collection = client.create_collection("docs")

# Add documents
collection.add(
    documents=["text1", "text2"],
    embeddings=[[0.1, ...], [0.2, ...]],
    metadatas=[{"source": "doc1"}, {"source": "doc2"}],
    ids=["1", "2"]
)

# Query
results = collection.query(
    query_embeddings=[[0.15, ...]],
    n_results=5
)

Pros:

  • ✅ Zero setup (pip install)
  • ✅ Free (local)
  • ✅ Persistence on disk
  • ✅ Perfect for development

Cons:

  • ❌ Doesn't scale beyond 100K docs (slow)
  • ❌ Single-machine (not distributed)
  • ❌ Not managed (you administer it)

Performance:

  • 10K docs: <50ms query ✅
  • 100K docs: ~200-500ms ⚠️
  • 1M+ docs: Not recommended ❌

When to use it:

  • Development and prototypes (modules 1-6)
  • Datasets under 50K documents
  • You don't need a managed service

Option B: Pinecone (managed cloud)

from pinecone import Pinecone

# Initialize
pc = Pinecone(api_key="your-api-key")

# Create an index
index = pc.create_index(
    name="my-index",
    dimension=1536,
    metric="cosine"
)

# Add documents
index.upsert(
    vectors=[
        ("id1", [0.1, ...], {"text": "text1"}),
        ("id2", [0.2, ...], {"text": "text2"})
    ]
)

# Query
results = index.query(
    vector=[0.15, ...],
    top_k=5,
    include_metadata=True
)

Pros:

  • ✅ Infinite scalability (millions of docs)
  • ✅ Consistent performance (<50ms, always)
  • ✅ Managed (you don't administer it)
  • ✅ Advanced features (namespaces, filtering)

Cons:

  • ❌ Cost ($70/month minimum for serverless)
  • ❌ Requires an API key and an account
  • ❌ Vendor lock-in

Performance:

  • 10K docs: <50ms ✅
  • 1M docs: <50ms ✅
  • 100M docs: <50ms ✅ (with the right pods)

When to use it:

  • Production (module 7)
  • Datasets over 100K documents
  • You need a guaranteed <100ms latency
  • You have the budget for a managed service

Option C: Weaviate (self-hosted or cloud)

import weaviate

# Cloud client
client = weaviate.Client(
    url="https://your-cluster.weaviate.network",
    auth_client_secret=weaviate.AuthApiKey("api_key")
)

# Create a schema
client.schema.create_class({
    "class": "Document",
    "vectorizer": "text2vec-openai"
})

# Add documents
client.data_object.create(
    data_object={"text": "text1"},
    class_name="Document"
)

# Query
results = client.query.get(
    "Document", ["text"]
).with_near_text({"concepts": ["query"]}).with_limit(5).do()

Pros:

  • ✅ Self-hosting is possible (total control)
  • ✅ GraphQL queries (flexible)
  • ✅ Built-in vectorizers

Cons:

  • ❌ More complex than ChromaDB
  • ❌ Self-hosting requires DevOps
  • ❌ Cloud pricing similar to Pinecone

When to use it:

  • You need self-hosting (privacy/compliance)
  • GraphQL queries are an advantage for you
  • Your company already uses Weaviate

Vector databases compared:

Vector DBSetupCostPerformanceMax docsManagedWhen to use
ChromaDBpip installFree10K: fast, 100K+: slow~100KDev, prototypes
PineconeAPI key$70+/moAlways <50msMillionsProduction
WeaviateDocker/CloudFree (self) or $70+/moGoodMillions✅/❌Self-hosting, GraphQL
QdrantDockerFree (self)Very goodMillionsSelf-hosting, Rust
MilvusDocker/CloudFree (self)ExcellentMillionsEnterprise, scale

Decision matrix:

Dataset <10K docs + Dev → ChromaDB ✅
Dataset 10K-100K + Dev → ChromaDB ⚠️
Dataset >100K + Production → Pinecone ✅
Privacy/Compliance → Weaviate/Qdrant (self-hosted) ✅
Zero budget + <100K docs → ChromaDB ✅
Budget available + >100K docs → Pinecone ✅

🎯 Decision 4: Re-ranking

The problem:

The top-K documents from similarity search include irrelevant ones (false positives).

When do you apply re-ranking?

Without re-ranking (baseline):

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

# The problem: some docs are irrelevant
# Doc 1: Relevant (cosine: 0.85)
# Doc 2: Irrelevant (cosine: 0.83) ← False positive
# Doc 3: Relevant (cosine: 0.81)

With re-ranking (Module 4):

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

# 2. Re-ranking with a cross-encoder
from sentence_transformers import CrossEncoder
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_indices = np.argsort(scores)[::-1][:5]
top_k_docs = [results['documents'][0][i] for i in top_5_indices]

# Result: only genuinely relevant documents

Re-ranking trade-offs:

AspectWithout re-rankingWith re-rankingDelta
Latency200ms400ms+200ms
Precision@565%85%+20%
Compute costLowMedium+an extra model
ComplexitySimpleMedium+a component

The decision:

# Decision tree for re-ranking
if precision_requirements > 0.80:
    if latency_budget > 500ms:
        use_reranking = True  # ✅ Worth it
    else:
        use_reranking = False  # ❌ Latency is critical
else:
    use_reranking = False  # ❌ 65% precision is enough

Typical use cases:

✅ Use re-ranking when:

  • Precision is critical (medical, legal, financial)
  • Your latency budget is >500ms
  • False positives are expensive

❌ DON'T use re-ranking when:

  • <300ms latency is critical
  • 60-70% precision is good enough
  • You're building simple prototypes

🔗 Decision 5: Optional components

Hybrid search (Module 5):

When do you add it?

  • ✅ Queries contain exact keywords (names, codes, IDs)
  • ✅ Semantic search fails in specific cases
  • ❌ Purely narrative content (semantic is enough)

Trade-off: +complexity, +better coverage


Metadata filtering (Module 6):

When do you add it?

  • ✅ You need to filter by context (date, author, category)
  • ✅ A large corpus with logical subsets
  • ❌ A small, homogeneous corpus

Trade-off: +metadata storage, +contextual relevance


Query optimization (Module 3):

When do you add it?

  • ✅ User queries are ambiguous
  • ✅ You need to boost recall (find more relevant docs)
  • ❌ Queries are already clear and specific

Trade-off: +LLM calls, +better retrieval


📋 The complete decision framework

Step 1: Define your requirements

My use case:
- Dataset size: [X documents]
- Query volume: [Y queries/day]
- Latency target: [Z ms]
- Precision target: [W%]
- Budget: [$X/month]
- Deployment: [Cloud/On-prem]

Step 2: Select your components

# Chunking
if document_type == "narrative":
    chunking = "semantic"  # Module 2
elif document_type == "technical":
    chunking = "recursive"  # Default
else:
    chunking = "fixed"  # Simple

# Embeddings
if budget == "zero":
    embeddings = "sentence-transformers"  # Local
elif multilingual:
    embeddings = "cohere"  # Better multilingual
else:
    embeddings = "openai"  # Default

# Vector DB
if dataset_size > 100_000:
    vector_db = "pinecone"  # Production
else:
    vector_db = "chromadb"  # Dev

# Re-ranking
if precision_target > 0.80 and latency_budget > 500:
    use_reranking = True  # Module 4
else:
    use_reranking = False

# Hybrid search
if queries_contain_keywords:
    use_hybrid = True  # Module 5
else:
    use_hybrid = False

# Metadata filtering
if need_contextual_filtering:
    use_metadata = True  # Module 6
else:
    use_metadata = False

Step 3: Estimate cost and performance

# Monthly cost estimate
embeddings_cost = (docs * avg_tokens_per_doc / 1000) * 0.0001
queries_cost = (queries_per_month * avg_tokens_per_query / 1000) * 0.0001
vector_db_cost = 70 if use_pinecone else 0
reranking_cost = queries_per_month * 0.001 if use_reranking else 0

total_monthly_cost = embeddings_cost + queries_cost + vector_db_cost + reranking_cost

# Performance estimate
baseline_latency = 200  # Retrieval + generation
reranking_latency = 200 if use_reranking else 0
total_latency = baseline_latency + reranking_latency

print(f"Estimated monthly cost: ${total_monthly_cost:.2f}")
print(f"Expected latency: {total_latency}ms")

🎯 Summary

Key concepts:

  • Chunking: Fixed (simple) vs Semantic (quality) vs Recursive (balance) - Module 2 goes deeper
  • Embeddings: OpenAI (quality) vs Local (free) vs Cohere (multilingual) - Module 1 uses OpenAI
  • Vector DB: ChromaDB (dev) vs Pinecone (production) - Modules 1-6 use Chroma, Module 7 migrates to Pinecone
  • Re-ranking: Add it if >80% precision is critical - Module 4 goes deeper
  • Decision framework: Requirements → Components → Cost/Performance → Decision
  • Trade-offs: Everything has one (cost vs quality, latency vs precision, simplicity vs features)

What's next:

Capsule 04 teaches you success metrics: how to define what "good" means in RAG (latency targets, accuracy targets, cost budgets), and how to measure your system with objective metrics.


📚 Additional resources

  1. Vector Database Comparison - A detailed comparison of vector DBs
  2. Embedding Models Benchmark - MTEB leaderboard (the official benchmark)
  3. ChromaDB vs Pinecone - When to migrate to a managed service
  4. Chunking Strategies Overview - An analysis of chunk size
  5. OpenAI vs Cohere Embeddings - A practical comparison
  6. RAG Architecture Patterns - LangChain patterns

Created: February 6, 2026
Version: 1.0