Skip to main content
LadeStack LogoLadeStack
Generative AI 15 min read July 8, 2024

Building RAG Applications with Vector Databases: Ingestion, Chunking, and Retrieval Architecture

A comprehensive guide to architecting production Retrieval-Augmented Generation (RAG) systems using vector databases, hybrid search, reranking, and citation verification.

Architecture diagram showing document ingestion, vector indexing, similarity search, and context augmentation for Large Language Models

Large Language Models (LLMs) excel at linguistic synthesis, code generation, and reasoning across generalized public corpora. However, off-the-shelf foundation models exhibit two fundamental operational constraints when deployed in enterprise software: temporal boundaries (their training cutoff precludes real-time organizational knowledge) and epistemic confabulation (the tendency to generate plausible-sounding falsehoods when internal weights fail to retrieve exact factual facts).

Retrieval-Augmented Generation (RAG) addresses these limitations by decoupling knowledge storage from parametric memory. Rather than asking an LLM to memorize all documentation, schemas, and historical logs, RAG retrieves relevant document passages from a specialized information retrieval system—typically a vector database—and injects them dynamically into the model’s inference context window.

However, moving a RAG pipeline from a proof-of-concept prototype to a resilient production service requires careful architectural engineering. A naive implementation that simply splits raw text every 1,000 characters and queries raw embeddings will encounter severe failure modes: misplaced semantic context, truncated tables, noisy retrieval sets, and hallucinations caused by conflicting evidence.

This tutorial breaks down the architectural layers of production RAG systems, spanning document ingestion, chunking strategies, embedding dynamics, vector similarity algorithms, cross-encoder reranking, and provenance-backed answer generation.


1. End-to-End RAG Architecture

A production RAG architecture operates across two distinct asynchronous lifecycles: the Offline Ingestion & Indexing Pipeline and the Online Retrieval & Generation Loop.

[ OFFLINE INGESTION PIPELINE ]
Raw Documents (PDF, MD, HTML, SQL)
       │
       ▼
Text Extraction & Cleaning
       │
       ▼
Semantic & Boundary Chunking (400-800 tokens, 10-15% overlap)
       │
       ▼
Dense Embedding Generation (e.g., text-embedding-3-small, 1536 dim)
       │
       ▼
Vector Database Indexing (HNSW / IVF-PQ with metadata payloads)

─────────────────────────────────────────────────────────────────

[ ONLINE RETRIEVAL & GENERATION LOOP ]
User Query
       │
       ▼
Query Expansion / HyDE / Rewriting
       │
       ▼
Hybrid Search (Dense Vector Cosine + BM25 Sparse Inverted Index)
       │
       ▼
Reciprocal Rank Fusion (RRF) -> Top 50 Candidates
       │
       ▼
Cross-Encoder Reranker (Top 5 Selected Passages)
       │
       ▼
Context Assembly & Citation Boundary Enforcement
       │
       ▼
Grounded LLM Generation (with strict provenance citations)

The Ingestion Pipeline

  1. Extraction & Normalization: Stripping markup noise, parsing complex structures (tables, headings, code snippets), and extracting document metadata (author, created date, access control tags, source URL).
  2. Chunking: Segmenting continuous text into discrete informational units that preserve contextual self-containment while staying within embedding and context window token limits.
  3. Embedding Vectorization: Transforming text chunks into high-dimensional geometric representations using dense neural embedding models.
  4. Vector Storage & Indexing: Persisting vectors alongside document IDs and metadata payloads into specialized vector indexes like Hierarchical Navigable Small World (HNSW).

The Retrieval & Generation Loop

  1. Query Processing: Normalizing user queries, performing spelling correction, or generating hypothetical document embeddings (HyDE) to resolve vocabulary mismatches.
  2. Candidate Retrieval: Executing similarity searches across dense vector spaces, augmented by sparse lexical keyword matching (BM25) to catch specific entity names or product SKUs.
  3. Reranking: Passing the top candidate chunks through a compute-intensive cross-encoder model that scores query-document relevance with token-level interaction.
  4. Context Augmentation: Formulating an augmented system prompt containing only the verified top passages with strict attribution markers ([Doc-1], [Doc-2]).
  5. Inference with Provenance: Directing the LLM to synthesize an answer derived solely from the provided context, requiring inline citations and refusal triggers if the context lacks necessary information.

2. Ingestion & Chunking Strategies

The quality of retrieval is bounded by the quality of your chunking strategy. If a chunk is too small, it loses critical contextual background; if it is too large, it dilutes the dense embedding vector with extraneous noise, lowering the cosine similarity score for targeted queries.

