Metadata Reference
Every metadata key the engine emits, per format and mode, with types and optionality — verified against live output.
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:
| Mode | Key | Type |
|---|---|---|
default / structural | section_heading | str | null |
document_metadata | object — see below | |
section | section_heading / section_level | str | null / int |
heading_path | list[str] | |
paragraph_count / char_count | int / int | |
block_types | list[str] — heading, paragraph, list, table, code_block | |
chunk_index | int, 0-based | |
split_part / split_total | int | null — set only when one section was too large and was split | |
semantic | section_heading / section_level / heading_path | as above |
paragraph_count / chunk_index | int | |
keyword_density | float, 0–1 | |
merge_reasons | list[str] — every signal that fired | |
primary_merge_reason | str — the winning one (values) | |
sliding_window | window_index / window_size / overlap | int |
paragraph_range | [int, int], inclusive, 0-based | |
chunk_index / section_heading / heading_path | as above | |
sentence | sentences_per_chunk / actual_sentence_count | int |
source_paragraph_index | int | |
chunk_index / section_heading / section_level / heading_path | as above | |
page_aware | page_break_type | str — heading_boundary, estimated, explicit, rendered, section, slide_boundary |
paragraph_count / chunk_index | int | |
section_heading / section_level / heading_path | as 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().
| Mode | Key(s) | Absent on |
|---|---|---|
sentence | sentences_per_chunk, actual_sentence_count, source_paragraph_index | every 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.) |
sentence | section_level | sentence chunks in HTML, TXT and EPUB |
section | char_count | heading pass-through chunks in HTML and EPUB |
page_aware | section_level | chunks 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:
| Formats | Adds | |
|---|---|---|
semantic, block variant | PDF, MD, RTF, JSON family, EML, MBOX, MSG, ODT, ODP, IPYNB | block_types (list[str]), has_list (bool), avg_block_length (int) — TXT gets avg_block_length only |
semantic, prose variant | HTML, EPUB | merge_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 variant | PDF, MD, RTF, JSON family, EML, MBOX, MSG, ODT, ODP, IPYNB | block_count (int) |
sliding_window, prose variant | HTML, TXT, EPUB | paragraph_count (int) |
Per-format additions
On top of the core, in all modes unless noted.
| Format | Extra keys |
|---|---|
none. section_level in default/structural. | |
| Markdown | none. section_level in default/structural. |
| HTML | default/structural: page_number, footnotes_captions (list), heading_path — and no section_level |
| TXT | default/structural: page_number, footnotes_captions — and no section_level, no heading_path |
| RTF | none beyond the core |
| JSON / JSONL / NDJSON | record_range ([int, int], inclusive, 0-based) |
| EPUB | spine_index (int), href (str), is_navigation (bool); default/structural also page_number, footnotes_captions, heading_path and no section_level |
| IPYNB | none beyond the core |
| EML / MSG | none per chunk — everything is in document_metadata |
| MBOX | per chunk: message_index, message_subject, message_from, message_date, message_id, in_reply_to (list), references (list) — absent on the mailbox title chunk |
| ODT | none beyond the core |
| ODP | slide_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.
| Format | document_metadata keys |
|---|---|
source_type, total_pages | |
| MD / HTML / TXT | source_type; the derived modes add total_input_blocks |
| RTF | source_type, title (str | null), author (str | null) — read from the \info group |
| JSON family | source_type, record_count, top_level ("array"/"object"/"lines"), envelope_key (str | null) |
| EPUB | source_type, title, language, creator, identifier, epub_version, spine_count, toc (list of {title, href}), plus plurals creators, identifiers, publishers, contributors, subjects, languages |
| IPYNB | source_type, nbformat (str), kernel (str | null), language (str | null), cell_count, code_cell_count, markdown_cell_count |
| EML | source_type, subject, from, to (list), cc, bcc, date, message_id, in_reply_to (list), references (list), has_attachments, attachment_count |
| MBOX | source_type, message_count |
| MSG | source_type, message_class, subject, from, to, cc, bcc, sent_date, received_date, importance, conversation_topic, has_attachments, attachment_count |
| ODT | source_type: "odt", title, creator |
| ODP | source_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 way — document_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
| Mode | Keys |
|---|---|
default / structural | section_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_count — no source_type) |
section | section_heading (str), section_heading_level, section_level, heading_path (a str here, not a list), document_metadata |
semantic | merge_reason (str, singular — values), paragraph_count, section_heading, section_heading_level, document_metadata |
sliding_window | window_index, window_size, overlap, paragraph_indices (list[int]), paragraph_meta (list of {is_heading, …}), heading_count, list_item_count, document_metadata |
sentence | sentences_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_aware | page_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.
| Format | Keys (all 7 modes) |
|---|---|
| DOC | source (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. |
| PPT | the 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
| Mode | Keys |
|---|---|
default / structural | slide_number (int), slide_range ([int, int]), slide_title (str | null), section_heading, document_metadata (source_type, total_slides) |
section | section_heading (str), slide_range, slide_count, split_part / split_total (int | null), document_metadata |
semantic | merge_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_window | window_index, window_size, overlap, slide_range, slide_count, truncated (bool), chunk_index, document_metadata |
sentence | sentences_per_chunk, actual_sentence_count, source_slide (int), slide_range, chunk_index, document_metadata |
page_aware | page_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.
| Mode | content_type | Keys |
|---|---|---|
row | row_document | sheet_name (str), sheet_index (int), row_index (int), header_row (list[str]), col_count, rows_per_chunk, actual_row_count, chunk_index, skipped_sheets |
table | table_region | table_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 |
sheet | sheet | row_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_window | row_window | window_index, window_size, overlap, start_row, end_row, actual_row_count, header_row, col_count, sheet_name, sheet_index, chunk_index, skipped_sheets |
page_aware | sheet_region | has_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 |
semantic | semantic_group | category_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
| Mode | content_type | Keys |
|---|---|---|
row / default / page_aware | row_group | row_start, row_end, row_count, col_count, header_row (list[str]), has_header (bool), delimiter_detected (str), encoding (str), chunk_index |
sliding_window | row_window | window_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: true—header_rowcame from the file, and that row is not emitted as data.has_header: false— the file is headerless. Every row is emitted as data andheader_rowholds syntheticColumn 1 … Column Nlabels.
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.)
| Format | Keys |
|---|---|
| DOCX family | image_name, alt_text |
image_name | |
| XLSX family (incl. ODS, XLSB) | image_name, alt_text, sheet_name, sheet_index |
| PPTX family | image_name, alt_text (str | null), slide_number, document_metadata |
| HTML | image_name, alt_text, document_metadata |
| EPUB | image_name, href (the manifest path) |
| DOC | image_name, paragraph_index (int | null for floating images), paragraph_type: "image", plus DOC's usual block |
| PPT | image_name, page_number (1-based slide, null if unattributable), paragraph_type: "image", plus PPT's usual block |
| IPYNB · EML / MBOX / MSG · ODT / ODP | image_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_type — heading_boundary, estimated, explicit, rendered,
section (DOCX), slide_boundary (PPTX).
block_types — heading, 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.
Rust — rs-chunks
The crate's public surface — six dispatch functions, four types, and the per-format facade that exposes everything the dispatch layer cannot.
Architecture
One Rust engine, vendored into two bindings, with dispatch and chunking in the engine and almost nothing in the bindings — and how that is enforced.