chunk-engine

Metadata Reference

Every metadata key the engine emits, per format and mode, with types and optionality — verified against live output.

View raw

metadata is a map whose keys depend on both the format and the mode. Output Schema explains the object; this page enumerates the keys.

Every table below was verified against live output at default parameters (window_size 3, overlap 1, sentences_per_chunk 3, paragraphs_per_page 15) over the project's fixture corpus.

Read every key optionally

A key listed here is emitted by that format in that mode — it is not a guarantee for any other combination, and a key that is emitted can still be null. Use chunk["metadata"].get(key) (Python), a presence check on chunk.metadata[key] (JS), or chunk.metadata.get(key) on the serde_json::Value (Rust). Key names are identical in all three SDKs — only the chunk's own content_type is camelCased (contentType) in JavaScript.

Two chunker families

Which table applies to your file depends on which chunker handles it.

  • The shared pipeline — PDF, Markdown, HTML, TXT, RTF, JSON/JSONL/NDJSON, EML/MBOX/MSG, ODT/ODP, EPUB, IPYNB. One implementation, so the per-mode key set is the same for all of them, plus a small set of format-specific additions.
  • Native chunkers — DOCX, DOC, PPTX, PPT, and the spreadsheet / delimited families. Each has its own key vocabulary.

The shared pipeline

Core keys by mode

Emitted by every pipeline format in that mode — but not on every chunk. The derived modes pass some blocks through untouched, and a pass-through chunk carries the vocabulary of what it is rather than of the mode that produced it. The exceptions are worth knowing before you index on a key — they are enumerated and verified below:

ModeKeyType
default / structuralsection_headingstr | null
document_metadataobject — see below
sectionsection_heading / section_levelstr | null / int
heading_pathlist[str]
paragraph_count / char_countint / int
block_typeslist[str]heading, paragraph, list, table, code_block
chunk_indexint, 0-based
split_part / split_totalint | null — set only when one section was too large and was split
semanticsection_heading / section_level / heading_pathas above
paragraph_count / chunk_indexint
keyword_densityfloat, 0–1
merge_reasonslist[str] — every signal that fired
primary_merge_reasonstr — the winning one (values)
sliding_windowwindow_index / window_size / overlapint
paragraph_range[int, int], inclusive, 0-based
chunk_index / section_heading / heading_pathas above
sentencesentences_per_chunk / actual_sentence_countint
source_paragraph_indexint
chunk_index / section_heading / section_level / heading_pathas above
page_awarepage_break_typestrheading_boundary, estimated, explicit, rendered, section, slide_boundary
paragraph_count / chunk_indexint
section_heading / section_level / heading_pathas above

Keys that are absent, not null

Verified over the fixture corpus at default parameters. In each case the key is missing from the map entirely, so chunk["metadata"]["actual_sentence_count"] raises KeyError — use .get().

ModeKey(s)Absent on
sentencesentences_per_chunk, actual_sentence_count, source_paragraph_indexevery pass-through chunk — content_type heading, table or code_block. Only sentence chunks carry them. (bullet_list chunks keep them, with actual_sentence_count: 0.)
sentencesection_levelsentence chunks in HTML, TXT and EPUB
sectionchar_countheading pass-through chunks in HTML and EPUB
page_awaresection_levelchunks whose content_type is page_aware, in HTML, TXT and EPUB (the heading chunks in those same documents do carry it)

Every other key in the table above was present on every chunk of every pipeline format tested. See sentence mode for the pass-through rule in context.

chunk_index is absent in default / structural

Only the derived modes carry it. In default / structural the pipeline emits section_heading, section_level and document_metadata and nothing positional — use the index of the list you were given.

Two variants

semantic and sliding_window come in two flavours inside the pipeline:

FormatsAdds
semantic, block variantPDF, MD, RTF, JSON family, EML, MBOX, MSG, ODT, ODP, IPYNBblock_types (list[str]), has_list (bool), avg_block_length (int) — TXT gets avg_block_length only
semantic, prose variantHTML, EPUBmerge_reason (str, duplicates primary_merge_reason), has_body_content (bool); chunks that were passed through unmerged instead carry page_number + footnotes_captions
sliding_window, block variantPDF, MD, RTF, JSON family, EML, MBOX, MSG, ODT, ODP, IPYNBblock_count (int)
sliding_window, prose variantHTML, TXT, EPUBparagraph_count (int)

