Chunking strategies, layout-aware parsing (Unstructured, MinerU, LlamaParse, Docling), ColPali vision parsing, metadata enrichment, deduplication, and incremental ingestion design.
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.
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.
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.
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.
Four distinct chunking philosophies, each suited to different document types and retrieval objectives.
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.
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.
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.
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.
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.
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:
def with no body.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.
For PDFs, Word documents, HTML, and other richly structured formats, text extraction must preserve layout semantics. Four tools dominate this space.
| Tool | Approach | Output format | Best for | Limitation |
|---|---|---|---|---|
| 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 integration | Table 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 boxes | Academic papers, Chinese-language PDFs, complex multi-column layouts | Requires PaddlePaddle; slower than Unstructured for simple docs |
| LlamaParse (cloud) | Cloud API by LlamaIndex; proprietary LLM-assisted parsing | Markdown, JSON, HTML | Highest quality on complex layouts; charts, nested tables | API cost; no self-host; privacy constraints |
| Docling (v2.x, IBM) | Multi-modal: layout model (DocLayNet-trained) + OCR (EasyOCR/Tesseract) + table structure recognition | DoclingDocument (JSON) + Markdown export | Enterprise documents; hierarchical section structure; open-source | Newer; smaller ecosystem than Unstructured |
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.
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.
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.
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)
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.
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.
source_url / file_path — for provenance and citationpage_num, section_heading, paragraph_indexdoc_type: PDF / DOCX / HTML / Markdown / codeingested_at Unix timestamp; doc_modified_atlanguage: ISO 639-1 code (from langdetect or lingua-py)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 chunkconfidence: OCR confidence score (flag low-quality extractions)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.
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.
| Technique | Mechanism | Similarity threshold | Cost | Use case |
|---|---|---|---|---|
| Exact hash | SHA-256 of normalised text. O(1) lookup. | 100% only | Negligible | Remove 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 + LSH | Estimate Jaccard similarity via min-hash signatures; use Locality Sensitive Hashing to bucket candidates. Scalable to billions. | Configurable (60–95%) | O(N) with tunable recall/precision | Large-scale corpus dedup (web crawls, CommonCrawl-style pipelines) |
| Embedding cosine | Compute cosine similarity between chunk embeddings; deduplicate above threshold (>0.97). | Semantic near-duplicates | O(N²) or ANN | Paraphrase dedup; requires embedding pass first |
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
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.
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.
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.