RAG & Retrieval Systems Series — Presentation 04

Chunking & Ingestion — The Unit You Index

Chunking strategies, layout-aware parsing (Unstructured, MinerU, LlamaParse, Docling), ColPali vision parsing, metadata enrichment, deduplication, and incremental ingestion design.

Chunking Unstructured MinerU LlamaParse Docling ColPali MinHash
Raw docs → Parse → Chunk → Enrich metadata → Dedup → Embed → Index
00

Topics We'll Cover

01

Chunking Philosophy — The Unit You Index Is the Unit You Retrieve

Chunking is the most consequential decision in a RAG pipeline and the most overlooked. The chunk is the atomic unit of retrieval: what you index is what you return to the LLM. A poor chunk boundary splits an answer across two chunks; a poor chunk size under-represents context or wastes token budget.

The embedding granularity tension

Short chunks (64–128 tokens): precise retrieval, poor context. The embedding captures the specific fact but the LLM sees it without surrounding explanation. Long chunks (512–1024 tokens): rich context, imprecise retrieval. The embedding averages over too many topics. The sweet spot for most corpora is 256–512 tokens with 20% overlap.

Parent–child chunking

Index small chunks for retrieval precision, but return the parent chunk (the surrounding paragraph or section) to the LLM for richer context. LlamaIndex's ParentDocumentRetriever and LangChain's ParentDocumentRetriever both implement this. The trade-off: higher LLM token cost per retrieved chunk.

The retrieval unit vs context unit distinction

Retrieval unit: the chunk whose embedding is compared against the query embedding. Should be specific and dense with relevant content. Context unit: what the LLM actually reads. May be the parent of the retrieval unit, augmented with surrounding sentences, or the full source document section. These are separate concepts — conflating them is the source of most chunking bugs.

02

Strategies — Fixed, Semantic, Structural, Contextual (Anthropic)

Four distinct chunking philosophies, each suited to different document types and retrieval objectives.

Fixed-size

Split every N tokens with an overlap of M tokens (e.g. 512/50). Fast, predictable, no NLP required. Works for homogeneous plain-text corpora. Fails badly on structured documents: splits mid-table, mid-code-block, mid-formula. Implementation: LangChain TokenTextSplitter with tiktoken.

Semantic / sentence-boundary

Use a sentence splitter (spaCy sentenciser, NLTK PunktTokenizer) to split on sentence boundaries, then group sentences until a token budget is reached. Produces more natural chunks. Semantic chunking (Kamradt 2023) uses embedding similarity between adjacent sentences to detect topic shifts; computationally expensive but highest quality for long prose.

Structural

Respect document structure: split at headings (Markdown H1/H2), XML/HTML tags, PDF logical sections, code function boundaries. Each chunk maps to a logical unit. Requires a parser that understands the document format. Docling and Unstructured both output structured elements that enable this natively.

Contextual (Anthropic)

Introduced in Anthropic's Contextual Retrieval blog post (2024). For each chunk, prepend a short context paragraph generated by Claude explaining what the chunk is about relative to the full document. This "context-enhanced chunk" is then embedded. Improves Recall@20 by 35% on some benchmarks. Cost: one LLM call per chunk at index time.

03

Recursive Character Splitting and Where It Breaks

RecursiveCharacterTextSplitter (LangChain) is the most widely used open-source chunker. It tries separators in order (["\n\n", "\n", ". ", " ", ""]) and recursively subdivides sections that exceed the target chunk size.

RecursiveCharacterTextSplitter — key configuration
from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,           # in characters, not tokens!
    chunk_overlap=64,
    length_function=len,
    # For token-accurate splitting, pass tiktoken counter:
    # length_function=lambda s: len(enc.encode(s))
    separators=["\n\n", "\n", ". ", " ", ""],
    is_separator_regex=False,
)

chunks = splitter.split_text(document_text)

Where it fails:

Use character count or token count?

