Accéder au contenu principal

Tutoriel PageIndex : un RAG fondé sur le raisonnement, sans vecteurs

Apprenez à créer un système de QA documentaire en Python avec PageIndex, puis comparez-le à une baseline RAG vectorielle avec FAISS sur un véritable rapport financier de la Federal Reserve.
Actualisé 6 août 2026  · 8 min lire

Explorer avec l’IA

Ouvrir dans ChatGPTOuvrir dans ClaudeOuvrir dans Perplexity

Imaginez que vous avez sous les yeux le Financial Stability Report de la Federal Reserve. Quelqu'un vous demande : quel pourcentage de répondants a cité l'inflation persistante comme principal risque à court terme ? Vous allez à la section 5, repérez l'encadré 5.1 et trouvez le chiffre en deux secondes : 72 %.

Confiez cette question à un pipeline RAG standard : la probabilité de tomber sur la bonne réponse dépendra de la manière dont le découpage a morcelé la page. Si l'encadré 5.1 est fragmenté entre plusieurs blocs, la recherche par similarité renverra des passages où l'on cite l'inflation quelque part, mais ratera le chiffre. C'est la limite fondamentale de la similarité : un texte qui ressemble à votre requête n'est pas nécessairement la section qui y répond.

Pour des documents longs et structurés, l'enjeu est majeur — et c'est précisément ce que PageIndex vise à corriger. Dans ce tutoriel, je vais construire un système de QA documentaire avec PageIndex et le tester face à une base RAG vectorielle sur le même rapport de la Fed, afin que vous repartiez en sachant quand chaque approche justifie son coût.

En bref

  • PageIndex est un framework RAG sans vecteurs qui effectue la recherche par raisonnement sur la structure du document.
  • Il corrige les points faibles du RAG vectoriel : tableaux découpés, termes répétés et renvois internes.
  • Nous comparons les deux sur le rapport 2023 de la Fed : PageIndex vs un baseline FAISS.
  • Des compromis, pas de victoire écrasante : l'avantage de PageIndex grandit avec la longueur des documents.

Qu'est-ce que PageIndex ?

PageIndex est un framework de recherche qui raisonnes sur la structure du document plutôt que de rechercher un texte similaire à votre requête. Il a été publié par VectifyAI en septembre 2025, conçu par Mingtian Zhang et Yu Tang. Il est open source sous licence MIT et, en juillet 2026, le référentiel GitHub a accumulé plus de 34 000 étoiles.

PageIndex vs RAG standard

La meilleure façon d'expliquer le fonctionnement de PageIndex est de le contraster avec le RAG standard. Si vous découvrez RAG, l'idée de base est de donner à un LLM l'accès à un document en récupérant, à l'exécution de la requête, les parties pertinentes à fournir en contexte. 

C'est précisément l'étape de recherche que PageIndex revoit de fond en comble. Plutôt que de comparer votre requête à chaque chunk du document, PageIndex construit d'abord un index arborescent hiérarchique à partir de sa structure. Les résultats s'en trouvent transformés :

  • La recherche vectorielle retrouve le texte qui ressemble à votre requête. 
  • La recherche dans l'arbre retrouve les sections susceptibles de contenir la réponse

Autrement dit, la différence entre un RAG standard et PageIndex revient à chercher des mots-clés à chaque page d'un livre, versus lire la table des matières pour trouver d'abord le bon chapitre.

Ces deux objectifs se recoupent pour des requêtes factuelles simples sur des documents courts, mais divergent nettement dès que les documents deviennent longs, structurés et riches en renvois internes.

Dimension

PageIndex

RAG vectoriel

Mécanisme de recherche

Raisonnement LLM sur un index arborescent

Similarité cosinus sur embeddings

Coût de mise en place

Traitement du document (une seule fois)

Chunking + embedding (une seule fois)

Latence par requête

3–8 secondes (plusieurs appels LLM)

