July 25, 202615 min read

Demystifying RAG: From Document Ingestion to Production-Grade Retrieval

Retrieval-Augmented Generation (RAG) has transitioned from an experimental pattern to the industry standard for grounding Large Language Models (LLMs) in proprietary data. But there is a huge gulf between what developers build in tutorials and what is required to run RAG in production.

What is Naive RAG?

When most developers get started, they implement what is known as Naive RAG. The mental model is straightforward:

Documents → Embeddings
Vector DB Store
Semantic Search
LLM Generation

In this basic setup, you parse documents, convert chunks to vectors, store them, search them using the user's raw query, and pass the results to the LLM. It works great for a demo with 10 documents. But when you deploy this to production with 10,000 documents and real-world users, it falls apart.

The Trade-offs and Failure Modes of Naive RAG

  • Low Precision (Garbage Context): Direct vector search often retrieves chunks that share semantic similarity but are irrelevant to the user's actual question, leading to noisy prompts.
  • Low Recall (Missing Context): Phrasing differences between the query and documents cause the database to miss critical paragraphs entirely.
  • Hallucinations: Incomplete or irrelevant context forces the LLM to speculate or rely on its training weights, producing confident but inaccurate answers.
  • Exact Match Failure: Vector search cannot match precise keywords, product SKUs, serial numbers, or error codes (e.g., ERR-1002).
  • Inefficiency and Cost: Feeding raw, redundant, or bloated chunks to the LLM consumes excessive context window space, driving up API token costs and increasing latency.

Moving to Production RAG: Modularity Over Dogma

To solve these issues, production systems add specialized optimization layers. However, not every step in a production pipeline is compulsory. A production RAG system is a modular toolkit, not a rigid framework. Each product or feature has unique constraints: a chatbot needs sub-second speed, while a legal analysis tool needs absolute precision. Developers design their architectures selectively, choosing the optimizations that solve their specific bottlenecks while omitting steps that add unnecessary cost or latency.

Let's walk through the full blueprint of a modern RAG system, divided into the Offline Ingestion Pipeline and the Online Retrieval/Generation Loop.

Phase 1: The Ingestion & Vectorisation Pipeline (Offline)

Before a system can retrieve information, it must parse, clean, split, and index documents. This pipeline runs offline or asynchronously as new data is uploaded.

[Document Upload]
[Text Extraction] (PDF/DOCX Parser)
[Cleaning] (Noise/HTML Removal)
[Chunking] (Splitting Logic)
[Metadata Extraction] (Source/Tags)
[Embedding Generation] (Dense Vectors)
[ANN Index Creation] (HNSW/IVF)
[Store in Vector DB] (Qdrant/Pinecone)

1. Document Upload

The ingestion pipeline begins when a user uploads raw files (PDFs, DOCX, Markdown, HTML, or CSV). This step acts as the entry queue where ingestion jobs are scheduled, file integrity is verified, and mime-types are classified.

2. Text Extraction

Computers don't read PDFs; they read characters. This stage uses specialized parsing libraries (such as PyPDF, Unstructured, PDFPlumber, or OCR models like Tesseract/Surya for scanned pages) to extract raw text coordinates, tables, and layouts. High-fidelity parsers are crucial here because a failure to preserve column structures or table boundaries will corrupt downstream data.

3. Cleaning

Raw text extraction is noisy. It contains page headers, footers, page numbers, duplicate whitespace, boilerplate copyright text, and formatting artifacts (like hyphens splitting words across lines). The cleaning step applies regular expressions, HTML tag filters, and unicode normalization to strip out non-content noise, preventing it from polluting the embedding model.

4. Chunking

LLM context windows are finite, and embedding models work best on short, highly focused passages. We split the cleaned text into smaller segments called chunks. The split strategy matters immensely:

  • Fixed-Size Chunking: Splitting strictly by character or token count (e.g., 500 tokens) with a sliding window overlap (e.g., 50 tokens) to ensure sentences aren't cut in half.
  • Recursive Character Chunking: Splitting recursively by paragraphs, then sentences, and finally words until the target size is met, preserving semantic unity.
  • Semantic Chunking: Analyzing distance changes in embeddings between adjacent sentences to split documents where the topic shifts.
