Source: http://www.poma-ai.com/docs/optimal-chunker-docling

# The Optimal Chunker for Docling

<ByAuthor />

**The short answer:** the optimal chunker for Docling is one that consumes the **DoclingDocument tree itself** — the typed `texts`/`tables`/`pictures`/`groups` structure — rather than a markdown flattening of it. POMA's bring-your-own-OCR connector does exactly this: you save `export_to_dict()` as JSON, upload it unmodified, and PrimeCut turns Docling's explicit `section_header` levels into real hierarchy depth, honors the furniture Docling already isolated, and emits **chunks** and **chunksets** — retrieval units that carry their own context, with no token-window tuning.

This page explains what the DoclingDocument gives you, why flattening it to markdown (or packing it into token windows) wastes it, and how to go from a `DocumentConverter` call to embedded chunksets in a few lines.

## What Docling gives you — and where it stops

Docling (IBM's open-source document converter, also served via docling-serve) is one of the strongest structure detectors in the open-source parsing world. Its output is not a string — it's the **DoclingDocument**, a typed tree:

```json
{
  "schema_name": "DoclingDocument",
  "texts": [
    { "label": "section_header", "level": 1, "text": "Master Services Agreement" },
    { "label": "section_header", "level": 2, "text": "Termination clauses" },
    { "label": "text", "text": "Either party may terminate…" },
    { "label": "page_footer", "text": "Confidential — page 41" }
  ],
  "tables": [ { "data": { "…": "cell grid" } } ],
  "pictures": [ { "…": "…" } ],
  "groups": [ { "name": "furniture", "…": "…" } ]
}
```

Three things in that tree are genuinely hard-won:

- **Explicit heading levels.** A `section_header` item carries a numeric `level` — Docling has already decided this is a level-2 heading, not just "a bold line."
- **Typed content.** Tables, pictures, and body text are separate item kinds with their own structure, not markdown approximations.
- **Furniture, pre-isolated.** Repeating page headers, footers, and page numbers are parked in the `furniture` group and labeled `page_header`/`page_footer`. Docling already did the noise classification for you.

What Docling deliberately does **not** do is decide what an embedding vector should represent. Its `HybridChunker` exists, but it is **token-window-based**: it packs document items into windows sized for your embedding model. That controls chunk *length* — it doesn't make each retrieved unit carry the heading path that explains it. And the more common failure is upstream of any chunker: most pipelines call `export_to_markdown()` first, flattening the tree into display text, then hand that string to a generic splitter.

## Why flattening the DoclingDocument wastes it

Run `export_to_markdown()` and then a `RecursiveCharacterTextSplitter`, and here is what happens to the structure Docling recovered:

| What Docling recovered | What markdown flattening + a character splitter does with it |
| --- | --- |
| `section_header` items with explicit `level` | Reduced to `#` glyphs, then ignored — cuts fall wherever the character count lands |
| Typed `tables` with cell grids | Serialized to markdown pipes, then sliced mid-row |
| `pictures` as positioned items | Reduced to refs, then regex-stripped silently |
| `furniture` group + `page_header`/`page_footer` labels | Flattened into the text stream — page furniture pollutes the index |
| Page provenance per item | Lost at flattening — chunks can't cite a page |

`HybridChunker` avoids the worst of this because it reads the tree, but its retrieval units are still token windows: the passage under *Early termination* arrives without machine-readable knowledge that it lives inside *Termination clauses* inside *Master Services Agreement*. At retrieval time the LLM reads an orphaned window and answers out of context. For the full taxonomy of these failure modes, see the [RAG chunking strategies guide](/rag-chunking-strategies-text-splitters).

## The optimal chunker: consume the tree natively

POMA's **bring-your-own-OCR** (BYOCR) connector treats Docling as a first-class precursor. It parses the **DoclingDocument tree, not the flattened markdown**, and runs only POMA's downstream stages on it:

1. **Shape validation, up front.** The payload is fingerprinted (`schema_name == "DoclingDocument"`, or an `md_content` envelope from docling-serve). A corrupted or mislabeled upload fails immediately with a clear 422 — never a silently degraded index.
2. **Heading levels, taken at face value.** `section_header` items with their explicit `level` become real `#`/`##`/`###` depth in POMA's hierarchy. Nothing is re-inferred that Docling already made explicit.
3. **Furniture honored.** Items in the `furniture` group and items labeled `page_header`/`page_footer` are dropped as the noise Docling already isolated — deterministic, and free, because your precursor did the classification. POMA's own running-header/footer strip runs on top, the same pass every natively ingested document gets.
4. **Tables stay tables.** Docling's table items survive into the chunk layer as HTML, so embeddings never see half a table.
5. **Pictures made visible.** Picture items go through the shared image stub/describe stage; any picture content stored outside the payload is counted in `content_metadata` with its refs neutralized — loss is surfaced, never silent.
6. **Hierarchy → chunks + chunksets.** Every sentence is emitted as a chunk with its `depth`, `page`, and a `to_embed` normalization — grouped into [chunksets](/learn/chunking/chunksets): root-to-leaf paths through the heading tree that keep each retrieved passage self-explanatory. No window size to tune.

Because you already ran the parsing step, **POMA charges only for the downstream structure and chunking value** — the OCR front-end is skipped entirely.

The result, measured on our reference legal-document benchmark: the same query answered with **337 tokens** of retrieved context instead of **1,542** for a recursive character splitter, with zero information loss. Methodology: [Document Ingestion & Chunking for RAG](/document-ingestion-chunking-rag).

## How to chunk Docling output with POMA

Run your Docling conversion exactly as you do today, save the tree as JSON, and hand it to PrimeCut:

```python
import json
from docling.document_converter import DocumentConverter
from poma import PrimeCut

# 1. Your existing Docling conversion — unchanged.
converter = DocumentConverter()
doc = converter.convert("contract.pdf").document
with open("contract.docling.json", "w") as f:
    json.dump(doc.export_to_dict(), f)

# 2. Chunk the DoclingDocument tree — POMA auto-detects the shape.
client = PrimeCut()  # reads POMA_API_KEY
result = client.ingest("contract.docling.json")

print(f"chunks: {len(result.chunks)}")
print(f"chunksets: {len(result.chunksets)}")
print(result.chunksets[0].to_embed)  # embedding-ready, hierarchy included
```

Three things worth knowing:

- **Auto-detection.** A JSON upload carrying `schema_name: "DoclingDocument"` is routed through the BYOCR seam automatically. To force the declaration — or to opt a look-alike JSON *out* of detection — set the `external_ocr_source` parameter to `"docling"` or `"none"` respectively on the ingest request.
- **docling-serve fallback.** If your payload is a docling-serve envelope carrying only `md_content` (the flattened markdown export), POMA accepts it via a markdown passthrough: noise stripping, heading-injection enrichment, hierarchy rebuild, chunks and chunksets. Prefer the full tree when you can request it — explicit `section_header` levels beat re-inferred ones.
- **Dedicated endpoint.** For explicit routing, the API exposes a `chunk_external_ocr_result` endpoint that accepts `external_ocr_source: "docling"`. See the [API reference](https://api.poma-ai.com/v3/docs).

From `result.chunks` / `result.chunksets` onward, everything is your stack: your vector store, your embedding model, your retrieval strategy.

## Chunking options for Docling output, compared

| Approach | Input | Structure used | Retrieval unit | Tables | Furniture |
| --- | --- | --- | --- | --- | --- |
| Recursive character splitter | Exported markdown | None | Fixed-size fragment | Cut mid-row | Indexed as noise |
| Markdown-header splitter | Exported markdown | `#` glyphs only | Isolated section fragment | Usually survive | Indexed as noise |
| Docling HybridChunker | DoclingDocument tree | Tree items | Token window, no ancestor path | Survive | Handled by Docling |
| **POMA BYOCR (PrimeCut)** | **DoclingDocument tree** | **Explicit `section_header` levels, full tree** | **Chunksets: leaf + all ancestors** | **HTML, never cut** | **Docling's labels honored + POMA strip** |

## Frequently asked questions

### How do I chunk Docling output for RAG?

Don't export to markdown and run a text splitter — you'd flatten the typed tree Docling just built. Save `export_to_dict()` as JSON and feed it to a structure-aware chunker. POMA's BYOCR connector walks the `texts`/`tables`/`pictures`/`groups` tree directly, keeps `section_header` levels as real hierarchy depth, and emits chunks plus chunksets ready for embedding.

### What is a good alternative to Docling's HybridChunker?

HybridChunker is token-window-based: it packs items into windows sized for your embedding model, which controls length but not meaning — a retrieved window still lacks the heading path that explains it. POMA consumes the DoclingDocument natively and emits chunksets, where every leaf travels with all of its ancestor headings.

### Can POMA ingest a DoclingDocument JSON directly?

Yes. Auto-detection fingerprints `schema_name == "DoclingDocument"`; you can also declare `external_ocr_source: "docling"` explicitly or `"none"` to opt out. A payload that doesn't match its declaration fails with a clear 422 — never silent degradation.

### Does POMA work with docling-serve output?

Yes. A full DoclingDocument is parsed as a tree; a response carrying only `md_content` falls back to a markdown passthrough with the same downstream stages. Request the tree when you can — explicit heading levels beat re-inferred ones.

### What happens to Docling's page headers and footers during chunking?

Docling parks them in the `furniture` group and labels them `page_header`/`page_footer`. POMA honors both signals and drops the noise deterministically — free, since your precursor already classified it — then applies its own running-header/footer strip on top.

### Should I export Docling to markdown before chunking?

Avoid it when you can. `export_to_markdown()` flattens explicit heading levels, typed tables, and furniture labels into display text that a downstream splitter must re-guess. Export the tree with `export_to_dict()` and chunk that; the `md_content` passthrough exists for when markdown is all you have.

## From Docling to your vector database

End-to-end recipes — Docling result in, optimal retrieval out:

- [Docling → Qdrant](/pipelines/docling-to-qdrant)
- [Docling → Pinecone](/pipelines/docling-to-pinecone)
- [Docling → Weaviate](/pipelines/docling-to-weaviate)
- [Docling → pgvector](/pipelines/docling-to-pgvector)
- [Docling → Milvus](/pipelines/docling-to-milvus)
- [Docling → Chroma](/pipelines/docling-to-chroma)

## The rest of the series

The optimal chunker, for every OCR and parsing tool you already run:

- [Mistral OCR](/optimal-chunker-mistral-ocr)
- [LlamaParse](/optimal-chunker-llamaparse)
- [Azure Document Intelligence](/optimal-chunker-azure-document-intelligence)
- [Unstructured.io](/optimal-chunker-unstructured)
- [Marker](/optimal-chunker-marker)
- [AWS Textract](/optimal-chunker-textract)

Or start with the fundamentals: [RAG chunking guide](/guides/rag-chunking/) · [Document ingestion guide](/guides/document-ingestion/) · [POMA chunksets](/learn/chunking/chunksets).