chunk-engine

Output Schema

The exact object every chunk is — with complete, real chunks for every content type, every mode, and every return shape.

View raw

Every chunk is an object with exactly three keys — content, content_type, metadata — in all three SDKs. Everything else on this page is about what actually turns up inside them.

Showing examples for Python
— your choice follows you across the docs.
KeyTypeAlways present
contentstringyes
content_typestring (contentType in JS)yes
metadataobject / dict / serde_json::Valueyes, but every key inside it is optional

A complete chunk

Not an excerpt — this is the whole object, captured from a real run at default parameters.

chunk = get_chunks("notes.md", mode="semantic")[3]

# Every chunk is a dict with exactly three keys:
{
    "content": "A naive character splitter cuts mid-sentence and mid-table. The retrieved\npassage then answers half a question, and the model fills in the rest. Keeping\na table whole costs nothing at index time and saves a wrong answer at query\ntime.",
    "content_type": "semantic",
    "metadata": {
        "avg_block_length": 234,
        "block_types": ["paragraph"],
        "chunk_index": 3,
        "document_metadata": {"source_type": "md", "total_input_blocks": 7},
        "has_list": False,
        "heading_path": ["Chunking Notes", "Why structure matters"],
        "keyword_density": 0.6,
        "merge_reasons": [],
        "paragraph_count": 1,
        "primary_merge_reason": "initial",
        "section_heading": "Why structure matters",
        "section_level": 2,
    },
}

Reading the fields

for chunk in chunks:
    chunk["content"]        # str
    chunk["content_type"]   # str, e.g. "heading", "semantic"
    chunk["metadata"]       # dict

What a whole call returns

from py_chunks import get_chunks, ChunksResult

# Without list_images the return is a plain list[dict].
chunks = get_chunks("report.docx")

# With it, a ChunksResult dataclass — two fields, never a bare list.
result = get_chunks("report.docx", list_images=True)
result.chunks    # list[dict] — text chunks AND image chunks, in document order
result.images    # dict[str, bytes] — {"75a3c27ad7854d78.png": b"\x89PNG\r\n..."}

# An image chunk: content is the image NAME, not the bytes.
# {"content": "75a3c27ad7854d78.png",
#  "content_type": "image",
#  "metadata": {"alt_text": "", "image_name": "75a3c27ad7854d78.png"}}

# Formats with no embedded-image support still return a ChunksResult —
# with an empty images dict, not an error.
SDKWithout imagesWith images
Pythonlist[dict]ChunksResult.chunks: list[dict], .images: dict[str, bytes]
JavaScriptPromise<Chunk[]>Promise<{ chunks, images }>images is an array of { name, data }, not a dict
RustVec<Chunk>(Vec<Chunk>, Vec<(String, Vec<u8>)>)

The images container differs by language

Python keys images by name (result.images["75a3c27ad7854d78.png"]); JavaScript and Rust hand you an ordered sequence of pairs. A port that assumes a dict on the JS side gets undefined, not an error.

Order and indexing

Chunks come back in document order — the order a person reads the file in. That is the only ordering guarantee, and it is the one worth relying on.

Position metadata is a different story, and the traps are worth stating plainly:

  • chunk_index is absent in default / structural for almost every format. It appears in the derived modes (section, semantic, sliding_window, sentence, page_aware) and in every spreadsheet and CSV mode. The exceptions run both ways: .doc and .ppt emit it in all modes, while DOCX omits it in section, semantic and sliding_window. If you need a stable position, use the index of the array you were handed.
  • total_chunks is emitted by .doc and .ppt in every mode, and by the whole DOCX family (.docx, .docm, .dotx, .dotm) in section and page_aware — measured across the fixture corpus 2026-08-16. In those two DOCX modes it means "part i of n of this section / this page", not a document-wide position, and it appears only on units that had to be split: a lists.docx run at default parameters returns seven page chunks, of which exactly two carry the keys. No other format emits it. Treat it as a split marker, never as a document length — for a stable document-wide position, use the index of the array you were handed.
  • Image chunks are not always where you expect. DOCX places each image chunk at its anchoring paragraph's position (indices 4 and 5 of 13 in all_round.docx). Every other image-bearing format puts all image chunks first — PDF, PPTX, XLSX, HTML, EPUB, IPYNB, EML/MBOX/MSG, ODT/ODP, DOC and PPT. So slicing "the first N chunks" as a preview can hand you nothing but filenames.

