01

Requirements & scope

Problem Statement & Requirements

Design a production Retrieval-Augmented Generation system that ingests enterprise knowledge bases (documents, wikis, databases), indexes them for semantic search, retrieves relevant context at query time, and augments LLM prompts to generate accurate, grounded answers with source citations — similar to systems powering Perplexity AI, ChatGPT with search, enterprise Q&A bots (Glean, Guru), and custom knowledge assistants.

Functional Requirements

  • Document ingestion — ingest documents from diverse sources: PDFs, web pages, Confluence, Notion, Slack, Google Drive, databases, APIs
  • Chunking & embedding — split documents into semantic chunks, generate vector embeddings for each chunk
  • Vector search (retrieval) — given a user query, find the most relevant document chunks via semantic similarity
  • Hybrid search — combine vector (semantic) search with keyword (BM25) search for better recall
  • LLM generation with context — send retrieved chunks as context to an LLM to generate a grounded answer
  • Source citations — every claim in the answer links back to the source document and chunk
  • Conversational memory — multi-turn conversations with context carried across turns
  • Access control — users only see results from documents they have permission to access
  • Incremental sync — documents update automatically when source changes (near real-time)
  • Evaluation & feedback — track answer quality; thumbs up/down; use feedback for improvement

Non-Functional Requirements

  • Answer quality — relevant, accurate, no hallucination; grounded in retrieved sources
  • Low latency — retrieval + generation end-to-end < 3 seconds for simple queries
  • Scalability — support 100M+ document chunks, 10K+ concurrent queries
  • Freshness — new/updated documents searchable within minutes of change
  • Multi-tenancy — thousands of organizations with isolated knowledge bases
  • Cost efficiency — minimize embedding compute and LLM token usage

Out of Scope

  • LLM training / fine-tuning
  • General web search (Perplexity-scale crawling)
  • Image/video understanding in documents
  • Full AI agent orchestration framework
02

Scale estimations

Scale Estimations

Traffic

Metric Value
Organizations (tenants) 10K
Total source documents 500M
Total chunks (after splitting) 5B
Embedding dimensions 1536 (OpenAI) or 1024 (open-source)
Queries / second 5K RPS
Peak queries / second 15K RPS
Documents ingested / day (new + updated) 10M
Chunks processed / day 100M

Storage

Metric Value
Average chunk size (text) 500 tokens ≈ 2 KB
Total chunk text storage 5B × 2 KB = ~10 TB
Vector size per chunk (1536 × float32) 6 KB
Total vector storage 5B × 6 KB = ~30 TB
Metadata per chunk (source, title, permissions, timestamps) 500 bytes
Total metadata 5B × 500 B = ~2.5 TB
BM25 inverted index ~5 TB
Total storage ~50 TB

Compute

Metric Value
Embedding compute (ingestion: 100M chunks/day) 100M × 0.1 ms/chunk = ~2.8 GPU-hours/day
Embedding compute (query: 5K/s) 5K × 0.5 ms = ~2.5 GPU continuously
LLM generation (5K/s × ~500 tokens output) 2.5M tokens/sec (significant GPU fleet)
Vector search (5K/s, top-20 across 5B vectors) ~50 HNSW index nodes
Reranker (5K/s × 20 candidates) 100K rerank operations/sec

Bandwidth

Metric Value
Query response (5K/s × 5 KB avg) ~25 MB/s
Embedding ingestion pipeline ~50 MB/s
Vector index replication ~30 MB/s
03

Layered architecture

High-Level Architecture

High-Level ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore

Ingestion Pipeline

Ingestion PipelineExcalidraw diagram · editable shapes · reveal step by stepExplore
04

API & contracts

API Design

Query (Chat with Knowledge Base)

EXAMPLE
POST /api/v1/query
Authorization: Bearer <token>

Request:
{
  "query": "How does our refund policy work for international orders?",
  "conversation_id": "conv-abc123",       // for multi-turn
  "knowledge_base_id": "kb-acme-corp",
  "filters": {
    "source_type": ["confluence", "notion"],
    "date_range": {"after": "2025-01-01"},
    "tags": ["policy", "customer-facing"]
  },
  "top_k": 5,                             // number of chunks to retrieve
  "stream": true,
  "include_sources": true
}

Response (SSE stream):
data: {"type": "retrieval_complete", "sources": [
  {"chunk_id": "chk-001", "source": "Refund Policy FAQ", "url": "https://confluence.acme.com/...", "relevance": 0.94},
  {"chunk_id": "chk-002", "source": "Terms of Service v3.2", "url": "...", "relevance": 0.89},
  {"chunk_id": "chk-003", "source": "International Shipping KB", "url": "...", "relevance": 0.85}
]}