< 1 seconde (lecture d'index)

Coût par requête

Plus élevé (2+ appels LLM)

Plus faible (lookup d'embeddings + un appel LLM)

Précision sur docs longs et structurés

98,7 % sur FinanceBench (Mafin 2.5)

30–50 % sur FinanceBench

Gère les renvois internes

Oui

Non

Gère les tableaux découpés

Oui

Non

Traçabilité

Trace de raisonnement complète + citations de pages

Scores top-k de chunks

Idéal pour

Déclarations financières, contrats, manuels

FAQs, docs produit, tickets de support

Comment fonctionne l'index arborescent

Le processus se déroule en deux étapes.

  1. Vous ingérez un document, et PageIndex génère un arbre. Chaque nœud contient un titre, un résumé et un index de page. L'arbre reflète la hiérarchie naturelle du document (chapitres, sections, sous-sections, annexes, selon sa structure réelle).
  2. Lorsqu'une requête arrive, le LLM reçoit la structure de l'arbre (sans le texte intégral, qui dépasserait la fenêtre de contexte) et raisonne sur les nœuds susceptibles de contenir la réponse. Il renvoie un ensemble d'IDs de nœuds avec une trace de raisonnement expliquant chaque sélection, puis le texte de ces nœuds est extrait et passé à l'étape de génération.

Pourquoi c'est crucial pour les documents structurés

J'ai suffisamment construit de pipelines RAGs pour savoir que le découpage est l'endroit où la plupart des systèmes en production se grippent discrètement. La stratégie paraît propre sur le papier, mais dès que votre document contient un tableau qui traverse deux pages, ou une note de bas de page définissant un terme utilisé trois sections plus tôt, les fissures apparaissent. 

Le RAG vectoriel présente trois faiblesses structurelles qui ressortent systématiquement sur les documents financiers et juridiques.

  • Contenu découpé : un bilan scindé sur deux chunks perd la relation entre ses postes, et ses deux moitiés obtiennent une pertinence faible car elles n'ont pas de sens isolément. (Le late chunking aide ici, mais ne résout pas les renvois.)
  • Termes répétés : un rapport annuel mentionne « revenue » des dizaines de fois ; une requête sur la croissance du chiffre d'affaires d'une division extrait des chunks de chaque occurrence, classés à peu près également, sans moyen de distinguer le bon.
  • Renvois internes : quand la section 4.3 indique « voir l'annexe G pour la réconciliation complète », une recherche par similarité n'a aucun mécanisme pour suivre ce pointeur. La réponse est dans l'annexe G, mais le récupérateur ne peut pas le savoir.

PageIndex gère ces trois cas car il recherche à travers la structure, et non des fragments. L'arbre conserve l'intégrité du document, l'étape de raisonnement peut suivre un renvoi ou comparer des sections, et comme chaque nœud porte un index de page et un résumé, le récupérateur navigue dans la géographie du document plutôt que dans la similarité textuelle de surface.

Je serai franc : lorsque j'ai vu pour la première fois l'annonce de 98,7 % de précision pour Mafin 2.5 (système de VectifyAI basé sur PageIndex) sur FinanceBench, ma réaction a été le scepticisme : un écart de près de 50 points vs le RAG standard ressemble à un benchmark taillé sur mesure. Mais FinanceBench évalue justement ces modes d'échec : raisonnement multi-étapes sur des déclarations à la SEC avec réponses numériques précises.

Premiers pas avec PageIndex

Ce tutoriel nécessite quelques packages et deux clés d'API. Voici tout ce qu'il vous faut avant d'écrire la moindre ligne de code.

Vous pouvez consulter tout le code utilisé dans ce tutoriel dans mon repo GitHub associé.

Pour suivre ce tutoriel, vous aurez besoin :

Getting pageindex API key

Installez les packages requis :

pip install pageindex openai requests faiss-cpu pymupdf

Définissez vos clés d'API en variables d'environnement plutôt que de les coder en dur :

export PAGEINDEX_API_KEY="your_pageindex_key_here"
export OPENAI_API_KEY="your_openai_key_here"

Une fois les deux clés en place, initialisez les clients :

import os
import copy
import time
import json
import asyncio
import requests
from pageindex import PageIndexClient
import pageindex.utils as utils
import openai

PAGEINDEX_API_KEY = os.environ["PAGEINDEX_API_KEY"]
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]

pi_client = PageIndexClient(api_key=PAGEINDEX_API_KEY)
openai_client = openai.AsyncOpenAI(api_key=OPENAI_API_KEY)

Et voilà pour la mise en place : neuf imports, deux variables d'environnement, deux clients.

Ingestion d'un document et construction de l'arbre PageIndex

Pour ce tutoriel, nous utiliserons le Financial Stability Report d'octobre 2023 de la Federal Reserve. C'est un PDF public structuré comme un vrai document de production : sections formelles, chapitres numérotés, annexes et renvois internes.

Téléchargement du document

La Fed publie ses rapports en PDF accessibles via une URL stable. Le test if not os.path.exists() garantit que vous ne le récupérez qu'une seule fois, utile lorsque vous itérez sur le reste du code sans retélécharger un PDF de 60 pages à chaque exécution.

DOWNLOAD_DIR = "./data"
os.makedirs(DOWNLOAD_DIR, exist_ok=True)

PDF_URL = "https://www.federalreserve.gov/publications/files/financial-stability-report-20231020.pdf"
PDF_PATH = os.path.join(DOWNLOAD_DIR, "fed_financial_stability_report_2023.pdf")

if not os.path.exists(PDF_PATH):
    print("Downloading Federal Reserve Financial Stability Report (Oct 2023)...")
    response = requests.get(PDF_URL, timeout=60)
    response.raise_for_status()
    with open(PDF_PATH, "wb") as f:
        f.write(response.content)
    print(f"Saved to {PDF_PATH}")
else:
    print(f"Already present: {PDF_PATH}")

Soumission du document

La soumission tient en un seul appel d'API. submit_document() téléverse le fichier et renvoie un doc_id à réutiliser pour toutes les opérations ultérieures sur ce document. Conservez-le : si vous redémarrez la session, vous pouvez sauter la soumission et aller directement à get_tree() avec le même ID.

print("Submitting document to PageIndex...")
submit_result = pi_client.submit_document(PDF_PATH)
doc_id = submit_result["doc_id"]
print(f"Document ID: {doc_id}")

PageIndex traite le document de manière asynchrone ; il faut donc interroger l'état jusqu'à ce que l'arbre soit prêt :

print("Waiting for tree generation...")
while True:
    status_result = pi_client.get_document(doc_id)
    status = status_result.get("status")
    print(f"  Status: {status}")
    if status == "completed":
        break
    elif status == "failed":
        raise RuntimeError(f"Processing failed: {status_result}")
    time.sleep(10)

print("Done.")

Le traitement d'un PDF de 60 pages prend généralement deux à quatre minutes, coût à payer une seule fois par document. Vous verrez l'état passer de queuedprocessingcompleted

Submitting document to PageIndex...
Document ID: pi-cmq2bp4ok00rx01qxmym7tnd1

Waiting for tree generation...
  Status: queued
  Status: processing
  Status: completed
Processing done.

Inspection de l'arbre

Une fois le traitement terminé, get_tree() renvoie la structure hiérarchique complète sous forme de liste imbriquée de nœuds. Le helper utils.create_node_mapping() l'aplatit ensuite en un dictionnaire cléé par ID de nœud : l'extraction de texte en sera bien plus rapide que de parcourir récursivement l'arbre à chaque requête.

tree_result = pi_client.get_tree(doc_id, node_summary=True)
tree = tree_result["result"]

# Build a flat node map for easy access later
node_map = utils.create_node_mapping(tree)

# Print the top-level nodes
print(f"\nTop-level nodes ({len(tree)} sections):\n")
for node in tree:
    print(f"  [{node['node_id']}] {node['title']}")
    print(f"       Page {node['page_index']}")
    print(f"       {node['summary'][:120]}...")
    print()

Sur le Financial Stability Report, vous obtenez d'un coup d'œil l'ossature du document :

Top-level nodes (9 sections):

  [0000] Financial Stability Report
       Page 1
       This document is the October 2023 Financial Stability Report from the Federal Reserve...
  [0001] Purpose and Framework
       Page 5
       This report outlines the Federal Reserve's framework for assessing U.S. financial stability...
  [0002] Overview
       Page 9
       This report evaluates the stability of the U.S. financial system by analyzing four key vulnerability areas...
  [0003] 1 | Asset Valuations
       Page 13
       ...
  [0012] 2 | Borrowing by Businesses and Households
       Page 23
       ...
  [0019] 3 | Leverage in the Financial Sector
       Page 33
       ...
  [0029] 4 | Funding Risks
       Page 45
       ...
  [0036] 5 | Near-Term Risks to the Financial System
       Page 53
       ...
  [0041] Appendix | Figure Notes
       Page 59
       …

L'arbre est un simple JSON : chaque nœud porte un node_id, un title, un summary, un page_index et une liste nodes pour ses sous-sections. Contrairement aux index vectoriels, pas d'espace d'embeddings opaque, ni de fichier binaire nécessitant un lecteur spécifique. Juste une liste imbriquée que vous pouvez afficher, inspecter et raisonner dessus directement.

Cette transparence est très utile en pratique : si le récupérateur ramène la mauvaise section, vous pouvez regarder l'arbre et comprendre pourquoi — chose impossible avec un embedding en 768 dimensions.

Interroger PageIndex avec la recherche arborescente LLM

L'étape de recherche envoie la structure de l'arbre (sans le texte intégral, qui déborderait la fenêtre de contexte) à un LLM et lui demande d'identifier les nœuds pertinents pour la requête.

La fonction de recherche dans l'arbre

Le prompt fournit à l'LLM l'arbre allégé (titres et résumés uniquement, sans le texte des sections) et lui demande de renvoyer un objet JSON contenant une trace de raisonnement et une liste d'IDs de nœuds. Deux choix de conception sont à noter.

Premièrement, utils.remove_fields() supprime le contenu textuel de chaque nœud avant sérialisation, ce qui maintient le prompt dans les limites de contexte, même sur un document long. L'appel à copy.deepcopy() est nécessaire car remove_fields modifie en place ; vous devez préserver l'arbre original pour l'extraction de texte qui suit. 

Deuxièmement, response_format={"type": "json_object"} force une sortie structurée, donc un parsing fiable. La température reste à 0 car il s'agit d'un exercice de raisonnement.

TREE_SEARCH_PROMPT = """You are a document retrieval assistant. 
Given a document's tree structure and a user query, identify which nodes (sections) 
are most likely to contain the answer.

Document tree:
{tree_json}

User query: {query}

Return a JSON object with the following format:
{{
  "reasoning": "Your step-by-step reasoning about which sections to retrieve",
  "node_ids": ["id1", "id2", ...]
}}

Return ONLY the JSON object, no other text."""

async def tree_search(tree, query: str, model: str = "gpt-4o") -> dict:
    """Use an LLM to reason over the tree and return relevant node IDs."""
    
    # remove_fields mutates in place; deepcopy protects the original tree
    slim_tree = utils.remove_fields(copy.deepcopy(tree), fields=["text"])
    tree_json = json.dumps(slim_tree, indent=2)
    
    prompt = TREE_SEARCH_PROMPT.format(tree_json=tree_json, query=query)
    
    response = await openai_client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
        response_format={"type": "json_object"}
    )
    
    result = json.loads(response.choices[0].message.content)
    return result

