Advanced February 14, 2026 · 13 min read

Production RAG Pipelines at Scale

Retrieval-Augmented Generation is simple in a demo and brutal in production. Chunk boundaries destroy context, embedding quality degrades on domain-specific content, and single-vector retrieval misses nuance. This guide covers the hard-won patterns for building RAG pipelines that actually work — from chunking strategies through hybrid search to automated evaluation — all with ElsiumAI’s @elsium-ai/rag package.


Beyond Basic RAG

The canonical RAG demo looks like this: split a document into chunks, embed them, store them in a vector database, retrieve the top-K nearest neighbors, and stuff them into a prompt. It works on a README. It falls apart on a 10,000-page regulatory corpus.

Here’s why naive RAG fails at scale:

The rest of this article addresses each of these failure modes with concrete code and configuration using ElsiumAI.

Chunking Strategies

Chunking is where most RAG pipelines go wrong first. ElsiumAI ships three chunkers, each suited to different content types. The right choice depends on your source material and how much structural information it carries.

Fixed-Size Chunker

The simplest approach. Splits text into chunks of N tokens with a configurable overlap. Use this when your source material has no meaningful structure — raw logs, transcripts, or flat text dumps.

import { fixedSizeChunker } from '@elsium-ai/rag'

const chunks = fixedSizeChunker(document, {
  chunkSize: 512,    // tokens per chunk
  overlap: 64,       // token overlap between consecutive chunks
  tokenizer: 'cl100k_base',  // match your embedding model's tokenizer
})

The overlap parameter is critical. Without it, information at chunk boundaries is lost. A good rule of thumb: set overlap to 10–15% of your chunk size. Too much overlap wastes storage and embedding compute; too little and you get boundary artifacts.

Sentence Chunker

Splits on sentence boundaries, then groups sentences until the token limit is reached. This preserves complete thoughts and works well for prose-heavy content — articles, legal text, documentation.

import { sentenceChunker } from '@elsium-ai/rag'

const chunks = sentenceChunker(document, {
  maxChunkSize: 512,
  minChunkSize: 100,   // avoid tiny trailing chunks
  overlap: 2,          // overlap by N sentences
  language: 'en',
})

Sentence-level overlap is more semantically meaningful than token-level overlap. Overlapping by 2 sentences ensures that cross-sentence references (“This regulation”, “The above criteria”) are captured in at least one chunk.

Recursive Chunker

The most sophisticated option. It recursively splits by structural markers — first by headings, then by paragraphs, then by sentences — and only falls back to token-level splitting as a last resort. Use this for structured documents like markdown, HTML, or anything with headings.

import { recursiveChunker } from '@elsium-ai/rag'

const chunks = recursiveChunker(document, {
  maxChunkSize: 1024,
  separators: ['\n## ', '\n### ', '\n\n', '\n', '. '],
  preserveMetadata: true,  // attach source heading to each chunk
})

When preserveMetadata is enabled, each chunk carries a metadata.heading field with the nearest parent heading. This is invaluable for retrieval — you can display the section title alongside retrieved content, and it improves reranking quality significantly.

When to Use Which

Content Type Chunker Chunk Size Overlap
Raw logs / transcripts fixedSizeChunker 256–512 10–15%
Articles / legal text sentenceChunker 512–768 2–3 sentences
Markdown / HTML docs recursiveChunker 512–1024 Structural
Code repositories recursiveChunker 1024–2048 Function-level

Embedding Pipeline

Once your documents are chunked, you need to convert them into vectors. The key challenge at scale is throughput: embedding a million chunks one-at-a-time is painfully slow and expensive. ElsiumAI provides batch embedding and incremental ingestion out of the box.

Basic Setup

import { createOpenAIEmbeddings } from '@elsium-ai/rag'

const embeddings = createOpenAIEmbeddings({
  model: 'text-embedding-3-large',
  dimensions: 1536,       // or 3072 for higher fidelity
  apiKey: process.env.OPENAI_API_KEY,
})

Batch Embedding for Millions of Documents

For large corpora, use createBatch to process chunks in parallel batches with automatic retries and rate-limit backoff. This is the difference between a 3-hour ingestion job and a 15-minute one.

import { createBatch } from '@elsium-ai/rag'

