RAG sans vecteurs avec PageIndex : Recherche par raisonnement plutôt que par vecteurs
PageIndex remplace les embeddings et le chunking par un LLM qui parcourt un arbre de document. Un bref exemple en Python, ainsi que les cas où RAG vectoriel reste le meilleur choix.
Sources: **I Stopped Using Vector Databases for RAG : PageIndex Vectorless RAG**
Tout ceux qui travaillent depuis un certain temps sur des systèmes RAG connaissent cette frustration. On passe des heures à créer des embeddings, à ajuster la taille des fragments, à choisir une base de données vectorielle, et le système donne quand même la mauvaise réponse. Il trouve bien quelque chose de similaire, mais « similaire » et « pertinent » ne sont pas la même chose.
Les rapports financiers le montrent clairement. Prenons un document 10-K de 120 pages : une configuration RAG vectorielle continue de sélectionner des extraits du résumé exécutif, alors que la réponse réelle se trouve dans une note de bas de page à la page 87. Les deux passages partagent des mots-clés et un sens similaires, mais seul l’un contient le chiffre dont on a besoin.
PageIndex a été conçu pour résoudre précisément ce problème.
Qu’est-ce que PageIndex ?
PageIndex est un framework RAG open source développé par l’équipe VectifyAI. Son idée centrale est simple et diffère de celle de la plupart des solutions existantes. Au lieu de transformer un document en vecteurs et d’effectuer une recherche par voisin le plus proche, il crée un arbre hiérarchique à partir du document et fait en sorte qu’un LLM trouve la réponse par déduction.
Il élimine complètement les embeddings, les bases de données vectorielles ainsi que le découpage en chunks.
Le projet qualifie cela de « RAG sans vecteurs », et cette appellation est assez précise. Il est apparu en septembre 2025 et a rapidement attiré l’attention, principalement grâce à un chiffre rapporté : une précision de 98,7 % sur FinanceBench, contre environ 50 % pour les solutions RAG vectorielles traditionnelles sur ce même benchmark. Il convient d’aborder avec prudence les chiffres de performance communiqués par les fournisseurs et de tester sur ses propres documents ; cependant, si cet écart se maintient ne serait-ce qu’en partie, il s’agit d’une toute autre catégorie de solution.
Où le RAG vectoriel échoue
L’hypothèse de base du RAG vectoriel est que le texte le plus semblable sur le plan sémantique à la requête est également le plus pertinent. En pratique, cette hypothèse échoue constamment.
Si l’on pose la question « Quel était le total de la dette de l’entreprise en 2023 ? », les extraits ayant le score le plus élevé pourraient être :
- une lettre du PDG abordant une stratégie de gestion de la dette,
- une section sur les risques liés aux clauses de dette,
- une entrée de glossaire définissant la dette à long terme.
Ces trois éléments sont proches du sens de la requête, mais aucun n’est la réponse, qui est une valeur précise figurant dans le bilan à la page 64.
D’autres problèmes s’ajoutent encore :
- Le découpage en fragments détruit le contexte. Diviser un document de 100 pages en morceaux de 500 tokens élimine sa structure. Une phrase telle que « comme indiqué dans le Tableau 3.2 » devient dissociée de ce tableau une fois que les deux se trouvent dans des fragments différents, et la logique ainsi que le flux du document sont ignorés.
- Les vecteurs ne peuvent pas suivre les références croisées. Les documents indiquent parfois « voir l’Annexe G » ou « se référer aux chiffres de l’année précédente à la Section 4 », mais la recherche par similarité ne dispose d’aucun moyen pour suivre ces indications. Il s’agit simplement de fragments de texte sans lien avec leurs cibles.
- Les requêtes expriment une intention, pas un contenu. L’utilisateur décrit ce qu’il souhaite savoir, et non à quoi ressemblera la réponse. La réponse peut utiliser un vocabulaire différent, ce qui complique la recherche vectorielle, car celle-ci compare le texte de la question avec celui de la réponse.
Récupération par raisonnement
Le changement de perspective réside ici : au lieu de se demander quels fragments ressemblent à la requête, PageIndex cherche à savoir où un expert humain chercherait dans ce document pour y répondre. Il s’agit d’une stratégie de récupération différente, qui convient bien mieux aux documents structurés et complexes.
Comment ça marche
Il y a deux phases.
Phase 1 : création de l’index en arbre. Face à un document, PageIndex ne le divise pas en fragments. Il le lit et construit une hiérarchie qui reflète la structure naturelle du document, à l’image d’un sommaire intelligent et profondément imbriqué. Chaque nœud représente une section ou un sous-section, avec un titre et un résumé de son contenu. Le LLM rédige ces résumés lors de l’indexation, qui deviennent ensuite les signaux de récupération.
Pour un rapport 10-K, l’arbre pourrait ressembler à ceci :
Annual Report 2023
├── Business Overview
│ ├── Products and Services
│ └── Market Position
├── Risk Factors
│ ├── Financial Risks
│ └── Operational Risks
├── Financial Statements
│ ├── Balance Sheet
│ │ ├── Assets
│ │ └── Liabilities
│ └── Income Statement
└── Notes to Financial Statements
├── Note 1: Accounting Policies
└── Note 12: Long-term Debt
Le document n’est plus plat. Il possède une structure, et le LLM peut le parcourir comme une personne feuillette un livre.
Phase 2 : recherche par arbre par raisonnement. Lorsqu’une requête arrive, rien n’est envoyé dans une base de données vectorielle. Le LLM reçoit la requête ainsi que le niveau supérieur de l’arbre et est invité à raisonner sur l’endroit où se trouve probablement la réponse. Il peut en conclure qu’une question concernant la dette à long terme appartient à la section des États financiers, probablement aux notes, et retourner un ID de nœud.
PageIndex développe ensuite ce nœud, affichant le niveau de détail suivant, et le LLM continue à explorer jusqu’à ce qu’il atteigne le contenu qui répond à la question.
Un analyste humain travaille de la même manière. Personne ne lit un rapport de 200 pages de la première à la dernière page. On consulte le sommaire, on trouve la section appropriée et on y va directement. PageIndex fait cela à la vitesse d’une machine.
À l’exception de la dernière récupération de contenu, chaque étape correspond au raisonnement du modèle. Aucun élément n’est identifié par vecteur ; le LLM détermine lui-même où chercher.
Une petite implémentation
La démonstration suivante crée une version simplifiée de cette idée en Python, en utilisant Gemini comme LLM.
Étape 1 : parser le document
Commencez par extraire le texte brut page par page. PyMuPDF gère les PDF de manière efficace.
import fitz # pip install pymupdf
def parse_pdf(pdf_path: str) -> list[dict]:
doc = fitz.open(pdf_path)
pages = []
for i, page in enumerate(doc):
text = page.get_text().strip()
if text:
pages.append({"page_num": i + 1, "text": text})
doc.close()
return pages
Ensuite, regroupez les pages consécutives en sections. Contrairement à un découpage arbitraire, cette méthode respecte les limites des pages et conserve le contenu lié ensemble.
def group_pages_into_sections(pages, per_section=3):
sections = []
for i in range(0, len(pages), per_section):
batch = pages[i : i + per_section]
section_id = f"S{str(i // per_section + 1).zfill(3)}"
combined_text = "\n\n".join(p["text"] for p in batch)
sections.append({
"section_id": section_id,
"start_page": batch[0]["page_num"],
"end_page": batch[-1]["page_num"],
"text": combined_text,
})
return sections
Étape 2 : créer l’index arborescent à l’aide d’un LLM
C’est là que réside l’essence de la méthode. Les embeddings sont ignorés, et le modèle lit un aperçu de chaque section pour en rédiger un résumé structuré. Le résultat est un arbre JSON imbriqué, dont chaque nœud contient un titre, un bref résumé et une liste des sujets clés.
from google import genai
from google.genai import types
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
def index_section(section: dict) -> dict:
preview = section["text"][:1500]
prompt = f"""Read this section from a document and summarize it.
Section pages: {section['start_page']} to {section['end_page']}
Text:
{preview}
Respond with ONLY valid JSON:
{{
"title": "short descriptive title (5-8 words)",
"summary": "2-3 sentence summary of what this section covers",
"key_topics": ["topic1", "topic2", "topic3"]
}}"""
response = client.models.generate_content(
model="gemini-2.0-flash",
contents=prompt,
config=types.GenerateContentConfig(temperature=0.0),
)
parsed = json.loads(response.text.strip())
return {
"node_id": section["section_id"],
"title": parsed["title"],
"pages": f"{section['start_page']}-{section['end_page']}",
"summary": parsed["summary"],
"key_topics": parsed["key_topics"],
}
Exécutez-le sur toutes les sections et l’arbre complet est enregistré sous forme de fichier JSON.
def build_tree_index(sections):
nodes = [index_section(s) for s in sections]
return {
"title": "Your Document Title",
"total_sections": len(nodes),
"nodes": nodes,
}
# Save for reuse
with open("tree.json", "w") as f:
json.dump(tree, f, indent=2)
Étape 3 : recherche dans un arbre guidée par le raisonnement
À ce stade, cette approche se sépare complètement de RAG vectoriel. Une question arrive, et au lieu de rechercher dans une base de données, on fournit au LLM tout l’arbre et on lui demande quels sections contiennent le plus probablement la réponse. Il agit comme un analyste qui parcourt un sommaire.
def retrieve_sections(tree: dict, query: str) -> dict:
# Build a compact text representation of the tree
tree_text = f"Document: {tree['title']}\n\n"
for node in tree["nodes"]:
tree_text += f"[{node['node_id']}] Pages {node['pages']} | {node['title']}\n"
tree_text += f" Summary: {node['summary']}\n"
tree_text += f" Topics: {', '.join(node['key_topics'])}\n\n"
prompt = f"""You are a document retrieval expert.
Given this document tree, identify which sections most likely answer the question.
Think step by step about where a human expert would look.
{tree_text}
QUESTION: {query}
Respond with ONLY valid JSON:
{{
"reasoning": "your step-by-step reasoning about where to look",
"selected_ids": ["S001", "S004"],
"confidence": "high/medium/low"
}}"""
response = client.models.generate_content(
model="gemini-2.0-flash",
contents=prompt,
config=types.GenerateContentConfig(temperature=0.0),
)
return json.loads(response.text.strip())
Le champ de raisonnement dans la réponse n’est pas seulement une décoration. Il indique pourquoi le modèle a sélectionné ces sections, ce qui constitue l’explicabilité en l’occurrence. Vous pouvez examiner la décision de récupération, ce que la recherche vectorielle ne permet pas.
Étape 4 : récupérer le contenu
Avec les IDs des sections choisies en main, récupérez le texte brut de ces sections. Aucune recherche vectorielle ni calcul d’embedding nécessaire, seulement une recherche dans un dictionnaire des sections que vous avez déjà analysées.
def retrieve_content(selected_ids: list, sections: list) -> str:
section_map = {s["section_id"]: s for s in sections}
context_parts = []
for sid in selected_ids:
if sid in section_map:
sec = section_map[sid]
context_parts.append(
f"--- Pages {sec['start_page']}-{sec['end_page']} ---\n"
+ sec["text"][:3000]
)
return "\n\n".join(context_parts)
Étape 5 : générer la réponse
Le texte récupéré est envoyé à Gemini accompagné de la question originale, et le modèle rédige une réponse précise avec des citations au niveau de la page.
def generate_answer(query: str, context: str) -> str:
prompt = f"""Answer the question using only the provided context.
Be specific. Include exact numbers, technical terms, and cite page numbers.CONTEXT:
{context}
QUESTION: {query}
ANSWER:"""
response = client.models.generate_content(
model="gemini-2.0-flash",
contents=prompt,
config=types.GenerateContentConfig(temperature=0.1),
)
return response.text.strip()
PageIndex contre le vector RAG
Ces deux approches diffèrent sur plusieurs points. Le vector RAG est rapide, s’adapte à de grandes collections et nécessite des embeddings ainsi qu’un index. PageIndex n’a besoin de rien de tout cela, mais effectue plusieurs appels séquentiels à un LLM par requête, offrant en retour une précision plus élevée pour les documents structurés ainsi qu’un suivi complet du raisonnement.
Quand choisir PageIndex
Ce n’est pas un remplaçant universel pour le vector RAG. C’est un outil différent destiné à une tâche différente.
Préférez PageIndex lorsque :
- Vous travaillez avec un seul document long et structuré où la précision prime sur la vitesse : documents financiers, contrats juridiques, documents réglementaires, manuels techniques, articles académiques. Tout document présentant une hiérarchie significative où « suffisamment proche » ne suffit pas.
- Vous avez besoin d’explicabilité, car les utilisateurs ou les parties prenantes doivent savoir précisément d’où provient une réponse. Vous obtenez ainsi un suivi du raisonnement avec des références de sections et des numéros de page.
- Vous travaillez dans des secteurs réglementés tels que la finance, le droit et la santé, où une réponse erronée a des conséquences réelles et où l’exactitude des informations est cruciale.
Préférez Vector RAG lorsque :
- Vous effectuez des recherches dans une grande collection et devez déterminer quels documents sont pertinents. Vector RAG excelle dans cette tâche, tandis que PageIndex se concentre sur un seul document et n’est pas conçu pour en sélectionner le bon parmi mille.
- La capacité de traitement et le temps de réponse sont importants pour vous. Effectuer plusieurs appels à un LLM consécutivement pour chaque requête devient coûteux et lent à mesure que le trafic augmente.
- Le matériel est bref, peu structuré ou de nature conversationnelle. La force de PageIndex réside dans la navigation hiérarchique, et sans hiérarchie, son avantage disparaît.
Combinez-les. Utilisez la recherche vectorielle pour trouver le bon document dans votre collection, puis utilisez PageIndex pour obtenir la réponse précise à l’intérieur de celui-ci. Un tel hybride combine les avantages des deux approches.
L’idée principale
Ce que fait réellement PageIndex, c’est de considérer la récupération d’informations comme un problème de raisonnement plutôt que comme un problème de similarité. Ce changement a des implications au-delà des documents. Selon certaines études, Claude Code a cessé de compter sur la recherche par embedding pour trouver du code et s’appuie désormais sur des recherches basées sur le raisonnement. Le schéma devient de plus en plus clair : lorsque une tâche exige de comprendre une structure et de suivre une logique, le raisonnement l’emporte sur la similarité.
Les bases de données vectorielles ne disparaîtront pas. Elles restent rapides, scalables et idéales pour la recherche sémantique sur de grandes collections. Mais pour résoudre le problème difficile consistant à trouver l’information exacte au sein d’un document complexe, l’approche basée sur le raisonnement semble être la meilleure option.
PageIndex fait partie des meilleures implémentations de cette idée aujourd’hui. Il est open source, dispose d’une API claire, et le projet propose un service cloud ainsi qu’un serveur MCP permettant de l’intégrer à Claude ou à d’autres workflows agents sans devoir écrire du code de recherche à partir de zéro. Les exemples fournis dans le guide officiel constituent un bon point de départ et permettent de mettre en place un système fonctionnel en environ une demi-heure. Si vos utilisateurs posent des questions précises concernant des rapports de 200 pages, cela vaut la peine d’essayer.
Lectures complémentaires
- Comment fonctionne RAG : embeddings, bases de données vectorielles et chunking — Suivez un morceau de texte depuis sa tokenisation, en passant par le pooling, jusqu’à la recherche par similarité cosinus, et découvrez pourquoi le chunking ainsi que les modèles d’embeddings déterminent la qualité de RAG.
- PageIndex : RAG sans vecteurs, basé sur le raisonnement et utilisant un index arborescent — PageIndex remplace les bases de données vectorielles par un index arborescent navigué par un LLM. Apprenez comment il fonctionne, consultez du code Python fonctionnel, des résultats de benchmark et découvrez quand l’utiliser.