Remarque : les appels await ci-dessous supposent un environnement Jupyter ou iPython où await au niveau supérieur est pris en charge. Si vous exécutez un simple script .py, encapsulez les appels async dans une fonction async def main() et lancez-la avec asyncio.run(main()).

Lancer une requête

Commencez par une question qui exige de naviguer entre plusieurs sections. Une requête sur les vulnérabilités liées aux valorisations d'actifs nécessite l'aperçu, le chapitre sur les valorisations et ses sous-sections par marché.

Ce sont des sections que le RAG vectoriel peut remonter séparément, mais rarement ensemble avec la bonne hiérarchie. C'est exactement le type de requête où la recherche arborescente fait la différence.

query_1 = "What are the main vulnerabilities the Fed identified in asset valuations as of October 2023, and which markets were flagged as stretched?"

result = await tree_search(tree, query_1)

print("LLM Reasoning:")
print(result["reasoning"])
print("\nSelected node IDs:", result["node_ids"])

La trace de raisonnement est la partie à surveiller de près. Voici le type de sortie que vous verrez :

LLM Reasoning:
To identify the main vulnerabilities in asset valuations as of October 2023,
we should focus on sections that specifically discuss asset valuations and
related market conditions. The '1 | Asset Valuations' section and its
subsections are directly relevant as they provide detailed insights into
asset valuation pressures, equity market conditions, and specific market
sectors flagged as stretched.

