/writing/rag-pipeline.md
Designing Production-Grade RAG Systems: Beyond Vector Search
Most tutorials stop at embeddings and a vector database. Real systems don't.
The standard RAG tutorial shows you a clean five-step pipeline: query → embed → search → retrieve → generate. It works in a notebook. It collapses under load.
This is the article about what comes after that. Building a RAG system that handles millions of requests, stays reliable when individual components fail, costs a fraction of naive implementations, and gives engineers the observability to actually debug it — that's a distributed systems problem, not an AI tutorial.
1. Why Naive RAG Breaks in Production
The hello-world RAG pipeline has four failure modes that only show up at scale:
Hallucination — When retrieved context is ambiguous or incomplete, LLMs fill the gap confidently with invented information. Naive systems have no grounding enforcement.
Retrieval failure — Pure vector search fails on precision queries — error codes, product SKUs, specific identifiers. Semantic similarity is the wrong tool for exact-match recall.
Latency & cost — Embedding every query, searching naively, and sending oversized context to a frontier LLM is expensive and slow. No production system can afford it at scale.
Document quality — Garbage in, garbage out — but in production, garbage is continuous. Without structured ingestion, stale and low-quality content corrupts the entire knowledge base.
The goal of this article is a scalable, observable, and reliable RAG architecture capable of serving millions of requests.
2. High-Level System Design
A production RAG system has two distinct planes: an offline ingestion plane that continuously processes documents, and an online query plane that serves requests in real time.
flowchart TB
subgraph ingest["INGESTION PLANE · offline"]
direction TB
DS["Data Sources"] --> IP["Ingestion Pipeline"]
IP --> CS["Chunking Service"]
CS --> ES["Embedding Service"]
ES --> VDB["Vector Database"]
end
subgraph query["QUERY PLANE · online"]
direction TB
UQ["User Query"] --> QP["Query Processor"]
QP --> HS["Hybrid Search"]
HS --> RR["Reranker"]
RR --> CB["Context Builder"]
CB --> LLM["LLM"]
LLM --> RESP["Response"]
end
VDB <--> CBEach component in both planes scales independently, fails gracefully, and is observable. The rest of this article walks through each one.
3. Document Ingestion Pipeline
Most people completely ignore the ingestion layer. That's a mistake — it's where data quality is decided, and data quality determines everything downstream.
Production systems don't ingest documents once. They ingest continuously, from heterogeneous sources:
- Confluence / Notion (wikis)
- PDFs (internal documentation)
- Websites (crawled content)
- Databases (structured records)
- Slack (conversations and threads)
- GitHub (code and READMEs)
- Zendesk (support tickets)
The standard pattern uses a message queue — Kafka is common — to decouple producers from processing workers. Each source publishes document events; parser workers consume them independently and push clean text downstream.
flowchart LR
CON["Confluence"] --> K["Kafka"]
K --> PW["Parser Workers"]
PW --> CS["Chunking Service"]
PW <--> DLQ["Dead Letter Queue<br/>failed parses"]Design principle: Decoupling ingestion from processing means a spike in Confluence edits doesn't stall the embedding service. Workers scale independently. Failed parses don't block the queue.
4. Document Processing & Chunking Strategy
How you split documents determines retrieval quality more than which embedding model you choose.
Fixed-size chunking (the naive approach)
Simple: split every document into N tokens with some overlap.
chunk_size = 500
overlap = 50
The problem is that this shreds meaning arbitrarily. A paragraph explaining an architectural decision gets split mid-sentence. The retrieved chunk is syntactically complete but semantically useless.
Semantic chunking (what production systems use)
Split on structural signals — paragraph breaks, headings, section boundaries, list items. The chunk boundary matches a natural boundary in the document's meaning. Longer sections get split by sub-headings; short paragraphs may be merged.
Production systems prefer semantic chunking because:
- Retrieved chunks are self-contained and meaningful
- Less context loss at boundaries
- Better LLM comprehension of retrieved content
Metadata enrichment
Every chunk gets metadata attached before indexing. This enables access control, filtering, and freshness-based ranking later:
{
"source": "Confluence",
"team": "Engineering",
"doc_id": "ENG-4821",
"last_updated": "2026-05-01",
"access_level": "internal"
}
This metadata becomes critical during filtered retrieval and access control.
5. Embedding Pipeline
The embedding pipeline converts text chunks into dense vectors. The architectural concerns are throughput, reliability, and eventual consistency with the vector database.
flowchart TB
CS["Chunking Service"] --> EQ["Embedding Queue"]
EQ --> EM["Embedding Model<br/>OpenAI / BGE / E5"]
EM --> VW["Vector DB Write"]
EQ -. retry + backoff .-> EQProduction systems batch chunks before calling embedding APIs. Sending one chunk at a time is 10–50× more expensive and slower than batching 64 or 256 at once.
Re-indexing is a first-class concern. Documents change. When a Confluence page is updated, the system needs to detect it, delete the old chunks, and re-embed. Chunking with deterministic IDs — a hash of source + content — makes idempotent upserts straightforward.
Common embedding model choices
| Model | Type | Dimensions | Notes |
|---|---|---|---|
text-embedding-3-large |
Managed | 3072 | OpenAI, supports MRL truncation |
BGE-M3 |
Open-source | 1024 | Multilingual, strong performance |
E5-mistral-7b |
Open-source | 4096 | High accuracy, GPU-heavy |
6. Vector Database Design
The vector database is the retrieval engine. The main candidates each make different trade-offs:
| Database | Type | Strengths |
|---|---|---|
| Pinecone | Managed | Zero ops, simple scaling |
| Qdrant | Self-hosted | Fast (Rust), flexible filtering |
| Weaviate | Self-hosted | Hybrid search built-in |
| Milvus | Self-hosted | Massive scale, GPU acceleration |
Approximate Nearest Neighbor (ANN) Search
Comparing a query vector against every stored vector — exact KNN — is O(n) and unacceptably slow at millions of documents. ANN trades a tiny accuracy loss for sub-millisecond retrieval.
The dominant index structure is HNSW (Hierarchical Navigable Small World), a graph-based approach that achieves very high recall with low latency. At extreme scale, PQ (Product Quantization) compresses vectors to reduce memory footprint by 4–16×.
Instead of: compare query against every vector O(n)
Use: HNSW graph traversal O(log n)
7. Query Processing Layer
Before a user's query reaches the vector database, a processing layer improves it. Raw user queries are often ambiguous, terse, or phrased in ways that don't match indexed content.
Query rewriting
The original query is rephrased for better retrieval.
Input: "How do I deploy k8s?"
Output: "Kubernetes cluster deployment guide step by step"
More terms, clearer intent, better surface area for embedding similarity.
Query expansion
Related queries are generated and searched in parallel, then results are merged.
User query: "kubectl apply"
Generated:
→ "kubectl apply -f deployment.yaml"
→ "kubernetes deployment manifest"
→ "k8s apply command flags"
This costs more — multiple embedding calls, multiple searches — but recall improvement is significant for ambiguous queries.
8. Hybrid Search: BM25 + Vector
Vector search is powerful, but it fails on precision queries. Ask for error code 0x80070005 and the embedding model may return thematically related results while missing the exact document. Keyword search finds it immediately.
Production RAG systems run both in parallel and merge results:
flowchart TB
Q["Query"] --> BM["BM25 / Keyword"]
Q --> VEC["Vector / ANN"]
BM --> BMK["Top-K results"]
VEC --> VECK["Top-K results"]
BMK --> RRF["Reciprocal Rank Fusion · RRF"]
VECK --> RRF
RRF --> MERGED["Merged candidate set"]The merge step uses Reciprocal Rank Fusion (RRF) — a formula that combines rank positions from both systems without needing score normalization. The combined candidate set then passes to the reranker.
Why hybrid? Exact keywords matter. Error codes, product names, version numbers, and identifiers are retrieved reliably by BM25. Conceptual and semantic queries are retrieved reliably by vector search. Neither alone covers the full query space.
9. The Reranking Layer
Vector retrieval is optimized for speed — it approximates similarity at scale. A reranker is optimized for quality — it deeply compares query against document pairs to find the truly best matches.
The two stages serve different purposes:
| Stage | Model type | Purpose | Speed |
|---|---|---|---|
| Retriever | Bi-encoder (ANN) | Recall — find all relevant candidates | Fast (ms) |
| Reranker | Cross-encoder | Precision — find the best 10 | Slower (100ms) |
flowchart TB
C["100 candidate chunks<br/>from hybrid search"] --> CE["Cross-encoder model<br/>sees query + document together"]
CE --> T10["Top 10 chunks"]
T10 --> CB["Context builder"]Cross-encoders are slower — they can't pre-compute embeddings — but operating on only 100 candidates keeps latency acceptable. The quality improvement in final answer accuracy is large enough that production systems almost universally include this layer.
10. Context Construction
The reranker returns the best chunks. The context builder turns them into what actually gets sent to the LLM — and this step has more nuance than it looks.
LLMs have finite context windows and finite attention. Longer context is not always better. Production systems apply three steps:
Deduplication — Chunks from the same source document are deduplicated. Near-duplicate content wastes tokens and can confuse the model.
Token budgeting — A hard limit is set for context (e.g., 15k tokens), and chunks are included in ranked order until the budget is exhausted.
Source citation metadata — Each chunk carries its source, so the LLM can produce citations and the system can later verify groundedness.
flowchart TB
C["100 chunks retrieved"] --> DEDUP["Deduplicate<br/>remove near-duplicates"]
DEDUP --> RANK["Rank by reranker score"]
RANK --> BUDGET["Budget to 15k tokens<br/>10 chunks selected"]
BUDGET --> FMT["Format with source metadata"]
FMT --> PROMPT["LLM prompt"]System prompt grounding:
Answer only using information from the provided context.
If the answer cannot be found in the context, say "I don't know."
Cite sources inline using [source_id] notation.
11. Caching Strategy
Caching is where RAG systems reclaim most of their cost. At scale, a large fraction of queries are repeated or near-repeated. Three cache layers target different points in the pipeline:
Embedding cache
- Same normalized query string → reuse the embedding vector
- Key:
hash(normalize(query)) - Invalidation: query strings rarely change; TTL of hours is safe
Retrieval cache
- Same query embedding → reuse the retrieved chunk set
- Key:
hash(embedding_vector) - Invalidation: triggered by document updates in the namespace
Response cache
- Identical questions → serve the cached LLM response directly
- Largest cost saving — skips embedding, retrieval, reranking, and LLM call
- Invalidation: time-based TTL or on document change events
Semantic cache (advanced)
Embed the query and check for sufficiently similar cached queries (cosine similarity > 0.95). Catches near-duplicate phrasings of the same question.
Impact: Companies report 50–90% cost reduction with aggressive caching strategies at scale.
12. Observability & Monitoring
A RAG system without observability is a black box. You can't improve what you can't measure, and debugging retrieval failures without traces is nearly impossible.
Retrieval metrics
| Metric | What it measures |
|---|---|
| Recall@K | Are the relevant documents in the top K results? |
| Precision@K | Of the top K results, how many are actually relevant? |
| MRR | Mean Reciprocal Rank — how high does the first relevant result appear? |
| NDCG | Normalized Discounted Cumulative Gain — ranked quality of results |
LLM quality metrics
| Metric | What it measures |
|---|---|
| Hallucination rate | Fraction of claims not grounded in retrieved context |
| Groundedness score | Overlap between response and retrieved context |
| Latency p50/p99 | Tail latency for end-to-end query |
Business metrics
| Metric | What it measures |
|---|---|
| User satisfaction | Thumbs up/down, ratings |
| Answer acceptance rate | Did the user act on the answer? |
| Escalation rate | Queries that required human follow-up |
Distributed traces — one per query, spanning every stage from query processing to LLM response — are essential. When a user gets a bad answer, you need to see exactly which chunks were retrieved, how they were scored, and what context was sent to the model.
13. Failure Handling
Individual components will fail. A production system degrades gracefully rather than returning errors.
| Failure | Fallback |
|---|---|
| Vector DB down | Fall back to BM25 keyword search |
| Embedding service failure | Queue writes, serve from cache |
| Primary LLM unavailable | GPT → Claude → smaller local model |
| Reranker timeout | Skip reranking, use raw retrieval scores |
| Hybrid search degraded | Vector-only search |
Circuit breakers prevent cascade failures. If the primary LLM starts timing out, the circuit breaker opens and routes traffic to the fallback model before the queue depth grows and the problem compounds.
flowchart TB
A["LLM primary timeout rate > threshold"] --> B["Circuit breaker opens"]
B --> C["Traffic routes to fallback model"]
C --> D["Half-open probe after 30s"]
D --> E["Resume primary if healthy"]14. Security & Access Control
Enterprise RAG has a non-negotiable requirement: users should only retrieve documents they're authorized to see. An HR employee must not receive Engineering-internal documentation in their context window, even if it's semantically relevant.
The implementation uses metadata filtering applied before or during retrieval — not after. Post-retrieval filtering is a security vulnerability: the model may have already processed unauthorized context.
query_with_filter = {
"query_vector": embed(user_query),
"filter": {
"department": user.department,
"access_level": { "$lte": user.clearance }
}
}
This filter runs at the vector database level. Unauthorized documents are never retrieved, never ranked, never sent to the LLM.
Access control is enforced in the retrieval layer, not the application layer.
15. Cost Optimization
Naive RAG is expensive. A production system applies several layers of cost reduction:
Smaller embeddings
Matryoshka embeddings (MRL) allow truncating vectors from 1536 to 256 dimensions with minimal accuracy loss — 6× less storage, 6× faster ANN search.
Context compression
Before sending chunks to the LLM, a smaller model extracts only the relevant sentences. Sending 2k tokens instead of 15k reduces LLM cost by 7×.
flowchart TB
A["15,000 tokens of retrieved context"] --> B["Small reranker / extractor model"]
B --> C["2,000 tokens of compressed context"]
C --> D["LLM"]Tiered model routing
Simple queries — FAQ, factual lookups — are routed to smaller, cheaper models. Only complex, multi-hop questions reach frontier models.
Batch embedding jobs
New documents are accumulated and embedded in overnight batch jobs rather than in real time. Reduces API costs significantly for ingestion-heavy systems.
Combined impact: Companies implementing all four strategies report 50–90% reduction in per-query cost compared to naive implementations.
16. Scaling Architecture
Each component in the query plane scales independently. The load balancer distributes traffic across query service replicas; the retriever, reranker, and context builder are separate services that scale based on their own bottlenecks.
flowchart TB
LB["Load Balancer"] --> QS["Query Service<br/>×N replicas"]
QS --> RET["Retriever"]
QS --> RR["Reranker"]
QS --> CB["Context Builder"]
RET --> VDB["Vector DB Cluster<br/>sharded + replicated"]Vector database scaling
- Sharding by namespace — each shard owns a subset of the vector space (e.g., by department, by data source)
- Replication (3×) — handles read throughput and node failure
- Distributed ANN — search fans out to all shards, results merge via RRF
Query service scaling
- Stateless — any replica can handle any request
- Horizontal autoscaling on query latency p99
- Request coalescing for identical in-flight queries (deduplication before hitting retrieval)
Complete Production Architecture
Putting it all together:
flowchart TB
subgraph ingest["INGESTION PLANE"]
direction TB
DS["Data Sources<br/>Confluence, PDFs, Slack, ..."] --> K["Kafka"]
K --> IW["Ingestion Workers<br/>parse, clean"]
IW --> CS["Chunking Service<br/>semantic chunking + metadata"]
CS --> EQ["Embedding Queue"]
EQ --> ESV["Embedding Service"]
ESV --> VDB["Vector DB"]
end
subgraph query["QUERY PLANE"]
direction TB
UQ["User Query"] --> AG["API Gateway<br/>auth, rate limiting"]
AG --> QP["Query Processor<br/>rewrite + expand"]
QP --> CL["Cache Layer<br/>embedding / retrieval / response"]
CL --> HS["Hybrid Search<br/>BM25 + ANN, filtered by metadata"]
HS --> RR["Reranker<br/>cross-encoder, top 100 → top 10"]
RR --> CB["Context Builder<br/>deduplicate, budget, format"]
CB --> LLM["LLM<br/>grounded, with citations"]
LLM --> RESP["Response"]
end
VDB --> HSWhat Makes This Different from Naive RAG
Most RAG implementations cover:
- Embeddings
- Vector database
- LLM generation
A production system covers all of that, plus:
| Layer | What it solves |
|---|---|
| Ingestion pipeline | Continuous, multi-source data quality |
| Semantic chunking | Meaningful retrieval units |
| Hybrid search | Precision + semantic recall |
| Reranking | Answer quality at scale |
| Caching | 50–90% cost reduction |
| Observability | Debuggability and improvement loops |
| Failure recovery | Reliability under component failure |
| Access control | Enterprise security requirements |
| Cost optimization | Sustainable economics at scale |
| Horizontal scaling | Millions of requests |
That transforms it from an AI tutorial into a true distributed systems architecture — which is the kind of system that's actually worth building.
Start with retrieval quality. Measure it. Then add the layers that protect it at scale.