chunking.ts — Recursive Splitting Example
function chunkText(text: string, size = 500, overlap = 50): string[] {
  const words = text.split(/s+/);
  const chunks: string[] = [];
  let i = 0;

  while (i < words.length) {
    const chunkWords = words.slice(i, i + size);
    chunks.push(chunkWords.join(" "));
    i += size - overlap; // advance with overlap
  }

  return chunks;
}

5. Metadata Extraction

To allow fast filtering and provide context, each chunk is annotated with metadata. This includes structured attributes like source_file, page_number, creation_date, and category. We can also use lightweight LLMs to extract summaries, key entities, or target questions for each chunk and append them as metadata tags.

6. Embedding Generation

We pass the clean text chunks through an embedding model (such as OpenAI's text-embedding-3-small, Cohere's embed-english-v3.0, or local open-source models like bge-large-en-v1.5). The model transforms the textual chunk into a high-dimensional vector (e.g., 1536 float values) representing the semantic meaning of the text.

7. ANN Index Creation

Searching through millions of vectors linearly (exact search) is computationally expensive and introduces high latency. Vector databases build Approximate Nearest Neighbor (ANN) indexes to accelerate search:

  • HNSW (Hierarchical Navigable Small World): A multi-layer graph structure that allows skipping through cluster nodes to find similar vectors in logarithmic time. It is fast and has high recall, but uses a lot of memory.
  • IVF (Inverted File Index): Partitions the vector space into Voronoi cells using k-means clustering. During query time, it only searches vectors within the closest centroids, reducing memory usage at the cost of slight recall loss.

8. Store in Vector DB

Finally, the generated embeddings, clean text chunks, and metadata are written to a specialized Vector Database (such as pgvector, Pinecone, Qdrant, Milvus, or Chroma). This completes the offline phase. The documents are now vectorized, indexed, and queryable.

Phase 2: The Online Query & Retrieval Pipeline

When a user asks a question, the online pipeline executes synchronously in real-time, querying the index and constructing a prompt for the LLM to generate the final response.

Query Expansion & Prep

  • 1. User Query
  • 2. Query Rewrite (Intent Clarification)
  • 3. Multi-Query Gen (Synonyms)
  • 4. HyDE (Hypothetical Answer)
  • 5. Query Embedding (Vectorization)

Retrieval & Reranking

  • 6. Metadata Filtering (Pre-filter)
  • 7. Dense + BM25 Search (Hybrid)
  • 8. Cross-Encoder Reranker
  • 9. Parent Retrieval & Compression
  • 10. LLM Generation & Grounding

1. User Query

The user enters a question: "What was our Q3 revenue in Europe according to last week's report?". This raw text starts the active loop.

2. Query Rewrite

Users often write ambiguous queries, use pronouns, or reference previous conversation history (e.g., "How does it compare to Asia?" where 'it' is Q3 revenue). In the Query Rewrite step, a fast LLM evaluates the chat history and transforms the raw input into a self-contained search query, clarifying intent before matching vectors.

3. Multi-Query Generation

A single query might miss relevant documents due to phrasing differences. To bypass this, we generate multiple variations of the rewritten query using an LLM. For instance: "European revenue Q3", "Europe earnings Q3", and "Q3 financial performance Europe". Running searches for all three maximizes recall.

4. HyDE (Hypothetical Document Embeddings)

Queries are short questions, while vector indexes contain longer answers. Comparing a query vector to document vectors directly sometimes yields weak matches. Under HyDE, we ask an LLM to write a hypothetical "ideal answer" to the user query. Even if the hypothetical answer contains hallucinated facts, its semantic format and phrasing will closely resemble the documents in our index. Embedding this hypothetical document, rather than the raw query, significantly improves similarity search accuracy.

5. Query Embedding

The rewritten query (or the HyDE document) is passed through the same embedding model used during document ingestion. This maps the user's intent into the exact same high-dimensional space as the index.

6. Metadata Filtering

If we know the user is only asking about "last week's report", searching the entire global index is inefficient. We use metadata pre-filtering (e.g., date > today - 7 days) to narrow down our vector search space, ensuring we only retrieve candidates matching these exact criteria.

7. Dense Search, BM25 Search & Hybrid Fusion