Chunking Strategy Comparison

Strategy Token Target Overlap Strengths Tradeoffs / Limitations
Fixed-Size Sliding Window 256–512 tokens 10–20% Trivial to implement, deterministic compute cost Often bisects sentences, paragraphs, or tabular rows
Recursive Character Split 400–800 tokens 50–100 tokens Respects natural paragraph (\n\n), sentence (. ), and word delimiters Can still split nested lists or multi-step code examples
Markdown / AST Structure-Aware Dynamic (per section) Header hierarchy Preserves header context, code blocks, and markdown tables intact Variable chunk sizes; deep documents require recursive rollups
Semantic Similarity Chunking Dynamic None Groups sentences based on embedding drift; high semantic coherence Requires running embedding inference on every sentence during ingestion

Structure-Aware Chunking Implementation

For technical documentation and developer knowledge bases, structure-aware chunking preserving the Markdown Abstract Syntax Tree (AST) delivers optimal retrieval fidelity.

Here is an enterprise-grade recursive chunker in TypeScript that enforces token budgets while preserving document section hierarchies:

// src/lib/rag/chunker.ts

export interface DocumentSection {
  title: string;
  sourceUrl: string;
  category: string;
  content: string;
}

export interface TextChunk {
  chunkId: string;
  sourceUrl: string;
  headingPath: string;
  content: string;
  tokenEstimate: number;
}

/**
 * Splits markdown text respecting headings (#, ##, ###) and paragraph boundaries.
 * Preserves structural metadata in chunk headers.
 */