Per-format additions

On top of the core, in all modes unless noted.

FormatExtra keys
PDFnone. section_level in default/structural.
Markdownnone. section_level in default/structural.
HTMLdefault/structural: page_number, footnotes_captions (list), heading_path — and no section_level
TXTdefault/structural: page_number, footnotes_captions — and no section_level, no heading_path
RTFnone beyond the core
JSON / JSONL / NDJSONrecord_range ([int, int], inclusive, 0-based)
EPUBspine_index (int), href (str), is_navigation (bool); default/structural also page_number, footnotes_captions, heading_path and no section_level
IPYNBnone beyond the core
EML / MSGnone per chunk — everything is in document_metadata
MBOXper chunk: message_index, message_subject, message_from, message_date, message_id, in_reply_to (list), references (list) — absent on the mailbox title chunk
ODTnone beyond the core
ODPslide_number (int), slide_title (str | null)

PDF emits no page_number — in any mode

This surprises everyone. The key exists on DOCX (default/structural and page_aware), on DOC and on PPT. HTML, TXT and EPUB emit it in default/structural but it is null for every chunk of every fixture in the corpus. PDF never emits it: a PDF chunk locates itself with section_heading / heading_path, and document_metadata.total_pages tells you how long the document is. If you need per-page PDF chunks, use page_aware and rely on chunk order, or the positional image names.

document_metadata by format

The one nested object. Its shape is a union, not a struct — read the keys you need with a default.

Formatdocument_metadata keys
PDFsource_type, total_pages
MD / HTML / TXTsource_type; the derived modes add total_input_blocks
RTFsource_type, title (str | null), author (str | null) — read from the \info group
JSON familysource_type, record_count, top_level ("array"/"object"/"lines"), envelope_key (str | null)
EPUBsource_type, title, language, creator, identifier, epub_version, spine_count, toc (list of {title, href}), plus plurals creators, identifiers, publishers, contributors, subjects, languages
IPYNBsource_type, nbformat (str), kernel (str | null), language (str | null), cell_count, code_cell_count, markdown_cell_count
EMLsource_type, subject, from, to (list), cc, bcc, date, message_id, in_reply_to (list), references (list), has_attachments, attachment_count
MBOXsource_type, message_count
MSGsource_type, message_class, subject, from, to, cc, bcc, sent_date, received_date, importance, conversation_topic, has_attachments, attachment_count
ODTsource_type: "odt", title, creator
ODPsource_type: "odp", title, creator, slide_count

.doc, the spreadsheet family and CSV/TSV emit no document_metadata at all, and DOCX's default/structural object is the one that carries no source_type.

What these formats produce

Real output, so the keys above have something to attach to.

JSON / JSONL / NDJSON — one chunk per record, rendered as key: value lines with nested objects indented, content_type: "bullet_list":

// elastic_products.ndjson, default
{ "content": "description: Latest flagship smartphone with titanium design…\nprice: 999.99\nproduct_name: iPhone 15 Pro",
  "content_type": "bullet_list",
  "metadata": { "record_range": [0, 0], "section_heading": null, "section_level": 0,
                "document_metadata": { "source_type": "ndjson", "top_level": "lines",
                                       "record_count": 25, "envelope_key": null } } }

record_range is [first, last], inclusive and 0-based. Element-level modes (default, structural, sentence) give a point range like [3, 3]; grouping modes partition the file into contiguous non-overlapping ranges; sliding_window ranges overlap by the window's overlap. The chunk's own record count is last - first + 1 — there is deliberately no chunk-level record_count, because document_metadata.record_count already means the whole file. envelope_key names the array a document was unwrapped from ("features" for GeoJSON) and is null when the document is the record set. A 50k-line log becomes 50k independently embeddable chunks — pair it with streaming.

EML / MBOX / MSG — the subject becomes a heading chunk, the body plain_paragraph chunks; the parser is MIME- and encoding-aware.

cpython_msg_01_plain.eml → 2 chunks
  [heading]          This is a test message
  [plain_paragraph]  From: John X. Doe bbb@ddd.com / To: bbb@zzz.org / Date: … / Hi, …

mimekit_simple.mbox → 18 chunks
  [heading]          Mailbox — 3 messages          (no message_index)
  [heading]          Message 1                     message_index: 1
  [heading]          this'll probably break a lot of mime parsers
  [plain_paragraph]  Hello world. …

