Home

Hierarchy & navigation

DocSlicer detects every heading in a document and arranges them into a tree — the document's outline, rebuilt from the content itself. You reach it as result.hierarchy, a HierarchyTree. This page is about why that tree exists and how to navigate with it; the HierarchyTree reference documents the fields and methods.

The tree is the structure that lets you (or an agent) look at a 200-page document's outline first, decide which sections matter, and pull only those — instead of reading the whole thing front to back.


From headings to a tree

DocSlicer works in two steps. First it identifies which lines are headings. Then it decides, for each heading, which heading it sits under — its parent. Every heading is assigned a parent_heading_id, except the top-level ones, which have no parent and become roots. Those parent links, taken together, are the tree: a heading with no parent is a root in hierarchy.roots, and every other heading appears in its parent's children. Each node also carries a level1 for roots, 2 for their children, and so on.

This assignment is deterministic — no LLM, no embeddings. DocSlicer reads the same structural and typographic cues a human eye uses to rank headings:

  • Typographic weight: bold, all-caps, centered, and larger font size all push a heading higher in the tree; italic pushes it lower.
  • Heading type: recognised schemes are ranked against each other. A part or item outranks a section, which outranks a free-form bold line.
  • Numeric depth: for numbered headings, the prefix itself sets the depth: 1. is shallower than 1.2, which is shallower than 1.2.3. A heading that goes deeper becomes a child; one that goes shallower pops back up to the matching ancestor.

Because the rules are fixed, the same document always produces the same tree. The full algorithm lives in the parser; you only ever consume the result.

Each heading also carries a heading_type naming the scheme it was recognised as. These fall into a handful of families — you rarely need the exact value, just the shape:

FamilyExamplesTypical heading_type
NamedItem 1A., Part II, Section 4.2, Article IV, Note 3item, part, section, article, note
Parenthetical(a), (iv), (a)(1)(i)single_parens_*, double_parens
Numbered10.2 Revenue, 1. Introductionnumbered_heading, roman_numbered_heading
AlphaA.1 Tasks, A1. Tasksalpha_dotted_numbered_heading, alpha_numbered_heading
Free-forma bold, unnumbered linefree_form (the fallback)

For the complete list of heading_type values with examples, see Heading types on the HierarchyTree reference.


Nesting goes as deep as the document does

There is no depth limit. DocSlicer adds as many levels as the document needs. This matters most for legal and financial documents, which tend to be long and deeply nested — a filing or agreement can stack parts, items, notes, and sub-clauses many layers down.

Here is a real six-level chain from a quarterly filing:

PART I — FINANCIAL INFORMATION                                   ← level 1
  Item 1. Financial Statements                                   ← level 2
    Notes to Condensed Consolidated Financial Statements         ← level 3
      Note 4 – Financial Instruments                             ← level 4
        Accounts Receivable                                      ← level 5
          Trade Receivables                                      ← level 6

Six levels is nothing special — if a document goes deeper, the tree goes deeper with it. Every node, at any depth, exposes the same fields and supports the same navigation, so you never have to special-case a level.


Hierarchy is not a table of contents

The tree DocSlicer builds is not the table of contents printed in the document.

  • result.hierarchy is rebuilt from every heading DocSlicer detects, so it exists even when the document has no printed TOC, and it is almost always more detailed. A legal agreement's printed TOC might list a dozen top-level entries while the document itself has hundreds of subsections that never appear in it.
  • result.export_toc() reproduces the literal table of contents the author wrote — nothing more.

Use result.hierarchy for programmatic navigation; reach for export_toc() only when you specifically want what the author considered the top-level structure.


Why agents love it: vectorless retrieval

The usual RAG recipe embeds an entire document up front, then hopes the chunks semantically nearest the query are the ones that actually answer it. The hierarchy enables a different pattern — sometimes called vectorless RAG — where an agent navigates structure instead of similarity:

  1. Hand the agent the outline (hierarchy.to_outline() or to_dict(minimal=True)) — a compact map of the whole document.
  2. Let the agent decide on the spot which heading is relevant to the question.
  3. Pull only the chunks under that heading with result.chunks_under(node).

The agent reads structure the way a person flips to the right section of a long contract, rather than waiting on an embedding pass and trusting that nearest-neighbour search surfaces the right passage.

The tree also makes follow-up reasoning cheap: once the agent has a relevant chunk, it can walk outward — to the parent section for context, or to sibling subsections for related detail — by navigating the tree. That tends to produce tighter, more traceable citations, because the agent points at a known heading and its descendants rather than a bag of similarity-ranked fragments.

Vectorless retrieval isn't a replacement for embeddings — it's a complement. Navigating by structure shines when a document has clear headings and the agent can reason about which section it needs. For fuzzy, cross-cutting queries over a large corpus, vector search still pulls its weight. Many pipelines use both: the hierarchy to scope down to the right document or section, then embeddings within it.


A few patterns cover almost everything. See HierarchyTree for the full method list.

Show an agent the whole structure

result = docslicer.parse_document("10-Q.pdf")
 
# Indented plain-text outline — readable, cheap to put in a prompt
print(result.hierarchy.to_outline())
 
# Or a compact nested dict (text + children only) — ideal for an LLM
structure = result.hierarchy.to_dict(minimal=True)

Jump to a section, then pull its content

# Find the heading, then read only what's under it
note = result.find_heading("Financial Instruments")[0]
 
chunks = result.chunks_under(note)                   # the section + its subsections
direct = result.chunks_under(note, recursive=False)  # just this heading's chunks

Walk the tree for context and related detail

# Top-level sections and how big each one is
for node in result.hierarchy.level(1):
    print(node.text, "→", len(result.chunks_under(node)), "chunks")
 
# Sibling subsections under a parent — for pulling related detail
section = result.find_heading("Financial Statements")[0]
for sibling in result.hierarchy.level(2, parent=section):
    print(sibling.text)

Each node's path field carries its ancestor heading texts, so you always know the chain from the root down to a given heading — handy for building a breadcrumb or a citation. For the page- and heading-based retrieval methods (chunks_under, blocks_under, tables_under, chunks_by_page), see ParseResult.


Next steps

  • HierarchyTree — the data type: every field and method, with the serialized JSON form.
  • ParseResult — the *_under and page-based retrieval methods the tree feeds into.
  • Sections — the region taxonomy that keeps the hierarchy accurate across cover pages, appendices, and back matter.