Recursive Chunking

~10 min read

Chunk first using natural separators like paragraphs or sections, then recursively split any chunk that still exceeds the size limit — preserving natural structure while still respecting a hard size cap.

Recursive chunking is a two-level approach that tries to get the best of both fixed-size and structure-aware chunking. First, the document is chunked based on inherent, natural separators — paragraph breaks or section markers. Then, each of those chunks is checked against a pre-defined chunk-size limit: if a chunk exceeds that limit, it's split further into smaller pieces; if it already fits within the limit, no further splitting happens.

Concretely: say a document has two paragraphs. Recursive chunking first defines those two paragraphs as two initial chunks. If paragraph 1 is small enough, it stays as-is. If paragraph 2 is too large for the size limit, it gets recursively split into smaller sub-chunks — using the same 'split, then check size, then split again if still too big' logic — until every resulting piece fits the limit.

Like semantic chunking, this approach maintains the natural flow of language and preserves complete ideas wherever possible, because the first-pass split always respects the document's own natural boundaries (paragraphs, sections) rather than an arbitrary character count. The size-limit enforcement only kicks in as a second pass, for chunks that are naturally too large to embed or retrieve efficiently on their own.

The trade-off is implementation and computational overhead: unlike a single fixed-size pass, recursive chunking requires checking sizes and potentially re-splitting at multiple levels, which is more logic to write and more computation to run per document, especially for large corpora with many oversized paragraphs.

💻 Code example

def recursive_chunks(text: str, size_limit: int = 500, separators: list[str] = None) -> list[str]:
    """Split on the first available natural separator; recurse into any
    piece still over `size_limit` using the next separator down the list."""
    separators = separators or ["\n\n", "\n", ". ", " "]  # paragraph -> line -> sentence -> word

    if len(text) <= size_limit or not separators:
        return [text]

    sep, rest_seps = separators[0], separators[1:]
    pieces = text.split(sep)

    chunks = []
    for piece in pieces:
        if len(piece) <= size_limit:
            chunks.append(piece)                          # fits — no further splitting
        else:
            chunks.extend(recursive_chunks(piece, size_limit, rest_seps))  # still too big — recurse
    return [c for c in chunks if c.strip()]

doc = "Paragraph one is short.\n\nParagraph two is much longer..."
chunks = recursive_chunks(doc, size_limit=200)

💬 Deep Dive with AI

Key points

  • Two-level strategy: first split on natural separators (paragraphs/sections), then recursively split any chunk still over the size limit
  • A chunk that already fits the size limit after the first pass is left untouched
  • Preserves natural language flow and complete ideas, like semantic chunking, but uses structure rather than embeddings to decide initial boundaries
  • The size-limit enforcement is a second-pass safety net, not the primary boundary-setting mechanism
  • Costs more implementation and computational overhead than a single fixed-size pass, due to the multi-level check-and-split logic