QRefAI
Contents
Advanced RAG

Part 5 — The Ingestion Plane: Where Accuracy Is Won or Lost

How do you parse every document format faithfully, and how do you know when parsing has failed?

15 min · Updated July 2026

The ingestion plane is the most underinvested part of almost every RAG system. Engineers spend days fine-tuning prompts and hours thinking about chunking, and an afternoon on parsing — when the reality is inverted: no retrieval or agent cleverness can recover information that was destroyed at ingest. A garbled table is garbled forever. A figure stripped of its caption is permanently orphaned. A sentence split mid-clause by a fixed-size chunker will never be retrieved in its correct context.

5.1 Every format breaks in its own way — one parser won't save you

Using a single parser for all content is as misguided as using a single database for all data access patterns. Each format below is framed as the problem you’ll actually run into, followed by what solves it.

“My PDF parser turns a clean two-column report into scrambled, out-of-order text.”
Digitally-born PDFs and office documents have machine-readable text layers but complex layout, and they need a layout-aware parser that understands multi-column flow, heading hierarchy, and table structure. Docling (IBM, MIT license) is the current state-of-the-art OSS parser for this class: it combines a DocLayNet-trained layout model with a TableFormer model for cell-level table extraction, and it does so without a generative model — meaning it cannot hallucinate.
“I ran a scanned contract through my pipeline and got back confident nonsense.”
Scanned documents need OCR before any text extraction is possible, and the real trap is calibrated escalation: low-quality OCR that passes silently is worse than no OCR, because it produces confidently wrong text. A tiered strategy routes pages to increasingly powerful (and expensive) engines based on measured confidence, with commercial vision APIs as the last resort.
“The answer is in an earnings call recording, and my system can’t see audio at all.”
Audio and video need ASR to produce a transcript, optionally with speaker diarization and word-level timestamps, plus keyframe extraction and captioning. The canonical OSS tool is faster-whisper for high throughput, with WhisperX adding forced word alignment and pyannote-audio-based diarization.
“My chunker keeps splitting functions in half, so retrieved code never compiles in context.”
Source code needs AST-aware chunking rather than character-count chunking. Tree-sitter parses 40+ languages and gives you function and class boundaries as natural chunk boundaries.
“Users ask for an exact number and get a plausible-but-wrong one from a flattened table.”
Structured data (CSV / Excel / JSON) has a subtlety: don’t retrieve it as chunks at all. Route exact numeric queries to a text-to-SQL agent operating directly over the structured data — vector similarity over flattened table text gives approximate answers to questions that demand exact ones.
“The page looks full in my browser but comes back empty from my scraper.”
HTML and web content needs boilerplate-aware extraction (Trafilatura) for static pages and headless browser rendering (Playwright) for JavaScript-rendered dynamic pages.

5.2 Ten things that go wrong at ingest — and the pattern that stops each

Each row below is a failure you’ll hit in production, written the way you’d describe it to a colleague, paired with the pattern that prevents it and the lifecycle stage it belongs to — parse-time (get the bytes right), chunk-time (keep meaning intact), or operate-time (catch drift at scale).

#The problem you’ll hitStageThe pattern that stops it
1“My parser invented text that isn’t on the page.”ParseFaithful modular parsing (no generative model)
2“Bad OCR slipped through as if it were clean text.”ParseConfidence-routed OCR escalation
3“The numbers came out right but landed in the wrong cells.”ParseTable structure fidelity
4“A chunk says ‘revenue fell 12%’ with no who, when, or where.”ChunkContextual chunk enrichment
5“Retrieval keeps returning half a sentence or half a function.”ChunkStructure-aware chunk boundaries
6“I got a figure back with no caption, so it’s meaningless.”ChunkCross-modal linking and figure captioning
7“An over-long input got silently truncated and embedded wrong.”ParseEmbedding input validation
8“One malformed document quietly broke the whole batch.”OperateIdempotent quarantining pipeline
9“Bad parses piled up in the index and nobody noticed.”OperateConfidence-gated human review
10“A parser upgrade silently degraded extraction and we shipped it.”OperateGolden ingestion regression set

Three of these patterns are pure prose in most write-ups, so here is the minimal code for each. First, Pattern 4 — contextual chunk enrichment, the fix for the “revenue fell 12%” problem: prepend each chunk with the document and section context it was extracted from, so it can stand alone at retrieval time.

# Pattern 4: enrich each chunk with the context that gives it meaning.
def enrich_chunk(chunk_text: str, doc_meta: dict, section_path: list[str]) -> str:
    header = (
        f"[Source: {doc_meta['title']} ({doc_meta.get('date','n.d.')})] "
        f"[Section: {' > '.join(section_path)}]"
    )
    return f"{header}
{chunk_text}"      # "revenue fell 12%" → now answerable

# "revenue fell 12%" becomes:
# [Source: FY25 Annual Report (2025-11)] [Section: EMEA > Hardware > Q3]
# revenue fell 12%

Next, Pattern 7 — embedding input validation, which guards the silent truncation that shows up as embedding.truncation_count in the metrics table below. An input longer than the model’s context is silently cut, producing a vector for only part of the text.

# Pattern 7: never embed silently-truncated input. Count and split instead.
import tiktoken

def validate_for_embedding(text: str, model: str, max_tokens: int = 8191):
    enc = tiktoken.encoding_for_model(model)
    n = len(enc.encode(text))
    if n > max_tokens:
        # Emit metric embedding.truncation_count, then split rather than truncate.
        raise EmbeddingTooLong(tokens=n, limit=max_tokens)   # caller re-chunks
    return text

Finally, Pattern 8 — the idempotent quarantining pipeline: a single malformed document must never corrupt or silently drop a whole batch. Wrap each document so failures are isolated, logged, and replayable.

