Advanced8 min read39 of 40

Recursive Summarization - Hierarchical Document Compression

Recursively summarize a document bottom-up — chunk summaries → section summaries → document summary — creating a compression tree for efficient retrieval at any level.

Read first:RAPTOR

Overview

Recursive Summarization builds a hierarchical compression tree over a long document by summarizing bottom-up: leaf chunks are summarized into section nodes, section nodes are summarized into chapter nodes, and so on up to a single document-level summary. At query time, retrieval searches across all levels of the tree, returning whichever node — leaf or summary — best matches the query's information need.

This is the core idea behind RAPTOR (Recursive Abstractive Processing for Tree-Organized Retrieval).

Pattern #10 (Recursive RAG) in the 37 RAG Patterns Collection

flowchart TD
  DOC --> L[Leaf chunks — base level]
  L --> LS[Summarize each leaf]
  LS --> CL2[Cluster similar summaries by topic]
  CL2 --> S2[Summarize each cluster — level 2]
  S2 --> CL3[Cluster level-2 summaries]
  CL3 --> S3[Root summary — level 3]
  L & LS & S2 & S3 --> IDX[(Multi-level index: all levels queryable)]
  Q[Query] --> SEARCH[Search all levels in parallel]
  IDX --> SEARCH
  SEARCH --> BEST[Select best level for this query type]
  BEST --> LLM[Generate]

The Problem

Very long documents can't be handled by either naive chunking or direct inclusion in context:

100-page annual report → naive chunking into 512-token chunks → ~800 chunks in the index Problem 1 (Global questions): Query: "What is the company's overall strategic direction?" → No single chunk contains the answer → Top-k retrieval returns 5 scattered fragments → LLM produces a patchwork answer missing the big picture Problem 2 (Context overflow): Query answered by Section 4 (15 pages) → Section 4 chunked into ~120 fragments → Retrieving top-10 captures ~8% of section content → Critical connecting paragraphs fall outside top-10

Chunking trades global coherence for granularity. The summary tree recovers both.

Architecture

The tree is built bottom-up and searched top-down:

Build (bottom-up): L0: [chunk_1] [chunk_2] [chunk_3] [chunk_4] [chunk_5] [chunk_6] \ / \ / L1: [summary_A] [summary_B] \ / L2: [document_summary] Retrieve (query-guided, any level): "Give me the gist" → match L2 summary "Details on Section A" → match L1 summary_A "Specific figure in chunk_2" → match L0 chunk_2

Each node is embedded and stored. A retrieval query is compared to all nodes; the most relevant node at the appropriate granularity is returned.

Implementation

1. Recursive Summarization (Bottom-Up Build)

python
SUMMARIZE_PROMPT = """Summarize the following text concisely.
Preserve all key facts, figures, named entities, and conclusions.
Do not add information not present in the source.

Text:
{text}

Summary:"""

def summarize_chunk(text, llm):
    """Produce an information-dense summary of a single text block"""
    prompt = SUMMARIZE_PROMPT.format(text=text)
    return llm.invoke(prompt)

def recursive_summarize(chunks, llm, group_size=3):
    """
    Build the summary tree bottom-up.
    Returns a list of (level, node_id, parent_id, content) tuples.
    """
    nodes = []
    current_level = [
        {"level": 0, "id": f"L0_{i}", "parent_id": None, "content": c}
        for i, c in enumerate(chunks)
    ]
    nodes.extend(current_level)

    level = 1
    while len(current_level) > 1:
        next_level = []
        # Group current-level nodes and summarize each group
        for i in range(0, len(current_level), group_size):
            group = current_level[i : i + group_size]
            combined = "\n\n".join(n["content"] for n in group)
            summary = summarize_chunk(combined, llm)
            parent_id = f"L{level}_{i // group_size}"

            # Link children to this parent
            for child in group:
                child["parent_id"] = parent_id

            node = {
                "level": level,
                "id": parent_id,
                "parent_id": None,
                "content": summary,
                "children": [n["id"] for n in group],
            }
            next_level.append(node)
            nodes.append(node)

        current_level = next_level
        level += 1

    return nodes