An .mbox is many messages in one file, so its identity keys sit on each chunk rather than in document_metadata — "which message is this from?" is a lookup, not a string-parse. in_reply_to and references are lists of message-ids, so a thread can be rebuilt without re-parsing the source.

Angle brackets: autolinks are kept, raw HTML is not

The chunk output above is real. <bbb@ddd.com> survives because CommonMark calls that an autolink, not raw HTML — as it does <https://example.com>. Both reach get_chunks as their own text. (This was a defect until 0.6.3: the pipeline classified them as tags and deleted them, losing 34 addresses across .eml, 210 across .mbox and 10 across .msg.)

Text that is neither an autolink nor a real tag is still removed, and for .md that is correct CommonMark: Use <Placeholder> and <bbb@ddd.com> and generic<T> here. chunks as Use and bbb@ddd.com and generic here. — the address is kept, <Placeholder> and <T> are not, because the specification says <…> is raw inline HTML. Plan for it if you write about <T>, <placeholder> or <your-api-key>: escape them (\<T\>), fence them as code (`<T>`), or read the text from get_markdown.

This applies to the formats that render through the shared Markdown pipeline — .md, .eml, .mbox, .msg, .odt, .odp, .rtf, .json/.jsonl/.ndjson, .ipynb, .pdf and .epub. DOCX, TXT, HTML and the spreadsheet family use separate chunkers and never had the behaviour. The metadata is unaffected either waydocument_metadata.from reads "John X. Doe <bbb@ddd.com>" in full.

RTF — headings come from paragraph styles, not inline markup, so a converter that flattens the style sheet yields prose chunks with no heading structure rather than guessed ones. Decoding is code-page correct (cp1252, Shift-JIS, …), character formatting survives into the markdown, and list markers are normalised to their kind (the glyph a writer painted for a bullet is only meaningful in its own symbol font). Underline has no markdown equivalent and is not represented.

// conv_libreoffice_heading123.rtf, default
{ "content": "First paragraph", "content_type": "heading",
  "metadata": { "section_heading": null, "section_level": 1,
                "document_metadata": { "source_type": "rtf",
                                       "title": null, "author": "Paolo Mottadelli" } } }

title and author are deliberately null rather than a guess when the writer stored the value as a \upr pair, whose ANSI half is a lossy ????? copy.

EPUB — chunked per spine item, in reading order.

// epubcheck_valid.epub, default
{ "content": "Loomings", "content_type": "heading",
  "metadata": { "spine_index": 0, "href": "OPS/content_001.xhtml", "is_navigation": false,
                "page_number": null, "footnotes_captions": [], "heading_path": null,
                "section_heading": null, "document_metadata": { "…": "see above" } } }

Dublin Core elements are repeatable and several fixtures use that — the plural keys carry every value, the singular ones keep the first. toc is the book's own table of contents as {title, href} in reading order, read from nav.xhtml (EPUB 3) or toc.ncx (EPUB 2), so you get chapter titles even when the XHTML uses no heading tags. is_navigation marks a chunk that came from the TOC page rather than the content — a link list chunks as ordinary prose otherwise and pollutes retrieval.

IPYNB — markdown cells become prose chunks; code cells and their outputs become separate code_block chunks, fenced and kept whole:

// nbc_helloworld.ipynb, default → 2 chunks
{ "content": "```\nprint(\"Hello World\")\n```", "content_type": "code_block",
  "metadata": { "section_heading": null, "section_level": 0,
                "document_metadata": { "source_type": "ipynb", "nbformat": "4.0",
                                       "kernel": null, "language": null,
                                       "cell_count": 1, "code_cell_count": 1,
                                       "markdown_cell_count": 0 } } }

ODT / ODP — the OpenDocument family mirrors its OOXML counterpart: .odt behaves like DOCX, .odp like PPTX (one heading chunk titled Slide N per slide, then its body), .ods follows the XLSX family below.

// odftoolkit_SlideTest1.odp, default
{ "content": "Slide 1", "content_type": "heading",
  "metadata": { "slide_number": 1, "slide_title": "This is the test documents for ODF toolkit",
                "section_heading": null, "section_level": 2,
                "document_metadata": { "source_type": "odp", "title": null,
                                       "creator": "hanbiao", "slide_count": 1 } } }