Chunk size

In the structural pipelinedefault / structural / section, which is what most formats run through — chunks target a maximum of 1,200 characters, widening to 1,550 when a short trailing paragraph is merged in rather than emitted as an orphan fragment. The bound is deliberate: a single 10,000-character chunk breaks the size contract every embedding model depends on, so oversized structures are split, not kept whole — a table larger than the budget becomes a run of table chunks, an oversized list or code block a run of bounded bullet_list / code_block chunks.

The 1,200/1,550 figures are per-mode, not global

Three modes bound their output differently, and reading 1,200 as a library-wide guarantee will surprise you:

modewhat bounds a chunk
default · structural · section1,200 chars, widening to 1,550
semanticthe 1,500-character cap (see semantic)
page_awareparagraph count, not charactersparagraphs_per_page (default 15). A page of 15 long paragraphs is legitimately several thousand characters.
sliding_windowwindow_size blocks, not characters
row (spreadsheets / CSV)rows_per_chunk; max_chunk_chars caps table / sheet / page_aware spreadsheet chunks only

Measured across the fixture corpus in section mode: 4 chunks out of hundreds of files exceed 2,000 characters, and every one is a single atomic unit — the exception described below, not a leak in the bound.

What the engine never does is cut through an atomic unit: a table split always lands between rows, a list split between items, a code split between lines — never mid-row or mid-item. The one case where a chunk exceeds the bound is a single atomic unit that is itself wider than the budget — one very wide table, CSV/TSV, or spreadsheet row lands whole in one oversized chunk, because slicing it would destroy the thing it represents.

Prose is always bounded: an over-long paragraph is cut at a sentence, and a sentence with no boundary at all is cut at a word.

content_type: two families, not one

The single most expensive assumption you can make about this API is that selecting a mode makes every chunk carry that mode's name. It does not, and which way it goes depends on the format.

Two real censuses, same mode, default parameters:

get_chunks("arxiv_1706.03762_attention.pdf", mode="section")
  44 chunks → heading: 16   section: 28          ← 36% are NOT "section"

get_chunks("all_round.docx", mode="section")
   9 chunks → section: 9                          ← everything absorbed

Filtering on the mode name silently drops content

[c for c in chunks if c["content_type"] == "section"] throws away sixteen heading chunks from that PDF — the section titles, the highest-signal text in the document. Filter on what you want to exclude, or don't filter at all.

Family A — structure survives the mode. PDF, Markdown, HTML, TXT, RTF, EPUB, IPYNB, EML/MBOX/MSG, ODT/ODP and JSON keep structural chunks alongside the mode's own chunks. Measured on the fixtures named above:

ModeWhat PDF emitsWhat Markdown emits
default / structuralheading, plain_paragraph, long_single_paragraph, short_disconnected_paragraph, bullet_list, tableheading, plain_paragraph, long_single_paragraph, short_disconnected_paragraph, code_block, bullet_list, table
sectionheading + sectionheading + section
semanticheading + table + semanticheading + code_block + table + semantic
sentenceheading + bullet_list + table + sentenceheading + code_block + table + bullet_list + sentence
page_awareheading + page_awareheading + page_aware
sliding_windowsliding_window onlysliding_window only

The rule inside this family: section and page_aware re-group prose and keep headings separate; semantic and sentence additionally pass atomic units (tables, code blocks, lists) through untouched, because re-grouping them would destroy them. HTML and EPUB are the soft edge — their semantic chunker can merge a heading into the chunk beneath it, so some headings do disappear there.

Family B — the mode replaces everything. DOCX, DOC, PPTX, PPT and the tabular formats emit one content type per mode, with no structural remainder. DOCX default emits mixed_content, heading and table; every derived mode emits only its own name.