Selected node IDs: ['0003', '0004', '0006', '0009', '0010', '0011']

Vous voyez précisément pourquoi chaque section a été sélectionnée : un niveau de traçabilité qu'un simple classement par cosinus ne fournit pas.

Générer des réponses à partir du contexte retrouvé

Une fois les IDs de nœuds pertinents obtenus, il faut en extraire le texte correspondant et le passer à un modèle de génération.

Extraction du contexte et génération

Deux fonctions couvrent l'étape de génération. 

  • generate_answer() recherche chaque nœud sélectionné dans le node_map, préfixe un en-tête avec le titre de section et la page (ce qui offre des citations auditables dans la réponse finale), puis transmet le contexte assemblé à un modèle de génération. 

  • pageindex_pipeline() enchaîne recherche dans l'arbre et génération en un seul appel, pour éviter de les chaîner à la main à chaque fois.

ANSWER_PROMPT = """You are a financial document analyst. Answer the user's question 
using ONLY the provided context. Cite the specific section(s) you are drawing from.
If the context does not contain enough information to answer, say so clearly.

Context:
{context}

Question: {question}

Provide a precise, well-cited answer."""

async def generate_answer(node_ids: list, query: str, node_map: dict,
                          model: str = "gpt-4o") -> str:
    """Extract text from the selected nodes and generate an answer."""
    
    # Gather the text from each selected node
    context_parts = []
    for node_id in node_ids:
        node = node_map.get(node_id)
        if node:
            section_header = f"[{node['title']} | Page {node['page_index']}]"
            context_parts.append(f"{section_header}\n{node.get('text', '')}")
    
    context = "\n\n---\n\n".join(context_parts)
    prompt = ANSWER_PROMPT.format(context=context, question=query)
    
    response = await openai_client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        temperature=0
    )
    
    return response.choices[0].message.content

