Rashid Ali
RASHID ALI
RashidBuilds
How to Build a RAG Application: From Document Processing to Reliable AI Answers
Back to Essays
AUTHORRashid Ali
CATEGORY // TIME
AI & LLMs13 min read

How to Build a RAG Application: From Document Processing to Reliable AI Answers

PUBLISHED // Oct 2026
(Scroll down)
(Intro)

Most Retrieval-Augmented Generation (RAG) prototypes look impressive in a Jupyter Notebook with a few clean PDF files. But the moment you deploy a naive RAG pipeline into production across complex enterprise documents—scanned contracts, multi-page financial tables, API documentation, and heterogeneous customer knowledge bases—the system breaks down. Chunks lose context, vector embeddings struggle with exact keyword queries, and language models hallucinate answers from partially retrieved noise.

Building a reliable RAG application is fundamentally an information retrieval and data pipeline challenge, not a prompting trick. Reliable AI answers require disciplined engineering at every tier: content-aware parsing and recursive chunking, hybrid search fusing dense embeddings with sparse lexical BM25 queries, two-stage cross-encoder reranking, and systematic evaluation harnesses. Here is the exact architectural blueprint for moving from fragile document vectorization to production-grade retrieval precision.

Summary

The Naive RAG Failure Modes in Production

The default tutorial architecture for RAG follows a predictable four-step loop: extract raw text from a document, chop it into fixed 500-character windows with a 50-character overlap, compute vector embeddings using an off-the-shelf embedding model, and query an approximate nearest neighbor (ANN) vector database for the top three nearest chunks. While this toy setup functions for basic prose, it fails catastrophically under enterprise workloads.

The first failure mode is structural fragmentation. Fixed-character chunking blindly severs sentences in mid-thought, separates table headers from numerical cells, and strips nested lists of their parent context. The resulting vector represents a mangled sentence fragment that lacks semantic coherence.

The second failure mode is embedding blindness. Dense vector models project semantic concepts into high-dimensional geometric space; they excel at knowing that 'infant' is related to 'baby', but perform abysmally at locating exact alphanumeric identifiers—such as error code '0x80070005', part serial number 'X-4902-A', or specific function definitions like 'useTenantSession()'. If a user searches for an exact identifier, pure cosine similarity frequently returns completely irrelevant passages.

The third failure mode is context stuffing and the 'Lost in the Middle' effect. Research by Liu et al. demonstrated that LLMs heavily prioritize tokens located at the extreme beginning and end of the prompt context window, routinely overlooking information placed in the center. Passing noisy or mediocre chunks directly to the model introduces distractions, increases token latency, and triggers plausible-sounding hallucinations.

ENGINEERING CHECKLIST & GUIDELINES
  • Eliminate fixed-character slicing that severs table rows, code syntax, and parent section context.
  • Mitigate dense vector search blind spots on exact product codes, acronyms, and technical identifiers.
  • Prevent 'lost in the middle' cognitive degradation by filtering low-scoring chunks before prompt injection.

Document Ingestion, Structural Parsing & Chunking Strategies