const batchEmbedder = createBatch(embeddings, {
  batchSize: 2048,         // chunks per API call
  concurrency: 8,          // parallel API calls
  retries: 3,              // retry on 429/5xx
  backoff: 'exponential',  // backoff strategy
  onProgress: (processed, total) => {
    console.log(`Embedded ${processed}/${total} chunks`)
  },
})

// Embed all chunks — returns EmbeddedChunk[]
const embeddedChunks = await batchEmbedder.embedAll(chunks)

Incremental Ingestion

In production, you rarely re-embed your entire corpus. New documents arrive daily, and re-embedding unchanged content wastes money. Use content hashing to skip already-embedded chunks:

import { createIncrementalIngester } from '@elsium-ai/rag'

const ingester = createIncrementalIngester({
  embeddings: batchEmbedder,
  store: vectorStore,
  hashAlgorithm: 'xxhash64',  // fast content hashing
})

// Only embeds and stores chunks whose content hash is new
const result = await ingester.ingest(newChunks)
console.log(`${result.added} new, ${result.skipped} unchanged, ${result.updated} modified`)

Vector Store Tuning

Your vector store is the backbone of retrieval performance. ElsiumAI’s createPgVectorStore wraps pgvector with connection pooling, index management, and query optimization built in.

Connection and Pooling

import { createPgVectorStore } from '@elsium-ai/rag'

const vectorStore = createPgVectorStore({
  connectionString: process.env.DATABASE_URL,
  tableName: 'document_embeddings',
  dimensions: 1536,
  pool: {
    min: 5,
    max: 20,
    idleTimeout: 30_000,   // ms before idle connections are released
  },
})

Indexing Strategies: IVFFlat vs HNSW

The choice of index type determines your latency/recall tradeoff. This is one of the most consequential decisions in a RAG pipeline.

// IVFFlat — faster to build, lower recall at low nprobe
await vectorStore.createIndex({
  type: 'ivfflat',
  lists: 1000,        // sqrt(N) is a good starting point
  probes: 10,         // higher = better recall, slower queries
})

// HNSW — slower to build, better recall, higher memory usage
await vectorStore.createIndex({
  type: 'hnsw',
  m: 16,              // connections per node (higher = better recall, more memory)
  efConstruction: 200, // build quality (higher = slower build, better graph)
  efSearch: 100,       // query quality (higher = better recall, slower search)
})
Factor IVFFlat HNSW
Build time Fast (minutes) Slow (hours for 10M+ vectors)
Query latency ~5–20ms ~1–5ms
Recall@10 85–95% (tunable via probes) 95–99% (tunable via efSearch)
Memory overhead Low High (2–3x vectors)
Best for <1M vectors, cost-sensitive >1M vectors, latency-critical

For most production deployments with more than a million vectors, HNSW is the right choice. The memory overhead is worth the recall improvement — a missed relevant chunk is far more costly than a few extra gigabytes of RAM.

Dense vector similarity alone is insufficient for production RAG. It excels at semantic matching but fails on exact keywords, entity names, regulation numbers, and other precise identifiers. Hybrid search combines dense retrieval with sparse (keyword) retrieval to cover both modes.

Combining Dense and Sparse Retrieval

import { createHybridRetriever, bm25Retriever } from '@elsium-ai/rag'

const hybrid = createHybridRetriever({
  dense: vectorStore.asRetriever({ topK: 20 }),
  sparse: bm25Retriever({
    index: bm25Index,
    topK: 20,
  }),
  weights: {
    dense: 0.7,   // semantic similarity weight
    sparse: 0.3,  // keyword match weight
  },
  topK: 10,        // final results after fusion
  fusion: 'reciprocal-rank',  // also: 'linear', 'relative-score'
})

The weights parameter is your primary tuning lever. For general Q&A over prose, bias toward dense (0.7/0.3). For technical documentation with specific identifiers, shift toward sparse (0.5/0.5 or even 0.4/0.6). Always measure with your actual query distribution.

MMR Reranking for Diversity

Maximal Marginal Relevance (MMR) reranking solves a subtle problem: when your top-K results are all paraphrases of the same paragraph. MMR balances relevance against diversity, ensuring your context window contains distinct information.

import { mmrRerank } from '@elsium-ai/rag'

