Découpage Hiérarchique : Préserver la Structure de vos Documents
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.
- Auteur
- Équipe de Recherche Ailog
- Date de publication
- Temps de lecture
- 11 min de lecture
- Niveau
- advanced
- Étape du pipeline RAG
- Chunking
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 :
`python 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
`python 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
`python 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 :
`python def hierarchical_retrieve(query: str, vector_db, k: int = 3) -> List[dict]: """ Récupère les chunks pertinents avec leur contexte parent. """ 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 ) 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 :
`python def drill_down_retrieve(query: str, vector_db, k: int = 3) -> List[dict]: """ Commence par les sections, puis affine vers les détails. """ Recherche au niveau section sections = vector_db.query( query_embedding=embed(query), filter={"level": 2}, limit=k ) 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 :
`python 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 Parser hiérarchique node_parser = HierarchicalNodeParser.from_defaults( chunk_sizes=[2048, 512, 128] Niveaux de granularité ) Créer les nodes nodes = node_parser.get_nodes_from_documents(documents) Indexer index = VectorStoreIndex(nodes) Retriever avec auto-merging retriever = AutoMergingRetriever( index.as_retriever(similarity_top_k=6), index.storage_context, verbose=True ) Query engine query_engine = RetrieverQueryEngine.from_args(retriever)
response = query_engine.query("Quelles sont les méthodes utilisées ?") `
LangChain : Parent Document Retriever
`python 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
`python 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
`python 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 document | Découpage fixe | Sémantique | Hiérarchique | |------------------|----------------|------------|--------------| | Documentation technique | 65% | 72% | 88% | | Rapports structurés | 58% | 68% | 85% | | Articles scientifiques | 62% | 75% | 82% | | Texte narratif | 70% | 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 : • Stratégies de Chunking - Vue d'ensemble des approches • Découpage Sémantique - Découpage basé sur le sens • Découpage à Taille Fixe - Approche classique
Retrieval : • Parent Document Retrieval - Récupération avec contexte parent • Stratégies de Récupération - Techniques avancées
---
Besoin d'aide pour implémenter le chunking hiérarchique sur vos documents complexes ? Parlons de votre projet →*