data: {"type": "content_delta", "text": "Our refund policy for international orders "}
data: {"type": "content_delta", "text": "allows returns within 30 days of delivery "}
data: {"type": "content_delta", "text": "[1]. "}
data: {"type": "content_delta", "text": "For international shipments, the return shipping "}
data: {"type": "content_delta", "text": "cost is borne by the customer unless the item was "}
data: {"type": "content_delta", "text": "defective [2]. "}
...
data: {"type": "done", "usage": {"retrieval_chunks": 5, "prompt_tokens": 1842, "completion_tokens": 287}}

Ingest Documents

EXAMPLE
POST /api/v1/knowledge-bases/{kb_id}/documents
Authorization: Bearer <token>

Request:
{
  "source": {
    "type": "confluence",
    "space_key": "ENG",
    "page_ids": ["12345", "67890"],       // specific pages, or null for full space
    "sync_mode": "incremental"            // full | incremental
  },
  "processing": {
    "chunking_strategy": "semantic",       // semantic | fixed | recursive
    "chunk_size_tokens": 512,
    "chunk_overlap_tokens": 50,
    "embedding_model": "text-embedding-3-large"
  },
  "access_control": {
    "inherit_source_permissions": true     // mirror Confluence page permissions
  }
}

Response (202 Accepted):
{
  "ingestion_job_id": "job-xyz789",
  "status": "processing",
  "documents_queued": 2,
  "estimated_chunks": 150
}

Get Ingestion Status

EXAMPLE
GET /api/v1/ingestion-jobs/{job_id}

Response (200 OK):
{
  "job_id": "job-xyz789",
  "status": "completed",
  "documents_processed": 2,
  "chunks_created": 147,
  "chunks_updated": 12,
  "chunks_deleted": 3,
  "errors": [],
  "completed_at": "2026-04-03T10:05:00Z"
}

Feedback

EXAMPLE
POST /api/v1/feedback
{
  "query_id": "q-abc123",
  "rating": "thumbs_up",               // thumbs_up | thumbs_down
  "feedback_text": "Answer was accurate but missed the EU exception",
  "correct_source": "chk-007"          // optional: which chunk should have been retrieved
}
05

Data model

Data Model

Document

DocumentExcalidraw diagram · editable shapes · reveal step by stepExplore

Chunk

ChunkExcalidraw diagram · editable shapes · reveal step by stepExplore

Conversation (Multi-Turn)

Conversation (Multi-Turn)Excalidraw diagram · editable shapes · reveal step by stepExplore
06

Core design decisions

Core Design Decisions

Decision 1: Chunking Strategy — How to Split Documents

The most impactful decision for retrieval quality. Bad chunking = bad retrieval = bad answers.

Decision 1: Chunking Strategy — How to Split DocumentsExcalidraw diagram · editable shapes · reveal step by stepExplore

Decision 2: Hybrid Retrieval (Vector + Keyword)

Decision 2: Hybrid Retrieval (Vector + Keyword)Excalidraw diagram · editable shapes · reveal step by stepExplore

Decision 3: Reranking — The Quality Amplifier

Decision 3: Reranking — The Quality AmplifierExcalidraw diagram · editable shapes · reveal step by stepExplore
07

Request flows

Detailed Flow Diagrams

Query Flow (End-to-End)

Query Flow (End-to-End)Excalidraw diagram · editable shapes · reveal step by stepExplore

Document Ingestion Flow

Document Ingestion FlowExcalidraw diagram · editable shapes · reveal step by stepExplore
08

Performance & caching

Workshop note · added for the website’s common reading format

Start with the dominant access pattern of RAG Pipeline. Document ingestion is one place to inspect capacity and tail latency. Measure before introducing a cache: define the cache key, invalidation policy, stale-data budget, and cold-start behavior.

Batch independent work where the latency budget allows it. Bound queues and concurrency, and verify that an optimization does not move the bottleneck to a dependency.

09

Advanced design

Vector Database Deep Dive

HNSW Index (Hierarchical Navigable Small World)

HNSW Index (Hierarchical Navigable Small World)Excalidraw diagram · editable shapes · reveal step by stepExplore

Scaling Vector Search to 5B Vectors

Scaling Vector Search to 5B VectorsExcalidraw diagram · editable shapes · reveal step by stepExplore

Advanced Retrieval Techniques

Query Expansion / Rewriting

Query Expansion / RewritingExcalidraw diagram · editable shapes · reveal step by stepExplore

Parent-Child Chunk Retrieval

Parent-Child Chunk RetrievalExcalidraw diagram · editable shapes · reveal step by stepExplore
10

Edge cases

Handling Edge Cases

Hallucination Prevention

EXAMPLE
The #1 risk in RAG: LLM generates plausible but wrong information.