The quality of your retrieval pipeline is bounded by the quality of your document ingestion. If you feed unstructured garbage into your vector index, you will retrieve unstructured garbage at query time. Production pipelines convert unstructured formats (PDFs, DOCX, scanned manuals) into clean semantic Markdown or structured HTML before any chunking occurs, preserving headers (#, ##, ###), bold text, tables, and code fences.

Once cleaned, documents should be split using content-aware strategies. The baseline standard is a Recursive Character Text Splitter, which attempts to split on double newlines (paragraphs) first, single newlines (lines) second, periods (sentences) third, and spaces (words) only as a last resort. For complex documents, Semantic Chunking determines chunk boundaries by computing rolling cosine distance between consecutive sentence embeddings and creating a boundary only when a significant semantic divergence occurs.

An even more effective production pattern is Hierarchical / Parent-Child Chunking. In this architecture, small 'child chunks' (150–300 tokens) are embedded and indexed for precise vector matching. However, each child chunk stores a reference foreign key to its larger 'parent chunk' (1,000–1,500 tokens). When a child chunk matches a user's query, the retrieval engine fetches and injects the parent chunk into the LLM context, guaranteeing that the model receives the full surrounding narrative.

ENGINEERING CHECKLIST & GUIDELINES
  • Normalize raw documents into structured Markdown to preserve heading hierarchies, lists, and tables.
  • Adopt hierarchical parent-child chunking: index small semantic chunks for search, supply parent chunks for synthesis.
  • Inject deterministic metadata (tenant ID, document version, section path breadcrumbs) into every chunk record.

Hybrid Search: Fusing Dense Embeddings & Sparse BM25 via RRF

To resolve the tension between conceptual semantic search and exact keyword matching, production RAG systems deploy Hybrid Search. This combines dense vector embeddings (e.g. OpenAI's text-embedding-3-small or open-source BGE-M3) with sparse lexical algorithms (such as BM25 or PostgreSQL's native tsvector and tsquery).

The challenge in hybrid search is score fusion: a cosine similarity score (ranging between 0.0 and 1.0) cannot be meaningfully added to a BM25 relevance score (which can range from 0 to 45+ depending on document frequency). Attempting to normalize and manually weight these scores (e.g. 0.7 * dense + 0.3 * sparse) creates brittle heuristics that break when document corpus distributions shift.

The mathematically robust solution is Reciprocal Rank Fusion (RRF). RRF evaluates only the relative ordinal rank of documents returned by each search method rather than their raw scores. By computing the sum of inverse ranks—1 / (k + rank)—where k is a smoothing constant (typically 60), RRF consistently surfaces documents that rank highly across both dense and sparse retrieval engines, providing exceptional accuracy on both conceptual and exact-match queries.

rag/retrieval.py
python
from typing import List, Dict, Any
from openai import OpenAI
import os
 
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
 
def reciprocal_rank_fusion(
dense_ranks: List[str],
sparse_ranks: List[str],
k: int = 60
) -> Dict[str, float]:
"""Combines dense vector and sparse lexical rankings using Reciprocal Rank Fusion."""
rrf_scores: Dict[str, float] = {}
for rank, doc_id in enumerate(dense_ranks):
rrf_scores[doc_id] = rrf_scores.get(doc_id, 0.0) + (1.0 / (k + rank + 1))
for rank, doc_id in enumerate(sparse_ranks):
rrf_scores[doc_id] = rrf_scores.get(doc_id, 0.0) + (1.0 / (k + rank + 1))
return dict(sorted(rrf_scores.items(), key=lambda item: item[1], reverse=True))
 
def execute_hybrid_search(conn, query: str, tenant_id: str, limit: int = 25) -> List[str]:
# 1. Generate query embedding for dense semantic search
emb_res = client.embeddings.create(
model="text-embedding-3-small",
input=query
)
query_vector = emb_res.data[0].embedding
 
with conn.cursor() as cur:
# 2. Dense Semantic Vector Retrieval (pgvector HNSW index)
cur.execute("""
SELECT id FROM document_chunks
WHERE tenant_id = %s
ORDER BY embedding <=> %s::vector
LIMIT %s;
""", (tenant_id, query_vector, limit))
dense_ids = [row[0] for row in cur.fetchall()]
 
# 3. Sparse Lexical Retrieval (PostgreSQL Full-Text Search tsvector)
cur.execute("""
SELECT id FROM document_chunks
WHERE tenant_id = %s AND search_tsv @@ plainto_tsquery('english', %s)
ORDER BY ts_rank_cd(search_tsv, plainto_tsquery('english', %s)) DESC
LIMIT %s;
""", (tenant_id, query, query, limit))
sparse_ids = [row[0] for row in cur.fetchall()]
 
# 4. Fuse rankings with Reciprocal Rank Fusion
fused_scores = reciprocal_rank_fusion(dense_ids, sparse_ids, k=60)
return list(fused_scores.keys())[:limit]
ENGINEERING CHECKLIST & GUIDELINES
  • Combine dense vector embeddings with sparse BM25 lexical search to capture both concepts and exact keywords.
  • Use Reciprocal Rank Fusion (RRF) with smoothing parameter k=60 to fuse multi-engine rankings without score normalization.
  • Implement HNSW indexes in pgvector or Qdrant for sub-20ms approximate nearest neighbor query execution.

Two-Stage Retrieval & Cross-Encoder Reranking

Vector databases use bi-encoder architectures: the query is mapped to a vector independently of the documents. While bi-encoders allow lightning-fast vector similarity searches across millions of records using HNSW indexes, they cannot capture cross-token interactions between the query and the candidate text.

Cross-encoders (such as Cohere Rerank v3 or the open-source bge-reranker-large), by contrast, feed the query and candidate chunk together into a multi-layer transformer network. Every token in the query attends directly to every token in the document chunk, producing dramatically higher relevance scoring.

Because cross-encoders are compute-intensive, production architectures use a Two-Stage Retrieval Pipeline. In Stage 1, the hybrid search engine quickly retrieves the top 30 to 50 candidate chunks in under 30 milliseconds. In Stage 2, the cross-encoder reranks these 50 candidates, scoring their true contextual relevance. We then enforce a strict score cutoff (e.g. relevance_score >= 0.70) and pass only the top 3 to 5 hyper-relevant chunks to the generation LLM, slashing token costs and eliminating hallucinations.

rag/reranker.py
python
import cohere
import os
from typing import List, Dict
 
co = cohere.Client(os.environ.get("COHERE_API_KEY"))
 
def rerank_candidates(
query: str,
candidate_chunks: List[Dict[str, str]],
top_n: int = 5,
min_relevance_threshold: float = 0.65
) -> List[Dict[str, str]]:
"""
Reranks broad candidates using a deep cross-encoder model to compute
joint query-document attention scores, discarding irrelevant chunks.
"""
if not candidate_chunks:
return []
 
documents = [c["content"] for c in candidate_chunks]
response = co.rerank(
model="rerank-english-v3.0",
query=query,
documents=documents,
top_n=top_n,
return_documents=False
)
 
filtered_results = []
for item in response.results:
if item.relevance_score >= min_relevance_threshold:
original_chunk = candidate_chunks[item.index]
filtered_results.append({
**original_chunk,
"relevance_score": round(item.relevance_score, 4)
})
 
return filtered_results
ENGINEERING CHECKLIST & GUIDELINES
  • Deploy a two-stage retrieval pipeline: wide hybrid candidate retrieval (top 30-50) followed by cross-encoder reranking (top 3-5).
  • Enforce a strict reranker confidence threshold to filter out marginal chunks and prevent context pollution.
  • Cut LLM input token consumption by 70% while improving answer accuracy by feeding only high-confidence evidence.

Context Synthesis, Prompt Grounding & Source Citations

Retrieving high-quality chunks is only half the battle; how you present that context to the generative language model determines whether it adheres to the facts or hallucinates plausible falsehoods.

First, the system prompt must explicitly define negative constraints: 'Answer the question using ONLY the provided context blocks. If the provided context does not contain sufficient facts to answer the question, state that you cannot answer based on the available documentation. Do not assume or extrapolate.' In production benchmarks, models given explicit permission to say 'I don't know' show an 85%+ reduction in factual hallucinations.

Second, enforce deterministic citations. Each injected chunk should be assigned a discrete citation key (e.g., [[Source ID: chunk_892 | Doc: API-Auth-Spec]]). The prompt instructs the model that every factual claim must end with the matching bracketed citation key. This provides enterprise users with immediate click-to-verify provenance and enables automated validation passes that verify claims against cited source text.

rag/synthesis.py
python
from openai import OpenAI
import os
 
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
 
def generate_grounded_answer(query: str, reranked_chunks: list[dict]) -> str:
# Format retrieved context with explicit chunk identifiers
context_blocks = []
for chunk in reranked_chunks:
context_blocks.append(
f"[[Source ID: {chunk['chunk_id']} | Title: {chunk['metadata'].get('title', 'Document')}]]\n"
f"{chunk['content']}\n"
)
formatted_context = "\n---\n".join(context_blocks)
 
system_prompt = (
"You are an enterprise technical copilot. Answer the query using ONLY the provided context.\n"
"Rules:\n"
"1. Every factual statement must cite its source identifier in brackets, e.g., [[Source ID: chunk_123]].\n"
"2. If the context does not contain sufficient facts to answer completely, state clearly: "
"'I cannot answer this question based on the provided documentation.'\n"
"3. Do not extrapolate, assume, or utilize outside training knowledge."
)
 
user_prompt = f"Context:\n{formatted_context}\n\nQuestion: {query}"
 
response = client.chat.completions.create(
model="gpt-4o",
temperature=0.0,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
)
return response.choices[0].message.content or ""
ENGINEERING CHECKLIST & GUIDELINES
  • Include explicit negative constraints instructing the model to refuse to answer if retrieved evidence is insufficient.
  • Structure prompt context with unique chunk IDs and enforce inline bracketed source citations for auditability.
  • Position the most authoritative chunks at the beginning and end of the context block to counteract position bias.

RAG Triad Evaluation: Precision, Recall & Faithfulness

You cannot improve a RAG pipeline through manual inspection and anecdotal 'vibe checks'. A prompt change or chunking modification that improves query A might silently degrade queries B, C, and D. Production teams implement automated evaluation using the RAG Triad framework (via tools like Ragas or TruLens).

The RAG Triad evaluates four distinct mathematical dimensions: 1) Context Precision: What fraction of retrieved chunks are genuinely relevant to the query? 2) Context Recall: Did the retrieval engine find all the factual chunks required to answer the prompt? 3) Faithfulness: Are the claims in the generated response directly entailed by the retrieved context, or did the model invent details? 4) Answer Relevance: Does the final answer directly resolve the user's intent without wandering into unrelated tangents?