ODP has no title element the way PPTX does, so slide_title is the slide's first line of text — what a reader would call its title.

DOCX

ModeKeys
default / structuralsection_heading (str | null), section_heading_level (int | null), page_number (int), footnotes (list of {id, text}), endnotes (list), document_metadata (header_text, footer_text, image_countno source_type)
sectionsection_heading (str), section_heading_level, section_level, heading_path (a str here, not a list), document_metadata
semanticmerge_reason (str, singular — values), paragraph_count, section_heading, section_heading_level, document_metadata
sliding_windowwindow_index, window_size, overlap, paragraph_indices (list[int]), paragraph_meta (list of {is_heading, …}), heading_count, list_item_count, document_metadata
sentencesentences_per_chunk, actual_sentence_count, chunk_index, source_paragraph_index, source_paragraph_is_heading, source_paragraph_heading_level, source_paragraph_is_list, source_paragraph_is_table, document_metadata
page_awarepage_number, page_break_type, paragraph_count, headings (list of {level, text}), list_item_count, table_count, section_heading_level, document_metadata; chunk_index + total_chunks only on the parts of a split page

DOCX indexing is the exception

DOCX emits no chunk_index in section, semantic or sliding_window — the only pipeline-adjacent format that doesn't. And in page_aware, chunk_index/total_chunks mean "part i of n of this page", not a document position: lists.docx returns seven page chunks, of which exactly two carry them.

DOC and PPT (97–2003)

Both legacy readers emit the same key set in every mode — the mode changes how text is grouped, not what is described.

FormatKeys (all 7 modes)
DOCsource (absolute path), chunk_index, total_chunks, paragraph_type (heading / normal / table / list_item / image), heading_level (1–9 or null), page_number (int | null), section_heading, section_heading_level, heading_path ("Chapter > Section"), list_level (0-based, list chunks only), table_rows / table_columns / table_cells (table chunks only). No document_metadata.
PPTthe same, minus list_level/table_*, plus slide_number, slide_title, and page_number carrying the same value as slide_number; document_metadata is {source_type, total_slides}

.doc page_number counts the hard page breaks the file declares and is null when it declares none — Word recomputes soft pagination at render time and does not store it.

PPTX

ModeKeys
default / structuralslide_number (int), slide_range ([int, int]), slide_title (str | null), section_heading, document_metadata (source_type, total_slides)
sectionsection_heading (str), slide_range, slide_count, split_part / split_total (int | null), document_metadata
semanticmerge_reasons (list), primary_merge_reason, merge_reason (duplicate of the primary), keyword_density, has_body_content (bool, not always present), slide_range, slide_count, slide_title, section_heading, chunk_index, document_metadata
sliding_windowwindow_index, window_size, overlap, slide_range, slide_count, truncated (bool), chunk_index, document_metadata
sentencesentences_per_chunk, actual_sentence_count, source_slide (int), slide_range, chunk_index, document_metadata
page_awarepage_break_type ("slide_boundary"), slides_per_chunk (int), slide_range, slide_count, chunk_index, document_metadata

It is slide_range, not slide_numbers

PPTX names the span slide_range — a two-element [first, last] — in every mode, including page_aware. There is no slide_numbers key anywhere in the engine.

Spreadsheets (XLSX / XLS)

Identical for the whole family — .xlsm, .xlsb, .xltx, .xltm and .ods included. No document_metadata; provenance is per chunk. chunk_index and skipped_sheets are present in every mode.

Modecontent_typeKeys
rowrow_documentsheet_name (str), sheet_index (int), row_index (int), header_row (list[str]), col_count, rows_per_chunk, actual_row_count, chunk_index, skipped_sheets
tabletable_regiontable_name (str | null), is_named_table (bool), start_row / end_row / start_col / end_col, row_count, col_count, header_row, is_split (bool), split_part (int | null), sheet_name, sheet_index, chunk_index, skipped_sheets
sheetsheetrow_count, col_count, header_row, has_named_tables (bool), named_tables (list), is_split, split_part, sheet_name, sheet_index, chunk_index, skipped_sheets
sliding_windowrow_windowwindow_index, window_size, overlap, start_row, end_row, actual_row_count, header_row, col_count, sheet_name, sheet_index, chunk_index, skipped_sheets
page_awaresheet_regionhas_print_area (bool), print_area_ref (str | null), region_index, start_row / end_row / start_col / end_col, row_count, col_count, header_row, is_split, split_part, sheet_name, sheet_index, chunk_index, skipped_sheets
semanticsemantic_groupcategory_column (int | null), category_value (str | null), used_fallback (bool), low_grouping_quality (bool), avg_group_size (float), group_index, start_row, end_row, actual_row_count, header_row, col_count, sheet_name, sheet_index, chunk_index, skipped_sheets

