2. ChunkingAvancé

Découpage Hiérarchique : Préserver la Structure de vos Documents

27 décembre 2025
11 min de lecture
Équipe de Recherche Ailog

Le découpage hiérarchique conserve les relations parent-enfant dans vos documents. Apprenez à implémenter cette technique avancée pour améliorer la qualité de récupération RAG.

TL;DR

  • Découpage hiérarchique = préserver les sections, sous-sections et paragraphes
  • Avantage : contexte riche + granularité fine simultanément
  • Implémentation : chunks imbriqués avec métadonnées de hiérarchie
  • Gain typique : +20-35% de pertinence sur documents structurés
  • Testez le chunking hiérarchique sur Ailog

Pourquoi le Découpage Hiérarchique ?

Les documents réels ont une structure :

  • Chapitres > Sections > Sous-sections > Paragraphes
  • Cette hiérarchie porte du sens sémantique

Le découpage classique (taille fixe ou sémantique) ignore cette structure :

Document original:
├── Chapitre 1: Introduction
│   ├── 1.1 Contexte
│   └── 1.2 Objectifs
└── Chapitre 2: Méthodes
    ├── 2.1 Approche A
    └── 2.2 Approche B

Découpage classique:
[Chunk 1: "...fin du contexte. 1.2 Objectifs..."]  ❌ Mélange de sections
[Chunk 2: "...début méthodes..."]                  ❌ Perte de hiérarchie

Principe du Découpage Hiérarchique

Créer des chunks à plusieurs niveaux avec des liens parent-enfant :

DEVELOPERpython
# Structure hiérarchique préservée { "id": "doc1_ch2_s1", "content": "2.1 Approche A - Description détaillée...", "metadata": { "level": 3, "parent_id": "doc1_ch2", "path": ["Chapitre 2: Méthodes", "2.1 Approche A"], "document_id": "doc1" } }

Implémentation Python

Extraction de la Hiérarchie

