Kurs
Stell dir vor, du hast den Financial Stability Report der Federal Reserve vor dir. Jemand fragt: Welcher Prozentsatz der Befragten nannte anhaltende Inflation als größtes kurzfristiges Risiko? Du blätterst zu Abschnitt 5, suchst Kasten 5.1 und hast die Zahl in zwei Sekunden. Es sind 72%.
Gib dieselbe Frage an eine Standard-RAG-Pipeline, und ob sie die Antwort findet, hängt davon ab, wie der Chunker die Seite zufällig zerschnitten hat. Wenn Kasten 5.1 über mehrere Chunks verteilt ist, liefert die Ähnlichkeitssuche Passagen, in denen irgendwo „Inflation“ vorkommt, verfehlt aber die Zahl. Das ist die zentrale Grenze der Ähnlichkeitssuche: Text, der deiner Anfrage ähnelt, ist nicht dasselbe wie die Stelle, die sie beantwortet.
Bei langen, strukturierten Dokumenten macht das einen großen Unterschied – und genau dafür wurde PageIndex entwickelt. In diesem Tutorial baue ich ein Dokument-QA-System mit PageIndex und teste es gegen eine Vektor-RAG-Baseline auf demselben Fed-Bericht, damit du weißt, wann welcher Ansatz seine Kosten wert ist.
Kurzfassung
- PageIndex ist ein vektorloses RAG-Framework, das über die Dokumentstruktur nachdenkt und darüber abruft.
- Es behebt die Schwächen von Vektor-RAG: geteilte Tabellen, wiederholte Begriffe und Querverweise.
- Wir benchmarken beide auf dem Fed-Report 2023: PageIndex vs. eine FAISS-Baseline.
- Ein Trade-off, kein klarer Sieg: Der Vorteil von PageIndex wächst mit der Dokumentlänge.
Was ist PageIndex?
PageIndex ist ein Retrieval-Framework, das über die Struktur eines Dokuments s statt nur nach Text zu suchen, der deiner Anfrage ähnelt. Es wurde im September 2025 von VectifyAI veröffentlicht, entwickelt von Mingtian Zhang und Yu Tang. Es ist Open Source unter der MIT-Lizenz, und Stand Juli 2026 hat das GitHub-Repository über 34.000 Stars accumulated.
PageIndex vs. Standard-RAG
Am besten versteht man PageIndex im Vergleich zu Standard-RAG. Wenn du neu bei RAG bist, ist die Grundidee, einem LLM Zugriff auf ein Dokument zu geben, indem zur Abfragezeit relevante Teile abgerufen und als Kontext übergeben werden.
Genau diesen Retrieval-Schritt denkt PageIndex neu. Anstatt die Anfrage mit jedem Chunk im Dokument zu vergleichen, baut PageIndex zuerst einen hierarchischen Baumindex aus der Dokumentstruktur. Das führt zu anderen Ergebnissen:
- Vektorsuche findet Text, der ähnlich aussieht wie deine Anfrage.
- Baumsuche findet Abschnitte, die die Antwort wahrscheinlich enthalten.
Anders gesagt: Der Unterschied zwischen Standard-RAG und PageIndex ist der zwischen dem Scannen jeder Buchseite nach Stichworten und dem Blick ins Inhaltsverzeichnis, um zuerst das richtige Kapitel zu finden.
Bei einfachen Faktenfragen in kurzen Dokumenten konvergieren die Ziele, doch je länger, strukturierter und stärker querverweisend Dokumente werden, desto stärker driften sie auseinander.
|
Dimension |
PageIndex |
Vektor-RAG |
|
Retrieval-Mechanismus |
LLM-Reasoning über Baumindex |
Kosinus-Ähnlichkeit über Embeddings |
|
Einrichtungskosten |
Dokumentenverarbeitung (einmalig) |
Chunking + Embedding (einmalig) |
|
Latenz pro Anfrage |
3–8 Sekunden (mehrere LLM-Calls) |
< 1 Sekunde (Index-Lookup) |
|
Kosten pro Anfrage |
Höher (2+ LLM-Calls) |
Niedriger (Embedding-Lookup + ein LLM-Call) |
|
Genauigkeit bei langen strukturierten Docs |
98,7% auf FinanceBench (Mafin 2.5) |
30–50% auf FinanceBench |
|
Handhabt Querverweise |
Ja |
Nein |
|
Handhabt geteilte Tabellen |
Ja |
Nein |
|
Nachvollziehbarkeit |
Vollständige Reasoning-Trace + Seitenangaben |
Top-k-Chunk-Scores |
|
Am besten geeignet für |
Finanzberichte, Verträge, Handbücher |
FAQs, Produktdokumentation, Support-Tickets |
So funktioniert der Baumindex
Der Prozess läuft in zwei Schritten ab.
- Du ingestierst ein Dokument, und PageIndex erzeugt einen Baum. Jeder Knoten hat einen Titel, eine Zusammenfassung und einen Seitenindex. Der Baum spiegelt die natürliche Hierarchie des Dokuments wider (Kapitel, Abschnitte, Unterabschnitte, Anhänge – was auch immer enthalten ist).
- Wenn eine Anfrage eintrifft, erhält das LLM die Baumstruktur (ohne Volltext, da dieser das Kontextfenster sprengen würde) und überlegt, welche Knoten die Antwort wahrscheinlich enthalten. Es gibt eine Liste von Knoten-IDs mit einer Begründung zurück, warum jeder ausgewählt wurde; anschließend wird der Text dieser Knoten extrahiert und an die Generierung übergeben.

