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)
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 nodes2. Index All Tree Nodes
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)
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
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:
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
| Metric | Recursive Summary Tree | Flat Chunking |
|---|---|---|
| Long-doc QA accuracy | 78% | 54% |
| Global/gist questions | 82% | 41% |
| Specific detail questions | 75% | 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
- ▸RAPTOR: RAPTOR: Recursive Abstractive Processing for Tree-Organized Retrieval (Sarthi et al., 2024)
- ▸Recursive Summarization: Recursively Summarizing Books with Human Feedback (Wu et al., 2021)
Next Steps
- ▸Try it: Run the Recursive RAG notebook
- ▸Tune: Adjust group size (3–5 chunks per summary) to balance tree depth vs. summary quality
- ▸Upgrade: Implement the RAPTOR variant — cluster chunks by embedding similarity before summarizing
- ▸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