export function chunkMarkdownDocument(
  doc: DocumentSection,
  maxTokens: number = 600,
  overlapTokens: number = 60
): TextChunk[] {
  const sections = doc.content.split(/\n(?=#{1,3}\s)/);
  const chunks: TextChunk[] = [];
  let chunkCounter = 0;

  for (const section of sections) {
    const lines = section.trim().split("\n");
    const heading = lines[0].startsWith("#") ? lines[0].replace(/^#+\s*/, "") : "General";
    const body = lines[0].startsWith("#") ? lines.slice(1).join("\n").trim() : section.trim();

    if (!body) continue;

    // Approximate token count: 1 token ~= 4 English characters
    const estimatedTokens = Math.ceil(body.length / 4);

    if (estimatedTokens <= maxTokens) {
      chunkCounter++;
      chunks.push({
        chunkId: `${doc.sourceUrl}#chunk-${chunkCounter}`,
        sourceUrl: doc.sourceUrl,
        headingPath: `${doc.title} > ${heading}`,
        content: `### ${heading}\n${body}`,
        tokenEstimate: estimatedTokens
      });
    } else {
      // Split large sections by paragraph
      const paragraphs = body.split(/\n\n+/);
      let currentBuffer: string[] = [];
      let currentTokenCount = 0;

      for (const paragraph of paragraphs) {
        const pTokens = Math.ceil(paragraph.length / 4);

        if (currentTokenCount + pTokens > maxTokens && currentBuffer.length > 0) {
          chunkCounter++;
          chunks.push({
            chunkId: `${doc.sourceUrl}#chunk-${chunkCounter}`,
            sourceUrl: doc.sourceUrl,
            headingPath: `${doc.title} > ${heading}`,
            content: `### ${heading}\n${currentBuffer.join("\n\n")}`,
            tokenEstimate: currentTokenCount
          });

          // Retain the last paragraph for overlap continuity
          const overlap = currentBuffer.slice(-1);
          currentBuffer = [...overlap, paragraph];
          currentTokenCount = Math.ceil(currentBuffer.join("\n\n").length / 4);
        } else {
          currentBuffer.push(paragraph);
          currentTokenCount += pTokens;
        }
      }

      if (currentBuffer.length > 0) {
        chunkCounter++;
        chunks.push({
          chunkId: `${doc.sourceUrl}#chunk-${chunkCounter}`,
          sourceUrl: doc.sourceUrl,
          headingPath: `${doc.title} > ${heading}`,
          content: `### ${heading}\n${currentBuffer.join("\n\n")}`,
          tokenEstimate: currentTokenCount
        });
      }
    }
  }

  return chunks;
}

3. Vector Embeddings and Indexing Architecture

Dense vector representations translate unstructured text into continuous vectors in $\mathbb{R}^d$ (typically $d \in [384, 1536, 3072]$). Models such as text-embedding-3-small, bge-large-en-v1.5, or gte-base map semantically similar sentences to proximal coordinates in vector space.

Similarity Metrics: Cosine Similarity vs. Dot Product

When querying vector stores, three distance metrics dominate:

  1. Cosine Similarity: Measures the cosine of the angle between two vectors: $$\cos(\theta) = \frac{\mathbf{u} \cdot \mathbf{v}}{|\mathbf{u}|_2 |\mathbf{v}|_2}$$ Range is $[-1, 1]$. Invariant to vector magnitude, making it ideal when document chunks vary slightly in length.
  2. Inner (Dot) Product: $\mathbf{u} \cdot \mathbf{v} = \sum_{i=1}^d u_i v_i$. If vectors are unit-normalized ($|\mathbf{u}|_2 = 1$), the dot product is mathematically equivalent to cosine similarity but computationally faster since it requires no runtime square roots.
  3. Euclidean (L2) Distance: $|\mathbf{u} - \mathbf{v}|_2 = \sqrt{\sum (u_i - v_i)^2}$. Measures spatial distance. Smaller values indicate closer proximity.

Production Tip: Always unit-normalize your vectors prior to insertion. This allows your vector database to use dot-product hardware acceleration (AVX-512 / BLAS) while obtaining cosine-equivalent ranking.

Approximate Nearest Neighbors (ANN) & HNSW

Exact k-Nearest Neighbors (k-NN) requires calculating the distance between the query vector and every stored vector in the database, resulting in an $O(N \cdot d)$ compute cost. In a production catalog containing 10 million chunks, sub-millisecond latencies are impossible with flat scans.

Vector databases utilize Hierarchical Navigable Small World (HNSW) graphs to perform Approximate Nearest Neighbor (ANN) search with logarithmic $O(\log N)$ complexity:

  • HNSW Topology: Constructs multi-layered graphs where upper layers feature long-range “expressways” between distant clusters, and bottom layers feature dense connections between local neighbors.
  • M Parameter: The maximum number of bidirectional connection links per node. Higher M (e.g., 16–64) yields higher retrieval recall at the expense of memory footprint and indexing time.
  • efConstruction: The size of the dynamic candidate list evaluated during index construction. A higher value (e.g., 128–256) builds a higher-quality graph but increases ingest processing.
  • efSearch: The size of the candidate list during runtime query execution. Increasing efSearch (e.g., 32–100) trades a few milliseconds of query latency for significantly higher recall.

4. Hybrid Search and Cross-Encoder Reranking

Relying solely on dense semantic vectors introduces a common failure mode: keyword blind spots. Vector models map concepts conceptually, but they often fail to distinguish between subtle identifiers, numerical product models, or programming syntax. For example, a query for OAuth2 RFC 6749 section 4.1 might return a generalized discussion on web authentication rather than the exact authorization code flow section.

The Hybrid Search Pattern

Production RAG combines Dense Vector Search (semantic understanding) with Sparse Lexical Search (BM25 token frequency) using Reciprocal Rank Fusion (RRF):

$$RRF(d) = \sum_{m \in M} \frac{1}{k + r_m(d)}$$

Where $M$ is the set of retrieval models (Dense and Sparse), $r_m(d)$ is the rank of document $d$ in model $m$, and $k$ is a constant smoothing factor (typically $k = 60$).

User Query: "PostgreSQL 16 connection pooling max_connections limit"
        ├── Dense Vector Query (Pinecone / pgvector) ──> Top 50 semantic matches
        └── Sparse BM25 Query (Elasticsearch / Meilisearch) ──> Top 50 exact keyword matches
                             │
                             ▼
              Reciprocal Rank Fusion (RRF)
                             │
                             ▼
                Merged Top 40 Candidates
                             │
                             ▼
          Cross-Encoder Reranker (Cohere Rerank / BGE-Reranker)
                             │
                             ▼
                 Top 5 Final Context Chunks

Bi-Encoders vs. Cross-Encoders

  • Bi-Encoder (Vector Embeddings): Encodes the query and document independently into separate vectors. Fast for indexing and initial filtering, but cannot capture nuanced token-to-token interactions between query terms and document terms.
  • Cross-Encoder (Reranker): Feeds the query and candidate passage simultaneously into a single transformer encoder ([CLS] Query [SEP] Passage [SEP]). Full self-attention operates across both sequences, producing a dramatically more accurate relevance score ($[0, 1]$) at the expense of computational latency.

Running a cross-encoder across 50 candidate passages takes approximately 15–30 ms and eliminates 80–90% of irrelevant or noisy context passages before LLM synthesis.


5. End-to-End Implementation: Retrieval and Grounded Synthesis

Below is a complete, production-ready implementation in TypeScript utilizing Pinecone (or any compatible vector store) with OpenAI embeddings, cross-validation scoring, and grounded context assembly.

// src/lib/rag/pipeline.ts
import { Pinecone } from '@pinecone-database/pinecone';

export interface RetrievedChunk {
  chunkId: string;
  sourceUrl: string;
  headingPath: string;
  content: string;
  score: number;
}

export interface GroundedRAGResponse {
  answer: string;
  citations: Array<{ citationIndex: number; sourceUrl: string; headingPath: string }>;
  tokenUsage: { promptTokens: number; completionTokens: number };
  refusal: boolean;
}

/**
 * Executes a production RAG retrieval and synthesis cycle.
 */
export async function executeGroundedRAG(
  query: string,
  pineconeClient: Pinecone,
  pineconeIndexName: string,
  embeddingApiKey: string,
  llmApiKey: string
): Promise<GroundedRAGResponse> {
  // Step 1: Generate query embedding vector (unit-normalized)
  const embeddingResponse = await fetch('https://api.openai.com/v1/embeddings', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${embeddingApiKey}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      input: query,
      model: 'text-embedding-3-small'
    })
  });

  if (!embeddingResponse.ok) {
    throw new Error(`Embedding API error: ${embeddingResponse.statusText}`);
  }

  const embeddingData = await embeddingResponse.json();
  const queryVector: number[] = embeddingData.data[0].embedding;

  // Step 2: Query Vector Database for Top-K Candidates
  const index = pineconeClient.index(pineconeIndexName);
  const queryResult = await index.query({
    vector: queryVector,
    topK: 10,
    includeMetadata: true
  });

  const candidates: RetrievedChunk[] = (queryResult.matches || []).map((match) => ({
    chunkId: match.id,
    sourceUrl: (match.metadata?.sourceUrl as string) || 'Unknown Source',
    headingPath: (match.metadata?.headingPath as string) || 'General',
    content: (match.metadata?.content as string) || '',
    score: match.score || 0
  }));

  // Step 3: Filter chunks below minimum similarity threshold
  const MIN_SIMILARITY_SCORE = 0.65;
  const filteredChunks = candidates.filter(c => c.score >= MIN_SIMILARITY_SCORE);

  // If no passages exceed the confidence threshold, exit early to prevent confabulation
  if (filteredChunks.length === 0) {
    return {
      answer: "I am unable to answer this question based on the provided enterprise documentation. No relevant passages met our retrieval confidence criteria.",
      citations: [],
      tokenUsage: { promptTokens: 0, completionTokens: 0 },
      refusal: true
    };
  }

  // Select top 4 most relevant chunks
  const selectedContext = filteredChunks.slice(0, 4);

  // Step 4: Construct Grounded System Prompt with Citation Delimiters
  const contextBlock = selectedContext
    .map((chunk, idx) => `[Source-${idx + 1}] (${chunk.headingPath})\n${chunk.content}`)
    .join("\n\n---\n\n");

  const systemPrompt = `You are a strict technical documentation assistant.
Answer the user's question using ONLY the provided verified source passages below.
Follow these rigid operational constraints:
1. Every factual statement must cite its source passage using [Source-N] notation inline.
2. If the provided sources do not contain sufficient evidence to answer the question, state: "The documentation does not provide sufficient information to answer this inquiry."
3. Do NOT extrapolate, speculate, or introduce external facts outside of the provided passages.

VERIFIED SOURCES:
${contextBlock}`;

  // Step 5: Synthesize Grounded Response with LLM
  const llmResponse = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${llmApiKey}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: 'gpt-4o-mini',
      messages: [
        { role: 'system', content: systemPrompt },
        { role: 'user', content: query }
      ],
      temperature: 0.1, // Minimal randomness to preserve fidelity
      max_tokens: 500
    })
  });

  if (!llmResponse.ok) {
    throw new Error(`LLM synthesis error: ${llmResponse.statusText}`);
  }

  const completionData = await llmResponse.json();
  const answerText = completionData.choices[0].message.content;

  // Extract cited sources for provenance verification
  const citations = selectedContext.map((chunk, idx) => ({
    citationIndex: idx + 1,
    sourceUrl: chunk.sourceUrl,
    headingPath: chunk.headingPath
  }));

  return {
    answer: answerText,
    citations,
    tokenUsage: {
      promptTokens: completionData.usage.prompt_tokens,
      completionTokens: completionData.usage.completion_tokens
    },
    refusal: answerText.includes("does not provide sufficient information")
  };
}

