Raisonnement à travers un arbre de documents plutôt que recherche de vecteurs
PageIndex crée un index hiérarchique qui permet à un LLM de le parcourir, en évitant le découpage en chunks et les embeddings. Son fonctionnement, les résultats obtenus sur FinanceBench, et les cas d’usage.
Sources: I Threw Out My Vector Database. RAG Got Way Better With PageIndex
Rares sont les situations aussi embarrassantes que celle où un modèle cite avec assurance la mauvaise section d’un rapport annuel de 200 pages au milieu d’une démonstration en direct. Une suite familière s’ensuit : l’équipe « corrige » le problème en améliorant la segmentation des données, en optimisant les chevauchements et en utilisant un modèle d’incorporation mieux ajusté, suivant ainsi le cycle habituel d’amélioration que traverse tout ingénieur RAG, convaincu que la prochaine modification fonctionnera enfin. Puis le système répond à une question sur les prévisions de revenus pour 2023 par un paragraphe tiré du plan de dépenses en capital de 2021. Ces deux sections utilisaient un langage financier similaire, si bien que les modèles d’incorporation les ont jugées liées. Or, elles ne l’étaient pas.
Ce type d’échec soulève une question légitime quant à savoir si toute l’approche est défaillante dès ses fondements.
Pourquoi la similarité ne suffit pas
Chaque tutoriel sur RAG enseigne la même méthode : diviser le document en segments de 500 mots, les intégrer, trouver les vecteurs les plus proches pour chaque question et les soumettre à un LLM. Cela semble élégant, une recherche sémantique associée à la génération de texte.
Ce que cette méthode omet, c’est que la similarité ne signifie pas de la pertinence. Une base de données vectorielle trouve du texte sémantiquement proche de la question, mais pas nécessairement du texte qui y répond. Un paragraphe sur les facteurs de risque d’un médicament et un autre sur son efficacité se trouvent proches l’un de l’autre dans l’espace d’incorporation, mais seul l’un d’eux traite d’une question concernant les risques financiers de l’entreprise.
Le découpage crée un second problème. En divisant un document juridique de 200 pages en fenêtres de 500 tokens, tout ce que son auteur a construit avec soin disparaît : les sections, les références croisées, le flux logique. Ce qui reste, c’est un amas de fragments sans aucune idée de leur origine ni de leur relation les uns avec les autres. Un meilleur découpage atténue légèrement le problème, mais ne résout jamais la cause profonde, car une structure ayant été éliminée ne peut être restaurée.
Un analyste humain ne lireait jamais de cette manière. Il parcourrait le sommaire, irait directement à la section appropriée, la lireait en connaissant le chapitre adjacent et vérifierait les notes de bas de page. Il navigue dans le document en le considérant comme tel.
Un arbre plutôt que des vecteurs
L’équipe de VectifyAI s’est demandé à quoi ressemblerait un système RAG s’il fonctionnait de cette manière, et a ainsi créé PageIndex. Une fois exprimée à voix haute, l’idée principale semble étonnamment évidente. Évitez de transformer les documents en vecteurs et de rechercher par similarité. Construisez plutôt un index en arbre hiérarchique du document, en quelque sorte un sommaire lisible par machine, et laissez un LLM suivre ce arbre pour trouver le matériel pertinent.
Cela signifie pas de base de données vectorielle, pas de segmentation en chunks et pas de scores de similarité, seulement un indexage structuré et une navigation intelligente.
Étape un : création de l’index
PageIndex ne fragmente pas le document qui lui est donné. Il analyse sa structure naturelle, c’est-à-dire les sections, sous-sections, titres et groupements logiques, afin de créer une hiérarchie similaire à celle dont un expert utiliserait pour organiser le contenu. Imaginez un sommaire généré automatiquement, mais encore plus complet. Chaque nœud contient plus qu’un titre : un résumé de ce que couvre sa branche, les questions qu’il peut raisonnablement répondre ainsi que sa relation avec les nœuds frères et le nœud parent.
Une spécification technique de 100 pages peut donner naissance à un arbre de trois ou quatre niveaux, avec les grandes zones fonctionnelles en haut et des sujets plus spécifiques dans les nœuds inférieurs. La hiérarchie logique du document reste telle que l’a définie son auteur. Cette étape s’exécute une seule fois par document, au début, et l’index est stocké tout en maintenant le contenu original intact et accessible.
Deuxième étape : raisonnement à travers l’arbre
Lorsqu’une requête arrive, PageIndex ne l’incorpore pas ni ne lance de recherche par voisin le plus proche. Un LLM navigue plutôt dans l’arbre d’index. Il lit les nœuds de niveau supérieur et détermine quel branchement contient le plus probablement la réponse, suit ce branchement, lit le niveau suivant et réfléchit à nouveau. Il continue à descendre jusqu’à avoir identifié les sections pertinentes.
C’est là que les choses changent. Il récupère ensuite ces sections complètes depuis le document original, sans fragmentation, et les utilise comme contexte pour la réponse.
La récupération des informations est conçue de manière explicite. Il est possible de voir exactement quels nœuds l’LLM a parcourus et sur quelles pages du document original il s’est arrêté. Il n’y a pas de score de cosinus opaque, et la chaîne de raisonnement est visible.
Essayer
La configuration est simple. Commencez par cloner le répertoire et installer ses dépendances :
git clone https://github.com/VectifyAI/PageIndex.git
cd PageIndex
pip3 install --upgrade -r requirements.txt
Créez ensuite un fichier .env dans le répertoire racine contenant la clé API :
CHATGPT_API_KEY=your_openai_api_key_here
Pour indexer un PDF, pointez le script vers celui-ci. Un rapport financier constitue un bon exemple, car c’est là que l’outil excelle :
python3 run_pageindex.py --pdf_path /path/to/annual_report.pdf
Les documents Markdown utilisent un drapeau différent :
python3 run_pageindex.py --md_path /path/to/technical_spec.md
Cela termine la configuration. Aucune base de données vectorielle à configurer, aucun modèle d’incorporation à choisir, aucun paramètre de segmentation sur lequel se tracasser.
Le traitement d’un document génère une structure d’index de ce type, simplifiée ici :
{
"document": "Apple Inc. Annual Report 2023",
"index": {
"title": "Apple Inc. Annual Report 2023",
"summary": "Comprehensive financial and operational report covering revenue, product segments, risks, and strategic outlook",
"children": [
{
"title": "Business Overview",
"summary": "Company description, product lines, and market position",
"pages": [1, 8],
"children": [...]
},
{
"title": "Financial Results",
"summary": "Revenue, operating income, EPS, and segment performance for fiscal 2023",
"pages": [45, 72],
"children": [
{
"title": "Revenue by Product Category",
"summary": "iPhone, Mac, iPad, Wearables, and Services revenue breakdown",
"pages": [46, 52]
},
{
"title": "Geographic Revenue Distribution",
"summary": "Americas, Europe, Greater China, Japan, Rest of Asia Pacific",
"pages": [53, 58]
}
]
},
{
"title": "Risk Factors",
"summary": "Operational, market, regulatory, and competitive risks",
"pages": [89, 110]
}
]
}
}
Imaginez qu’un analyste demande les revenus générés par les iPhone au cours de l’exercice 2023. Au lieu de chercher des fragments de texte similaires, le LLM consulte l’index, en déduit que "Résultats financiers > Revenus par catégorie de produit" est la section à consulter, s’y rend et récupère intégralement les pages 46 à 52. La réponse est accompagnée d’une citation précise de la page, et comme le modèle n’a jamais vu la section des facteurs de risque, rien d’indésirable ne vient la troubler.
Une version simplifiée de cette récupération basée sur le raisonnement se présente ainsi en code :
import json
from openai import OpenAI
client = OpenAI()
def navigate_index(query: str, index_node: dict, depth: int = 0) -> list[dict]:
"""
Recursively navigate the document index using LLM reasoning.
Returns list of relevant leaf nodes with page references.
"""
children = index_node.get("children", [])
if not children:
# Leaf node: return this section as relevant
return [index_node]
# Ask the LLM which branches are relevant to the query
children_summary = "\n".join([
f"[{i}] {child['title']}: {child['summary']}"
for i, child in enumerate(children)
])
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": (
"You are navigating a document index to find sections relevant "
"to a query. Select the index numbers of sections that are likely "
"to contain the answer. Return a JSON array of selected indices."
)
},
{
"role": "user",
"content": (
f"Query: {query}\n\n"
f"Available sections:\n{children_summary}\n\n"
f"Which sections should I look into? Return JSON array of indices only."
)
}
],
temperature=0,
response_format={"type": "json_object"}
)
selected = json.loads(response.choices[0].message.content).get("indices", [])
relevant_nodes = []
for idx in selected:
if idx # Recurse into selected branches
relevant_nodes.extend(
navigate_index(query, children[idx], depth + 1)
)
return relevant_nodes
def answer_with_pageindex(query: str, index: dict, document_pages: dict) -> str:
"""
Full PageIndex retrieval and answer generation.
"""
# Navigate the index to find relevant sections
relevant_nodes = navigate_index(query, index)
# Retrieve full text from identified pages
context_parts = []
citations = []
for node in relevant_nodes:
pages = node.get("pages", [])
if pages:
page_start, page_end = pages[0], pages[1]
for page_num in range(page_start, page_end + 1):
if page_num in document_pages:
context_parts.append(document_pages[page_num])
citations.append(f"p.{page_num}")
context = "\n\n".join(context_parts)
# Generate answer with full, unchunked context
answer_response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": (
"Answer the question based on the provided document sections. "
"Be precise. If the answer involves numbers or dates, quote them exactly."
)
},
{
"role": "user",
"content": f"Document sections:\n{context}\n\nQuestion: {query}"
}
],
temperature=0
)
answer = answer_response.choices[0].message.content
citation_str = ", ".join(set(citations))
return f"{answer}\n\n**Source:** {citation_str}"
Regardez ce qui fait défaut : les embeddings, la similarité cosinus, les bases de données vectorielles et la logique de segmentation en chunks. Le raisonnement structuré sur un index de documents remplace l’ensemble du processus de récupération.
Études de cas dans le domaine financier
FinanceBench rassemble des questions concernant des documents financiers réels tels que des déclarations à la SEC, des rapports annuels et des appels sur les résultats financiers. Les réponses doivent être précises sur le plan numérique, les sections doivent être référencées avec exactitude et les notes de bas de page doivent être lues attentivement. Le RAG basé sur des vecteurs a tendance à rencontrer des difficultés dans ce contexte, car les documents financiers sont remplis de langage semblable portant sur des sujets différents.
Mafin 2.5, basé sur PageIndex, a atteint une précision de 98,7 % sur FinanceBench. Les systèmes vectoriels RAG typiques sur des benchmarks financiers comparables atteignent généralement entre 65 et 80 %, en fonction de leur configuration. Cette différence n’est pas négligeable : elle distingue un outil dont un analyste financier peut réellement se fier d’un autre qui produit des absurdités plausibles nécessitant une vérification ligne par ligne.
La structure explique cela : les documents financiers sont précisément le domaine où la hiérarchie est primordiale. Un bilan consolidé, les notes aux états financiers ainsi que les documents MD&A suivent tous un agencement clair et intentionnel sur lequel les analystes s’appuient constamment. PageIndex préserve et utilise cet agencement, tandis que le chunking vectoriel le détruit.
Quand choisir l’un ou l’autre
PageIndex ne remplace pas universellement les systèmes vectoriels RAG. Le choix approprié dépend du projet.
Il se révèle efficace dans les cas suivants :
- Les documents présentent une hiérarchie claire, comme c’est le cas pour les rapports, les manuels, les contrats et les spécifications.
- La précision et les citations exactes sont indispensables.
- Le domaine est spécialisé, tel que la finance, le droit, la médecine ou les travaux réglementaires.
- Les questions nécessitent des références croisées entre plusieurs sections d’un même document.
- L’explicabilité est importante et le système doit montrer pourquoi il a donné le résultat obtenu.
Le RAG vectoriel traditionnel reste pertinent lorsque :
- Le corpus se compose de milliers de documents courts et indépendants, tels que des tickets de support ou des avis de produits.
- Les documents n’ont pas de structure cohérente.
- Le but est une recherche sémantique approximative sur un corpus vaste.
- La latence est la principale préoccupation et le chunking est déjà optimisé.
La plupart des cas d’usage liés aux documents d’entreprise, où un système doit être véritablement utile et non simplement impressionnant lors d’une démonstration, ressemblent bien plus au premier groupe que au second.
Pour une utilisation en production sans la charge liée à l’infrastructure, PageIndex propose également une API REST et une intégration MCP. La configuration requise pour le développeur est la suivante :
import requests
PAGEINDEX_API_KEY = "your_api_key"
BASE_URL = "https://api.pageindex.ai/v1"
def upload_document(file_path: str) -> str:
"""Upload a document and get back a document_id."""
with open(file_path, "rb") as f:
response = requests.post(
f"{BASE_URL}/documents",
headers={"Authorization": f"Bearer {PAGEINDEX_API_KEY}"},
files={"file": f}
)
return response.json()["document_id"]
def query_document(document_id: str, question: str) -> dict:
"""Query an indexed document and get a cited answer."""
response = requests.post(
f"{BASE_URL}/query",
headers={
"Authorization": f"Bearer {PAGEINDEX_API_KEY}",
"Content-Type": "application/json"
},
json={
"document_id": document_id,
"question": question
}
)
return response.json()
# Example usage
doc_id = upload_document("q3_earnings_report.pdf")
result = query_document(doc_id, "What was total revenue in Q3?")
print(result["answer"])
print(f"Sources: {result['citations']}")
On télécharge les versions API qui construisent automatiquement l’index, et lorsqu’une requête est faite, elles renvoient des réponses structurées avec des citations de page pouvant être traçées.
L’idée principale
Au-delà des scores de référence, il se passe quelque chose de plus profond. Le RAG vectoriel standard considère un document comme un ensemble de fragments de texte liés et ignore la structure que son auteur y a mise en place. Chaque titre, saut de section et référence croisée constitue un signal, c’est-à-dire le moyen pour l’auteur d’indiquer aux lecteurs où aller, mais le découpage en blocs jette tout cela au rebut avant même de tenter de reconstruire la pertinence à partir de rien. PageIndex traite un document tel qu’il est : un artefact structuré conçu pour être navigué par un lecteur compétent.
Un analyste senior qui consulte un rapport annuel ne lit pas à partir de la première page. Il consulte le sommaire, se rend directement à la partie qui l’intéresse, vérifie une donnée chiffrée à l’aide des notes, puis retourne au document MD&A. L’LLM intégré à PageIndex fait de même.
C’est aussi pour cette raison qu’il peut être appliqué à des domaines spécialisés. Un rapport de essai pharmaceutique possède une hiérarchie différente d’un contrat juridique ou d’une spécification technique, mais dans chacun de ces documents, l’auteur a suivi une logique interne que les lecteurs experts exploitent. PageIndex capture cette logique et permet de retrouver l’information en s’appuyant sur elle.
Un diagnostic différent
La conclusion alarmante est que des années d’efforts ont pu viser le mauvais problème. L’attention s’est portée sur les stratégies de segmentation alors que le véritable défaut détruisait la structure dès le départ. Les modèles d’incorporation ont été ajustés bien que la similarité sémantique n’ait jamais été le bon signal de recherche. Des outils de réclassement ont été ajoutés pour améliorer la qualité de la recherche, alors que c’était la méthode de recherche elle-même qui constituait le goulot d’étranglement.
L’écosystème RAG a mis en place des mécanismes complexes pour compenser le péché originel de la segmentation. PageIndex propose une alternative : pas de segmentation, pas d’incorporation, pas de recherche par similarité. Il s’agit de créer un index reflétant la structure réelle du document et de le parcourir par le biais du raisonnement.
Cela ne répond pas à tout. La recherche vectorielle reste supérieure pour la recherche sémantique sur des millions de documents courts et non structurés. Cependant, pour l’ensemble des questions relatives à des documents longs et structurés, qui couvrent la plupart des besoins des entreprises, un arbre hiérarchique mérite une attention sérieuse. Relancez la démonstration qui a échoué à deux reprises en demandant des projections de revenus pour 2023 : le système se dirige désormais vers la bonne section, affiche les bonnes pages et répond avec des citations exactes. Aucune année erronée, aucune section incorrecte, aucune confiance mal placée.
PageIndex est open source et peut être hébergé soi-même sur GitHub ; une interface de chat hébergée ainsi que des documents d’API avec des références REST et MCP sont disponibles pour l’essayer sur vos propres documents.
Lectures complémentaires
- 30 questions pour les interviews sur RAG et les bases de données vectorielles avec des réponses concises — Trente questions d’interview sur le RAG et la recherche vectorielle accompagnées de réponses courtes : récupération hybride, HNSW vs IVF, segmentation en blocs, réclassement, métriques d’évaluation et sécurité multi-locataires.
- RAG sans vecteurs : comment PageIndex lit les documents sans embeddings — La recherche vectorielle divise les documents longs en blocs, ce qui entraîne une perte de contexte. Découvrez comment un index arborescent basé sur le raisonnement trouve des réponses dans des rapports complexes, avec du code.