Warum das bei strukturierten Dokumenten zählt
Ich habe genug RAG-Pipelines gebaut, um zu wissen, dass Chunking der Punkt ist, an dem viele produktive Systeme leise scheitern. Auf dem Papier wirkt die Strategie sauber, doch sobald dein Dokument eine Tabelle über Seitenumbrüche hinweg hat oder eine Fußnote einen Begriff definiert, der drei Abschnitte früher verwendet wurde, tun sich Risse auf.
Vektor-RAG hat drei strukturelle Schwächen, die bei Finanz- und Rechtsdokumenten regelmäßig auftreten.
- Geteilte Inhalte: Eine Bilanz, die über zwei Chunks aufgeteilt ist, verliert die Beziehung zwischen den Posten, und beide Hälften bekommen niedrige Relevanz-Scores, weil keine für sich allein Sinn ergibt. (Late Chunking hilft hier, löst aber Querverweise nicht.)
- Wiederholte Begriffe: In einem Geschäftsbericht fällt „Umsatz“ dutzendfach, sodass eine Anfrage zu Umsatzwachstum einer Sparte Chunks aus allen Erwähnungen zieht, grob gleich gerankt – ohne Möglichkeit, die relevante Stelle zu erkennen.
- Querverweise: Wenn Abschnitt 4.3 sagt „siehe Anhang G für die vollständige Überleitung“, hat eine Ähnlichkeitssuche keinen Mechanismus, diesem Verweis zu folgen. Die Antwort steht in Anhang G – der Retriever weiß es nicht.
PageIndex löst alle drei Punkte, weil es über Struktur statt Fragmente abruft. Der Baum hält das Dokument als Ganzes zusammen, der Reasoning-Schritt kann einem Querverweis folgen oder Abschnitte vergleichen, und da jeder Knoten einen Seitenindex und eine Zusammenfassung trägt, navigiert der Retriever durch die Dokumentgeografie statt durch oberflächliche Textähnlichkeit.
Ehrlich gesagt: Als ich erstmals die 98,7%-Genauigkeitsangabe für Mafin 2.5 sah (VectifyAIs PageIndex-basiertes System) auf FinanceBench, war ich skeptisch – ein Vorsprung von fast 50 Punkten gegenüber Standard-RAG wirkt nach Benchmarking zu eigenen Gunsten. Doch FinanceBench prüft genau diese Fehlermodi: mehrstufiges Reasoning über SEC-Filings mit präzisen numerischen Antworten.
Erste Schritte mit PageIndex
Für dieses Tutorial brauchst du ein paar Pakete und zwei API-Schlüssel. Hier ist alles, was du vor dem ersten Code brauchst.
Den gesamten Code aus dem Tutorial findest du im begleitenden GitHub-Repo.
Um mitzumachen, benötigst du:
- Python 3.10+ installiert
- Grundkenntnisse in RAG-Konzepten. Wenn du neu bei Retrieval-Augmented Generation bist, lies What is Retrieval Augmented Generation (RAG)? bevor dem Start
- Einen OpenAI-API-Schlüssel von platform.openai.com/api-keys
- Einen PageIndex-API-Schlüssel von dash.pageindex.ai/api-keys (das Free-Tier reicht für dieses Tutorial)

Installiere die benötigten Pakete:
pip install pageindex openai requests faiss-cpu pymupdf
Setze die API-Schlüssel als Umgebungsvariablen statt sie hart zu codieren:
export PAGEINDEX_API_KEY="your_pageindex_key_here"
export OPENAI_API_KEY="your_openai_key_here"
Mit beiden Schlüsseln initialisierst du die 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)
Das war die gesamte Einrichtung: neun Importe, zwei Umgebungsvariablen, zwei Clients.
Ein Dokument ingestieren und den PageIndex-Baum bauen
Für dieses Tutorial nutzen wir den Financial Stability Report der Federal Reserve vom Oktober 2023. Es ist ein öffentlich zugängliches PDF, strukturiert wie ein echtes Produktivdokument: formale Abschnitte, nummerierte Kapitel, Anhänge und interne Querverweise.
Das Dokument herunterladen
Die Fed veröffentlicht ihre Berichte als barrierearme PDFs unter stabilen URLs. Die if not os.path.exists()-Prüfung sorgt dafür, dass du die Datei nur einmal holst – praktisch, wenn du am restlichen Code iterierst und nicht bei jedem Lauf ein 60-seitiges PDF erneut herunterladen willst.
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}")
Das Dokument einreichen
Ein einziger API-Call reicht. submit_document() lädt die Datei hoch und gibt eine doc_id zurück, die du für jede weitere Aktion an diesem Dokument nutzt. Speichere sie – beim Neustart kannst du die Einreichung überspringen und direkt mit get_tree() weitermachen.
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 verarbeitet das Dokument asynchron, daher musst du pollen, bis der Baum fertig ist:
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.")
Die Verarbeitung eines 60-seitigen PDFs dauert typischerweise zwei bis vier Minuten, und diese Kosten fallen pro Dokument nur einmal an. Du siehst die Statusphasen queued → processing → completed.
Submitting document to PageIndex...
Document ID: pi-cmq2bp4ok00rx01qxmym7tnd1
Waiting for tree generation...
Status: queued
Status: processing
Status: completed
Processing done.
Den Baum inspizieren
Sobald die Verarbeitung abgeschlossen ist, liefert get_tree() die vollständige hierarchische Struktur als verschachtelte Liste von Knoten. Der Helper utils.create_node_mapping() flacht sie dann zu einem Dictionary nach Knoten-ID ab – das macht spätere Textextraktion deutlich schneller als bei jeder Anfrage den Baum rekursiv zu durchlaufen.
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()
Beim Financial Stability Report bekommst du so das Gerüst des Dokuments auf einen Blick:
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
…
Der Baum ist schlichtes JSON, jeder Knoten trägt eine node_id, einen title, eine summary, einen page_index und eine nodes-Liste für enthaltene Unterabschnitte. Anders als Vektorindizes gibt es keinen opaken Embedding-Raum oder Binärindex, der einen Spezialleser erfordert. Es ist einfach eine verschachtelte Liste, die du ausgeben, prüfen und direkt nachvollziehen kannst.
Diese Transparenz ist in der Praxis sehr hilfreich: Wenn der Retriever die falsche Stelle liefert, kannst du in den Baum schauen und verstehen, warum – etwas, das mit einem 768-dimensionalen Embedding nicht möglich ist.
PageIndex mit LLM-Baumsuche abfragen
Beim Retrieval wird die Baumstruktur (ohne Volltext, der das Kontextfenster sprengen würde) an ein LLM geschickt, das die relevanten Knoten zur Anfrage identifiziert.
Die Baumsuchfunktion
Das Prompt übergibt den schlanken Baum (nur Titel und Zusammenfassungen, ohne Abschnittstext) an das LLM und bittet um ein JSON-Objekt mit der Begründung und einer Liste von Knoten-IDs. Zwei Designentscheidungen sind hier wichtig.
Erstens entfernt utils.remove_fields() die eigentlichen Inhalte aus jedem Knoten vor der Serialisierung, damit das Prompt selbst bei langen Dokumenten gut im Kontextlimit bleibt. Der Aufruf copy.deepcopy() ist nötig, weil remove_fields in place mutiert und du den Originalbaum für die anschließende Textextraktion brauchst.
Zweitens erzwingt response_format={"type": "json_object"} strukturierten Output, wodurch das Parsen zuverlässig statt fragil ist. Die Temperatur bleibt bei 0, weil es um Reasoning geht.
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
Hinweis: Die folgenden await-Aufrufe setzen eine Jupyter- oder iPython-Umgebung mit Top-Level-await voraus. Wenn du das als .py-Skript ausführst, kapsle die async-Aufrufe in eine async def main()-Funktion und rufe sie mit asyncio.run(main()) auf.
Eine Abfrage ausführen
Starte mit einer Frage, die über mehrere Abschnitte navigieren muss. Eine Anfrage zu Verwundbarkeiten bei Asset-Bewertungen benötigt Material aus dem Überblick, dem Kapitel Asset Valuations und dessen Unterabschnitten zu spezifischen Märkten.
Das sind Abschnitte, die Vektor-RAG zwar einzeln finden kann, aber selten gemeinsam und in der richtigen Hierarchie. Genau hier spielt die Baumsuche ihren Vorteil aus.
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"])
Die Reasoning-Trace ist der Teil, dem du die meiste Aufmerksamkeit schenken solltest. So sieht eine typische Ausgabe aus:
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']
Du siehst genau, warum jeder Abschnitt gewählt wurde – ein Maß an Nachvollziehbarkeit, das du aus einem Kosinus-Ähnlichkeitsranking nicht bekommst.
Antworten aus abgerufenem Kontext generieren
Sobald du die relevanten Knoten-IDs hast, extrahierst du den zugehörigen Text und übergibst ihn an ein Generierungsmodell.
Kontext extrahieren und Antworten generieren
Zwei Funktionen übernehmen die Generierung.
-
generate_answer()schlägt jeden ausgewählten Knoten in dernode_mapnach, stellt einen Header mit Abschnittstitel und Seitenzahl voran (damit bekommst du prüfbare Zitate in der Antwort) und übergibt den zusammengebauten Kontext an ein Generierungsmodell. -
pageindex_pipeline()kapselt Baumsuche und Generierung in einem Aufruf, sodass du sie nicht jedes Mal manuell verkettest.
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
}
Führe die vollständige Pipeline aus:
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']}")
Die Antwort kommt mit Abschnittszitaten und Seitenzahlen zurück. Diese Nachvollziehbarkeit liefert PageIndex standardmäßig.
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 gegen Vektor-RAG vergleichen
Hier wird es praktisch. Wir bauen eine Vektor-RAG-Baseline auf demselben Dokument und lassen beide Pipelines auf identische Anfragen laufen.
Die Vektor-RAG-Baseline bauen
Die Baseline folgt dem Standardmuster: Rohtext mit PyMuPDF aus dem PDF extrahieren, in überlappende 500-Wort-Chunks splitten, jeden Chunk mit text-embedding-3-small embedden und alles in einen In-Memory-FAISS-Index für Kosinus-Lookups laden.
Wenn du mehr Hintergrund dazu willst, warum Chunk-Größe und Überlappung die Retrieval-Qualität beeinflussen, lies unseren Artikel Chunking Strategies parallel dazu – und das What Is FAISS? Explainer, falls dir die Indexschritte neu sind.
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
Mit den Helfern baust du jetzt den Index. Bei 60 Seiten ist das Embedding der langsame Teil: Rechne mit ein bis zwei Minuten – abhängig von Verbindung und OpenAI-Durchsatz.
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")
Den Vergleich ausführen
Ein 60-seitiger Bericht ergibt mit 500 Wörtern und 50 Wörtern Überlappung etwa 47 Chunks.
Extracting text from PDF...
Chunking text...
47 chunks created
Embedding chunks (takes 1-2 min)...
Embeddings shape: (47, 1536)
Building FAISS index...
Done.
Drei Anfragen, jeweils mit einem anderen Retrieval-Problem:
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?"
]
Mit beiden Pipelines bereit, schickt die Schleife unten jede Anfrage durch beide Systeme und sammelt die Ergebnisse. Der [:500]-Slice hält die Konsolenausgabe lesbar; die vollständigen Antworten landen in results zur späteren Prüfung.
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)
Ergebnisvergleich
So sehen typische Resultate über die drei Fragetypen hinweg aus:
|
Frage |
PageIndex |
Vektor-RAG |
Korrekte Antwort |
Gewinner |
|
Größtes kurzfristiges Risiko |
Breite Antwort über mehrere Risiken mit Abschnittszitaten |
Fand die spezifische 72%-Zahl (hier präziser) |
Anhaltende Inflation und straffere Geldpolitik, genannt von 72% der Befragten (Kasten 5.1) |
Unentschieden, leichter Vorteil für Vektor-RAG bei der Zahl |
|
Methodik Klimarisiko (Querverweis) |
Rief den Abschnitt ab, wählte aber den Elternknoten statt Kasten 5.2, daher fehlte die Methodik im Kontext |
Chunkte direkt durch Kasten 5.2 und lieferte die Methodikschritte |
Kasten 5.2: Übersetzung physischer und Übergangsrisiken in finanzielle Exposures via Szenarioanalyse |
Vektor-RAG |
|
Interaktion: Asset-Bewertungen + Leverage |
Rief 18 Knoten aus Abschnitten 1 und 3 ab und erklärte den Verstärkungsmechanismus |
Verband beide Abschnitte und erklärte den Verstärkungsmechanismus |
Erhöhte Bewertungen erhöhen das Risiko scharfer Korrekturen; Leverage verstärkt Verluste, wenn Korrekturen auf gehebelte Institute treffen (Abschnitte 1 und 3) |
Unentschieden |
Ergebnisse einordnen
Die Ergebnisse sind nuancierter als ein Durchmarsch eines Systems – und genau deshalb hilfreich.
PageIndex navigierte in allen drei Fällen in die richtigen Abschnitte. Bei der direkten Faktenfrage antworteten beide korrekt, Vektor-RAG fand die 72%-Zahl, weil sein flaches Chunking Kasten 5.1 zufällig intakt erfasste – mehr Chunking-Glück als struktureller Vorteil. Bei der Multi-Abschnittsfrage rief PageIndex 18 Knoten über Abschnitt 1 und 3 ab und erklärte die Interaktion, etwa auf Augenhöhe mit Vektor-RAG. Die eine klare Niederlage war die Querverweisfrage – und das hat Gründe.
Bei der Klimamethodik fand PageIndex per Reasoning zu Abschnitt 5 und dem Überblick, die Methodik steht aber in Kasten 5.2, einem eigenen Kindknoten. Die Baumsuche wählte den Elternabschnitt, nicht den Kasten; der Elterntext verweist nur, wiederholt ihn aber nicht, also erreichte die Methodik die Generierung nicht. Vektor-RAG gewann, weil es gerade durch Kasten 5.2 chunkte.
Das ist ein Thema der Retrieval-Granularität, kein Reasoning-Fehler: Einen Elternknoten zu wählen, zieht nicht automatisch die Kindtexte nach. Du kannst die Lücke schließen, indem du die Baumsuche bittest, Kindknoten relevanter Abschnitte mit zurückzugeben, oder indem du jeden gewählten Knoten vor der Generierung um seine Nachkommen erweiterst.
Der Vorteil von PageIndex zeigte sich am deutlichsten im separaten Pipeline-Run früher im Tutorial (Asset-Valuations-Anfrage), wo es korrekt über die Dokumentstruktur „dachte“ und gut zitierte, abschnittsattribuierte Antworten lieferte. Die obigen Vergleichsanfragen wurden bewusst so gewählt, dass sie anspruchsvollere Retrieval-Szenarien testen – deshalb bevorzugen sie bei einem 60-seitigen Dokument teils Vektor-RAG.
Die ehrliche Erkenntnis: Der Vorsprung von PageIndex gegenüber Vektor-RAG wird mit Dokumentlänge und Dichte interner Querverweise schärfer. Bei 60 Seiten ist die Lücke moderat. Bei einem 200-seitigen 10-K mit Dutzenden sich gegenseitig referenzierender Fußnoten wird sie größer.
Wenn du die Vektor-RAG-Baseline noch weiter pushen willst, bevor du sie abschreibst, deckt How to Improve RAG Performance die fünf wirksamsten Techniken ab.
Wann du PageIndex einsetzen solltest
Der obige Vergleich bezieht sich auf einen Dokumenttyp und eine Länge. Ob PageIndex passt, hängt von drei Faktoren ab: Dokumentstruktur, Fragetyp und Volumen.
Wann PageIndex punktet
PageIndex spielt seine Überhänge bei langen, strukturierten Fachdokumenten aus, bei denen falsche Antworten teuer sind:
- 10-Ks und andere Finanzberichte
- Rechtsverträge mit definierten Begriffen und Querverweisen
- Regulatorische Leitfäden
- Technische Handbücher mit nummerierten Abschnitten
Das sind auch die Fälle, in denen Reasoning-Traces dir echte Prüfbarkeit liefern. Der Sweet Spot ist niedriges Volumen, hohe Stakes: ein paar Dutzend Dokumente pro Tag, bei denen jede Antwort sitzen muss – dann sind Latenz- und Kostenaufschlag gerechtfertigt.
Wann Vektor-RAG die bessere Wahl ist
Vektor-RAG ist der bessere Default für hohen Durchsatz bei niedriger Latenz, wo mehrere LLM-Calls pro Anfrage zum Flaschenhals werden und gecachte Embeddings die Last für einen Bruchteil der Kosten stemmen.
Es passt auch zu flachen Dokumenten mit wenig Hierarchie zum „Darüber-Nachdenken“, etwa:
- Nachrichtenartikel
- Produktbeschreibungen
- Support-Tickets
Ebenso ist es richtig, wenn ungefähre Antworten genügen. Ein Support-Chatbot, der in neun von zehn Fällen die richtige FAQ findet, erfüllt wahrscheinlich seinen Zweck.
Die ehrlichen Trade-offs
Der Preis sind Zeit und Geld. Jede PageIndex-Anfrage braucht mindestens zwei LLM-Calls (Baumsuche und Generierung) plus die Vorverarbeitung. Rechne mit drei bis acht Sekunden pro Anfrage auf 60 Seiten gegenüber unter einer Sekunde bei Vektor-RAG mit gecachtem FAISS-Index – und die Kosten skalieren entsprechend.
Der Financial Stability Report ist ein fairer Testfall für PageIndex: strukturiert, hierarchisch, querverweislastig. Ein Korpus aus Tausenden kurzer Support-Tickets ist es nicht – dort schlägt Vektor-RAG PageIndex zu einem Bruchteil der Kosten.
Abschließende Gedanken
Vektor-RAG hat eine stille Annahme eingebaut: Dass die Passage, die deiner Anfrage am ähnlichsten ist, auch die Antwort enthält. Bei kurzen, locker strukturierten Dokumenten hält das oft genug. Bei langen, strukturierten Fachdokumenten bricht es auf vorhersehbare Weise: geteilte Tabellen, irreführende Termhäufigkeit, unsichtbare Querverweise.
PageIndex adressiert alle drei, indem es Retrieval als Reasoning über Dokumentstruktur behandelt – nicht als Ähnlichkeitssuche über Fragmente.
Die praktische Regel ist einfach: Haben deine Dokumente Hierarchie, erfordern die Anfragen interne Verweise zu folgen, und ist dir Richtigkeit wichtiger als Schnelligkeit, lohnt sich PageIndex trotz zusätzlicher Latenz und Kosten. Für hohes Suchvolumen auf kurzen oder flachen Dokumenten bleibt Vektor-RAG der richtige Standard.
Du willst tiefer in produktive RAG- und agentische Systeme einsteigen? Unser AI Engineering with LangChain-Lernpfad führt dich von Anwendungsgrundlagen über Retrieval und Evaluation bis zu toolnutzenden Agenten.
PageIndex-FAQs
Was ist PageIndex?
PageIndex ist ein Open-Source-RAG-Framework von VectifyAI (September 2025), das die Vektor-Ähnlichkeitssuche durch LLM-Reasoning über einen hierarchischen Baumindex ersetzt: keine Embeddings, keine Vektordatenbank, kein Chunking.
Wie unterscheidet sich PageIndex von Standard-RAG?
Standard-RAG zerlegt ein Dokument in Fragmente, embedden sie und ruft die zur Anfrage ähnlichsten Chunks ab. PageIndex baut aus der natürlichen Dokumentstruktur einen Baum und bittet ein LLM darum zu überlegen, welche Abschnitte die Antwort wahrscheinlich enthalten. Der Retrieval-Schritt ist ein Reasoning-Problem, keine Ähnlichkeitssuche.
Benötigt PageIndex einen OpenAI-API-Schlüssel?
Nicht zwingend. Das selbst gehostete Open-Source-Repo funktioniert mit jedem von LiteLLM unterstützten Anbieter. Dieses Tutorial nutzt OpenAI für das Retrieval-Reasoning und die Antwortgenerierung. Wenn du es genau so nachbaust, brauchst du also einen OpenAI-Schlüssel plus einen PageIndex-Schlüssel von dash.pageindex.ai/api-keys.
Ist PageIndex für alle Dokumente geeignet?
Nein. PageIndex glänzt bei langen, strukturierten Dokumenten mit Hierarchie und Querverweisen. Für große Korpora kurzer, locker strukturierter Texte – z. B. Support-Tickets oder FAQ-Seiten – ist Vektor-RAG schneller und günstiger.
Welche Genauigkeit erzielte PageIndex auf FinanceBench?
Mafin 2.5, VectifyAIs PageIndex-basiertes System, erreichte 98,7% – im Vergleich zu rund 30–50% bei klassischem Vektor-RAG. FinanceBench prüft Financial-QA auf SEC-Filings und erfordert mehrstufiges Reasoning und exakte Zahlenretrieval. Genau diese Lücke soll PageIndex schließen.
Josep ist Data Scientist und Projektmanager beim katalanischen Fremdenverkehrsamt und nutzt Daten, um die Erfahrungen von Touristen in Katalonien zu verbessern. Sein Fachwissen umfasst das Management von Datenspeicherung und -verarbeitung, gekoppelt mit fortschrittlichen Analysen und der effektiven Kommunikation von Datenerkenntnissen.
Er ist auch ein engagierter Pädagoge, der den Big-Data-Masterstudiengang an der Universität von Navarra unterrichtet und regelmäßig aufschlussreiche Artikel über Datenwissenschaft auf Medium und KDNuggets veröffentlicht.
Er hat einen BS in technischer Physik von der Polytechnischen Universität von Katalonien und einen MS in intelligenten interaktiven Systemen von der Universität Pompeu Fabra.
Derzeit engagiert er sich leidenschaftlich dafür, datenbezogene Technologien durch die Medium-Publikation ForCode'Sake einem breiteren Publikum zugänglich zu machen.


