Weiter zum Inhalt

PageIndex-Tutorial: Reasoning-basierter RAG ohne Vektoren

Lerne, ein Dokument-QA-System in Python mit PageIndex zu bauen, und vergleiche es an einem echten Finanzbericht der Federal Reserve mit einer Vektor-RAG-Baseline via FAISS.
Aktualisiert 6. Aug. 2026  · 8 Min. lesen

Mit KI erkunden

In ChatGPT öffnenIn Claude öffnenIn Perplexity öffnen

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.

  1. 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).
  2. 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:

Getting pageindex API key

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 queuedprocessingcompleted

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 der node_map nach, 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 Ferrer's photo
Author
Josep Ferrer
LinkedIn
Twitter

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.

Themen

Lerne RAG mit DataCamp!

Kurs

Retrieval Augmented Generation (RAG) mit LangChain

3 Std.
19.1K
Dieser Kurs vermittelt moderne Methoden zur Einbindung externer Daten in LLMs mit Retrieval Augmented Generation (RAG) und LangChain.
Details anzeigenRight Arrow
Kurs Starten
Mehr anzeigenRight Arrow
Verwandt

Tutorial

Python-Arrays

Python-Arrays mit Code-Beispielen. Lerne noch heute, wie du mit Python NumPy Arrays erstellen und ausdrucken kannst!
DataCamp Team's photo

DataCamp Team

Tutorial

Python-Methode list() mit Beispielen erklärt

Lerne, wie du mit der Python-Funktion index() die Position von Elementen in Listen findest.
Sejal Jaiswal's photo

Sejal Jaiswal

Tutorial

Fibonacci-Folge in Python: Lerne und entdecke Programmiertechniken

Finde raus, wie die Fibonacci-Folge funktioniert. Schau dir die mathematischen Eigenschaften und die Anwendungen in der echten Welt an.
Laiba Siddiqui's photo

Laiba Siddiqui

Tutorial

Python Datenstrukturen Tutorial

Mach dich mit Python-Datenstrukturen vertraut: Lerne mehr über Datentypen und primitive sowie nicht-primitive Datenstrukturen wie Strings, Listen, Stapel usw.
Sejal Jaiswal's photo

Sejal Jaiswal

Tutorial

Python-Tutorial zum Verknüpfen von Zeichenfolgen

Lerne verschiedene Methoden zum Verknüpfen von Zeichenfolgen in Python kennen, mit Beispielen, die jede Technik zeigen.
DataCamp Team's photo

DataCamp Team

Tutorial

Zahlen in Python quadrieren: Grundlagen und fortgeschrittene Methoden

Quadrieren in Python ist einfach: Nutze den eingebauten Operator ** oder setze für vielseitigere Lösungen NumPy, pow(), math.pow(), Bitoperatoren und weitere Funktionen ein.
Allan Ouko's photo

Allan Ouko

Mehr AnzeigenMehr Anzeigen