Parent-Child Retrieval Architecture#
Parent-child retrieval is a hierarchical RAG strategy where child chunks are embedded and indexed in the vector store, while parent chunks are stored in the database unembedded. At query time, semantic search operates over the fine-grained child chunks, but the system returns the broader parent context to the LLM. This gives the precision of small-chunk similarity search without sacrificing the coherence of larger context windows.
The index structure is identified by IndexStructureType.PARENT_CHILD_INDEX and is implemented in ParentChildIndexProcessor.
Indexing Pipeline#
ParentChildIndexProcessor.transform() supports two parent modes (controlled by the ParentMode enum):
PARAGRAPHβ the document is first split into paragraph-sized parent chunks; each parent is then further split into child chunks via_split_child_nodes(). Parents get their own UUIDdoc_id; children get separate UUIDs .FULL_DOCβ the entire document is concatenated into a single parent; child chunks are subsets of that parent .
Only child chunks go into the vector store. load() calls vector.create(formatted_child_documents) on the child list β parents are never embedded .
Persistence to the database is handled by DatasetDocumentStore.add_documents(save_child=True):
- Parent documents are written to the
document_segmentstable asDocumentSegmentrows . - Each child is written to the
child_chunkstable as aChildChunkrow with asegment_idforeign key pointing to its parent, and a sequentialpositionfield .
Query-Time: Child-to-Parent Reconstruction#
All retrieval methods (semantic, full-text, hybrid) return child chunk doc_ids from the vector store. The reconstruction happens in RetrievalService.format_retrieval_documents().
Vector search β child doc_ids β ChildChunk rows (by index_node_id) β segment_id β DocumentSegment (parent)
The key steps:
- Route by
doc_form: documents withdoc_form == PARENT_CHILD_INDEXpopulatechild_index_node_idsinstead ofindex_node_ids. - Child β parent lookup:
ChildChunkrows are fetched byindex_node_id; theirsegment_idvalues are collected to identify which parent segments to load . - Parent segment fetch: parent
DocumentSegmentrows are queried bysegment_id, filtered toenabled=Trueandstatus='completed'. - Max-score aggregation: for each parent segment, the system iterates over all matching child chunks and assigns
max_score = max(child.score for all matched children). This score is stored in aSegmentChildMapDetaildict . - Final sort: results are sorted by
max_scoredescending before returning . Child chunks are also sorted by score descending within each parent .
Data Flow Diagram#
Key Types & Data Models#
| Type | Location | Purpose |
|---|---|---|
ParentChildIndexProcessor | parent_child_index_processor.py | Indexing pipeline (extract / transform / load / clean) |
ChildDocument | core/rag/models/document.py | In-memory child chunk before persistence |
ChildChunk | models/dataset.py | DB model: segment_id, index_node_id, position, content |
DocumentSegment | models/dataset.py | Parent chunk in DB; holds full text |
SegmentChildMapDetail | retrieval_service.py:62-65 | {max_score, child_chunks} per parent during reconstruction |
DatasetDocumentStore | dataset_docstore.py | Writes parent segments and child chunks to DB |
Cleanup#
ParentChildIndexProcessor.clean() handles deletion:
- Resolves child
index_node_ids from thechild_chunkstable via asegment_idjoin . - Deletes child vectors from the vector store via
vector.delete_by_ids(child_node_ids). - Optionally deletes
ChildChunkDB rows whendelete_child_chunks=True. - Supports a
precomputed_child_node_idskwarg to avoid race conditions when segments are deleted before the cleanup runs .