XLS vs XLSX

.xls cannot expose named tables or print areas: in table mode is_named_table is always false (heuristic bounding-box detection is used), in sheet mode has_named_tables is always false with an empty named_tables, and page_aware always falls back to the full sheet (has_print_area: false). Every other mode is identical to .xlsx.

skipped_sheets

skipped_sheets lists any sheets calamine could not read (chart sheets, XLM macro sheets, corrupt parts). It is an empty list when nothing was dropped, so its absence never has to be interpreted. A workbook where no sheet is readable raises instead.

CSV / TSV

Modecontent_typeKeys
row / default / page_awarerow_grouprow_start, row_end, row_count, col_count, header_row (list[str]), has_header (bool), delimiter_detected (str), encoding (str), chunk_index
sliding_windowrow_windowwindow_index, window_size, overlap, row_start, row_end, actual_row_count, col_count, header_row, has_header, delimiter_detected, encoding, chunk_index

CSV has no way to declare whether its first row is a header, so the engine infers it: a non-numeric cell above a numeric column reads as a label, and in a textual column a first cell whose length differs from the rest reads as a label.

  • has_header: trueheader_row came from the file, and that row is not emitted as data.
  • has_header: false — the file is headerless. Every row is emitted as data and header_row holds synthetic Column 1 … Column N labels.

A single-row file is always treated as data, never as a lone header, so it still produces a chunk.

Image chunks

With list_images on, content_type: "image" chunks carry a deliberately small and non-uniform key set. (Without list_images, an image chunk is a structural placeholder with no image_name at all — see Output Schema.)

FormatKeys
DOCX familyimage_name, alt_text
PDFimage_name
XLSX family (incl. ODS, XLSB)image_name, alt_text, sheet_name, sheet_index
PPTX familyimage_name, alt_text (str | null), slide_number, document_metadata
HTMLimage_name, alt_text, document_metadata
EPUBimage_name, href (the manifest path)
DOCimage_name, paragraph_index (int | null for floating images), paragraph_type: "image", plus DOC's usual block
PPTimage_name, page_number (1-based slide, null if unattributable), paragraph_type: "image", plus PPT's usual block
IPYNB · EML / MBOX / MSG · ODT / ODPimage_name

Naming. DOCX, PPTX, XLSX, HTML, ODT/ODP and the legacy DOC/PPT path use a content hash — "<16 hex>.<ext>", FNV-1a 64-bit, pinned in the engine so the name is identical in Python, JavaScript and Rust. PDF is positional (image_p3_1.png) and always re-encoded to .png. EPUB, IPYNB and the email formats keep the source name (7433694763631080598_cover.jpg, output_image_1.png, msg1_image_1.gif, testPNG.png).

Enumerated values

primary_merge_reason (pipeline semantic, and PPTX) — the winning signal: keyword_overlap, short_paragraph, reference_continuity, elaboration, example, cause_effect, contrast_continuation, question_answer, definition_expansion, list_continuation, heading_merge, heading (HTML/EPUB), section_divider and single_unit (PPTX), plus two "nothing fired" values: structural_boundary and initial (a singleton chunk). merge_reasons holds every signal that fired and is empty for those two.

DOCX merge_reason (singular, DOCX only) — keyword_overlap, short_paragraph, heading_merge, size_limit, reference_continuity, list_continuation, docx_heading, transition_break. size_limit marks a chunk closed because the next paragraph would exceed the 1,500-character cap; list_continuation marks a run of list items held together as one unit.

page_break_typeheading_boundary, estimated, explicit, rendered, section (DOCX), slide_boundary (PPTX).

block_typesheading, paragraph, list, table, code_block.

HTML, EPUB and PPTX also emit merge_reason as a duplicate of primary_merge_reason; PDF, MD, TXT, RTF, JSON, ODT/ODP, IPYNB and the email formats emit only the list plus the primary. See Chunking Modes → semantic.

On this page