LlamaIndex’s RecursiveRetriever lets you search through nested documents, but its real power comes from how it fundamentally changes the retrieval landscape from flat lists to navigable knowledge graphs.

Imagine you’ve got a bunch of documents about a complex topic, say, the history of a specific programming language. You might have a high-level overview, then chapters on major versions, then sections on specific features within those versions, and finally, code examples for those features. A traditional retriever would just dump a flat list of all these chunks, forcing you to sift through them to find the relevant context.

The RecursiveRetriever changes this. It indexes your documents not just as individual chunks, but also understands their hierarchical relationships. When you query, it doesn’t just find matching chunks; it can traverse this hierarchy to find the most relevant level of detail.

Let’s see it in action.

First, we need some nested documents. We’ll use LlamaIndex’s Document object, which supports a children attribute for nesting.

from llama_index.core import Document

# Top-level document
doc1 = Document(text="The history of Python", id_="doc1")

# Children of doc1
doc1_1 = Document(text="Python 1.0: The Beginning", id_="doc1_1")
doc1_2 = Document(text="Python 2.x: The Era of Stability", id_="doc1_2")
doc1_3 = Document(text="Python 3.x: A New Generation", id_="doc1_3")

doc1.children = [doc1_1, doc1_2, doc1_3]

# Children of doc1_2
doc1_2_1 = Document(text="Key Features of Python 2.7", id_="doc1_2_1")
doc1_2_2 = Document(text="The GIL in Python 2", id_="doc1_2_2")

doc1_2.children = [doc1_2_1, doc1_2_2]

# Children of doc1_3
doc1_3_1 = Document(text="Python 3.5: Asyncio Revolution", id_="doc1_3_1")
doc1_3_2 = Document(text="Python 3.9: Dictionary Merge Operators", id_="doc1_3_2")

doc1_3.children = [doc1_3_1, doc1_3_2]

# A completely separate document
doc2 = Document(text="Introduction to Rust Programming", id_="doc2")

Now, let’s set up the RecursiveRetriever. It needs an underlying Retriever (like a vector store retriever) to actually find chunks, and it needs to know how to traverse the structure.

from llama_index.core.retrievers import RecursiveRetriever
from llama_index.core.indices.vector_store import VectorStoreIndex
from llama_index.core import Settings
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI

# Configure embeddings and LLM (replace with your actual keys/models)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-ada-002")
Settings.llm = OpenAI(model="gpt-3.5-turbo")

# Create a vector store index from our documents
# We'll flatten the documents for indexing, but the retriever will reconstruct the hierarchy
index = VectorStoreIndex.from_documents([doc1, doc2])

# Get the base retriever
base_retriever = index.as_retriever(similarity_top_k=5)

# Initialize the RecursiveRetriever
# We specify the base retriever and the 'parent_id'/'children' keys used in our Document objects
recursive_retriever = RecursiveRetriever(
    retriever=base_retriever,
    index_struct_type="vector", # Or "knowledge_graph", etc. depending on your base index
    verbose=True,
    # These are the default keys LlamaIndex expects for Document objects
    # if you used custom keys, you'd specify them here.
    # parent_id_key="parent_id",
    # children_key="children",
)

Now, let’s query. If we ask about "Python 2 features," a regular retriever might give us chunks from doc1, doc1_2, and doc1_2_1. The RecursiveRetriever understands that doc1_2_1 is a child of doc1_2, which is a child of doc1, and can retrieve them in that context.

query_engine = recursive_retriever.as_query_engine()
response = query_engine.query("What were key features of Python 2?")
print(response)

Notice how the response might implicitly or explicitly favor the more granular document (doc1_2_1) while still understanding its place within the broader hierarchy (doc1_2, doc1). The verbose=True flag will show you the retrieval steps, demonstrating how it navigates the structure.

The core problem the RecursiveRetriever solves is moving beyond a bag-of-words retrieval to a more semantically structured retrieval. Instead of just finding text that matches your query, it finds nodes in your knowledge structure that match, and can then expand or contract the retrieved context based on the hierarchy. This is crucial for complex, multi-level information where context is as important as the content itself. You can control the depth of retrieval and the traversal strategy, allowing you to fine-tune how the retriever explores the nested documents.

The most surprising aspect of the RecursiveRetriever is its ability to act as a form of "contextual zoom." When you query for something specific like "the GIL in Python 2," it doesn’t just fetch the chunk containing that phrase. Instead, it retrieves that specific chunk, but because it knows it’s a child of doc1_2 (which is about "Python 2.x: The Era of Stability") and a sibling to doc1_2_1 ("Key Features of Python 2.7"), it can provide a richer, more contextualized answer. This isn’t just about finding the right words; it’s about finding the right place in your knowledge hierarchy.

The next step is often integrating this with a more sophisticated query engine that can leverage this hierarchical retrieval for complex question answering, or exploring how different base retriever types (like knowledge graph retrievers) interact with the recursive traversal.

Want structured learning?

Take the full Llamaindex course →