6. Critical Fact-Checking: Does RAG Eliminate Hallucinations?

A prevalent marketing claim in developer ecosystems is that implementing a vector database and RAG pipeline “completely eliminates LLM hallucinations.”

This claim is technically false.

While RAG substantially mitigates confabulation by grounding generation in retrieved text, it introduces distinct new failure modes that can still produce inaccurate responses:

                  ┌────────────────────────────────────────┐
                  │          RAG Pipeline Failures         │
                  └───────────────────┬────────────────────┘
                                      │
         ┌────────────────────────────┼────────────────────────────┐
         ▼                            ▼                            ▼
  [ Retrieval Miss ]           [ Noise Injection ]        [ Faithfulness Failure ]
Query fails to match       Top-K chunks contain        LLM prioritizes internal
target chunk; LLM answers  conflicting/irrelevant info; parametric memory over the
from memory or guesses.    LLM synthesizes false facts. provided context passages.

1. Retrieval Misses (False Negatives)

If the embedding distance between a complex user query and the source passage exceeds the index threshold, the correct passage is never fetched. If the system prompt does not explicitly enforce a strict refusal policy, the LLM will fall back on its pre-trained parametric weights and confabulate an answer.

2. Context Window Dilution and “Lost in the Middle”

Empirical studies in transformer attention mechanisms indicate that LLMs retrieve information most reliably from the very beginning and very end of their input contexts. When feeding 10–20 chunks into a large context window, facts positioned in the middle chunks are frequently overlooked, leading to partial or hallucinated syntheses.

