Docling for RAG
From document to retrieval-ready chunks: which chunker to pick, how to size chunks with the right tokenizer, how tables and citations survive, which framework integration fits, and which vector store to pair — checked against the official chunking concepts and serialization reference.
Which RAG path fits you?
| Your situation | Path | Why |
|---|---|---|
| Already on LangChain | langchain-docling DoclingLoader | Official loader; DOC_CHUNKS mode chunks natively, MARKDOWN mode + header splitter as fallback. |
| Already on LlamaIndex | DoclingReader + Docling Node Parser | Reader loads lossless JSON or lossy Markdown; parser turns Docling format into Nodes. |
| Already on Haystack | docling-haystack converter | Docling as a Haystack converter component. |
| Framework-free / custom store | HybridChunker directly | Full control: chunk, contextualize, embed, upsert anywhere (Qdrant, Milvus, Chroma, Pinecone…). |
| Just need chunk files fast | CLI --to chunks | No Python: hybrid or hierarchical chunks straight from the terminal. |
| Mass ingestion at scale | Data Prep Kit + Docling | Chunk + tokenize pipelines built for large corpora. |
Where Docling sits in RAG
A RAG pipeline has six stages; Docling owns the first three — the ones that decide answer quality before any embedding exists:
- Parse — layout, reading order, tables, formulas, pictures → one structured DoclingDocument.
- Serialize / chunk — structure-aware chunks with headings, captions and provenance (or a Markdown/HTML/JSON export for post-splitting).
- Enrich text —
contextualize()prepends heading context so each chunk stands alone. - Embed → store → retrieve → generate — your embedding model, vector store and framework; Docling-agnostic.
There are two chunking philosophies: export to Markdown and post-split (e.g. LangChain’s MarkdownHeaderTextSplitter), or chunk natively on the DoclingDocument. Native chunking preserves tables, headings and provenance that Markdown post-splitting silently drops — prefer it unless you have a reason not to.
Convert: keep the DoclingDocument
docling convert report.pdf --to mdFor a programmatic pipeline, convert in Python and keep the document object — chunkers operate on it, not on files:
from docling.document_converter import DocumentConverter
converter = DocumentConverter()
result = converter.convert("report.pdf")
document = result.document
Choose your export: Markdown, JSON, HTML, DocTags
Exports are shorthands over serializers (MarkdownDocSerializer etc.). For RAG the choice matters mostly for tables:
| Export | Table spans | RAG use |
|---|---|---|
export_to_markdown() | Flattened — no span syntax; spanned cells render empty | Default text path; fine unless merged header cells matter. |
export_to_html() | Preserved — native rowspan/colspan | Best text export when merged cells carry meaning. |
export_to_dict() / JSON | Preserved losslessly — full TableData incl. spans | Lossless path (LlamaIndex reader JSON mode); heaviest payloads. |
| DocTags / Docling Language | Preserved — OTSL continuation tokens | Compact structure-preserving format for Docling-native tooling. |
Serializers are customizable: subclass BaseTableSerializer and friends for full control (see the advanced chunking & serialization example).
Chunker comparison
| Chunker | Strategy | Pick when |
|---|---|---|
| HybridChunker (default) | Hierarchical structure + tokenizer-aware split-when-oversized + merge-when-undersized | Default for RAG. Balanced, heading-aware, table-aware. |
| HierarchicalChunker | One chunk per document element; merges list items (opt-out via merge_list_items) | Fine-grained, element-level chunks with full metadata. |
| LineBasedTokenChunker | Keeps line boundaries; splits a line only if it alone exceeds the limit | Tables, code, logs, lists — anything line-structured. |
| TrivialChunker | Minimal baseline chunking | Debugging and benchmarking, not production retrieval. |
| Markdown post-splitting | Export to Markdown, then split (e.g. MarkdownHeaderTextSplitter) | Only when a framework pipeline demands Markdown input. |
All native chunkers implement BaseChunker (chunk() → chunk stream, contextualize() → enriched text), so LlamaIndex-style integrations accept any built-in, custom or third-party chunker through the same interface.
HybridChunker in depth (the default)
from docling.chunking import HybridChunker
chunker = HybridChunker()
chunks = list(chunker.chunk(document))
print(len(chunks), chunks[0].text[:120])
How it works: start from hierarchical chunks, then one pass splits only oversized chunks (token-aware) and another merges only undersized successive peers with the same headings & captions. Key parameters:
tokenizer— align to your embedding model’s tokenizer (section 9). Default derives limits from the tokenizer.max_tokens— cap per chunk; the enriched (contextualized) form is what should fit.merge_peers=True— merge undersized peers; set False to keep splits strict.repeat_table_header=True— every table chunk starts with the header row (section 7).omit_header_on_overflow=False— drop the header for rows that fit without it but overflow with it (wide tables, strict budgets).serializer_provider— e.g. Markdown tables viaChunkingSerializerProviderfor chunk text shape control.
Embed the contextualized text, not chunk.text:
for chunk in chunker.chunk(document):
enriched = chunker.contextualize(chunk) # headings prepended — embed THIS
vector = embed(enriched)
chunk.text alone — the prepended headings are what make chunks retrievable.Two operational notes: the transformers “Token indices sequence length …” warning during chunking is a documented false alarm (see the official FAQ), and set TOKENIZERS_PARALLELISM=false to silence fork warnings in servers.
Tables in RAG: headers, spans, formats
- Repeat headers (
repeat_table_header=True, default): each chunk of a split table starts with the header row, so every chunk is self-describing for the embedding model. - Overflow escape (
omit_header_on_overflow=True): for wide tables, rows that fit without the header but overflow with it skip the header — token efficiency without breaking row integrity. - Merged cells: Markdown flattens spans; if merged headers matter, chunk from HTML/JSON serialization or a custom table serializer instead.
- Line-based alternative:
LineBasedTokenChunkerwith a repeated prefix keeps CSV-like rows intact and supports the same overflow logic viaomit_prefix_on_overflow.
docling convert data.csv --to mdWorked demonstrations: hybrid chunking (incl. header repetition on a wide CSV), line-based chunking, table extraction.
Metadata & citations (DocMeta)
Every chunk carries dl_meta — keep it in your vector payload; it is what turns “an answer” into “an answer with a source”:
| Field | Contains | Use for |
|---|---|---|
headings | Heading path, e.g. ["3.2 AI models"] | Section labels, contextual prefixes, filters. |
origin | Mimetype, filename, binary hash | Document identity, dedup, source links. |
doc_items | Self refs, labels, provenance: page_no, bbox, charspan | Page citations, bounding-box highlighting, visual grounding. |
Provenance includes page numbers and bounding boxes per item — enough to cite “page 3” or draw the source region (see visual grounding).
Tokenizers: match your embedding model
| Tokenizer | Install | Notes |
|---|---|---|
HuggingFace (HuggingFaceTokenizer) | pip install "docling-core[chunking]" | Default path. max_tokens optional — derived from the tokenizer. Example: sentence-transformers/all-MiniLM-L6-v2 (also the CLI default). |
OpenAI / tiktoken (OpenAITokenizer) | pip install "docling-core[chunking-openai]" | Requires explicit max_tokens (context window, e.g. 128 * 1024 for gpt-4o). |
from transformers import AutoTokenizer
from docling_core.transforms.chunker.tokenizer.huggingface import HuggingFaceTokenizer
from docling.chunking import HybridChunker
EMBED_MODEL_ID = "sentence-transformers/all-MiniLM-L6-v2"
tokenizer = HuggingFaceTokenizer(
tokenizer=AutoTokenizer.from_pretrained(EMBED_MODEL_ID),
max_tokens=512,
)
chunker = HybridChunker(tokenizer=tokenizer, merge_peers=True)
At corpus scale, the Data Prep Kit chunk+tokenize pipeline applies the same principle in batch.
Framework integrations
LangChain — langchain-docling
pip install langchain-docling langchain-huggingface langchain_milvusfrom transformers import AutoTokenizer
from docling_core.transforms.chunker.tokenizer.huggingface import HuggingFaceTokenizer
from docling.chunking import HybridChunker
from langchain_docling import DoclingLoader
from langchain_docling.loader import ExportType
tokenizer = HuggingFaceTokenizer(
tokenizer=AutoTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2")
)
loader = DoclingLoader(
file_path=["https://arxiv.org/pdf/2408.09869"],
export_type=ExportType.DOC_CHUNKS, # native chunks (default)
chunker=HybridChunker(tokenizer=tokenizer),
)
docs = loader.load() # one LangChain Document per chunk, dl_meta in metadata
Two modes: DOC_CHUNKS (default — one LangChain Document per native chunk, metadata preserved) and MARKDOWN (one Document per file; split downstream, e.g. with MarkdownHeaderTextSplitter on #/##/###). Full flow in the official LangChain RAG example (Milvus + Mixtral) and the LangChain guide.
LlamaIndex — reader + node parser
pip install llama-index-readers-docling llama-index-node-parser-doclingfrom llama_index.readers.docling import DoclingReader
reader = DoclingReader(export_type="json") # lossless; "markdown" for lossy
documents = reader.load_data("report.pdf")
DoclingReader populates LlamaIndex Documents (lossless JSON or lossy Markdown); the Docling Node Parser turns Docling-format Documents into Nodes using its knowledge of the format. Any BaseChunker implementation plugs into the same interface. See the official LlamaIndex RAG example.
Haystack and the rest
pip install docling-haystackHaystack ships Docling as a converter component (example, integration docs). Beyond the big three: txtai, Kotaemon, DocETL, Vectara, Semantica, Hector, haiku.rag, Bee, CrewAI, Langflow, Open WebUI, spaCy, NVIDIA, Granite cookbook RAG and Data Prep Kit all integrate Docling — browse the official integrations index.
Vector stores: worked examples
The store never affects Docling — chunks + embeddings upsert anywhere. Official worked examples exist for:
| Store | Example | Notes |
|---|---|---|
| Milvus | RAG with Milvus | Also the store in the LangChain example (local docling.db, FLAT index). |
| Weaviate | RAG with Weaviate | Native vector + hybrid search options. |
| Qdrant | Retrieval with Qdrant | Retrieval-focused recipe. |
| OpenSearch | RAG with OpenSearch | Search + vector in one engine. |
| MongoDB + VoyageAI | RAG with MongoDB | Atlas Vector Search + VoyageAI embeddings. |
| Azure AI Search | RAG with Azure AI Search | Managed retrieval on Azure. |
| Chroma / Pinecone / others | — | No official recipe; same pattern: embed contextualize(chunk), upsert text + DocMeta. |
Chunks from the CLI (no Python)
docling convert report.pdf --to chunks --chunks-type hybriddocling convert report.pdf --to chunks --chunks-type hybrid --chunks-max-tokens 512 --chunks-tokenizer sentence-transformers/all-MiniLM-L6-v2--chunks-type accepts hybrid (default) or hierarchical; the tokenizer defaults to sentence-transformers/all-MiniLM-L6-v2. Remote conversion (docling convert-remote / docling-serve) supports the same chunk options server-side. See the command finder.
Production ingestion checklist
- Tokenizer == embedding model. Always. Re-check on every model swap.
- Embed
contextualize()output, storechunk.textalongside for display. - Keep DocMeta (headings, page_no, bbox, origin) in the payload for citations.
- Repeat table headers; use HTML/JSON source for merged-cell tables.
- Trim conversion cost:
--no-ocrfor digital PDFs, skip enrichment you don’t rank on, batch-convert and pre-download models (docling-tools models download --all). - Scale-out: Data Prep Kit chunk+tokenize for corpora; docling-serve
/v1/convert/sourcewith--to chunksbehind a queue for services.
Troubleshooting RAG ingestion
- Answers lack context / “orphan” chunks — you embedded
chunk.text; switch tocontextualize()and confirm headings appear. - Table answers are wrong — enable
repeat_table_header; check merged cells (Markdown flattens spans → use HTML/JSON). - Transformers sequence-length warning — documented false alarm, safe to ignore (official FAQ).
- Chunks too big/small for the embedding model — tokenizer mismatch; set the chunker tokenizer to the embedding model’s and tune
max_tokens. - No citations possible — metadata dropped at upsert; persist
dl_meta(page_no, headings, origin). - Ingestion too slow — see conversion is slow: disable unneeded OCR/enrichment, use GPU, batch.
RAG FAQ
Which chunker should I start with?
chunk.text or contextualize() — what do I embed?
chunker.contextualize(chunk) for embeddings. It prepends the heading path, which is what makes a chunk self-contained for retrieval. Keep chunk.text for display.Do I need LangChain / LlamaIndex / Haystack at all?
Markdown export + text splitter vs native chunking?
How do I keep table structure in retrieval?
repeat_table_header), mind overflow (omit_header_on_overflow), and source tables from HTML/JSON when merged cells matter. LineBasedTokenChunker is the specialist for row-oriented data.How do answers cite pages?
dl_meta (headings, page_no, bbox, origin) in the vector payload and return it with hits. Provenance supports page citations and even bounding-box highlighting.Which vector store works with Docling?
How do I scale ingestion?
Verified with Docling v2.129.0 · Last checked 2026-09-22 · Official source