Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 1

Beyond Naive Chunking: Implementing a Parent-Document Retriever

Recommended resources

Welcome to your first lesson in our course on advanced Retrieval-Augmented Generation. Given your experience building RAG pipelines, we'll dive directly into sophisticated techniques that address the common failure points of simpler approaches.

This lesson focuses on a foundational challenge in RAG: document chunking. We'll analyze why the straightforward method of splitting documents into fixed-size pieces often falls short and then implement a more robust solution, the Parent-Document Retriever, using LangChain. This will be our first step toward building production-grade retrieval systems that are both precise and context-aware.

The Trade-Off in Naive Chunking

You're already familiar with the basic RAG workflow: load documents, split them into chunks, embed them, and store them in a vector database for retrieval. The most critical and deceptively complex step here is the splitting, or chunking. The size of your chunks presents a fundamental trade-off:

  • Small Chunks: These are great for creating precise, specific embeddings. A small chunk about a single concept (e.g., a specific function's return value in an API document) will have a very distinct vector representation. This leads to high accuracy during similarity search. However, the retrieved chunk may lack the surrounding context needed for the LLM to generate a comprehensive answer.
  • Large Chunks: These retain broad context, which is excellent for the LLM's generation step. The problem is that their embeddings become "diluted" or "washy." A large chunk covering multiple topics will have a vector that represents the average of those topics, making it less likely to be a top match for a very specific query.

This tension is a primary failure mode in basic RAG. A crucial piece of information might be split across two chunks, meaning neither chunk contains the full answer and the system fails to retrieve it.

This diagram illustrates how a key fact can be fragmented by a chunk boundary. When a user asks a question about this fact, no single chunk contains the complete answer, leading to retrieval failure.

The following article section and video explain this dilemma clearly.

RAG IX — Parent Document Retriever | by DhanushKumar

This article clearly outlines the challenge of balancing embedding accuracy with context retention.

Please read the section titled What Problem Does This Solve?, focusing on the two key challenges described: Embedding Accuracy vs. Context Retention and Efficient Retrieval.

Advanced RAG 02 - Parent Document Retriever

This video offers a great visual and conceptual breakdown of why standard chunking can be problematic.

Watch the segment from the start of the explanation to understand how embedding specificity changes with chunk size. The speaker uses the term "washy" to describe the diluted embeddings of large chunks.


The Solution: The Parent-Document Retriever

To get the best of both worlds—precise search and contextual richness—we can use a strategy called the Parent-Document Retriever (PDR). It's a form of multi-vector retrieval where we index our data in one way but use it to retrieve data in another.

The core mechanism is as follows:

  1. Splitting: Each original document is first split into larger "parent" chunks. Then, each parent chunk is further split into smaller "child" chunks.
  2. Indexing: Only the child chunks are embedded and stored in the vector database. Their small size ensures the embeddings are specific and semantically precise.
  3. Storage: The parent chunks are stored in a separate, simple key-value document store (docstore). A reference to the parent chunk's ID is stored in the metadata of each child chunk.
  4. Retrieval: When a query comes in, the system performs a similarity search against the small, specific child chunks.
  5. Contextualization: Instead of returning the retrieved child chunks, the system uses their metadata to look up and return their corresponding parent chunks from the docstore.

This way, we leverage the precision of small-chunk embeddings for the search, but provide the LLM with the rich, surrounding context from the larger parent chunks for generation.

This diagram shows the PDR process. Original documents are split into parent and child chunks. Only child chunks are embedded for similarity search. The search identifies relevant child chunks, but the system retrieves the corresponding parent chunks to provide full context to the LLM.

This video segment provides an excellent conceptual animation of this process.

Advanced RAG 02 - Parent Document Retriever

This clip visually explains the PDR mechanism.

Watch from the introduction of the PDR idea. The diagram illustrates how you search over child documents but pass the parent document back to the LLM.


Implementing the Parent-Document Retriever in LangChain

Your experience with Python, LangChain, and system design will make this implementation straightforward. We'll use the components you're already familiar with (TextSplitter, VectorStore) and introduce the ParentDocumentRetriever and InMemoryStore.

The following resource provides a complete, hands-on code walkthrough. We will follow its structure to build our retriever.

Parent Document Retriever | Full Stack Retrieval

This guide offers a clear, step-by-step implementation of the Parent Document Retriever in LangChain, from loading data to integrating it into a full RAG chain.

I recommend reading through the entire code walkthrough, from the beginning to the end. As you read, pay close attention to the following key steps, which I've summarized for you below.

Let's break down the implementation process based on the guide above.

1. Define Parent and Child Splitters

First, you need two instances of a text splitter, typically RecursiveCharacterTextSplitter.

  • The child_splitter is configured with a small chunk_size (e.g., 400 characters) to create the small documents for embedding.
  • The parent_splitter is configured with a larger chunk_size (e.g., 2000 characters) to create the context-rich documents that will be passed to the LLM.
from langchain.text_splitter import RecursiveCharacterTextSplitter




# This text splitter is used to create the child documents
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)