Multi-layer defense:

  Layer 1: Grounding Prompt
    System: "Answer ONLY based on the provided context.
    If the context doesn't contain the answer, say
    'I don't have enough information to answer this.'
    NEVER make up facts not in the context."

  Layer 2: Citation Enforcement
    System: "Every factual claim must include [N] citing
    the source chunk number. If you cannot cite a source,
    do not include the claim."

  Layer 3: Post-Generation Verification
    After LLM generates answer:
    → Extract all factual claims
    → For each claim: verify it exists in the retrieved chunks
    → Flag unsupported claims → remove or mark as "unverified"
    Latency cost: ~200-500 ms (can be async)

  Layer 4: Confidence Scoring
    If retrieval scores are all low (top chunk < 0.5 relevance):
    → Don't attempt to answer
    → "I couldn't find relevant information. Try rephrasing
       your question or checking [suggested sources]."

  Layer 5: User Feedback Loop
    Thumbs down → flag for review
    → Identify: was it a retrieval failure or generation failure?
    → Feed back into chunking/embedding improvement

Stale / Outdated Information

EXAMPLE
Problem: Policy changed yesterday but old version still in index

Solution: Near-real-time incremental sync

  1. Source connectors poll for changes every 5 minutes
     (or webhook-based for supported sources)
  2. Changed documents: re-chunk + re-embed ONLY changed chunks
     (content_hash comparison → skip unchanged chunks)
  3. Deleted documents: remove all chunks from vector + BM25 index
  4. Freshness metadata:
     → Each chunk carries source_last_modified timestamp
     → Retrieval can boost recent chunks: score × recency_boost
     → LLM prompt: "Note: [Source 2] was last updated 2 hours ago"

  For critical freshness:
  → Pre-retrieval filter: date_range.after = "7 days ago"
  → Or: always include most-recent version of each source document

Access Control at Scale

EXAMPLE
Problem: User queries KB but should only see documents they can access

  Enterprise scenario:
  Alice (engineering) can see: eng-docs, all-company, public
  Bob (sales) can see: sales-docs, all-company, public
  
  Implementation:

  Option A: Post-Retrieval Filtering (simple but wasteful)
    Retrieve top-100 from index (ignoring ACL)
    Filter out chunks user can't access
    Return remaining top-5
    Problem: if top 95 are restricted, return only 5 of low quality

  Option B: Pre-Retrieval Filtering (recommended)
    Each chunk has access_groups: ["engineering", "all-company"]
    At query time: filter = user's groups
    Vector search: WHERE access_groups INTERSECT user_groups
    → Only search within permitted chunks
    → Correct results, no wasted retrieval

  Option C: Separate Indexes per Access Level
    Index per permission group
    Query: search ONLY indexes user has access to
    + Fastest (no filter overhead)
    - More indexes to maintain
    - Shared documents duplicated across indexes

  Recommendation: Pre-retrieval filtering (Option B) via metadata filter
  Most vector DBs support filtered search natively (Qdrant, Pinecone, Weaviate)

Multi-Turn Conversation Context

EXAMPLE
Problem: Turn 2 query "What about for EU?" is meaningless without Turn 1

Solution: Query Contextualization via LLM

  Conversation:
  Turn 1: "How does our refund policy work?"
  Turn 2: "What about for EU customers?"

  Before retrieval, rewrite Turn 2 using conversation context:

  Prompt to small LLM (fast, cheap):
  "Given this conversation, rewrite the latest query to be standalone:
   User: How does our refund policy work?
   Assistant: [previous answer...]
   User: What about for EU customers?
   Rewritten: What is the refund policy specifically for EU customers?"

  Now search with: "What is the refund policy specifically for EU customers?"
  → Much better retrieval than searching "What about for EU?"

  Cost: ~50 tokens input, ~20 tokens output = negligible
  Latency: ~100 ms (use small model like Haiku)
11

Tradeoffs

Tradeoffs & Design Decisions Summary

Decision Option A Option B Chosen Why
Chunking Fixed-size (512 tokens) Semantic/structure-aware Semantic Respects document structure; heading context dramatically improves retrieval
Search Pure vector search Hybrid (vector + BM25) Hybrid + RRF fusion +10-30% recall; handles exact terms and semantic similarity
Reranking Skip (use retrieval scores) Cross-encoder reranker Cross-encoder Dramatically improves precision; 20 pairs × 5 ms = 100 ms acceptable
Embedding model OpenAI text-embedding-3 Open-source (BGE/E5) Depends on requirements OpenAI: higher quality, API cost; open-source: self-hosted, no data leaving org
Vector DB pgvector (simple) Qdrant/Milvus (dedicated) Dedicated for >10M vectors pgvector for small scale; Qdrant/Milvus for 5B vectors with HNSW at scale
Vector sharding Hash-based (uniform) Tenant-based Tenant-based No cross-shard queries; natural isolation; query stays within one tenant's data
Query expansion Raw query only HyDE + multi-query HyDE for complex, raw for simple HyDE adds 500 ms + LLM cost; worth it for complex queries, skip for simple ones
Chunk size 256 tokens 512 tokens 256-512 with parent-child Small for precise retrieval; parent chunk returned for richer LLM context
Access control Post-retrieval filter Pre-retrieval (metadata filter) Pre-retrieval Correct results guaranteed; no wasted retrieval on restricted chunks
Freshness Full re-index daily Incremental (content hash) Incremental Re-embed only changed chunks; saves 90%+ compute on updates
12