Always use token count for chunks destined for an LLM context window. Character count underestimates cost for CJK text (each character is 1 char but 1–3 tokens) and overestimates for ASCII code (1 identifier may be 3+ tokens). Pass a tiktoken encoder as the length function; the overhead is negligible.

04

Layout-Aware Parsing — Unstructured, MinerU, LlamaParse, Docling

For PDFs, Word documents, HTML, and other richly structured formats, text extraction must preserve layout semantics. Four tools dominate this space.

ToolApproachOutput formatBest forLimitation
Unstructured (v0.14)Rule-based + ML element detection (YOLOv8 for layout, Tesseract for OCR). Open-source core + hosted API.List of Element objects (Title, NarrativeText, Table, ListItem…)General-purpose; large pipelines; LangChain/LlamaIndex integrationTable extraction quality lags behind commercial tools
MinerU (v1.x)PDF-focused pipeline: PyMuPDF layout + PaddleOCR + custom table detector. Open-source (AGPL).Markdown + JSON with bounding boxesAcademic papers, Chinese-language PDFs, complex multi-column layoutsRequires PaddlePaddle; slower than Unstructured for simple docs
LlamaParse (cloud)Cloud API by LlamaIndex; proprietary LLM-assisted parsingMarkdown, JSON, HTMLHighest quality on complex layouts; charts, nested tablesAPI cost; no self-host; privacy constraints
Docling (v2.x, IBM)Multi-modal: layout model (DocLayNet-trained) + OCR (EasyOCR/Tesseract) + table structure recognitionDoclingDocument (JSON) + Markdown exportEnterprise documents; hierarchical section structure; open-sourceNewer; smaller ecosystem than Unstructured
Docling is the emerging open-source leader

Docling v2 (2024) uses IBM's DocLayNet dataset (80k+ annotated pages) and produces a DoclingDocument with a full document hierarchy: document → sections → paragraphs → cells. The hierarchy directly enables structural chunking. Install: pip install docling. It supports PDF, DOCX, PPTX, HTML, and image inputs and runs fully locally including OCR and table structure recognition.

05

ColPali & Vision-Based Document Parsing

ColPali (Faysse et al. 2024) takes a radically different approach: instead of extracting text from PDFs, it embeds each page image directly using a PaliGemma-based vision encoder, then indexes per-patch token embeddings using ColBERT-style late interaction.

PDF page
(rendered as image)
→
PaliGemma
vision encoder
→
1030 patch
token vectors
[1030 × 128]
→
Index in
Qdrant (multi-vector)
→
MaxSim
retrieval

Why this matters: ColPali retrieves pages containing charts, complex tables, and diagrams that would be mangled by text extraction. On the DocVQA and SlideVQA benchmarks, ColPali achieves 81.3% NDCG@5 vs 60.1% for the best text-extraction + BGE-M3 pipeline.

ColPali indexing via byaldi (pip install byaldi)
from byaldi import RAGMultiModalModel

model = RAGMultiModalModel.from_pretrained("vidore/colpali-v1.3")

# Index a folder of PDFs (converts to page images internally)
model.index(
    input_path="./pdfs/",
    index_name="my_docs",
    store_collection_with_index=True,
    overwrite=True,
)

# Retrieve: returns ranked list of (page_image, score, page_num)
results = model.search("quarterly revenue breakdown by region", k=5)
# Pass page images directly to a VLM (e.g. GPT-4o, Claude 3)
ColPali storage cost is significant

Each PDF page produces ~1030 patch vectors of 128 dims: ~528 KB per page in float32. A 1000-page document corpus costs ~500 MB. For large corpora, combine with binary quantisation (Qdrant multi-vector support) to reduce to ~16 KB per page. ColPali is best used for document types where text extraction routinely fails: scanned documents, slide decks, financial tables, engineering drawings.

06

Metadata Enrichment — Sections, Authors, Dates, Citations

Metadata is the invisible infrastructure of effective filtered retrieval. Well-structured metadata turns a slow post-filter into a fast pre-filter and enables faceted search that embedding similarity cannot provide.

