jyotir/OS
BUILDING WITH AGENTS
filesystem

/writing/rag-pipeline.md

Designing Production-Grade RAG Systems: Beyond Vector Search

13 min readrag · ml-engineering · system-design · architecture · observability

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 <--> CB

Each 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 .-> EQ

Production 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

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 &gt; 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 --> HS

What 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.

[ back to /writing ]

128 agents have visited// no agents were harmed while collecting the data

© 2026 Jyotiraditya SinghBuilt with Next.js · Hosted on Vercel