Thursday AI Tool/Workflow. This week's tool: Docling, the open-source document converter started by IBM Research Zurich, now hosted by the LF AI & Data Foundation. It turns PDFs, DOCX, PPTX, HTML and more into clean Markdown or lossless JSON, and it can chunk the result for RAG without throwing away your section headings.
If your RAG pipeline starts with pdftotext or a basic PDF text extractor, you've probably seen what comes out: two-column pages read straight across, tables flattened into a stream of numbers, page headers and footers mixed into body text, and no idea which section a sentence came from. Then you split that text every N characters and wonder why retrieval is weak.
Docling attacks the problem at the source. Here's exactly how to use it.
What Docling gives you
From the official README, the features that matter for RAG:
- Parsing of many formats, including PDF, DOCX, PPTX, XLSX, HTML, EPUB, images and more
- Advanced PDF understanding: page layout, reading order, table structure, code, formulas and image classification
- OCR support for scanned PDFs and images
- A unified
DoclingDocumentrepresentation, exportable to Markdown, HTML, DocTags and lossless JSON - Local execution, which matters for sensitive or air-gapped data
- Integrations with LangChain, LlamaIndex, Crew AI and Haystack
- MIT-licensed codebase (individual models have their own licenses)
1. Install
pip install docling
Docling needs Python 3.10 or higher (Python 3.9 support was dropped in version 2.70.0). It runs on macOS, Linux and Windows, on both x86_64 and arm64.
2. Convert a document from the CLI
The fastest way to see what Docling does:
docling https://arxiv.org/pdf/2206.01062
This writes a .md file into the current directory. The CLI takes local files, directories or URLs, and a few options are worth knowing:
# Markdown + lossless JSON into ./out
docling ./policies --to md --to json --output ./out
# Scanned documents: OCR is on by default; turn it off for born-digital PDFs to save time
docling report.pdf --no-ocr
# Table structure model in accurate (default) or fast mode
docling report.pdf --table-mode fast
# Ask the CLI for chunks directly (hybrid chunker by default)
docling report.pdf --to chunks --chunks-max-tokens 512
Run docling --help or check the CLI reference for the full list. The default chunk tokenizer for --to chunks is sentence-transformers/all-MiniLM-L6-v2.
3. Convert in Python
The Python API is three lines:
from docling.document_converter import DocumentConverter
converter = DocumentConverter()
result = converter.convert("policies/leave-policy.pdf") # local path or URL
doc = result.document
print(doc.export_to_markdown())
doc is a DoclingDocument. Besides Markdown you can call export_to_dict(), export_to_text(), export_to_doctags(), or save lossless JSON with doc.save_as_json(path). The JSON keeps structure and provenance that Markdown drops, so it is the better format to store if you might re-chunk later.
4. Tables and layout
Tables are where naive extraction hurts most. Docling runs a table structure model and gives you each table as a real object:
for i, table in enumerate(doc.tables):
df = table.export_to_dataframe(doc=doc) # pandas DataFrame
df.to_csv(f"table-{i + 1}.csv")
In Markdown output, tables come out as Markdown tables instead of loose numbers. If you need to tune the PDF pipeline, pass pipeline options:
from docling.datamodel.base_models import InputFormat
from docling.datamodel.pipeline_options import PdfPipelineOptions
from docling.document_converter import DocumentConverter, PdfFormatOption
opts = PdfPipelineOptions()
opts.do_ocr = False # born-digital PDFs don't need OCR
opts.do_table_structure = True # keep table structure
converter = DocumentConverter(
format_options={InputFormat.PDF: PdfFormatOption(pipeline_options=opts)}
)
5. Chunk with HybridChunker
Docling has native chunkers that work on the DoclingDocument instead of on flat text. The docs describe HybridChunker as tokenization-aware refinements on top of document-based hierarchical chunking:
- It starts from the hierarchical chunker (one chunk per document element, with headings and captions attached).
- It splits chunks that are oversized for your tokenizer.
- It merges undersized neighbouring chunks that share the same headings (
merge_peers, on by default). - For tables that span chunks,
repeat_table_header(on by default) repeats the header row in each chunk.
The docs stress one thing: the chunker and your embedding model should use the same tokenizer.
from docling.chunking import HybridChunker
from docling_core.transforms.chunker.tokenizer.huggingface import HuggingFaceTokenizer
from transformers import AutoTokenizer
EMBED_MODEL_ID = "sentence-transformers/all-MiniLM-L6-v2"
tokenizer = HuggingFaceTokenizer(
tokenizer=AutoTokenizer.from_pretrained(EMBED_MODEL_ID),
max_tokens=256, # pick to fit your embedding model
)
chunker = HybridChunker(tokenizer=tokenizer)
for chunk in chunker.chunk(dl_doc=doc):
print(chunk.meta.headings) # e.g. ["Leave Policy", "Carry-over"]
text_to_embed = chunker.contextualize(chunk=chunk)
Note contextualize(): it returns the chunk text enriched with metadata such as its headings. The docs say this is typically the text you want to feed the embedding model, while chunk.text is the raw content. If you prefer OpenAI tokenizers, install docling-core[chunking-openai] and use OpenAITokenizer with tiktoken.
6. Example workflow: a folder of policy PDFs into a RAG index
This is an example workflow, not a benchmark. Say HR has a folder of policy PDFs and you want a Q&A bot that cites the right section.
from pathlib import Path
from docling.chunking import HybridChunker
from docling.datamodel.base_models import ConversionStatus
from docling.document_converter import DocumentConverter
converter = DocumentConverter()
chunker = HybridChunker() # default tokenizer; align it with your embedder in production
pdfs = sorted(Path("policies").glob("*.pdf"))
Path("parsed").mkdir(exist_ok=True)
records = []
for res in converter.convert_all(pdfs, raises_on_error=False):
if res.status != ConversionStatus.SUCCESS:
print("needs review:", res.input.file.name, res.status)
continue
doc = res.document
doc.save_as_json(Path("parsed") / f"{res.input.file.stem}.json") # keep lossless copy
for i, chunk in enumerate(chunker.chunk(dl_doc=doc)):
records.append({
"id": f"{res.input.file.stem}-{i}",
"text": chunker.contextualize(chunk=chunk), # embed this
"source": res.input.file.name,
"section": " > ".join(chunk.meta.headings or []),
})
# embed records[*]["text"] and upsert into your vector store
# (pgvector, Qdrant, Milvus...), storing source + section as metadata
What this buys you:
- Section headings travel with every chunk. "Carry-over" under "Leave Policy" doesn't get confused with "Carry-over" under "Expense Policy".
-
Citations are free. Store
sourceandsection, and your answer can say "Leave Policy > Carry-over" instead of "page text, somewhere". -
Failures are visible.
convert_all(..., raises_on_error=False)keeps going, and you log the files that only partially converted instead of silently indexing garbage. -
You can re-chunk without re-parsing. The saved JSON is a
DoclingDocument, so changing chunk size later doesn't mean running the PDF models again.
Tips before you ship
- Measure retrieval, not vibes. Build 20 to 50 real questions with known answer sections and check recall@k before and after switching to Docling.
- Turn OCR off for born-digital PDFs and on for scans. It is a real speed/quality trade-off.
- Spot-check tables. Open a few Markdown exports next to the original PDF, especially for merged cells.
- Match tokenizers. Chunk limits only mean something if they're counted with your embedding model's tokenizer.
Garbage in, garbage retrieved. Fix the parsing step and every step after it gets easier.
References
- Docling on GitHub: https://github.com/docling-project/docling
- Docling documentation: https://docling-project.github.io/docling/
- Usage (Python + CLI): https://docling-project.github.io/docling/usage/
- CLI reference: https://docling-project.github.io/docling/reference/cli/
- Chunking concepts (HybridChunker): https://docling-project.github.io/docling/concepts/chunking/
- Hybrid chunking example: https://docling-project.github.io/docling/_generated/examples/hybrid_chunking/
- Supported formats: https://docling-project.github.io/docling/usage/supported_formats/
- Docling Technical Report (arXiv 2408.09869): https://arxiv.org/abs/2408.09869
What does your RAG pipeline use to parse PDFs today, and where does it break?
Top comments (0)