async def pageindex_pipeline(tree, query: str, node_map: dict) -> dict:
    """Full PageIndex pipeline: tree search + answer generation."""
    search_result = await tree_search(tree, query)
    node_ids = search_result["node_ids"]
    reasoning = search_result["reasoning"]
    answer = await generate_answer(node_ids, query, node_map)
    return {
        "query": query,
        "reasoning": reasoning,
        "retrieved_sections": node_ids,
        "answer": answer
    }

Lancez le pipeline complet :

result = await pageindex_pipeline(tree, query_1, node_map)

print(f"Query: {result['query']}\n")
print(f"Retrieved sections: {result['retrieved_sections']}\n")
print(f"Answer:\n{result['answer']}")

La réponse revient avec des citations au niveau des sections et des numéros de page. C'est la traçabilité que PageIndex offre par défaut.

Query: What are the main vulnerabilities the Fed identified in asset valuations
as of October 2023, and which markets were flagged as stretched?

Retrieved sections: ['0003', '0004', '0006', '0009', '0010', '0011']

Answer:
The main vulnerabilities identified by the Fed in asset valuations as of
October 2023 include:

1. Equity Markets: Valuations increased modestly from an already high level,
with the forward price-to-earnings ratio rising further above its historical
median (Page 13, "Equity market valuation pressures remained notable").

2. Residential Real Estate: House prices started increasing again, and the
price-to-rent ratio was close to its previous peak from the mid-2000s
(Page 20, "House prices started increasing again in recent months").