# Pattern 8: isolate failures. One bad doc can't poison the batch, and reruns are safe.
def ingest_batch(docs, process, quarantine, seen: set[str]):
    for doc in docs:
        if doc.content_hash in seen:
            continue                          # idempotent — safe to re-run
        try:
            result = process(doc)
            seen.add(doc.content_hash)
            yield result
        except Exception as e:
            quarantine.put(doc, reason=repr(e))   # isolated, replayable, visible

5.3 "Which tool does what?" — Docling, embeddings, and VLMs without overlap

These three components are not interchangeable — each occupies a distinct, non-overlapping role in the pipeline.

Docling is the authority on document structure. It runs deterministic, model-driven layout analysis (DocLayNet) and table extraction (TableFormer) — no generative component. Its outputs are structural: bounding boxes, element types, heading hierarchy, table cell grids, reading order. It also provides a calibrated confidence report per document and per page.

docling_confidence.py
from docling.document_converter import DocumentConverter, ConversionStatus

converter = DocumentConverter()

for result in converter.convert_all(sources, raises_on_error=False):
    if result.status == ConversionStatus.SUCCESS:
        confidence = result.document.confidence
        # confidence.mean_grade: POOR / FAIR / GOOD / EXCELLENT
        # confidence.low_grade: lowest single-page grade
        # confidence.ocr_score: 0.0 - 1.0 OCR quality
        # confidence.layout_score: 0.0 - 1.0 layout recognition quality
        route_by_confidence(result, confidence)
    else:
        quarantine(result)

Commercial vision APIs are used only for what Docling cannot handle deterministically. There are exactly three cases: tier-escalation OCR when Docling’s ocr_score is below threshold; figure captioning when a figure element has no associated caption in the document structure; and complex table re-extraction when structural ambiguity is present. Vision API calls are conditional, not universal. Applying them to every element would be extremely expensive and would introduce generative risk on clean documents that Docling handles deterministically.

OpenAI text-embedding-3 is applied after structural integrity is confirmed. It never embeds raw, unvalidated parser output. Garbage in, garbage embedded — and garbage embedded means garbage retrieved.

The data flow is a routing system with explicit quality gates, not a pipeline:

Diagram of Docling confidence-gated routing: documents are parsed and routed through GOOD/EXCELLENT, FAIR, POOR, and FAILED quality gates to chunking, VLM retranscription, human review, or quarantine

5.4 "How do I even know ingestion is failing?" — observability’s three jobs

Observability in ingestion serves three distinct functional roles.

Role 1: Observability is the routing signal.
The OCR escalation trigger (Pattern 2), the table-VLM fallback trigger (Pattern 3), and the human-review gate (Pattern 9) are all driven by Docling confidence metrics emitted as OTel spans. Without the telemetry, those routing decisions cannot be made. Observability is not decoration on the pipeline; it is structural plumbing.
Role 2: Observability is the early warning system.
When a new class of scanned documents arrives from a new vendor, the escalation rate metric will spike before any user notices degraded answers. The distribution of ocr_score, layout_score, and mean_grade across your document corpus tells you where the parsers are struggling.
Role 3: Observability is the regression gate.
Pattern 10 turns ingestion quality into a CI metric. Every pipeline change re-runs against the golden document set and compares extraction F1 and structural match against the approved baseline.

If you watch only one metric to start, watch docling.escalation_rate — a spike for a new source is the earliest signal that a document class is breaking your parsers, and it moves before answer quality visibly degrades.

MetricSourceAlert condition
docling.conversion_statusConversionStatus enumAny FAILED count above threshold
docling.mean_gradeconfidence.mean_gradeDistribution shift toward POOR/FAIR
docling.ocr_scoreconfidence.ocr_scoreMean below 0.7 for a source type
docling.escalation_rateOCR routing logicSudden spike for a new source
embedding.cache_hit_rateRedis cacheDrop below 60% (costly)
embedding.truncation_countPre-flight tiktoken checkAny non-zero in a batch
ingestion.quarantine_depthQuarantine queueAbsolute threshold
ingestion.dedup_ratioHash dedupUnexpected zero (may indicate hash collision)
contextual_prepend.cache_hit_rateLiteLLM prompt cacheDrop signals cost increase

5.5 "Do I really have to label everything?" — no, only what the gates flag

This is the part that most RAG implementations skip entirely, and it is the part that explains why some systems compound accuracy over time while others decay. The human’s role is not to replace automation — it is to curate the quality gates and training signals that automation cannot generate for itself.

The principle: label selectively, never in bulk. You never review every document. You review what the confidence signals flag. Steady-state human effort is low; it is front-loaded in bootstrapping and then sustained by a small, targeted review queue.

The three pipeline gates where humans can intervene:

Gate 1 (Quality review before indexing):
Documents with POOR grades are held. A human reviews in Argilla or Label Studio, corrects, and approves. The approved version goes to indexing. This prevents bad documents from ever entering the search index.
Gate 2 (Spot-check of chunk quality):
A random sample of chunks is surfaced in Argilla for a human reviewer to verify context enrichment, boundary quality, and cross-modal links. Identified problems become pattern updates.
Gate 3 (Query-answer review):
When a query returns a low-faithfulness or low-relevance score in online eval, a human reviews the trace — what was retrieved, what was generated — and annotates the correct retrieval. This becomes a golden query-answer pair.

The recommended annotation platform is Argilla (Apache-2) for AI-engineer and domain-expert collaboration on datasets. Label Studio (Apache-2) is preferred when reviewers need to work visually on the document image itself — bounding boxes, OCR regions, table cells.

Found this useful?