How to build production-grade RAG pipelines for enterprise knowledge systems. Covers chunking strategies, hybrid search, reranking, context management, and evaluation methodologies.
Enterprise RAG pipelines are critical when an organization must deliver accurate, up-to-date, and contextually rich answers from large volumes of internal documentation, knowledge bases, and structured data—without retraining models from scratch. These pipelines power enterprise search, customer support bots, executive dashboards, and AI-driven decision assistants. They matter when latency, accuracy, and consistency across hundreds of documents are non-negotiable.
A production-ready RAG pipeline processes user queries through a chain of stages: document ingestion, chunking, embedding, vector storage, retrieval, and response synthesis. Each stage must be decoupled, observable, and idempotent.
Documents arrive via file upload, database sync, or API ingestion. They are normalized into a standard format: { id: string, title: string, content: string, metadata: { source: string, updated_at: iso8601, author: string, tags: [string] } }.
Ingestion is triggered by curl -X POST http://ingest-service:8080/ingest -d @document.json, where document.json contains:
{
"id": "doc-12345",
"title": "Q3 Financial Report 2024",
"content": "Revenue rose 18% year-on-year...",
"metadata": {
"source": "sharepoint://docs/fin-reports/q3-2024.pdf",
"updated_at": "2024-10-05T12:30:00Z",
"author": "[email protected]",
"tags": ["finance", "report", "q3"]
}
}
The ingestion service writes the document to a PostgreSQL database using:
INSERT INTO documents (id, title, content, metadata)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO UPDATE SET
title = EXCLUDED.title,
content = EXCLUDED.content,
metadata = EXCLUDED.metadata,
updated_at = NOW();
Documents are split into chunks using a sliding window algorithm. Each chunk is no more than 512 tokens, with 25% overlap. The chunker reads from documents table, writes to chunks table.
CREATE TABLE chunks (
id UUID PRIMARY KEY,
document_id UUID REFERENCES documents(id),
text TEXT NOT NULL,
token_count INT NOT NULL,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Chunks are generated with a service that reads from documents, processes each document using a tokenizer (e.g., tiktoken), and emits chunks in batches:
python -m chunker \
--input-db postgresql://db:5432/knowledge \
--output-db postgresql://db:5432/knowledge \
--chunk-size 512 \
--overlap 128 \
--tokenizer tiktoken \
--batch-size 100
Each chunk is assigned a unique id, the original document_id, and metadata including source, page, and section.
Embeddings are computed using a pre-trained model (e.g., sentence-transformers/all-MiniLM-L6-v2) and stored in a vector database.
A dedicated service pulls chunks, computes embeddings, and writes them to the vector store:
import torch
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
with psycopg2.connect("postgresql://db:5432/knowledge") as conn:
with conn.cursor() as cur:
cur.execute("SELECT id, text FROM chunks WHERE embedding IS NULL LIMIT 1000")
chunks = cur.fetchall()
texts = [chunk[1] for chunk in chunks]
embeddings = model.encode(texts, batch_size=64, show_progress_bar=True)
for i, (chunk_id, embedding) in enumerate(zip([c[0] for c in chunks], embeddings)):
cur.execute(
"UPDATE chunks SET embedding = %s WHERE id = %s",
(embedding.tolist(), chunk_id)
)
conn.commit()
The vector store uses a HNSW index with ef_search=128, ef_construction=256, and m=64.
Retrieval is the heart of RAG. Given a query, the pipeline fetches top-k relevant chunks using vector similarity.
A user query arrives as:
{
"query": "What were the key drivers of revenue growth in Q3?",
"user_id": "[email protected]",
"session_id": "sess-98765"
}
The retrieval service computes the query embedding:
query_embedding = model.encode("What were the key drivers of revenue growth in Q3?")
Then performs a vector search:
SELECT c.id, c.text, c.metadata, c.token_count,
(c.embedding <-> query_embedding) AS distance
FROM chunks c
ORDER BY c.embedding <-> query_embedding
LIMIT 5
Results are returned with distance (cosine or L2), and the pipeline uses distance < 0.4 as a soft threshold for relevance.
Failure mode 1: Low recall due to poor chunking. When query terms span multiple chunks but no chunk contains the full context, retrieval misses key information. This is silent: the top 5 results have high scores but miss a critical sentence.
Failure mode 2: Query embedding mismatch. The query embedding model differs from the chunk embedding model (e.g., all-MiniLM-L6-v2 for chunks, all-MiniLM-L12-v2 for queries). This causes a shift in the embedding space, degrading retrieval quality. The fix is to ensure consistent model loading and versioning.
Failure mode 3: Index drift. The vector index was built with older data. New chunks are added, but retrieval uses the old index. The solution is to recompute the index weekly and maintain a current_index_version in settings.
For high-precision retrieval, a two-stage approach is used:
cross-encoder/ms-marco-MiniLM-L-6-v2).def re_rank(query, candidates):
pairs = [(query, c["text"]) for c in candidates]
scores = cross_encoder.predict(pairs)
ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
return [c for c, _ in ranked]
The re-ranker is deployed as a gRPC service, called after vector search.
After retrieval, the pipeline combines the top-k context chunks into a single prompt for a LLM.
A standard prompt template is used, with placeholders:
You are a helpful assistant. Use the following context to answer the question.
---
{context}
---
Question: {question}
Answer:
The context is constructed by joining the retrieved chunks, with a separator:
context = "\n\n---\n\n".join([
f"Source: {chunk['metadata']['source']}\n{chunk['text']}"
for chunk in top_chunks
])
The prompt is sent to an LLM inference server via HTTP:
curl -X POST http://llm-inference:8000/generate \
-H "Content-Type: application/json" \
-d '{
"prompt": "You are a helpful assistant. Use the following context to answer the question.\n\n---\n\nSource: sharepoint://docs/fin-reports/q3-2024.pdf\nRevenue rose 18% year-on-year...\n\n---\n\nQuestion: What were the key drivers of revenue growth in Q3?\nAnswer:",
"max_tokens": 512,
"temperature": 0.3,
"top_p": 0.8,
"seed": 42
}'
The inference server returns:
{
"text": "The key drivers of revenue growth in Q3 were expansion into APAC markets, higher enterprise licensing, and improved customer retention.",
"tokens_used": 287,
"generated_at": "2024-10-05T12:31:05Z"
}
Response latency is critical. The pipeline caches query results using Redis:
# Value: JSON of { "answer", "context", "retrieval_time", "llm_time" }
SETEX rag:query:8d123a4e 3600 {
"answer": "The key drivers...",
"context": [
{ "source": "sharepoint://...", "text": "Revenue rose 18% year-on-year..." },
...
],
"retrieval_time": 124,
"llm_time": 89
}
Cache hits are prioritized; cache misses trigger full pipeline execution.
Each stage is monitored via structured logs, metrics, and traces.
All steps emit JSON logs with event_type, timestamp, pipeline_stage, duration, and error.
Example: chunking failure
{
"event_type": "chunking_failed",
"timestamp": "2024-10-05T12:28:45Z",
"pipeline_stage": "chunking",
"document_id": "doc-12345",
"error": {
"type": "TokenizationError",
"message": "Text exceeds max_tokens (1200) after 512 token chunks",
"details": {
"text_length": 1400,
"chunk_count": 4,
"expected": 3,
"actual": 4
}
}
}
Key metrics are collected per stage:
ingest_success_rate: total ingested / total attemptedchunking_duration_avg: average time to chunk a documentembedding_computation_rate: embeddings computed per minuteretrieval_latency_p95: 95th percentile retrieval timellm_inference_tokens_per_secondAlerts trigger when:
retrieval_latency_p95 > 500msembedding_computation_rate < 50 embeddings/secchunking_duration_avg > 2sSilent failure: chunking with no overlap. The chunker is configured with overlap=0, but the output misses overlapping context. The pipeline works, but users get disjoint chunks. The fix is to validate overlap via a checksum of adjacent chunk texts.
Latency spike: vector search timeout. The vector database returns 408 Request Timeout after 10 seconds. The service retries with exponential backoff and falls back to approximate search (e.g., ANN with LSH) when full search fails.
Cache miss cascade: LLM inference timeout. LLM inference takes 2 seconds, but the cache miss triggers 100 concurrent queries. The inference server becomes saturated. The solution is to queue requests using a message broker (e.g., RabbitMQ), with a priority queue for high-priority queries.
Ingestion and embedding are batched to reduce overhead:
# Ingest 1000 documents in one batch
python -m ingestor \
--batch-size 1000 \
--parallel 4 \
--write-to-db postgresql://db:5432/knowledge
Batching reduces database round trips and allows parallel embedding computation.
Chunking is tuned per document type. Financial reports use chunk_size=1024, overlap=256. Technical documentation uses chunk_size=768, overlap=192.
Before retrieval, metadata filters are applied:
SELECT * FROM chunks
WHERE metadata ? 'tags'
AND metadata->'tags' ? 'finance'
AND (metadata->'source') ILIKE '%/fin-reports/%'
AND embedding IS NOT NULL
ORDER BY embedding <-> query_embedding
LIMIT 10
This reduces the search space and improves precision.
When a document is updated, only the changed chunks are re-embedded. The pipeline computes a delta_hash of the document content and compares it to the stored hash.
If the hash differs, the pipeline:
page or section metadata)UPSERT operationsThis avoids full recomputation and keeps the system responsive.
Enterprise RAG pipelines are not a single service but a coordinated system of ingestion, chunking, embedding, retrieval, and synthesis. They succeed when each stage is observable, idempotent, and optimized for production scale. The most common failure is not a broken query but a silent drift in retrieval quality—where results are accurate but slowly degrade over time, unnoticed. The solution is to monitor not just the end-to-end pipeline, but each component in isolation, with clear failure modes and recovery patterns.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy