Cursus
Stel je voor dat je het Financial Stability Report van de Federal Reserve voor je hebt. Iemand vraagt: Welk percentage van de respondenten noemde aanhoudende inflatie als het grootste risico op korte termijn? Je slaat naar sectie 5 om, vindt Box 5.1 en ziet het getal in twee seconden. Het is 72%.
Geef die vraag aan een standaard RAG-pijplijn, en of die het antwoord vindt hangt ervan af hoe de chunker die pagina toevallig heeft opgedeeld. Als Box 5.1 in stukken is geknipt, levert een gelijkeniszoektocht passages op die ergens inflatie noemen maar de figuur missen. Dat is de kernbeperking van gelijkeniszoektocht: tekst die lijkt op je vraag is niet hetzelfde als de sectie die het antwoord geeft.
Voor lange, gestructureerde documenten maakt dat veel uit, en precies dat is wat PageIndex oplost. In deze tutorial bouw ik een document-QA-systeem met PageIndex en test ik het tegen een vector-RAG-baseline op datzelfde Fed-rapport, zodat je weet wanneer elke aanpak zijn kosten waard is.
TL;DR
- PageIndex is een vectorloze RAG-framework dat ophaalt door te redeneren over de structuur van een document.
- Het pakt de zwakke plekken van vector-RAG aan: gesplitste tabellen, herhaalde termen en kruisverwijzingen.
- We benchmarken beide op het 2023-rapport van de Fed: PageIndex vs. een FAISS-baseline.
- Een afweging, geen zuivere overwinning: het voordeel van PageIndex groeit met de lengte van het document.
Wat is PageIndex?
PageIndex is een retrieval-framework dat redenert over de structuur van een document in plaats van te zoeken naar tekst die lijkt op je query. Het werd uitgebracht door VectifyAI in september 2025, gebouwd door Mingtian Zhang en Yu Tang. Het is open source onder de MIT-licentie, en sinds juli 2026 heeft de GitHub-repository meer dan 34.000 sterren vergaard.
PageIndex vs standaard RAG
De beste manier om uit te leggen hoe PageIndex werkt, is door het te contrasteren met standaard RAG. Als je nieuw bent met RAG, is het basisidee dat je een LLM toegang geeft tot een document door op het moment van de query de relevante delen op te halen en die als context mee te geven.
Precies die retrieval-stap wordt door PageIndex heruitgevonden. In plaats van je query te vergelijken met elke chunk in het document, bouwt PageIndex eerst een hiërarchische boomindex op basis van de documentstructuur. Dat leidt tot andere resultaten:
- Vectorzoektocht vindt tekst die er vergelijkbaar uitziet met je query.
- Boomzoektocht vindt secties die waarschijnlijk het antwoord bevatten.
Met andere woorden, het verschil tussen standaard RAG en PageIndex is dat tussen elke pagina in een boek scannen op relevante trefwoorden en eerst de inhoudsopgave lezen om het juiste hoofdstuk te vinden.
Die twee doelen komen samen bij simpele feitelijke vragen over korte documenten, maar lopen sterk uiteen zodra documenten lang, gestructureerd en vol interne kruisverwijzingen zijn.
|
Dimensie |
PageIndex |
Vector RAG |
|
Retrievalmechanisme |
LLM-redeneren over boomindex |
Cosinusgelijkenis over embeddings |
|
Opstartkosten |
Documentverwerking (eenmalig) |
Chunking + embedding (eenmalig) |
|
Latentie per query |
3–8 seconden (meerdere LLM-calls) |
< 1 seconde (indexlookup) |
|
Kosten per query |
Hoger (2+ LLM-calls) |
Lager (embedding-lookup + één LLM-call) |
|
Nauwkeurigheid op lange gestructureerde docs |
98,7% op FinanceBench (Mafin 2.5) |
30-50% op FinanceBench |
|
Gaat om met kruisverwijzingen |
Ja |
Nee |
|
Gaat om met gesplitste tabellen |
Ja |
Nee |
|
Herleidbaarheid |
Volledige redeneer-trace + paginaverwijzingen |
Top-k chunk-scores |
|
Best voor |
Financiële filings, contracten, handleidingen |
FAQ's, productdocumentatie, supporttickets |
Hoe de boomindex werkt
Het proces verloopt in twee stappen.
- Je neemt een document op, en PageIndex genereert een boom. Elke knoop bevat een titel, een samenvatting en een pagina-index. De boom weerspiegelt de natuurlijke hiërarchie van het document (hoofdstukken, secties, subsecties, bijlagen, wat het document ook bevat).
- Wanneer er een query binnenkomt, krijgt de LLM de boomstructuur (zonder de volledige tekst, die het contextvenster zou overschrijden) en redeneert over welke knopen waarschijnlijk het antwoord bevatten. Het retourneert een set knoop-ID's samen met een redeneer-trace die uitlegt waarom elke knoop is geselecteerd, waarna de tekst uit die knopen wordt geëxtraheerd en doorgegeven aan een generatiestap.