const results = await hybrid.retrieve('OSHA respiratory protection requirements')

const reranked = mmrRerank(results, {
  lambda: 0.7,   // 1.0 = pure relevance, 0.0 = pure diversity
  topK: 5,        // final context window size
})

The lambda parameter controls the relevance–diversity tradeoff. Start at 0.7 and decrease if you notice redundant chunks in your retrieved context. For broad research questions, values around 0.5 work well. For highly specific factual queries, keep lambda at 0.8 or higher.

Multi-Source Ingestion

Real-world RAG pipelines ingest from many sources simultaneously — HTML documentation, CSV exports, JSON API responses, and plain text files. ElsiumAI provides loaders for each format and a workflow system to orchestrate them in parallel.

Source Loaders

import {
  htmlLoader,
  textLoader,
  csvLoader,
  jsonLoader,
} from '@elsium-ai/rag'

const htmlDocs = await htmlLoader('./docs/**/*.html', {
  selectors: ['article', 'main'],   // extract only content areas
  stripTags: true,
  preserveLinks: true,
})

const csvDocs = await csvLoader('./data/products.csv', {
  contentColumns: ['description', 'specifications'],
  metadataColumns: ['sku', 'category'],
})

const jsonDocs = await jsonLoader('./api-dump/*.json', {
  contentPath: '$.items[*].body',
  metadataPath: '$.items[*].metadata',
})

const textDocs = await textLoader('./legal/**/*.txt')

Parallel Ingestion Workflow

Use defineParallelWorkflow to run all ingestion pipelines concurrently, then merge results into a single chunking and embedding step:

import { defineParallelWorkflow, defineStep } from '@elsium-ai/workflows'
import { recursiveChunker, createBatch } from '@elsium-ai/rag'

const ingestWorkflow = defineParallelWorkflow({
  name: 'multi-source-ingest',
  branches: {
    html: defineStep('load-html', async () => {
      return htmlLoader('./docs/**/*.html', { selectors: ['article'] })
    }),
    csv: defineStep('load-csv', async () => {
      return csvLoader('./data/products.csv', {
        contentColumns: ['description'],
      })
    }),
    json: defineStep('load-json', async () => {
      return jsonLoader('./api-dump/*.json', {
        contentPath: '$.items[*].body',
      })
    }),
    text: defineStep('load-text', async () => {
      return textLoader('./legal/**/*.txt')
    }),
  },
  merge: defineStep('chunk-and-embed', async (results) => {
    const allDocs = [
      ...results.html,
      ...results.csv,
      ...results.json,
      ...results.text,
    ]

    const chunks = recursiveChunker(allDocs, {
      maxChunkSize: 1024,
      separators: ['\n## ', '\n### ', '\n\n', '\n', '. '],
      preserveMetadata: true,
    })

    const batchEmbedder = createBatch(embeddings, {
      batchSize: 2048,
      concurrency: 8,
    })

    const embedded = await batchEmbedder.embedAll(chunks)
    await vectorStore.upsert(embedded)

    return { total: embedded.length }
  }),
})

const result = await ingestWorkflow.run()
console.log(`Ingested ${result.total} chunks from all sources`)

RAG Evaluation

You cannot improve what you do not measure. ElsiumAI’s @elsium-ai/testing package includes a full evaluation suite for RAG pipelines, measuring retrieval quality and generation faithfulness.

Setting Up an Evaluation Suite

import {
  runEvalSuite,
  precisionAtK,
  recallAtK,
  relevanceScore,
  faithfulness,
  createEvalDataset,
} from '@elsium-ai/testing'

// Define your evaluation dataset — ground-truth query/answer pairs
const evalData = createEvalDataset([
  {
    query: 'What is the maximum dosage for ibuprofen?',
    expectedChunks: ['chunk-id-42', 'chunk-id-43'],
    expectedAnswer: 'The maximum daily dosage for ibuprofen is 1200mg for OTC use.',
  },
  {
    query: 'OSHA regulation 1910.134 requirements',
    expectedChunks: ['chunk-id-891'],
    expectedAnswer: 'OSHA 1910.134 mandates employer-provided respiratory protection...',
  },
  // ... hundreds more for statistical significance
])

Running Evaluations