By running this evaluation harness against a synthetic golden dataset generated from your documentation, you establish regression tests in your CI/CD pipeline. If an engineer tweaks the embedding model, chunk size, or reranking threshold, the automated test suite immediately flags any regression in faithfulness or recall before code reaches production.

ENGINEERING CHECKLIST & GUIDELINES
  • Automate continuous RAG Triad evaluations measuring Context Precision, Context Recall, Faithfulness, and Answer Relevance.
  • Generate synthetic golden QA evaluation datasets directly from document corpora for reproducible benchmarking.
  • Integrate retrieval regression tests into CI/CD pipelines to block releases that drop faithfulness scores.

Key Architectural Takeaways

Building a reliable RAG application is fundamentally an information retrieval challenge. By pairing structural parsing, hybrid search, cross-encoder reranking, and quantitative RAG Triad evaluations, you eliminate hallucinations and deliver enterprise-grade answers.

KEY ARCHITECTURAL TAKEAWAYS
Normalize complex documents into Markdown and use parent-child chunking for high-resolution retrieval.
Fuse dense vector embeddings with sparse BM25 lexical search using Reciprocal Rank Fusion (RRF).
Implement two-stage retrieval: broad candidate search (top 50) followed by cross-encoder reranking (top 5).
Instruct models with explicit negative constraints to refuse answering when retrieved context is insufficient.
Enforce discrete chunk-level bracketed citations to provide verifiable audit trails for enterprise compliance.
Automate quantitative evaluations using the RAG Triad (Precision, Recall, Faithfulness) in CI/CD pipelines.
#RAG#Vector Search#Python#pgvector#Embeddings#LLMs

READY TO MOVE SMARTER?

We are here to help you grow without hassle.
No call centres. No runaround. Just experienced people ready to help.