# Chunking Pipeline ## Purpose Transform a parsed canonical document into retrieval-optimised chunk records that carry enough context for both dense (vector) and lexical (BM25) search. ## Entry Point `ChunkDocumentService.execute(ChunkDocumentCommand)` — called by `IngestionJobRunner` in the chunk stage after successful parsing. ## Stage Flow ``` parsed document → ChunkingBuilder.build_chunks(document) → ChunkingService (section + table + document chunks) → ExtractiveSummaryBuilder (preamble source for the document chunk) → [optional] AssistantBackedDocumentSummarizer (abstractive summaries) → ChunkSet attached to document aggregate ``` ## Chunk Layers Every chunk record carries a `layer` field identifying its origin: | Layer | Source | Purpose | | --- | --- | --- | | `section` | A heading-bounded text section of the document | Core retrieval unit for prose | | `table_row` | A row or row group from a parsed table | Structured data retrieval | | `document` | The whole document synthesised into one chunk | Workspace-level overview retrieval | ## Section Chunks `ChunkingService` walks the canonical `SectionNode` tree. For each section it builds one `ChunkRecord` whose `retrieval_text` is structured as: ``` Document summary: Document: Section summary: <section summary if available> Content: <section text> ``` Each section chunk carries two ordinal fields: - `ordinal` — global sequential position of the section across the whole document (1, 2, 3, … regardless of heading level or numbering) - `section_ordinal` — the number embedded in the heading itself, extracted by `terminal_heading_number()` (e.g. "Chapter 3" → `3`, "Foreword" → `0`) `section_ordinal` is what the execution planner emits and the document retriever filters on. It maps user intent ("first chapter", "Chapter 3") to the correct set of chunks without relying on position in the parse tree. ## Table Chunks `TableChunker` builds one or more `ChunkRecord` entries per table. Each row group becomes a chunk carrying the table caption, column headers, and row data. `parent_table_id` links table-row chunks back to their source table for abstractive summary injection. ## Document Chunk One `document`-layer chunk is built per document. It carries: - the document-level summary (extractive or abstractive) as its primary content - a `summary_packet` in its metadata with `global_synopsis`, `overview_summary`, and `retrieval_lenses` (overview, table summary, section guide) This chunk is used for workspace-level "what is this about?" queries without needing to retrieve individual sections. ## Extractive Summary Source `ExtractiveSummaryBuilder` selects a representative subset of sections and tables to feed into the document-level summary. It avoids: - orphan sections (TOC entries, index pages, back matter detected by the `_orphans` section id suffix) - injecting the TOC into every chunk's preamble — long narrative sequences (books) are capped to one representative section to prevent the table of contents from dominating the preamble The builder detects long narrative sequences by counting numbered heading sequences. When a document has more than twice the representative section limit of numbered headings (as in a novel with many short chapters), only one section is used in the extractive preamble. ## Abstractive Summaries (Optional) When `AIBS_CHUNKING_ABSTRACTIVE_SUMMARIES_ENABLED=true`, after extractive chunking `AssistantBackedDocumentSummarizer` uses the dedicated `AIBS_LLM_SUMMARIZER_*` assistant lane. It sends one overview call, then section batches of 3, then table batches of 3. The calls are sequential within one document, while the summarizer lane has its own concurrency gate so ingestion enrichment cannot consume the interactive chat lane. The summarizer receives: - `overview_summary` — document-level overview replacing the extractive preamble - `section_summaries` — one summary per section id, injected into each matching section chunk's `retrieval_text` as `Section summary: …` - `table_summary` — cross-table summary added to the document chunk - `table_summaries` — one summary per table id, injected into matching table-row chunks as `Table summary: …` The summarizer guards against LLM hallucination by: - validating all returned `section_id` and `table_id` values against the `valid_ids` frozenset from the extractive source - deduplicating repeated ids with a `seen_ids` set - clipping per-section and per-table prompt input before each batch - passing the detected ISO language code directly when available, or asking the model to infer language from the extractive text when no code is available - raising out only when the overview call fails; malformed section/table batch responses are recorded as partial enrichment failures If abstractive summarization fails (LLM offline, malformed JSON, timeout), the service logs a warning and falls back to the extractive summary. The chunk set is still persisted and the document advances to indexed status. If only a section/table batch fails, the document chunk keeps the existing `metadata["abstractive_summary"]` block and adds partial-enrichment fields: `overview_present`, `section_summary_count`, `table_summary_count`, `missing_section_ids`, `missing_table_ids`, `failed_batches`, and `degraded_summary`. A degraded summary remains searchable; Stage 1 extractive metadata is the fallback. The gate condition requires either `selected_sections` or `selected_tables` to be non-empty. Table-only documents (e.g. data sheets with no narrative sections) still receive an abstractive table summary. ## Chunk Metadata Fields Key fields on `ChunkRecord` used downstream by retrieval and answering: | Field | Type | Description | | --- | --- | --- | | `layer` | str | `section`, `table_row`, or `document` | | `ordinal` | int | Global sequential position in the document | | `section_ordinal` | int | Heading-derived chapter/section number (0 = unnumbered) | | `retrieval_text` | str | Full structured text sent to both embedding and BM25 | | `summary_text` | str | One-sentence summary of this chunk | | `parent_section_id` | str \| None | Section id for table-row chunks | | `parent_table_id` | str \| None | Table id for table-row chunks | | `fingerprint` | str | Section id for section-layer chunks (used as the summary key) | ## Chunk Policy Version `ChunkingBuilder.build_policy_version()` returns a hash of the current chunking configuration. It is stored on the `ChunkSet` and used to detect when a document's chunks are stale relative to a new chunking policy version, enabling future selective re-chunking without full re-ingestion.