We execute two distinct types of searches concurrently:

  • Dense Search: Evaluates semantic similarity (e.g., Cosine Distance) between the query vector and the index using the ANN graph (HNSW/IVF). It excels at matching concepts (e.g., matching "earnings" with "revenue").
  • BM25 Search: A lexical search algorithm that looks for exact keyword matches. It excels at matching technical serial numbers, specific names, or product codes (e.g., finding "ERR_404").

We merge these results into a single candidate list using Hybrid Search, typically powered by Reciprocal Rank Fusion (RRF). RRF ranks items based on their position in both search lists, producing a balanced list of semantically and lexically relevant matches.

rrf.ts — Reciprocal Rank Fusion
interface RankMap { [id: string]: number }

function rrf(denseResults: string[], sparseResults: string[], k = 60): string[] {
  const scores: { [id: string]: number } = {};

  const addScores = (results: string[]) => {
    results.forEach((id, index) => {
      const rank = index + 1;
      scores[id] = (scores[id] || 0) + 1 / (k + rank);
    });
  };

  addScores(denseResults);
  addScores(sparseResults);

  return Object.keys(scores).sort((a, b) => scores[b] - scores[a]);
}

8. Top-K Retrieval & Cross-Encoder Reranker

From the hybrid search, we retrieve the initial candidates (e.g., Top-K = 50). While bi-encoder embedding models are fast, they evaluate queries and documents independently, missing subtle relevance cues. To fix this, we pass the 50 candidates through a Cross-Encoder Reranker (such as Cohere Rerank or BGE-Reranker). The Cross-Encoder evaluates the query and document together, outputting a highly accurate relevance score. We then select the top 5-10 chunks from this reranked list.

9. Parent Retrieval

Small chunks (e.g., 200 tokens) are excellent for high-precision vector matches, but they often lack the surrounding context needed for an LLM to generate a complete answer. In the Parent Retrieval pattern, when a small "child" chunk is matched, we replace or expand it with its larger "parent" document (e.g., 1000 tokens) from our relational store. This ensures the generator has full, rich context without losing semantic search precision.

10. Context Compression & Similarity Threshold

Feeding massive parent documents to the LLM increases token costs and latency. In this stage, we clean the retrieved context:

  • Similarity Thresholding: We discard chunks whose reranker scores fall below a minimum confidence boundary, ensuring we don't present irrelevant noise to the model if no good match exists.
  • Context Compression: Algorithms like LLMLingua analyze token information gain to strip out redundant filler sentences and words from the context block, compressing context size by up to 50% without losing information.

11. Prompt Construction & LLM Generation

The final prompt is built dynamically. We inject the compressed, highly relevant document chunks into a structured system template alongside the user's original query. This system prompt instructs the model to answer only using the provided facts, avoiding speculation. The prompt is dispatched to the LLM (e.g., Claude 3.5 Sonnet, GPT-4o) to generate the response.

System: You are a precise assistant. Answer the user question ONLY using the facts in the Context section below. If the context does not contain the answer, say "I don't know."

Context:
[Document 1: European Q3 revenue reached €14.2M, driven by UK and Germany.]
[Document 2: Q3 reports were finalized on Oct 14, 2026.]

User Question: What was our Q3 revenue in Europe?
Answer:

12. Grounding Verification & Source Attribution

Before returning the generated text to the user, we run a validation check. A fast Natural Language Inference (NLI) model or check-step verifies if every claim in the LLM's response is directly supported by (grounded in) the retrieved context. If a hallucination is detected, the pipeline rejects the response or regenerates it. Additionally, we map the text to exact source IDs, appending citations (e.g., [1] Report Page 4) to the final output.

13. Cache Response & Return

To avoid repeating expensive LLM calls for identical queries, we pass the final response through a semantic caching layer (e.g., Redis). When a new query arrives, if its vector similarity is >98% to a cached query, the cached response is served instantly. Finally, the verified, cited, and low-latency answer is returned to the client.

The Engineering Trade-Offs

Optimizing a RAG system is a continuous balance of speed, recall, and cost. While adding layers like HyDE, Cross-Encoders, and Grounding Verification increases retrieval accuracy, it also adds latency. In practice, production engineers configure these steps based on the use case—using simple caching and dense retrieval for low-latency features, and activating the full multi-query, reranked pipeline for complex, high-stakes enterprise search.

By treating RAG as an engineering pipeline rather than a single LLM call, you can systematically audit, measure, and optimize every phase to build a truly robust AI product.

Share this story