2. Index All Tree Nodes

python
def index_tree(nodes, vectorstore):
    """Embed and index every node in the summary tree"""
    for node in nodes:
        vectorstore.add({
            "id": node["id"],
            "content": node["content"],
            "level": node["level"],
            "metadata": {"level": node["level"], "node_id": node["id"]},
        })

3. Hierarchical Retrieval (Top-Down Search)

python
def hierarchical_retrieve(query, vectorstore, top_k=5):
    """
    Search all tree levels and return the most relevant nodes.
    The vector index naturally surfaces the right level:
    abstract queries match high-level summaries,
    specific queries match leaf chunks.
    """
    results = vectorstore.search(query, top_k=top_k)
    # Results already span all levels — return as-is
    return results

def adaptive_level_retrieve(query, vectorstore,
                             detail_keywords=None, top_k=5):
    """
    Optionally bias toward a specific tree level based on query intent.
    'give me the gist' → prefer higher levels
    'find the specific figure' → prefer level 0
    """
    all_results = vectorstore.search(query, top_k=top_k * 2)

    if detail_keywords and any(kw in query.lower() for kw in detail_keywords):
        # Bias toward leaf nodes for detail-seeking queries
        filtered = [r for r in all_results if r["level"] == 0]
        return filtered[:top_k] or all_results[:top_k]

    return all_results[:top_k]

4. Full Pipeline

python
DETAIL_KEYWORDS = ["specific", "exact", "figure", "number", "quote", "line"]

def recursive_summarization_rag(query, vectorstore, llm,
                                 documents=None, top_k=5):
    # Build tree once at index time (skip if already indexed)
    if documents:
        chunks = chunk_documents(documents, chunk_size=512)
        nodes = recursive_summarize(chunks, llm, group_size=3)
        index_tree(nodes, vectorstore)

    # Retrieve from tree (all levels searchable)
    results = adaptive_level_retrieve(
        query, vectorstore, DETAIL_KEYWORDS, top_k=top_k
    )

    # Generate
    context = "\n\n".join([r["content"] for r in results])
    response = llm.generate(
        f"Context: {context}\n\nQuestion: {query}"
    )

    return response, {
        "retrieved_levels": [r["level"] for r in results],
        "num_results": len(results),
    }

Complete Notebook

Run the full implementation:

Recursive RAG AWS Notebook

Includes:

  • Bottom-up tree construction with configurable group size
  • Multi-level vector indexing strategy
  • Adaptive level retrieval based on query intent
  • RAPTOR-style cluster-based summarization variant
  • Benchmark comparisons on long-document QA

Performance

MetricRecursive Summary TreeFlat Chunking
Long-doc QA accuracy78%54%
Global/gist questions82%41%
Specific detail questions75%71%
Index build time~3× flat (one-time cost)Baseline

The summary tree lifts long-document QA accuracy by 24 percentage points, with the largest gains on global and summary-level questions.

When to Use

Best For

  • Book-length documents — technical books, policy documents, lengthy reports
  • Annual reports and 10-Ks — where both high-level strategy and specific figures matter
  • Large codebases — module-level summaries enable high-level code search
  • Research paper collections — abstract-level retrieval before diving into methodology

Combine With

  • RAPTOR — extends this pattern with cluster-based summarization (group semantically similar chunks rather than linearly adjacent ones) for richer intermediate nodes
  • Hierarchical Indexing (#22) — use the summary tree nodes as the hierarchy; query both index levels together
  • Contextual Compression (#6) — compress retrieved nodes before generation when even summary nodes are long

Related Papers

Next Steps

  1. Try it: Run the Recursive RAG notebook
  2. Tune: Adjust group size (3–5 chunks per summary) to balance tree depth vs. summary quality
  3. Upgrade: Implement the RAPTOR variant — cluster chunks by embedding similarity before summarizing
  4. Combine: Index the tree with Hierarchical Indexing (#22) to query level by level in a structured way

Part of: 37 RAG Patterns Collection See also: Complete Pattern Index

Go Deeper With Live Instruction

Covered in depth in our llm engineering program.