DEVELOPERpython
import re from dataclasses import dataclass from typing import List, Optional @dataclass class HierarchicalChunk: id: str content: str level: int title: str parent_id: Optional[str] path: List[str] children_ids: List[str] def extract_hierarchy(text: str, patterns: dict = None) -> List[HierarchicalChunk]: """ Extrait la structure hiérarchique d'un document. patterns: Regex pour détecter les niveaux """ if patterns is None: patterns = { 1: r'^# (.+)$', # Titre principal 2: r'^## (.+)$', # Sections 3: r'^### (.+)$', # Sous-sections 4: r'^#### (.+)$', # Sous-sous-sections } chunks = [] current_path = [] parent_stack = [] # Stack of (level, chunk_id) # Split by headers lines = text.split('\n') current_content = [] current_title = "Document" current_level = 0 chunk_counter = 0 for line in lines: header_found = False for level, pattern in patterns.items(): match = re.match(pattern, line, re.MULTILINE) if match: # Save previous chunk if current_content: chunk_id = f"chunk_{chunk_counter}" parent_id = parent_stack[-1][1] if parent_stack else None chunk = HierarchicalChunk( id=chunk_id, content='\n'.join(current_content).strip(), level=current_level, title=current_title, parent_id=parent_id, path=current_path.copy(), children_ids=[] ) chunks.append(chunk) chunk_counter += 1 # Update hierarchy current_title = match.group(1) current_level = level current_content = [] # Update path and parent stack while parent_stack and parent_stack[-1][0] >= level: parent_stack.pop() if current_path: current_path.pop() current_path.append(current_title) parent_stack.append((level, f"chunk_{chunk_counter}")) header_found = True break if not header_found: current_content.append(line) # Don't forget last chunk if current_content: chunk_id = f"chunk_{chunk_counter}" parent_id = parent_stack[-1][1] if parent_stack else None chunk = HierarchicalChunk( id=chunk_id, content='\n'.join(current_content).strip(), level=current_level, title=current_title, parent_id=parent_id, path=current_path.copy(), children_ids=[] ) chunks.append(chunk) return chunks

Indexation Multi-Niveaux

DEVELOPERpython
def index_hierarchical_chunks(chunks: List[HierarchicalChunk], vector_db): """ Indexe les chunks avec leur contexte hiérarchique. """ for chunk in chunks: # Créer le contexte enrichi path_context = " > ".join(chunk.path) enriched_content = f"{path_context}\n\n{chunk.content}" # Générer l'embedding embedding = embed(enriched_content) # Stocker avec métadonnées vector_db.upsert( id=chunk.id, embedding=embedding, metadata={ "content": chunk.content, "title": chunk.title, "level": chunk.level, "parent_id": chunk.parent_id, "path": path_context, "path_list": chunk.path } )

Récupération Contextuelle

Stratégie : Small-to-Big

Rechercher dans les chunks fins, retourner le contexte parent :

DEVELOPERpython
def hierarchical_retrieve(query: str, vector_db, k: int = 3) -> List[dict]: """ Récupère les chunks pertinents avec leur contexte parent. """ # 1. Recherche fine (niveau le plus bas) results = vector_db.query( query_embedding=embed(query), filter={"level": {"$gte": 3}}, # Sous-sections et plus limit=k * 2 ) # 2. Enrichir avec contexte parent enriched_results = [] seen_parents = set() for result in results: parent_id = result.metadata.get("parent_id") # Récupérer la chaîne de parents context_chain = [result.metadata["content"]] current_parent = parent_id while current_parent and current_parent not in seen_parents: parent = vector_db.get(current_parent) if parent: context_chain.insert(0, parent.metadata["content"]) seen_parents.add(current_parent) current_parent = parent.metadata.get("parent_id") else: break enriched_results.append({ "chunk": result, "full_context": "\n\n---\n\n".join(context_chain), "path": result.metadata["path"] }) return enriched_results[:k]

Stratégie : Big-to-Small

Rechercher au niveau section, puis drill-down :

DEVELOPERpython
def drill_down_retrieve(query: str, vector_db, k: int = 3) -> List[dict]: """ Commence par les sections, puis affine vers les détails. """ # 1. Recherche au niveau section sections = vector_db.query( query_embedding=embed(query), filter={"level": 2}, limit=k ) # 2. Pour chaque section pertinente, chercher les détails detailed_results = [] for section in sections: # Chercher les enfants de cette section children = vector_db.query( query_embedding=embed(query), filter={ "parent_id": section.id }, limit=3 ) detailed_results.append({ "section": section, "details": children, "combined_context": ( section.metadata["content"] + "\n\n" + "\n".join([c.metadata["content"] for c in children]) ) }) return detailed_results

LlamaIndex : Parent Document Retriever

LlamaIndex offre une implémentation native :

DEVELOPERpython
from llama_index import VectorStoreIndex, ServiceContext from llama_index.node_parser import HierarchicalNodeParser from llama_index.retrievers import AutoMergingRetriever from llama_index.query_engine import RetrieverQueryEngine # 1. Parser hiérarchique node_parser = HierarchicalNodeParser.from_defaults( chunk_sizes=[2048, 512, 128] # Niveaux de granularité ) # 2. Créer les nodes nodes = node_parser.get_nodes_from_documents(documents) # 3. Indexer index = VectorStoreIndex(nodes) # 4. Retriever avec auto-merging retriever = AutoMergingRetriever( index.as_retriever(similarity_top_k=6), index.storage_context, verbose=True ) # 5. Query engine query_engine = RetrieverQueryEngine.from_args(retriever) response = query_engine.query("Quelles sont les méthodes utilisées ?")

LangChain : Parent Document Retriever

DEVELOPERpython
from langchain.retrievers import ParentDocumentRetriever from langchain.storage import InMemoryStore from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.vectorstores import Chroma # Splitters pour différents niveaux parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000) child_splitter = RecursiveCharacterTextSplitter(chunk_size=400) # Store pour les parents docstore = InMemoryStore() # Vectorstore pour les enfants (recherche fine) vectorstore = Chroma(embedding_function=embeddings) # Parent Document Retriever retriever = ParentDocumentRetriever( vectorstore=vectorstore, docstore=docstore, child_splitter=child_splitter, parent_splitter=parent_splitter, ) # Ajouter les documents retriever.add_documents(documents) # Recherche : enfants matchent, parents retournés results = retriever.get_relevant_documents("Question sur les méthodes")

Optimisation des Métadonnées

Enrichir le Chemin Sémantique

DEVELOPERpython
def create_semantic_path(chunk: HierarchicalChunk) -> str: """ Crée un chemin sémantique lisible pour le LLM. """ path_parts = [] for i, title in enumerate(chunk.path): level_prefix = { 0: "Document:", 1: "Chapitre:", 2: "Section:", 3: "Sous-section:", 4: "Paragraphe:" }.get(i, "") path_parts.append(f"{level_prefix} {title}") return " → ".join(path_parts) # Exemple de sortie: # "Document: Manuel technique → Chapitre: Installation → Section: Prérequis"

Ajouter des Breadcrumbs au Contexte

DEVELOPERpython
def format_context_with_breadcrumbs(chunks: List[dict]) -> str: """ Formate le contexte avec des breadcrumbs pour le LLM. """ formatted = [] for chunk in chunks: breadcrumb = chunk['path'] content = chunk['content'] formatted.append(f""" 📍 {breadcrumb} {content} """) return "\n---\n".join(formatted)

Quand Utiliser le Découpage Hiérarchique

Utilisez-le quand :

  • Documents longs et structurés (manuels, docs techniques)
  • Hiérarchie claire (chapitres, sections, sous-sections)
  • Besoin de contexte large ET de précision fine
  • Questions qui traversent plusieurs niveaux

Évitez quand :

  • Documents plats (emails, chats, logs)
  • Contenu très homogène
  • Contraintes de latence strictes (overhead de récupération)
  • Documents très courts (< 2000 tokens)

Benchmarks

Type de documentDécoupage fixeSémantiqueHiérarchique
Documentation technique65%72%88%
Rapports structurés58%68%85%
Articles scientifiques62%75%82%
Texte narratif70%78%72%

MRR@5 sur datasets de test internes

Le hiérarchique excelle sur les documents structurés mais n'apporte pas de gain sur le contenu narratif.


Guides connexes

Chunking :

Retrieval :


Besoin d'aide pour implémenter le chunking hiérarchique sur vos documents complexes ? Parlons de votre projet →

Tags

chunkinghiérarchiestructuredocuments

Articles connexes

Ailog Assistant

Ici pour vous aider

Salut ! Pose-moi des questions sur Ailog et comment intégrer votre RAG dans vos projets !