# This text splitter is used to create the parent documents
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)

2. Set Up the Vector Store and Document Store

Next, you need two storage components:

  • A vector store (e.g., Chroma, FAISS) to hold the embeddings of the child documents. This is the component that will be searched during retrieval.
  • A document store (docstore) to hold the parent documents. LangChain provides a simple InMemoryStore for this, which acts as a basic Python dictionary. We'll discuss more robust, production-ready stores shortly.
from langchain_community.vectorstores import Chroma
from langchain.storage import InMemoryStore
from langchain_openai import OpenAIEmbeddings




# The vectorstore to use to index the child chunks
vectorstore = Chroma(
    collection_name="split_parents", 
    embedding_function=OpenAIEmbeddings()
)




# The storage layer for the parent documents
store = InMemoryStore()

3. Instantiate and Use the Retriever

Finally, you instantiate the ParentDocumentRetriever. You pass it the stores and splitters you just configured.

from langchain.retrievers import ParentDocumentRetriever

retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=store,
    child_splitter=child_splitter,
    parent_splitter=parent_splitter, # Optional, if you want to split parents from original docs
)

When you call retriever.add_documents(docs), it automates the entire indexing workflow:

  1. Splits the original documents using parent_splitter.
  2. Saves the resulting parent chunks to the docstore.
  3. Further splits the parent chunks using child_splitter.
  4. Embeds these child chunks and adds them to the vectorstore, including metadata that links each child to its parent.

When you call retriever.invoke("your query"), it finds the relevant child documents in the vectorstore but returns the corresponding full parent documents from the docstore.


From In-Memory to Production: Persistent Document Stores

The InMemoryStore is convenient for notebooks and prototypes, but it's unsuitable for production. Since your goal is to learn production-level techniques, it's crucial to consider persistence. If your application restarts, an in-memory store loses all its data, forcing a costly re-indexing process.

For a production system, you would replace InMemoryStore with a persistent key-value store like Redis, or even a relational database like PostgreSQL. To do this, you would create a custom class that inherits from LangChain's BaseStore and implements its methods (mget, mset, mdelete, yield_keys) to interact with your chosen database.

The following video provides an excellent deep dive into this exact process, showing how to build a custom Docstore using PostgreSQL. This is a powerful technique for building robust, scalable RAG systems.

LangChain - Parent-Document Retriever Deepdive with Custom PgVector Store

This video is a deep-dive into creating a custom, persistent docstore for the Parent-Document Retriever using pgvector and PostgreSQL. It's a perfect example of moving from a prototype to a production-ready component.

I highly recommend watching these segments: Conceptual Overview: Watch the introduction from the start for a recap of the problem PDR solves. In-Memory Implementation: The clip from this timestamp shows the basic in-memory setup, similar to what we just discussed. Building a Custom PostgreSQL Store: The core of the video is from this section. Pay close attention to how the BaseStore interface is implemented with methods like mget and mset that execute SQL queries. Integration and Verification: Finally, watch from the integration part to see how this custom store is plugged into the ParentDocumentRetriever and how the database tables are inspected to verify that parent and child documents are stored correctly.

This hands-on approach of implementing a custom store is directly aligned with the system design skills you've developed and is a key step in operationalizing advanced RAG.


Conclusion

In this lesson, we dissected a critical weakness of basic RAG pipelines and introduced a powerful solution. Here are the key takeaways:

  • The Problem: Naive chunking creates a trade-off between the precision of small chunks and the context of large chunks, often leading to incomplete or irrelevant retrievals.
  • The Solution: The Parent-Document Retriever resolves this by indexing small, specific "child" chunks for accurate searching but retrieving larger, context-rich "parent" chunks for the LLM.
  • The Implementation: In LangChain, this involves using two splitters (child_splitter, parent_splitter), a VectorStore for the children, and a Docstore for the parents.
  • Production-Readiness: For real-world applications, the default InMemoryStore should be replaced with a custom, persistent document store (e.g., using PostgreSQL or Redis) to ensure scalability and data durability.

By mastering the Parent-Document Retriever, you've taken a significant step toward building more accurate and reliable RAG systems.

In our next lesson, we will explore the Multi-Vector Retriever. This is a generalization of the PDR concept, where we can index documents based on various alternative representations, such as LLM-generated summaries, to further enhance retrieval performance.

End of Module,Exit lesson