3. Commercial Real Estate: Despite recent price declines, valuations remained
elevated relative to rental income (Page 19, "Commercial real estate
valuations remained elevated").

4. Farmland: Prices near the peak of their historical distribution, driven
by strong agricultural commodity prices (Page 21, "Farmland valuations
remained elevated").

Comparer PageIndex au RAG vectoriel

Voici la partie vraiment utile. Construisons une baseline RAG vectorielle sur le même document et faisons tourner les deux pipelines sur les mêmes requêtes.

Construire la baseline RAG vectorielle

Le baseline suit le schéma standard : extraction du texte brut du PDF avec PyMuPDF, découpage en chunks de 500 mots avec recouvrement, embedding de chaque chunk avec text-embedding-3-small, puis chargement dans un index FAISS en mémoire pour des recherches par similarité cosinus.

Si vous souhaitez comprendre comment la taille et le recouvrement des chunks influencent la qualité de la récupération, notre article Chunking Strategies est un bon complément, et l'explication What Is FAISS? est également utile si les étapes de construction d'index vous sont nouvelles.

from openai import OpenAI
import numpy as np
import faiss

sync_openai = OpenAI(api_key=OPENAI_API_KEY)

def extract_text_from_pdf(pdf_path: str) -> str:
    """Extract raw text from a PDF using PyMuPDF."""
    import fitz  # pip install pymupdf
    doc = fitz.open(pdf_path)
    pages = []
    for page_num, page in enumerate(doc):
        text = page.get_text()
        if text.strip():
            pages.append(f"[Page {page_num + 1}]\n{text}")
    return "\n\n".join(pages)


def chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
    """Split text into overlapping chunks by word count."""
    words = text.split()
    chunks = []
    start = 0
    while start < len(words):
        end = min(start + chunk_size, len(words))
        chunks.append(" ".join(words[start:end]))
        start += chunk_size - overlap
    return chunks


def embed_chunks(chunks: list[str], model: str = "text-embedding-3-small") -> np.ndarray:
    """Embed a list of text chunks with the OpenAI embeddings API."""
    all_embeddings = []
    batch_size = 100
    for i in range(0, len(chunks), batch_size):
        batch = chunks[i : i + batch_size]
        response = sync_openai.embeddings.create(input=batch, model=model)
        batch_embeddings = [item.embedding for item in response.data]
        all_embeddings.extend(batch_embeddings)
    return np.array(all_embeddings, dtype="float32")


def build_faiss_index(embeddings: np.ndarray) -> faiss.IndexFlatIP:
    """Build an in-memory FAISS index for inner-product (cosine) search."""
    dim = embeddings.shape[1]
    faiss.normalize_L2(embeddings)
    index = faiss.IndexFlatIP(dim)
    index.add(embeddings)
    return index


def vector_rag_pipeline(query: str, chunks: list[str], index: faiss.IndexFlatIP,
                         k: int = 5, model: str = "gpt-4o") -> str:
    """Full vector RAG pipeline: embed query, retrieve top-k, generate answer."""
    
    # Embed the query
    query_embedding = sync_openai.embeddings.create(
        input=[query], model="text-embedding-3-small"
    ).data[0].embedding
    query_vec = np.array([query_embedding], dtype="float32")
    faiss.normalize_L2(query_vec)
    
    # Retrieve top-k chunks
    _, indices = index.search(query_vec, k)
    retrieved_chunks = [chunks[i] for i in indices[0] if i < len(chunks)]
    context = "\n\n---\n\n".join(retrieved_chunks)
    
    # Generate answer
    response = sync_openai.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": "Answer the question using only the provided context. Be precise."},
            {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {query}"}
        ],
        temperature=0
    )
    return response.choices[0].message.content

Avec ces helpers en place, construisez l'index. Sur un document de 60 pages, l'embedding est l'étape la plus lente : prévoyez une à deux minutes, selon votre connexion et le débit d'OpenAI.

print("Extracting text from PDF...")
raw_text = extract_text_from_pdf(PDF_PATH)

print("Chunking text...")
chunks = chunk_text(raw_text, chunk_size=500, overlap=50)
print(f"  {len(chunks)} chunks created")

print("Embedding chunks...")
embeddings = embed_chunks(chunks)
print(f"  Embeddings shape: {embeddings.shape}")

print("Building FAISS index...")
faiss_index = build_faiss_index(embeddings)
print("Done.\n")

Exécuter la comparaison

Un rapport de 60 pages produit environ 47 chunks avec 500 mots et 50 mots de recouvrement.

Extracting text from PDF...
Chunking text...
  47 chunks created
Embedding chunks (takes 1-2 min)...
  Embeddings shape: (47, 1536)
Building FAISS index...
Done.

Trois requêtes, chacune conçue pour exposer un défi de recherche différent :

queries = [
    # Direct factual lookup
    "What does the Fed consider the most significant near-term risk to financial stability as of October 2023?",

    # Cross-reference question (Box 5.2 is referenced from Section 5, requires following an internal pointer)
    "What methodology does the Fed use to assess climate-related financial risks, and where in the report is it described?",

    # Multi-section reasoning (requires connecting Section 1 and Section 3)
    "How do elevated asset valuations interact with leverage in the financial sector to amplify systemic risk, according to the report?"
]

Avec les deux pipelines prêts, la boucle ci-dessous envoie chaque requête dans les deux systèmes et collecte les résultats. La troncature [:500] sur chaque réponse garde la console lisible ; les réponses complètes sont stockées dans results pour analyse ultérieure.

results = []

for query in queries:
    print(f"\nQuery: {query}\n")
    
    # PageIndex
    pi_result = await pageindex_pipeline(tree, query, node_map)
    pi_answer = pi_result["answer"]
    
    # Vector RAG
    vec_answer = vector_rag_pipeline(query, chunks, faiss_index)
    
    results.append({
        "query": query,
        "pageindex_answer": pi_answer,
        "vector_rag_answer": vec_answer
    })
    
    print("PageIndex answer:")
    print(pi_answer[:500])
    print("\nVector RAG answer:")
    print(vec_answer[:500])
    print("\n" + "="*60)

Comparatif des résultats

Voici le type de résultats que vous observerez selon les trois profils de requêtes :

Requête

PageIndex

RAG vectoriel

Bonne réponse

Gagnant

Risque à court terme le plus important

Réponse large couvrant plusieurs risques avec citations de sections

A retrouvé le chiffre précis de 72 % (plus précis ici)

Inflation persistante et resserrement monétaire, cités par 72 % des répondants (encadré 5.1)

Match nul, léger avantage au RAG vectoriel sur la statistique

Méthodologie risque climat (renvoi)

A retrouvé la section, mais a choisi le nœud parent au lieu de l'encadré 5.2, donc la méthodologie n'est pas passée à la génération

A découpé pile à travers l'encadré 5.2 et renvoyé les étapes méthodologiques

L'encadré 5.2 décrit la traduction des risques physiques et de transition en expositions financières via des analyses de scénarios

RAG vectoriel

Valorisations d'actifs + effet de levier

A retrouvé 18 nœuds entre les sections 1 et 3 et expliqué le mécanisme d'amplification

A connecté les deux sections et expliqué le même mécanisme

Des valorisations élevées accroissent le risque de corrections brutales ; l'effet de levier amplifie les pertes quand elles frappent des acteurs endettés (sections 1 et 3)

Match nul

Interpréter les résultats

Les résultats sont plus nuancés qu'une victoire nette de l'un ou l'autre, ce qui les rend plus instructifs. 

PageIndex a navigué vers les bonnes sections dans les trois cas. Sur la question factuelle directe, les deux ont répondu correctement, mais le RAG vectoriel a ressorti le 72 % car son découpage a, par chance, capturé l'encadré 5.1 en entier — plus une chance de chunking qu'un avantage structurel. Sur la requête multi-sections, PageIndex a extrait 18 nœuds couvrant les sections 1 et 3 et expliqué l'interaction, à peu près à l'égal du RAG vectoriel. Le seul revers net est la requête à renvoi interne : et voici pourquoi.

Sur la méthodologie climat, PageIndex a raisonné vers la section 5 et l'aperçu, mais la méthodologie réside dans l'encadré 5.2, un nœud enfant distinct. La recherche a choisi la section parente, pas l'encadré, et le texte du parent renvoie à l'encadré sans le reproduire : l'étape de génération n'a donc jamais vu la méthodologie. Le RAG vectoriel l'a emporté en découpant directement à travers l'encadré 5.2. 

C'est un sujet de granularité de recherche, pas de raisonnement : sélectionner un nœud parent n'inclut pas automatiquement le texte de ses enfants. Vous pouvez combler l'écart en incitant le prompt à renvoyer les nœuds enfants de toute section pertinente, ou en étendant chaque nœud sélectionné à ses descendants avant la génération.

L'avantage de PageIndex est apparu plus nettement lors de l'exécution isolée plus haut (requête sur les valorisations), où il a correctement exploité la structure du document et renvoyé des réponses bien sourcées par section. Les requêtes de comparaison ci-dessus ont été choisies pour mettre à l'épreuve des scénarios de recherche plus délicats, d'où un léger biais en faveur du RAG vectoriel sur un document de 60 pages.

Conclusion honnête : l'avantage de PageIndex sur le RAG vectoriel s'accentue avec la longueur des documents et la densité des renvois internes. Sur 60 pages, l'écart est modéré. Sur un 10-K de 200 pages avec des dizaines de notes se renvoyant l'une l'autre, attendez-vous à un avantage notable.

Si vous voulez pousser le baseline RAG vectoriel avant de le déclasser, How to Improve RAG Performance couvre les cinq techniques les plus efficaces.

Quand utiliser PageIndex

La comparaison ci-dessus concerne un type et une longueur de document précis. Le choix de PageIndex dépend de trois facteurs : structure du document, type de requête et volume.

Quand PageIndex l'emporte

PageIndex justifie son surcoût pour les documents professionnels longs et structurés où une erreur coûte cher :

  • 10-K et autres déclarations financières
  • Contrats juridiques avec termes définis et renvois
  • Guides réglementaires
  • Manuels techniques numérotés 

Ce sont aussi les cas où les traces de raisonnement offrent une base d'audit concrète. Le point d'équilibre, c'est le faible volume à fort enjeu : quelques dizaines de documents par jour, où chaque réponse doit être juste, et la latence comme le coût supplémentaire se justifient.

Quand préférer le RAG vectoriel

Le RAG vectoriel est le meilleur choix par défaut pour des charges à haut débit et faible latence, où les multiples appels LLM de PageIndex par requête deviennent un goulet, tandis que des embeddings en cache absorbent la charge pour une fraction du coût. 

Il convient aussi aux documents plats sans grande hiérarchie à exploiter, comme :

  • Articles d'actualité
  • Descriptions produit
  • Tickets de support

C'est également le bon choix lorsque des réponses approximatives suffisent. Un chatbot de support qui retrouve la bonne FAQ neuf fois sur dix remplit probablement son contrat.

Les vrais compromis

Le coût, c'est la vitesse et l'argent. Chaque requête PageIndex implique au moins deux appels LLM (recherche dans l'arbre et génération), plus le traitement initial : comptez 3 à 8 secondes par requête sur 60 pages, contre moins d'une seconde pour un RAG vectoriel avec index FAISS en cache. La facture par requête suit la même logique.

Le Financial Stability Report est un cas d'usage pertinent pour PageIndex : structuré, hiérarchique, avec renvois. Un corpus de milliers de courts tickets de support ne l'est pas, et le RAG vectoriel l'y battra pour une fraction du coût.

Conclusion

Le RAG vectoriel intègre une hypothèse implicite : le passage le plus similaire à votre requête contient également la réponse. Sur des documents courts et peu structurés, cette hypothèse tient assez bien. Sur des documents professionnels longs et structurés, elle se fissure de manière prévisible : tableaux découpés, fréquences de termes trompeuses, renvois invisibles.

PageIndex répond à ces trois points en traitant la recherche comme un problème de raisonnement sur la structure du document, plutôt que comme une similarité sur des fragments de texte.

La règle pratique est simple : si vos documents ont une hiérarchie, si vos requêtes exigent de suivre des renvois internes et si l'exactitude prime sur la vitesse, PageIndex vaut la latence et le coût supplémentaires. Si vous exécutez des recherches à fort volume sur des documents courts ou plats, le RAG vectoriel reste le bon réflexe.

Envie d'aller plus loin sur le RAG en production et les systèmes agentiquess ? Notre parcours AI Engineering with LangChain vous fait passer des fondamentaux applicatifs à la recherche, l'évaluation et les agents outillés.

FAQ sur PageIndex

Qu'est-ce que PageIndex ?

PageIndex est un framework RAG open source de VectifyAI (septembre 2025) qui remplace la recherche par similarité vectorielle par le raisonnement d'un LLM sur un index arborescent hiérarchique : pas d'embeddings, pas de base de données vectorielle, pas de chunking.

En quoi PageIndex diffère-t-il d'un RAG standard ?

Un RAG standard découpe un document en fragments, les embedde, puis retrouve les chunks les plus similaires à une requête. PageIndex construit un arbre à partir de la structure naturelle du document et demande à un LLM de raisonner sur les sections susceptibles de contenir la réponse. La recherche devient un problème de raisonnement, pas de similarité.

PageIndex nécessite-t-il une clé d'API OpenAI ?

Pas nécessairement. Le dépôt open source auto-hébergé fonctionne avec tout fournisseur pris en charge par LiteLLM. Ce tutoriel utilise OpenAI pour le raisonnement de recherche et la génération de réponses ; si vous le suivez tel quel, il vous faudra une clé OpenAI ainsi qu'une clé PageIndex depuis dash.pageindex.ai/api-keys.

PageIndex convient-il à tous les documents ?

Non. PageIndex excelle sur des documents longs et structurés, avec hiérarchie et renvois. Pour de grands corpus de textes courts et peu structurés (tickets de support, pages FAQ), le RAG vectoriel sera plus rapide et moins cher.

Quel niveau de précision PageIndex a-t-il atteint sur FinanceBench ?

Mafin 2.5, le système de VectifyAI basé sur PageIndex, a atteint 98,7 %, contre environ 30–50 % pour un RAG vectoriel traditionnel. FinanceBench évalue le QA financier sur des déclarations à la SEC, qui requiert un raisonnement multiétapes et une restitution numérique exacte. C'est précisément l'écart que PageIndex vise à combler.


Josep Ferrer's photo
Author
Josep Ferrer
LinkedIn
Twitter

Josep est data scientist et chef de projet à l'Office du tourisme de Catalogne, où il utilise les données pour améliorer l'expérience des touristes en Catalogne. Son expertise comprend la gestion du stockage et du traitement des données, associée à des analyses avancées et à la communication efficace des données.

Il est également un éducateur dévoué, enseignant le programme de Master Big Data à l'Université de Navarre, et contribuant régulièrement à des articles perspicaces sur la science des données sur Medium et KDNuggets.

Il est titulaire d'une licence en ingénierie physique de l'université polytechnique de Catalogne et d'une maîtrise en systèmes interactifs intelligents de l'université Pompeu Fabra.

Actuellement, il s'engage avec passion à rendre les technologies liées aux données plus accessibles à un public plus large par le biais de la publication ForCode'Sake sur Medium.

Sujets

Apprenez le RAG avec DataCamp !

Cours

Retrieval Augmented Generation (RAG) avec LangChain

3 h
19.1K
Découvrez des méthodes d’intégration de data externes à des LLM à l'aide de la Génération à enrichissement contextuel (RAG) avec LangChain.
Afficher les détailsRight Arrow
Commencer Le Cours
Voir plusRight Arrow
Contenus associés

Tutoriel

Méthode index() de Python expliquée à l'aide d'exemples

Découvrez comment utiliser la fonction index() de Python pour trouver la position d'éléments dans des listes.
Sejal Jaiswal's photo

Sejal Jaiswal

cursor ai code editor

Tutoriel

Cursor AI : Un guide avec 10 exemples pratiques

Apprenez à installer Cursor AI sur Windows, macOS et Linux, et découvrez comment l'utiliser à travers 10 cas d'utilisation différents.

Tutoriel

Tableaux Python

Tableaux Python avec exemples de code. Découvrez comment créer et imprimer des tableaux à l'aide de Python NumPy dès aujourd'hui.
DataCamp Team's photo

DataCamp Team

Tutoriel

Tutoriel Python sur les structures de données

Initiez-vous aux structures de données de Python : apprenez-en plus sur les types de données et les structures de données primitives et non primitives, telles que les chaînes de caractères, les listes, les piles, etc.
Sejal Jaiswal's photo

Sejal Jaiswal

Tutoriel

Séquence de Fibonacci en Python : Apprenez et explorez les techniques de codage

Veuillez découvrir le fonctionnement de la suite de Fibonacci. Veuillez explorer ses propriétés mathématiques et ses applications concrètes.
Laiba Siddiqui's photo

Laiba Siddiqui

Tutoriel

Tutoriel sur les boucles « for » en Python

Apprenez à implémenter des boucles « for » en Python pour itérer une séquence ou les lignes et colonnes d'un DataFrame pandas.
Aditya Sharma's photo

Aditya Sharma

Voir PlusVoir Plus