const results = await runEvalSuite({
  retriever: hybrid,
  generator: llm,
  dataset: evalData,
  metrics: [
    precisionAtK(5),     // of top-5 retrieved, how many are relevant?
    recallAtK(10),       // of all relevant chunks, how many are in top-10?
    relevanceScore(),    // LLM-judged relevance of retrieved context
    faithfulness(),      // is the generated answer grounded in retrieved context?
  ],
  concurrency: 10,       // parallel evaluation runs
})

console.log('Retrieval Metrics:')
console.log(`  Precision@5:  ${results.precisionAt5.toFixed(3)}`)
console.log(`  Recall@10:    ${results.recallAt10.toFixed(3)}`)
console.log(`  Relevance:    ${results.relevance.toFixed(3)}`)
console.log(`  Faithfulness: ${results.faithfulness.toFixed(3)}`)

Run this suite on every pipeline change — new chunking strategy, different embedding model, index parameter tuning. Track metrics over time. A 2% drop in recall@10 after a chunker change tells you more than any amount of manual spot-checking.

Production Architecture

A production RAG pipeline is more than retrieval. Here’s the full architecture, from document ingestion through to generation:

# Production RAG Pipeline Architecture

Source Files (HTML, CSV, JSON, TXT)
        |
        v
  +-----------+
  |  Loaders  |  htmlLoader, csvLoader, jsonLoader, textLoader
  +-----------+
        |
        v
  +-----------+
  |  Chunker  |  recursiveChunker (with metadata preservation)
  +-----------+
        |
        v
  +------------+
  | Embeddings |  createBatch → OpenAI text-embedding-3-large
  +------------+
        |
        v
  +--------------+      +----------------+
  | Vector Store | <--- | HNSW Index     |
  | (pgvector)   |      | (efSearch=100) |
  +--------------+      +----------------+
        |
        v
  +------------------+
  | Hybrid Retriever |  dense (0.7) + BM25 sparse (0.3)
  +------------------+
        |
        v
  +-------------+
  | MMR Rerank  |  lambda=0.7, topK=5
  +-------------+
        |
        v
  +-----------+      +-----------------+
  |  Context  | <--- | Response Cache  |
  |  Assembly |      | (Redis, TTL=1h) |
  +-----------+      +-----------------+
        |
        v
  +------------+
  | Generation |  LLM with system prompt + retrieved context
  +------------+

Caching layer: Cache at two levels. First, cache embedding results by content hash (so re-ingesting unchanged documents costs nothing). Second, cache final retrieval results by query hash with a short TTL — identical queries within a window skip the entire retrieval pipeline.

Cost controls: Embedding is the most expensive step. Use text-embedding-3-small for initial development and switch to text-embedding-3-large only when evaluation metrics justify it. Batch embedding with high concurrency minimizes wall-clock time, and incremental ingestion ensures you never pay to re-embed unchanged content.

Performance Checklist

Before shipping your RAG pipeline to production, validate each layer against this checklist:

Concern Solution Package
Chunk boundary context loss recursiveChunker with preserveMetadata and structural separators @elsium-ai/rag
Slow embedding at scale createBatch with high concurrency and exponential backoff @elsium-ai/rag
Re-embedding unchanged docs createIncrementalIngester with content hashing @elsium-ai/rag
Low recall on exact keywords createHybridRetriever combining dense + BM25 sparse search @elsium-ai/rag
Redundant retrieved chunks mmrRerank with lambda tuning for diversity @elsium-ai/rag
Vector index latency HNSW index with tuned efSearch and m parameters @elsium-ai/rag
Connection pool exhaustion createPgVectorStore with pool min/max and idle timeout @elsium-ai/rag
Multi-format source ingestion defineParallelWorkflow with per-format loader branches @elsium-ai/workflows
No retrieval quality metrics runEvalSuite with precision, recall, relevance, and faithfulness @elsium-ai/testing
Embedding cost overruns Incremental ingestion + response caching + model size selection @elsium-ai/rag

What’s Next

This article covered the core mechanics of production RAG. From here, you can extend your pipeline with:

Check out the @elsium-ai/rag docs for the full API reference, or explore 30 more products you can build with ElsiumAI.


E

Eric Utrera

Creator of ElsiumAI. Building production AI infrastructure.