sliding_window is Family B in every format, without exception — it is a flat window over the block stream and has no structure to preserve.

The catalogue

ValueWhere it comes from
headingA heading or section title, alone in its own chunk.
plain_paragraphA standard paragraph.
long_single_paragraphOne paragraph long enough to stand alone as a chunk.
short_disconnected_paragraphA short paragraph with no neighbour to merge with.
bullet_listA bulleted or numbered list — also the per-record type for JSON/JSONL/NDJSON.
tableA table, or one row-aligned segment of an oversized one.
code_blockA fenced or indented code block; an IPYNB code cell and its output.
imageSee Image chunks — two different things share this name.
mixed_content, footnote_caption, header_footerDOCX only.
section, semantic, sliding_window, sentence, page_awareThe mode's own chunks.
row_document, table_region, sheet, row_window, sheet_region, semantic_groupSpreadsheets (XLSX/XLS/XLSM/XLSB/ODS/XLTX/XLTM).
row_group, row_windowCSV / TSV.

One real chunk per type

Every block below is verbatim engine output at default parameters.

// heading — md/test.md, default
{ "content": "Complex Markdown Test Document",
  "content_type": "heading",
  "metadata": { "document_metadata": { "source_type": "md" },
                "section_heading": null, "section_level": 1 } }

// table — md/test.md, default (content abridged to two rows for width)
{ "content": "| Region | Nodes | Avg Latency |\n|---|---:|---:|\n| us-east-1 | 12 | 140 ms |",
  "content_type": "table",
  "metadata": { "document_metadata": { "source_type": "md" },
                "section_heading": "2.2 Secondary Table — Region Breakdown",
                "section_level": 3 } }

// code_block — md/test.md, default
{ "content": "```\n[Source File]\n\n\n[/extract]  ──►  ExtractedData (JSON)\n```",
  "content_type": "code_block",
  "metadata": { "document_metadata": { "source_type": "md" },
                "section_heading": "3.2 Data-flow diagram (text)",
                "section_level": 3 } }

// bullet_list — md/test.md, default
{ "content": "Docs: Pylint C0415 guidance\nAPI home: Example API\nIssue tracker: GitHub Issues\nChangelog: CHANGELOG.md",
  "content_type": "bullet_list",
  "metadata": { "document_metadata": { "source_type": "md" },
                "section_heading": "6.1 Links and images", "section_level": 3 } }

// long_single_paragraph — pdf/arxiv_1706.03762_attention.pdf, default
{ "content": "The dominant sequence transduction models are based on complex recurrent or convolutional neural networks…",
  "content_type": "long_single_paragraph",
  "metadata": { "document_metadata": { "source_type": "pdf", "total_pages": 15 },
                "section_heading": "Abstract", "section_level": 3 } }

// row_document — excel/file_example_XLSX_50.xlsx, mode="row"
{ "content": "0: 1 | First Name: Dulce | Last Name: Abril | Gender: Female | Country: United States | Age: 32 | Date: 15/10/2017 | Id: 1562",
  "content_type": "row_document",
  "metadata": { "sheet_name": "Sheet1", "sheet_index": 0, "row_index": 1,
                "header_row": ["0","First Name","Last Name","Gender","Country","Age","Date","Id"],
                "col_count": 8, "rows_per_chunk": 1, "actual_row_count": 1,
                "chunk_index": 0, "skipped_sheets": [] } }

// row_group — csv/plotly_iris.csv, mode="row" (3 rows per chunk, the default)
{ "content": "sepal length: 5.1 | sepal width: 3.5 | petal length: 1.4 | petal width: 0.2 | class: Iris-setosa\nsepal length: 4.9 | …",
  "content_type": "row_group",
  "metadata": { "row_start": 1, "row_end": 3, "row_count": 3, "col_count": 5,
                "header_row": ["sepal length","sepal width","petal length","petal width","class"],
                "has_header": true, "delimiter_detected": ",", "encoding": "utf-8",
                "chunk_index": 0 } }

// semantic — md/test.md, mode="semantic"
{ "content": "Project: temp-doc\nOwner: Platform Team\nDate: 2026-04-21\n\nJump to Metrics | Jump to Architecture | …",
  "content_type": "semantic",
  "metadata": { "avg_block_length": 89, "block_types": ["paragraph"], "chunk_index": 1,
                "document_metadata": { "source_type": "md", "total_input_blocks": 90 },
                "has_list": false, "heading_path": ["Complex Markdown Test Document"],
                "keyword_density": 0.607, "merge_reasons": ["short_paragraph"],
                "paragraph_count": 2, "primary_merge_reason": "short_paragraph",
                "section_heading": "Complex Markdown Test Document", "section_level": 1 } }

One real chunk per mode

The metadata is where a mode actually shows itself. Same file, different mode:

// mode="section" — pdf/arxiv_1706.03762_attention.pdf
{ "content_type": "section",
  "metadata": { "block_types": ["paragraph"], "char_count": 455, "chunk_index": 2,
                "document_metadata": { "source_type": "pdf", "total_pages": 15 },
                "heading_path": ["scholarly works."], "paragraph_count": 4,
                "section_heading": "scholarly works.", "section_level": 2,
                "split_part": null, "split_total": null } }

// mode="sliding_window" — md/test.md  (window_size 3, overlap 1)
{ "content_type": "sliding_window",
  "metadata": { "block_count": 3, "chunk_index": 0, "window_index": 0,
                "window_size": 3, "overlap": 1, "paragraph_range": [0, 2],
                "document_metadata": { "source_type": "md", "total_input_blocks": 90 },
                "heading_path": ["Complex Markdown Test Document"],
                "section_heading": "Complex Markdown Test Document" } }

// mode="sentence" — md/test.md  (sentences_per_chunk 3)
{ "content_type": "sentence",
  "metadata": { "sentences_per_chunk": 3, "actual_sentence_count": 2,
                "source_paragraph_index": 1, "chunk_index": 1,
                "heading_path": ["Complex Markdown Test Document"],
                "section_heading": "Complex Markdown Test Document", "section_level": 1,
                "document_metadata": { "source_type": "md", "total_input_blocks": 90 } } }

// mode="page_aware" — docx/lists.docx  (a page that had to be split in two)
{ "content_type": "page_aware",
  "metadata": { "page_number": 5, "page_break_type": "estimated",
                "paragraph_count": 15, "list_item_count": 3, "table_count": 0,
                "headings": [{ "level": 1, "text": "5. Definition-Style List" },
                              { "level": 1, "text": "6. Mixed Content with Lists" }],
                "section_heading_level": 1,
                "chunk_index": 1, "total_chunks": 2,
                "document_metadata": { "source_type": "docx" } } }

semantic is above; structural is what default resolves to for prose formats, so its chunks look like the heading / plain_paragraph examples. Full per-mode behaviour lives in Chunking Modes.

Reading metadata

metadata is a flat map of scalars, lists and one nested object. Two rules cover almost every mistake:

  1. Read every key optionally. No key is guaranteed by the contract — not page_number, not chunk_index, not section_heading. Which keys exist is a function of format × mode, and it is enumerated in the Metadata Reference.
  2. A key that exists can still be null. section_heading is null for a chunk before the first heading; .doc page_number is null when the file declares no hard page breaks. null means "known to be absent", missing means "this format/mode does not produce this at all".

document_metadata

One nested object carries file-level provenance — the same for every chunk in the call. Its shape depends entirely on the format, so treat it as a union, not a struct:

{ "source_type": "pdf",  "total_pages": 15 }
{ "source_type": "md",   "total_input_blocks": 90 }
{ "source_type": "docx", "header_text": null, "footer_text": null, "image_count": 2 }
{ "source_type": "pptx", "total_slides": 48 }
{ "source_type": "ipynb", "nbformat": "4.0", "kernel": null, "language": null,
  "cell_count": 1, "code_cell_count": 1, "markdown_cell_count": 0 }
{ "source_type": "epub", "title": "Minimal EPUB 3.0", "epub_version": "3.0",
  "creator": null, "creators": [], "contributors": [], "publishers": [], "subjects": [],
  "identifier": "NOID", "identifiers": ["NOID"], "language": "en", "languages": ["en"],
  "spine_count": 1, "toc": [  ] }
{ "source_type": "eml",  "subject": "This is a test message", "from": "John X. Doe <bbb@ddd.com>",
  "to": ["bbb@zzz.org"], "cc": [], "bcc": [], "date": "2001-05-04T14:05:44-04:00",
  "message_id": "15090.61304.110929.45684@aaa.zzz.org", "in_reply_to": [], "references": [],
  "has_attachments": false, "attachment_count": 0 }

Two absences are worth knowing: .doc emits no document_metadata at all, and spreadsheets and CSV/TSV emit none either — their provenance lives in per-chunk keys such as sheet_name and encoding. Note also that DOCX's default mode is the one format whose document_metadata has no source_type.

Image chunks

content_type: "image" covers two different objects. Confusing them is the most common image bug.

1. The structural placeholder — plain default / structural, no list_images. The chunk's content is the paragraph text with an inline [Image: …] marker, and there is no image_name. It tells you an image sits here; it does not give you one.

// docx/corpus_floating_image.docx, mode="default", list_images off
{ "content": "A floating image is anchored to this paragraph.\n[Image: Float 1]",
  "content_type": "image",
  "metadata": { "page_number": 1, "section_heading": null, "section_heading_level": null,
                "footnotes": [], "endnotes": [],
                "document_metadata": { "header_text": null, "footer_text": null, "image_count": 2 } } }

2. The extraction chunk — with list_images on. Its content is the image filename, not bytes and not text. The bytes live in the result's images container under that same name.

// docx/all_round.docx, list_images=True  → chunks[4]
{ "content": "69400fb8a9cb4130.gif",
  "content_type": "image",
  "metadata": { "image_name": "69400fb8a9cb4130.gif", "alt_text": "" } }

// pdf/arxiv_1706.03762_attention.pdf, list_images=True → chunks[0]
{ "content": "image_p3_1.png",
  "content_type": "image",
  "metadata": { "image_name": "image_p3_1.png" } }

Turning list_images on replaces the placeholders with extraction chunks; the two never coexist.

What an image chunk carries

Deliberately minimal, and not uniform:

FormatImage-chunk metadata
DOCXimage_name, alt_text — nothing else
PDFimage_name — nothing else
XLSX familyimage_name, alt_text, sheet_name, sheet_index
PPTX familyimage_name, alt_text, slide_number, document_metadata
HTMLimage_name, alt_text, document_metadata
EPUBimage_name, href
DOC / PPTimage_name plus that format's full structural block (source, chunk_index, total_chunks, paragraph_type: "image", page_number)
IPYNB, EML/MBOX/MSG, ODT/ODPimage_name only

Names

Most formats name an image by a content hash"<16 hex>.<ext>", FNV-1a 64-bit over the bytes, pinned in the engine so Python, JavaScript and Rust all produce the same string. Identical bytes collapse to one entry: the HTML fixture sample_with_image.html emits two image chunks that share 2668ca08e54e28cc.png with different alt_text, and one entry in images.

Two families do not hash:

  • PDF names positionally: image_p{page}_{n}.{ext}image_p3_1.png, image_p4_1.png, image_p4_2.png. Always re-encoded to .png.
  • EPUB, IPYNB and the email formats keep the source name: 7433694763631080598_cover.jpg (the EPUB manifest basename), output_image_1.png (a notebook cell output), msg1_image_1.gif (message 1 of an mbox), testPNG.png (an .eml attachment).

Only web-renderable rasters are extracted (.png .jpg .jpeg .gif .webp); .emf / .wmf / .tiff payloads are skipped silently. A format with no image support still returns a result object — with an empty images container, never an error. See Supported Formats.

get_markdown(..., list_images=True) mirrors all of this, returning markdown (with ![](name.ext) references) and the same images container.

Next

On this page