3. Noise and Conflicting Ingested Information

If your vector store contains contradictory documentation (e.g., v1 API deprecations alongside v2 specifications), the vector search will retrieve both. The model may blend the conflicting parameters together, creating a synthetic, syntactically plausible API call that exists in neither version.

4. Faithfulness Failures (Context Sycophancy)

Even when accurate documentation is present in the prompt, foundation models will occasionally contradict the context if the true factual statement directly conflicts with strong associations reinforced during pre-training.

The Mitigation Strategy

To address these residual failure modes:

  • Calibrate Distance Cutoffs: Reject retrieval results whose cosine similarity or reranker score falls below a verified threshold.
  • Enforce Explicit Refusal Instructions: Direct the model to decline answering if evidence is absent.
  • Implement Automated Evaluation (The RAG Triad):
    • Context Relevance: Did the retriever fetch passages that directly address the user query?
    • Groundedness / Faithfulness: Is every sentence in the model’s answer mathematically supported by the retrieved context?
    • Answer Relevance: Does the final answer directly solve the user’s intent?

7. Operational Best Practices and Common Pitfalls

Best Practices for Enterprise RAG

  1. Track Ingestion Hashes: Compute SHA-256 hashes of every source document and chunk. Only re-embed and upsert chunks whose content has actually changed to minimize embedding API costs and indexing churn.
  2. Isolate Multi-Tenant Vectors: Store tenant IDs or team identifiers as searchable vector metadata. Always apply metadata filtering (tenant_id == current_tenant) at query time to prevent cross-tenant data leakage.
  3. Monitor Index Freshness: Establish an automated staleness alert. Chunks corresponding to deleted or modified documentation in your CMS must be actively pruned from the vector index.
  4. Log Retrieval Recall Metrics: Collect user feedback (thumbs up / thumbs down) paired with the exact chunk IDs returned in the prompt. Periodically inspect downvoted interactions to identify chunking boundaries that sliced crucial information.

Common Pitfalls to Avoid

  • Over-Chunking: Splitting text into 50-token micro-chunks destroys grammatical context and sentence continuity.
  • Ignoring Document Structure: Stripping markdown tables into flat text often renders relational rows uninterpretable to vector embeddings.
  • Blind Vector Search Without Metadata: Relying strictly on vector similarity without date, version, or permission filters leads to outdated responses.
  • Embedding Sensitive PII Unchecked: Vector databases persist data in unencrypted payloads unless configured with column encryption. Never embed raw credentials, session tokens, or personal identifiers.

8. Summary and Next Steps

Vector databases and Retrieval-Augmented Generation form the foundation for modern enterprise AI applications. By combining AST-aware structure chunking, hybrid dense/sparse search, cross-encoder reranking, and citation-enforced system prompts, engineering teams can build knowledge systems that provide trustworthy, verifiable answers with clear provenance.

To take your RAG pipeline further, explore our architectural deep dive on Multi-Tenant SaaS Architecture Patterns for tenant isolation strategies, or examine our guide on Fine-Tuning LLMs for Domain-Specific Tasks when parameter adaptation is required alongside retrieval.

More Engineering Articles