Waarom het ertoe doet voor gestructureerde documenten
Ik heb genoeg RAG-pijplijnen gebouwd om te weten dat chunking is waar de meeste productiesystemen stilletjes falen. De strategie oogt netjes op papier, maar zodra je document een tabel heeft die over paginagrensen heen loopt, of een voetnoot die een term definieert die drie secties eerder is gebruikt, worden de scheuren zichtbaar.
Vector-RAG heeft drie structurele zwaktes die consequent opduiken in financiële en juridische documenten.
- Gesplitste content: Een balans die over twee chunks is verdeeld verliest de relatie tussen de afzonderlijke regels, en beide helften scoren als laag-relevant omdat geen van beide op zichzelf logisch is. (Late chunking helpt hier, maar lost kruisverwijzingen niet op.)
- Herhaalde termen: In een jaarverslag komt "revenue" tientallen keren voor, dus een query over de omzetgroei van één divisie haalt chunks op van elke vermelding, grofweg gelijk gerangschikt, zonder manier om de relevante te onderscheiden.
- Kruisverwijzingen: Wanneer sectie 4.3 zegt "zie Bijlage G voor de volledige reconciliatie", heeft een gelijkeniszoektocht geen mechanisme om die verwijzing te volgen. Het antwoord staat in Bijlage G, en de retriever kan dat niet weten.
PageIndex kan alle drie aan omdat het ophaalt over structuur in plaats van fragmenten. De boom houdt het document als geheel, de redeneerstap kan een kruisverwijzing volgen of secties vergelijken, en omdat elke knoop een pagina-index en samenvatting draagt, navigeert de retriever door de documentgeografie in plaats van oppervlakkige tekstgelijkenis.
Ik ben eerlijk: toen ik voor het eerst de claim van 98,7% nauwkeurigheid voor Mafin 2.5 (VectifyAI's PageIndex-gebaseerde systeem) op FinanceBench zag, was mijn eerste reactie scepsis, want een kloof van bijna 50 punten ten opzichte van standaard RAG oogt als een benchmark die vleit. Maar FinanceBench test precies deze faalmodi: meerstapsredeneren over SEC-filings met precieze numerieke antwoorden.
Aan de slag met PageIndex
Voor deze tutorial heb je een paar pakketten en twee API-sleutels nodig. Dit is alles wat je nodig hebt voordat je een regel code schrijft.
Je kunt alle code die in de tutorial is gebruikt bekijken in mijn bijbehorende GitHub-repo.
Om mee te doen met deze tutorial, heb je nodig:
- Python 3.10+ geïnstalleerd
- Basiskennis van RAG-concepten. Als je nieuw bent met retrieval-augmented generation, lees dan What is Retrieval Augmented Generation (RAG)? voordat je verdergaat
- Een OpenAI API-sleutel van platform.openai.com/api-keys
- Een PageIndex API-sleutel van dash.pageindex.ai/api-keys (de gratis laag is voldoende voor deze tutorial)

Installeer de vereiste pakketten:
pip install pageindex openai requests faiss-cpu pymupdf
Stel de API-sleutels in als omgevingsvariabelen in plaats van ze hard te coderen:
export PAGEINDEX_API_KEY="your_pageindex_key_here"
export OPENAI_API_KEY="your_openai_key_here"
Met beide sleutels ingesteld, initialiseer je je 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)
Dat is de hele setup: negen imports, twee omgevingsvariabelen, twee clients.
Een document inlezen en de PageIndex-boom bouwen
Voor deze tutorial gebruiken we het Financial Stability Report van de Federal Reserve van oktober 2023. Het is een publiek beschikbare PDF die is opgebouwd als een echt productiedocument: formele secties, genummerde hoofdstukken, bijlagen en interne kruisverwijzingen.
Het document downloaden
De Fed publiceert haar rapporten als toegankelijke PDF's op een stabiele URL. De if not os.path.exists()-check betekent dat je het maar één keer ophaalt, wat uitmaakt wanneer je aan de rest van de code sleutelt en niet bij elke run opnieuw een PDF van 60 pagina's wilt downloaden.
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}")
Het document indienen
Indienen is één API-call. submit_document() uploadt het bestand en retourneert een doc_id die je voor elke volgende bewerking op dit document gebruikt. Sla die ergens op, want als je de sessie opnieuw start, kun je de indienstap overslaan en direct naar get_tree() gaan met dezelfde 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 verwerkt het document asynchroon, dus je moet pollen totdat de boom klaar is:
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.")
Het verwerken van een PDF van 60 pagina's duurt doorgaans twee tot vier minuten, en deze kosten maak je maar één keer per document. Je ziet de status cyclen door queued → processing → completed.
Submitting document to PageIndex...
Document ID: pi-cmq2bp4ok00rx01qxmym7tnd1
Waiting for tree generation...
Status: queued
Status: processing
Status: completed
Processing done.
De boom inspecteren
Zodra de verwerking voltooid is, retourneert get_tree() de volledige hiërarchische structuur als een geneste lijst van knopen. De helper utils.create_node_mapping() vlakt die vervolgens af tot een simpele dictionary met knoop-ID als sleutel, wat later tekstextractie veel sneller maakt dan de boom bij elke query recursief doorlopen.
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()
Dit uitvoeren op het Financial Stability Report geeft je in één oogopslag het skelet van het 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
…
De boom is platte JSON, waarbij elke knoop een node_id, een title, een summary, een page_index en een nodes-lijst bevat voor eventuele subsections. In tegenstelling tot vectorindexen is er geen ondoorzichtige embedding-ruimte of binair indexbestand dat een speciale reader vereist. Het is gewoon een geneste lijst die je direct kunt printen, inspecteren en over kunt redeneren.
Die transparantie is in de praktijk erg nuttig: als de retriever de verkeerde sectie teruggeeft, kun je naar de boom kijken en begrijpen waarom, iets wat je simpelweg niet kunt met een embedding van 768 dimensies.
PageIndex bevragen met LLM-boomzoektocht
De retrieval-stap stuurt de boomstructuur (zonder de volledige tekst, die het contextvenster zou overschrijden) naar een LLM en vraagt om te bepalen welke knopen relevant zijn voor de query.
De boomzoekfunctie
De prompt geeft de slanke boom (alleen titels en samenvattingen, ontdaan van volledige sectietekst) door aan de LLM en vraagt om een JSON-object terug te geven met een redeneer-trace en een lijst met knoop-ID's. Twee ontwerpkeuzes zijn hier het vermelden waard.
Ten eerste verwijdert utils.remove_fields() de daadwerkelijke content uit elke knoop voordat deze wordt geserialiseerd, waardoor de prompt netjes binnen de contextlimieten blijft, zelfs bij een lang document. De aanroep copy.deepcopy() is nodig omdat remove_fields in-place muteert, en je de originele boom intact nodig hebt voor de daaropvolgende stap van tekstextractie.
Ten tweede dwingt response_format={"type": "json_object"} gestructureerde output af, zodat het parsen betrouwbaar is in plaats van fragiel. De temperatuur blijft op 0 omdat dit een redeneertak is.
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
Opmerking: de await-calls hieronder gaan uit van een Jupyter- of iPython-omgeving waar top-level await wordt ondersteund. Als je dit als een gewone .py-script uitvoert, wikkel de async-aanroepen dan in een async def main()-functie en roep die aan met asyncio.run(main()).
Een query uitvoeren
Begin met een vraag die vereist dat je over meerdere secties navigeert. Een query over kwetsbaarheden in activawaarderingen heeft materiaal nodig uit de overzichtssectie, het hoofdstuk over activawaarderingen en de subsections over specifieke markten.
Dat zijn secties die vector-RAG afzonderlijk kan bovenhalen, maar zelden samen met de juiste hiërarchie intact. Dit is precies het soort query waarbij boomzoektocht zijn waarde bewijst.
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"])
De redeneer-trace is het deel waar je het meest op moet letten. Zo ziet een typische output eruit:
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']
Je ziet precies waarom elke sectie is geselecteerd, een niveau van herleidbaarheid dat je simpelweg niet krijgt van een cosinusgelijkenis-ranking.
Antwoorden genereren uit opgehaalde context
Zodra je de relevante knoop-ID's hebt, is de volgende stap de bijbehorende tekst extraheren en doorgeven aan een generatiemodel.
Context extraheren en antwoorden genereren
Twee functies verzorgen de generatiestap.
-
generate_answer()zoekt elke geselecteerde knoop op in denode_map, plaatst er een header boven met de sectietitel en pagina-index (dit levert controleerbare verwijzingen op in het uiteindelijke antwoord), en geeft de samengestelde context door aan een generatiemodel. -
pageindex_pipeline()omhult boomzoektocht en generatie in één aanroep, zodat je ze niet elke keer handmatig hoeft te koppelen.
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
}
Voer de volledige pijplijn uit:
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']}")
Het antwoord komt terug met sectieniveau-verwijzingen en paginanummers. Dat is de herleidbaarheid die PageIndex standaard biedt.
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").
PageIndex vergelijken met vector-RAG
Dit is de sectie die je echt iets nuttigs vertelt. Laten we een vector-RAG-baseline bouwen op hetzelfde document en beide pijplijnen op dezelfde queries draaien.
De vector-RAG-baseline bouwen
De baseline volgt het standaardpatroon: ruwe tekst uit de PDF extraheren met PyMuPDF, deze splitsen in overlappende chunks van 500 woorden, elke chunk embedden met text-embedding-3-small, en alles in een in-memory FAISS-index laden voor cosinusgelijkenis-lookup.
Als je meer achtergrond wilt over waarom keuzes in chunkgrootte en overlap de kwaliteit van de retrieval beïnvloeden, is ons artikel Chunking Strategies het lezen waard naast dit stuk, en de What Is FAISS?-uitleg is ook handig als de indexbouwstappen je onbekend voorkomen.
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
Met de helperfuncties gedefinieerd, bouw je de index. Bij een document van 60 pagina's is de embedding-stap het trage deel: reken op een minuut of twee, afhankelijk van je verbinding en de doorvoer van 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")
De vergelijking uitvoeren
Een rapport van 60 pagina's levert ongeveer 47 chunks op bij 500 woorden met een overlap van 50 woorden.
Extracting text from PDF...
Chunking text...
47 chunks created
Embedding chunks (takes 1-2 min)...
Embeddings shape: (47, 1536)
Building FAISS index...
Done.
Drie queries, elk ontworpen om een andere retrieval-uitdaging bloot te leggen:
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?"
]
Met beide pijplijnen klaar, stuurt de onderstaande lus elke query door beide systemen en verzamelt de resultaten. De [:500]-slice op elk antwoord houdt de console-uitvoer leesbaar; de volledige antwoorden worden opgeslagen in results voor latere inspectie.
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)
Resultaten vergelijken
Dit is het soort resultaat dat je zult zien over de drie querytypes heen:
|
Query |
PageIndex |
Vector RAG |
Correct antwoord |
Winnaar |
|
Meest significante risico op korte termijn |
Breed antwoord dat meerdere risico's dekt met sectieverwijzingen |
Haalde de specifieke 72%-enquêtewaarde naar boven (hier preciezer) |
Aanhoudende inflatie en krapper monetair beleid, genoemd door 72% van de respondenten (Box 5.1) |
Gelijkspel, lichte voorsprong voor Vector RAG op de specifieke statistiek |
|
Methodologie klimaatrisk (kruisverwijzing) |
Haalt de sectie op, maar selecteerde de bovenliggende knoop in plaats van Box 5.2, waardoor de methodologie-tekst nooit de generatiestap bereikte |
Chunkte dwars door Box 5.2 en gaf de daadwerkelijke methodologiestappen terug |
Box 5.2 beschrijft het vertalen van fysieke en transitierisico's naar financiële blootstellingen via scenario-analyse |
Vector RAG |
|
Interactie activawaarderingen + leverage |
Haalt 18 knopen op over secties 1 en 3 en legt het versterkingsmechanisme uit |
Verbindt de twee secties en legt het versterkingsmechanisme uit |
Hoge waarderingen vergroten het risico op scherpe prijscorrecties; leverage versterkt verliezen wanneer correcties hefboominstellingen raken (Secties 1 en 3) |
Gelijkspel |
De resultaten interpreteren
De resultaten zijn subtieler dan een clean sweep voor één van beide systemen, wat ze waardevoller maakt.
PageIndex navigeerde in alle drie de gevallen naar de juiste secties. Bij de directe feitelijke query antwoordden beide correct, al haalde vector-RAG de specifieke 72%-waarde omdat de vlakke chunking toevallig Box 5.1 intact meenam — meer chunkinggeluk dan een structureel voordeel. Bij de multisection-query haalde PageIndex 18 knopen op over secties 1 en 3 en legde de interactie uit, ruwweg gelijk aan vector-RAG. Het enige duidelijke verlies was de kruisverwijzingsquery, en het is de moeite waard om te begrijpen waarom.
Bij de klimaatmethode-query redeneerde PageIndex naar sectie 5 en de overzichtssectie, maar de methodologie staat in Box 5.2, een aparte kindknoop onder sectie 5. Boomzoektocht selecteerde de bovenliggende sectie, niet de box, en de eigen tekst van de bovenliggende sectie verwijst naar de box zonder die te reproduceren, waardoor de generatiestap de methodologie nooit zag. Vector-RAG won door dwars door Box 5.2 te chunken.
Dit is een probleem van retrievalgranulariteit, geen redeneerfout: het selecteren van een bovenliggende knoop haalt niet automatisch de tekst van zijn kinderen op, tenzij je de selectie uitbreidt om ze mee te nemen. Je kunt het gat dichten door boomzoektocht te laten vragen om de kindknopen van elke relevante sectie terug te geven, of door elke geselecteerde knoop uit te breiden met zijn afstammelingen vóór generatie.
Het voordeel van PageIndex bleek het duidelijkst in de zelfstandige pijplijnrun eerder in de tutorial (de activawaarderingsquery), waar het correct over de documentstructuur redeneerde en goed onderbouwde, aan secties toegeschreven antwoorden teruggaf. De vergelijkingsqueries hierboven zijn specifiek gekozen om uitdagendere retrievalscenario's te stresstesten, en daarom bevoordelen ze vector-RAG bij een document van 60 pagina's.
De eerlijke conclusie: het voordeel van PageIndex ten opzichte van vector-RAG wordt scherper naarmate het document langer is en de dichtheid van interne kruisverwijzingen toeneemt. Bij een rapport van 60 pagina's is de kloof matig. Bij een 10-K van 200 pagina's met tientallen voetnoten die naar elkaar verwijzen, mag je een grotere kloof verwachten.
Als je de vector-RAG-baseline verder wilt pushen voor je die afschrijft, How to Improve RAG Performance beent de vijf technieken met de meeste impact.
Wanneer gebruik je PageIndex
De vergelijking hierboven is specifiek voor één documenttype en -lengte. Of PageIndex de juiste keuze is, komt neer op drie factoren: documentstructuur, querytype en volume.
Wanneer PageIndex wint
PageIndex verdient zijn overhead bij lange, gestructureerde professionele documenten waar een fout antwoord duur is:
- 10-K's en andere financiële filings
- Juridische contracten met gedefinieerde termen en kruisverwijzingen
- Regelgevende richtsnoeren
- Technische handleidingen met genummerde secties
Dit zijn ook de gevallen waarin de redeneer-traces je iets concreets geven om te auditen. Het zoete punt is laag volume, hoge inzet: een paar dozijn documenten per dag waarbij elke query goed moet zijn, en de latentie- en kostenpremie het waard is.
Wanneer vector-RAG de betere keuze is
Vector-RAG is de betere default voor hoge doorvoer, lage latentie-workloads, waar de meerdere LLM-calls per query van PageIndex een bottleneck worden en gecachte embeddings de belasting afhandelen voor een fractie van de kosten.
Het past ook bij platte documenten met weinig hiërarchie om over te redeneren, zoals:
- Nieuwsartikelen
- Productbeschrijvingen
- Supporttickets
Het is ook de juiste keuze voor gevallen waarin benaderende antwoorden goed genoeg zijn. Een supportchatbot die negen van de tien keer de juiste FAQ vindt, haalt waarschijnlijk de norm.
De eerlijke afwegingen
De prijs is snelheid en geld. Elke PageIndex-query draait minimaal twee LLM-calls (boomzoektocht en antwoordgeneratie) plus de initiële verwerking, dus reken op drie tot acht seconden per query bij een document van 60 pagina's tegenover minder dan één seconde voor vector-RAG met een gecachte FAISS-index, en de kosten per query schalen op dezelfde manier.
Het Financial Stability Report is een eerlijke testcase voor PageIndex: gestructureerd, hiërarchisch, met kruisverwijzingen. Een corpus van duizenden korte supporttickets is dat niet, en daar zal vector-RAG het winnen voor een fractie van de kosten.
Slotgedachten
Vector-RAG heeft een verborgen aanname ingebouwd: dat de passage die het meest lijkt op je query ook de passage is die het antwoord bevat. Voor korte, losjes gestructureerde documenten gaat die aanname vaak genoeg op. Voor lange, gestructureerde professionele documenten valt ze voorspelbaar uit elkaar: gesplitste tabellen, misleidende termfrequentie en onzichtbare kruisverwijzingen.
PageIndex pakt alle drie aan door retrieval te behandelen als een redeneerprobleem over documentstructuur in plaats van een gelijkeniszoektocht over tekstfragmenten.
De praktische vuistregel is eenvoudig: als je documenten hiërarchie hebben, je queries interne verwijzingen moeten volgen en het belangrijker is om het antwoord goed te krijgen dan om het snel te krijgen, dan is PageIndex de extra latentie en kosten waard. Als je high-volume zoekacties draait op korte of platte documenten, blijft vector-RAG de juiste default.
Dieper duiken in productie-RAG en agentische systeemen? Onze AI Engineering with LangChain-track neemt je mee van applicatiefundamenten tot retrieval, evaluatie en tool-using agents.
PageIndex FAQ's
Wat is PageIndex?
PageIndex is een open-source RAG-framework van VectifyAI (september 2025) dat vectorsimilariteitszoektocht vervangt door LLM-redeneren over een hiërarchische boomindex: geen embeddings, geen vectordatabase, geen chunking.
Hoe verschilt PageIndex van standaard RAG?
Standaard RAG hakt een document in fragmenten, embedt ze en haalt de meest vergelijkbare chunks op bij een query. PageIndex bouwt een boom op basis van de natuurlijke structuur van het document en vraagt een LLM te redeneren over welke secties waarschijnlijk het antwoord bevatten. De retrieval-stap is een redeneerprobleem, geen gelijkeniszoektocht.
Vereist PageIndex een OpenAI API-sleutel?
Niet per se. De self-hosted open-source-repo werkt met elke LiteLLM-ondersteunde provider. Deze tutorial gebruikt OpenAI voor de retrievalredenering en antwoordgeneratie, dus als je hem volgt zoals geschreven, heb je een OpenAI-sleutel nodig plus een PageIndex-sleutel van dash.pageindex.ai/api-keys.
Is PageIndex goed voor alle documenten?
Nee. PageIndex blinkt uit bij lange, gestructureerde documenten met hiërarchie en kruisverwijzingen. Voor grote corpora korte, losjes gestructureerde tekst, zoals supporttickets of FAQ-pagina's, zal vector-RAG sneller en goedkoper zijn.
Welke nauwkeurigheid behaalde PageIndex op FinanceBench?
Mafin 2.5, het PageIndex-gebaseerde systeem van VectifyAI, behaalde 98,7%, vergeleken met ongeveer 30–50% voor traditionele vectorgebaseerde RAG. FinanceBench dekt financiële Q&A op SEC-filings, wat meerstapsredeneren en exacte numerieke retrieval vereist. Dat is precies de kloof die PageIndex moest dichten.
Josep is een freelance Data Scientist die zich richt op Europese projecten, met expertise in dataopslag, -verwerking, geavanceerde analyses en impactvolle data storytelling.
Als docent geeft hij Big Data in de masteropleiding aan de Universiteit van Navarra en deelt hij inzichten via artikelen op platforms als Medium, KDNuggets en DataCamp. Josep schrijft ook over Data en Tech in zijn nieuwsbrief Databites (databites.tech).
Hij heeft een bachelor in Engineering Physics van de Polytechnische Universiteit van Catalonië en een master in Intelligent Interactive Systems van de Pompeu Fabra-universiteit.