Reliability & fault tolerance

Reliability & Fault Tolerance

Single Points of Failure & Mitigations

Single Points of Failure & MitigationsExcalidraw diagram · editable shapes · reveal step by stepExplore

Graceful Degradation

EXAMPLE
Tier 1 (Reranker down):
  → Skip reranking step; use fusion scores directly
  → Slightly lower quality but functional
  → Latency actually decreases by ~100 ms

Tier 2 (BM25 index down):
  → Pure vector search only (no hybrid)
  → Exact term matches may be missed
  → Retrieval still works semantically

Tier 3 (Embedding service degraded):
  → Cache frequent query embeddings (Redis, TTL=1 hour)
  → Queue new document ingestion (process when recovered)
  → Existing indexed content still searchable

Tier 4 (Primary LLM down):
  → Failover to secondary LLM provider
  → Or: return retrieved chunks directly WITHOUT generation
    → "Here are relevant sources:" + chunk summaries
  → User gets information, just not synthesized

Tier 5 (Vector DB down):
  → Fall back to BM25 only (keyword search)
  → Significantly degraded but still useful
  → Rebuild vector index from stored chunks when recovered
13

Production architecture

Full System Architecture (Production-Grade)

Full System Architecture (Production-Grade)Excalidraw diagram · editable shapes · reveal step by stepExplore
14

Further exploration

Evaluation & Continuous Improvement

Evaluation & Continuous ImprovementExcalidraw diagram · editable shapes · reveal step by stepExplore
15

Interview playbook

Interview Tips

  1. Start with the retrieval-generation separation — "RAG solves the knowledge freshness problem. LLMs have a training cutoff; RAG gives them access to current, private data without retraining. The architecture has two pipelines: ingestion (document → chunks → embeddings → vector index) and query (embed query → retrieve chunks → rerank → generate with context)."

  2. Chunking strategy is the #1 quality lever — "Chunk quality determines retrieval quality, which determines answer quality. Semantic chunking with heading context preserves document structure. A chunk about 'EU refund policy' prepended with 'Refund Policy > International > EU-Specific Rules' retrieves far better than a context-free 512-token block."

  3. Hybrid search (vector + BM25) is essential — Don't say "just use embeddings." Explain: vector search handles semantic similarity ("return policy" ↔ "refund rules"), BM25 handles exact terms ("ERR-4231"). RRF fusion combines them. +10-30% recall improvement. This is the single biggest retrieval improvement.

  4. Two-stage retrieval: recall then precision — Stage 1: fast approximate search (HNSW) returns top-50 candidates. Stage 2: cross-encoder reranker scores each (query, chunk) pair with full token-level attention. Top-5 after reranking are dramatically better than top-5 from retrieval alone.

  5. Hallucination prevention is multi-layered — Grounding prompt ("only use provided context"), citation enforcement (every claim must cite [N]), post-generation verification (check each claim against sources), confidence threshold (low retrieval scores → "I don't know"). No single layer is sufficient.

  6. Access control must be pre-retrieval, not post — "If I retrieve top-100 and filter to authorized-only, I might return poor-quality results. Pre-retrieval filtering (metadata filter in vector DB) ensures only authorized chunks are even considered. Most vector DBs support filtered HNSW natively."

  7. Incremental ingestion saves 90%+ compute — "Hash each chunk's content. On document update, only re-embed chunks where hash changed. A policy doc with 20 chunks where 2 paragraphs changed = re-embed 2 chunks, not 20. At 100M chunks/day, this matters enormously."

  8. Multi-turn requires query rewriting — "'What about for EU?' is meaningless without Turn 1. Use a small fast LLM to rewrite: 'What is the refund policy specifically for EU customers?' Then search with the rewritten query. ~100 ms cost, massive quality improvement."

  9. Parent-child chunking solves the precision-context tradeoff — "Small chunks (256 tokens) for precise retrieval. But return the parent chunk (1024 tokens) to the LLM for richer context. Best of both worlds: precise recall + sufficient context for generation."

  10. End with evaluation metrics — "RAG quality is measurable: retrieval recall@5, MRR, NDCG for retrieval; faithfulness, relevance, hallucination rate for generation; thumbs up/down ratio end-to-end. Nightly automated eval on a test suite of 500 queries catches regressions. Without evaluation, you're flying blind."