Structural metadata (always extract)

  • source_url / file_path — for provenance and citation
  • page_num, section_heading, paragraph_index
  • doc_type: PDF / DOCX / HTML / Markdown / code
  • ingested_at Unix timestamp; doc_modified_at
  • language: ISO 639-1 code (from langdetect or lingua-py)

Semantic metadata (enrich with LLM/NER)

  • summary: 1–3 sentence chunk summary (embed separately for parent-child retrieval)
  • entities: named entities (persons, orgs, products, CVEs)
  • topics: top-k topic labels (zero-shot classifier or LLM)
  • citations: DOI, arXiv ID, URL references within the chunk
  • confidence: OCR confidence score (flag low-quality extractions)
Index metadata separately from embeddings

Do not concatenate all metadata into the chunk text before embedding — it biases the embedding towards the metadata rather than the content. Store metadata as payload fields in the vector DB (Qdrant, Weaviate) or as separate columns (pgvector). Build payload indexes on high-cardinality filter fields (tenant_id, doc_type, date) so that the vector DB can use them for pre-filtering without a full scan.

07

Deduplication — MinHash, SimHash, Near-Duplicate Detection

Duplicate and near-duplicate chunks harm retrieval quality: they consume index space, inflate the apparent corpus size, and cause the reranker to return the same information multiple times. Web-scraped and CMS-exported corpora routinely contain 5–30% near-duplicate content.

TechniqueMechanismSimilarity thresholdCostUse case
Exact hashSHA-256 of normalised text. O(1) lookup.100% onlyNegligibleRemove literal duplicates (copy-paste, re-ingested files)
SimHash (Charikar 2002)Bit-similiarity of 64-bit fingerprints; Hamming distance ≤ 3 ≈ ≥90% Jaccard. Fast comparison.≈90%+O(N log N)Near-duplicate detection in real-time ingestion pipelines
MinHash + LSHEstimate Jaccard similarity via min-hash signatures; use Locality Sensitive Hashing to bucket candidates. Scalable to billions.Configurable (60–95%)O(N) with tunable recall/precisionLarge-scale corpus dedup (web crawls, CommonCrawl-style pipelines)
Embedding cosineCompute cosine similarity between chunk embeddings; deduplicate above threshold (>0.97).Semantic near-duplicatesO(N²) or ANNParaphrase dedup; requires embedding pass first
MinHash deduplication with datasketch
from datasketch import MinHash, MinHashLSH

lsh = MinHashLSH(threshold=0.8, num_perm=128)
for idx, chunk in enumerate(chunks):
    m = MinHash(num_perm=128)
    for word in chunk.split():
        m.update(word.encode("utf-8"))
    duplicates = lsh.query(m)
    if not duplicates:
        lsh.insert(str(idx), m)   # unique chunk
    # else: skip chunk as near-duplicate
08

Pipeline Shape — Incremental Ingestion, Embedding Refresh

Most production RAG systems ingest continuously (new documents arrive daily or hourly) and occasionally need to re-embed (new embedding model, chunking strategy change). These are separate operational concerns.

Document store (S3 / GCS / Postgres)
↓
Change detection: file hash / modified_at / Kafka event
↓
Parse & chunk (Docling / Unstructured)
↓
Dedup check (SimHash / MinHash)
↓
Embed (batch encode, GPU, bf16)
↓
Upsert into vector DB (Qdrant / pgvector)
Embedding refresh without downtime

When migrating to a new embedding model: (1) build a shadow index with the new model in a separate collection; (2) dual-read from old and new during the migration window; (3) A/B test on 5% of traffic; (4) cut over. Never rebuild the primary index in place — a half-migrated index has mixed-model vectors and the similarity scores are meaningless. Qdrant's collection aliases (PUT /aliases) make the cutover atomic from the application's perspective.

09

What to Take Away

Where to next

Deck 05 covers agentic RAG patterns: HyDE, query decomposition, Self-RAG, CRAG, iterative retrieval loops, query routing, and tool-using RAG combining retrieval with code execution and web search.