Lewati ke konten utama

Tutorial PageIndex: RAG Berbasis Penalaran Tanpa Vektor

Pelajari membangun sistem tanya jawab dokumen di Python dengan PageIndex, lalu benchmarking pada laporan keuangan Federal Reserve yang nyata terhadap baseline RAG vektor menggunakan FAISS.
Diperbarui 6 Agu 2026  · 8 mnt baca

Jelajahi dengan AI

Buka di ChatGPTBuka di ClaudeBuka di Perplexity

Bayangkan Anda membuka Financial Stability Report milik Federal Reserve di depan Anda. Seseorang bertanya: Berapa persen responden yang menyebut inflasi yang persisten sebagai risiko utama jangka pendek? Anda membalik ke Bagian 5, menemukan Kotak 5.1, dan melihat angkanya dalam dua detik. Angkanya 72%.

Jika pertanyaan itu Anda serahkan ke pipeline RAG standar, apakah jawabannya ditemukan atau tidak bergantung pada bagaimana chunker kebetulan membagi halaman. Jika Kotak 5.1 terpecah ke beberapa chunk, pencarian kemiripan akan mengembalikan bagian yang menyebut inflasi di suatu tempat tetapi melewatkan angkanya. Itulah keterbatasan inti dari pencarian kemiripan: teks yang terlihat mirip dengan kueri Anda bukanlah hal yang sama dengan bagian yang menjawabnya.

Untuk dokumen yang panjang dan terstruktur, hal ini sangat berpengaruh, dan itulah yang dibenahi oleh PageIndex. Dalam tutorial ini, saya akan membangun sistem tanya jawab dokumen dengan PageIndex dan mengujinya terhadap baseline RAG vektor pada laporan The Fed yang sama, sehingga Anda paham kapan masing-masing pendekatan sepadan dengan biayanya.

Ringkasnya

  • PageIndex adalah kerangka RAG tanpa vektor yang melakukan pengambilan dengan menalar atas struktur dokumen.
  • Ini memperbaiki titik lemah RAG vektor: tabel terbelah, istilah berulang, dan rujukan silang.
  • Kami melakukan benchmark keduanya pada laporan The Fed 2023: PageIndex vs. baseline FAISS.
  • Ini soal trade-off, bukan kemenangan telak: keunggulan PageIndex tumbuh seiring panjang dokumen.

Apa Itu PageIndex?

PageIndex adalah kerangka kerja retrieval yang menalar atas struktur dokumen alih-alih mencari teks yang terlihat mirip dengan kueri Anda. Ini dirilis oleh VectifyAI pada September 2025, dibangun oleh Mingtian Zhang dan Yu Tang. Bersifat open source di bawah lisensi MIT, dan per Juli 2026, repositori GitHub telah mengakumulasi lebih dari 34.000 bintang.

PageIndex vs RAG standar

Cara terbaik menjelaskan bagaimana PageIndex bekerja adalah dengan mengontraskannya dengan RAG standar. Jika Anda baru mengenal RAG, gagasan dasarnya adalah Anda memberi LLM akses ke dokumen dengan mengambil bagian relevan saat waktu kueri dan memberikannya sebagai konteks. 

Langkah retrieval itulah yang dibayangkan ulang oleh PageIndex. Alih-alih membandingkan kueri Anda terhadap setiap chunk dalam dokumen, PageIndex terlebih dahulu membangun indeks pohon hierarkis dari struktur dokumen. Ini menghasilkan perbedaan hasil:

  • Pencarian vektor menemukan teks yang terlihat mirip dengan kueri Anda. 
  • Pencarian pohon menemukan bagian yang kemungkinan besar memuat jawabannya

Dengan kata lain, perbedaan antara RAG standar dan PageIndex adalah seperti antara memindai setiap halaman buku mencari kata kunci relevan dan membaca daftar isi untuk menemukan bab yang tepat terlebih dahulu.

Kedua tujuan itu berimpit untuk kueri faktual sederhana pada dokumen pendek, namun menyimpang tajam saat dokumen menjadi panjang, terstruktur, dan penuh rujukan silang internal.

Dimensi

PageIndex

Vector RAG

Mekanisme retrieval

Penalaran LLM atas indeks pohon

Kemiripan kosinus atas embedding

Biaya penyiapan

Pemrosesan dokumen (sekali saja)

Chunking + embedding (sekali saja)

Latensi per kueri

3–8 detik (beberapa panggilan LLM)

< 1 detik (lookup indeks)

Biaya per kueri

Lebih tinggi (2+ panggilan LLM)

Lebih rendah (lookup embedding + satu panggilan LLM)

Akurasi pada dokumen panjang terstruktur

98,7% pada FinanceBench (Mafin 2.5)

30-50% pada FinanceBench

Menangani rujukan silang

Ya

Tidak

Menangani tabel terbelah

Ya

Tidak

Keterlacakan

Jejak penalaran penuh + sitasi halaman

Skor chunk top-k

Terbaik untuk

Pengajuan keuangan, kontrak, manual

FAQ, dokumentasi produk, tiket dukungan

Cara kerja indeks pohon

Prosesnya berlangsung dalam dua langkah.

  1. Anda memasukkan dokumen, dan PageIndex menghasilkan sebuah pohon. Setiap node membawa judul, ringkasan, dan indeks halaman. Pohon mencerminkan hierarki alami dokumen (bab, bagian, subbagian, lampiran, apa pun yang memang ada di dokumen).
  2. Saat kueri datang, LLM menerima struktur pohon (tanpa teks lengkap, yang akan melampaui jendela konteks) dan menalar node mana yang kemungkinan memuat jawabannya. Ia mengembalikan serangkaian ID node beserta jejak penalaran yang menjelaskan alasan pemilihan tiap node, setelah itu teks dari node-node tersebut diekstrak dan diteruskan ke langkah generasi.

Mengapa penting untuk dokumen terstruktur

Saya telah membangun cukup banyak pipeline RAG untuk tahu bahwa chunking adalah tempat sebagian besar sistem produksi diam-diam gagal. Strateginya terlihat rapi di atas kertas, tetapi begitu dokumen Anda memiliki tabel yang melintasi batas halaman, atau catatan kaki yang mendefinisikan istilah yang digunakan tiga bagian sebelumnya, retakannya muncul. 

RAG vektor memiliki tiga kelemahan struktural yang konsisten muncul pada dokumen keuangan dan legal.

  • Konten terbelah: Neraca yang terpecah ke dua chunk kehilangan hubungan antar pos barisnya, dan kedua bagiannya mendapat skor relevansi rendah karena masing-masing tidak masuk akal sendiri. (Late chunking membantu di sini, tetapi tidak menyelesaikan rujukan silang.)
  • Istilah berulang: Laporan tahunan menyebut "revenue" puluhan kali, jadi kueri tentang pertumbuhan pendapatan satu divisi menarik chunk dari setiap penyebutan, diperingkat kira-kira setara, tanpa cara untuk membedakan yang relevan.
  • Rujukan silang: Saat Bagian 4.3 mengatakan "lihat Lampiran G untuk rekonsiliasi lengkap," pencarian kemiripan tidak punya mekanisme untuk mengikuti penunjuk itu. Jawabannya ada di Lampiran G, dan retriever tidak punya cara untuk mengetahuinya.

PageIndex menangani ketiganya karena melakukan retrieval atas struktur alih-alih fragmen. Pohon menjaga dokumen tetap utuh, langkah penalaran bisa mengikuti rujukan silang atau membandingkan antarbagian, dan karena setiap node membawa indeks halaman dan ringkasan, retriever menavigasi “geografi” dokumen alih-alih kemiripan teks permukaan.

Saya jujur: saat pertama kali melihat klaim akurasi 98,7% untuk Mafin 2.5 (sistem berbasis PageIndex milik VectifyAI) pada FinanceBench, naluri saya skeptis, karena selisih hampir 50 poin atas RAG standar terlihat seperti benchmark yang dipilih untuk menguntungkan. Namun FinanceBench menguji persis mode kegagalan ini: penalaran multi-langkah atas pengajuan SEC dengan jawaban numerik presisi.

Mulai Menggunakan PageIndex

Tutorial ini memerlukan beberapa paket dan dua kunci API. Berikut semua yang Anda butuhkan sebelum menulis satu baris kode.

Anda dapat memeriksa semua kode yang digunakan sepanjang tutorial di repo GitHub pendamping saya.

Untuk mengikuti tutorial ini, Anda memerlukan:

Mendapatkan kunci API pageindex

Instal paket yang diperlukan:

pip install pageindex openai requests faiss-cpu pymupdf

Setel kunci API sebagai variabel lingkungan alih-alih menuliskannya langsung di kode:

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

Dengan kedua kunci disetel, inisialisasi klien Anda:

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)

Itu seluruh penyiapan: sembilan import, dua variabel lingkungan, dua klien.

Mengimpor Dokumen dan Membangun Pohon PageIndex

Untuk tutorial ini, kita akan menggunakan Financial Stability Report edisi Oktober 2023 milik Federal Reserve. Ini adalah PDF yang tersedia publik dan terstruktur seperti dokumen produksi nyata: bagian formal, bab bernomor, lampiran, dan rujukan silang internal.

Mengunduh dokumen

The Fed menerbitkan laporannya sebagai PDF aksesibel di URL yang stabil. Pemeriksaan if not os.path.exists() berarti Anda hanya mengambilnya sekali, yang penting saat Anda mengiterasi kode lainnya dan tidak ingin mengunduh ulang PDF 60 halaman di setiap eksekusi.

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}")

Mengirimkan dokumen

Pengiriman hanya satu panggilan API. submit_document() mengunggah berkas dan mengembalikan sebuah doc_id yang akan Anda gunakan untuk setiap operasi selanjutnya pada dokumen ini. Simpan di suatu tempat, karena jika Anda memulai ulang sesi, Anda bisa melewati langkah pengiriman dan langsung lompat ke get_tree() menggunakan ID yang sama.

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 memproses dokumen secara asinkron, jadi Anda perlu melakukan polling hingga pohonnya siap:

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.")

Memproses PDF 60 halaman biasanya memakan waktu dua hingga empat menit, dan biaya ini hanya dibayar sekali per dokumen. Anda akan melihat status berputar melalui queuedprocessingcompleted

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

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

Memeriksa pohon

Setelah pemrosesan selesai, get_tree() mengembalikan struktur hierarkis penuh sebagai daftar bersarang node. Helper utils.create_node_mapping() kemudian meratakannya menjadi kamus biasa dengan key berupa ID node, sehingga ekstraksi teks nanti jauh lebih cepat ketimbang menelusuri pohon secara rekursif untuk setiap kueri.

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()

Menjalankan ini pada Financial Stability Report memberi Anda kerangka dokumen sekilas:

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
       …

Pohon tersebut adalah JSON polos, dengan setiap node membawa node_id, title, summary, page_index, dan daftar nodes untuk setiap subbagian yang dikandungnya. Tidak seperti indeks vektor, tidak ada ruang embedding buram atau berkas indeks biner yang memerlukan pembaca khusus. Ini hanya daftar bersarang yang dapat Anda cetak, periksa, dan nalar secara langsung.

Transparansi itu sangat berguna dalam praktik: jika retriever mengembalikan bagian yang salah, Anda bisa melihat pohonnya dan memahami alasannya, sesuatu yang tidak bisa Anda lakukan dengan embedding 768 dimensi.

Mengajukan Kueri ke PageIndex dengan Pencarian Pohon LLM

Langkah retrieval mengirim struktur pohon (tanpa teks lengkap, yang akan melampaui jendela konteks) ke LLM dan memintanya mengidentifikasi node mana yang relevan dengan kueri.

Fungsi pencarian pohon

Prompt mengoper pohon yang ramping (hanya judul dan ringkasan, tanpa teks penuh bagian) ke LLM dan memintanya mengembalikan objek JSON yang berisi jejak penalaran dan daftar ID node. Dua pilihan desain patut dicatat di sini. 

Pertama, utils.remove_fields() menghapus konten aktual dari setiap node sebelum menserialkannya, yang menjaga prompt tetap dalam batas konteks bahkan pada dokumen panjang. Pemanggilan copy.deepcopy() diperlukan karena remove_fields melakukan mutasi in-place, dan Anda memerlukan pohon asli tetap utuh untuk langkah ekstraksi teks berikutnya. 

Kedua, response_format={"type": "json_object"} memaksa keluaran terstruktur, sehingga parsing andal alih-alih rapuh. Temperatur tetap 0 karena ini tugas penalaran.

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

Catatan: pemanggilan await di bawah ini mengasumsikan lingkungan Jupyter atau iPython yang mendukung await di level atas. Jika Anda menjalankan ini sebagai skrip .py biasa, bungkus pemanggilan async di dalam fungsi async def main() dan panggil dengan asyncio.run(main()).

Menjalankan kueri

Mulailah dengan pertanyaan yang memerlukan navigasi lintas beberapa bagian. Kueri tentang kerentanan valuasi aset memerlukan materi dari ringkasan, bab valuasi aset, dan subbagiannya yang membahas pasar tertentu.

Itu adalah bagian yang mungkin diangkat RAG vektor secara terpisah, tetapi jarang bersama dengan hierarki yang tepat tetap utuh. Inilah jenis kueri di mana pencarian pohon benar-benar berguna.

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"])

Jejak penalaran adalah bagian yang paling layak diperhatikan. Berikut contoh keluaran tipikal:

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']

Anda dapat melihat persis mengapa setiap bagian dipilih, tingkat keterlacakan yang tidak Anda dapatkan dari peringkat kemiripan kosinus.

Menghasilkan Jawaban dari Konteks yang Diambil

Setelah Anda memiliki ID node yang relevan, langkah berikutnya adalah mengekstrak teks terkait dan memberikannya ke model generasi.

Mengekstrak konteks dan menghasilkan jawaban

Dua fungsi menangani langkah generasi. 

  • generate_answer() mencari setiap node terpilih di node_map, menambahkan header dengan judul bagian dan indeks halaman (inilah yang memberi Anda sitasi yang dapat diaudit dalam jawaban akhir), dan memberikan konteks tersusun ini ke model generasi. 

  • pageindex_pipeline() membungkus pencarian pohon dan generasi menjadi satu pemanggilan, sehingga Anda tidak perlu merantaikannya secara manual setiap saat.

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
    }

Jalankan pipeline penuh:

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']}")

Jawaban kembali dengan sitasi tingkat bagian dan nomor halaman. Itulah keterlacakan yang diberikan PageIndex secara default.

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").

Membandingkan PageIndex dengan Vector RAG

Inilah bagian yang benar-benar memberi Anda sesuatu yang berguna. Mari bangun baseline RAG vektor pada dokumen yang sama dan jalankan kedua pipeline pada kueri yang sama.

Membangun baseline Vector RAG

Baseline mengikuti pola standar: mengekstrak teks mentah dari PDF dengan PyMuPDF, membaginya menjadi chunk tumpang tindih 500 kata, melakukan embedding tiap chunk dengan text-embedding-3-small, dan memuat semuanya ke dalam indeks FAISS dalam memori untuk lookup kemiripan kosinus.

Jika Anda ingin latar belakang lebih lanjut tentang mengapa pilihan ukuran chunk dan overlap memengaruhi kualitas pengambilan, artikel Chunking Strategies kami layak dibaca bersamaan dengan ini, dan penjelasan What Is FAISS? juga bermanfaat jika langkah membangun indeks terlihat asing.

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

Dengan fungsi helper didefinisikan, bangun indeksnya. Pada dokumen 60 halaman, langkah embedding adalah bagian lambat: perkirakan satu atau dua menit, tergantung koneksi Anda dan throughput 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")

Menjalankan perbandingan

Laporan 60 halaman menghasilkan sekitar 47 chunk pada 500 kata dengan tumpang tindih 50 kata.

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

Tiga kueri, masing-masing dirancang untuk mengekspos tantangan retrieval yang berbeda:

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?"
]

Dengan kedua pipeline siap, loop di bawah ini mengirim setiap kueri melalui kedua sistem dan mengumpulkan hasilnya. Potongan [:500] pada setiap jawaban hanya untuk menjaga keluaran konsol tetap mudah dibaca; jawaban lengkap disimpan di results untuk diperiksa setelahnya.

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)

Perbandingan hasil

Berikut tipe hasil yang akan Anda lihat di tiga jenis kueri:

Kueri

PageIndex

Vector RAG

Jawaban Benar

Pemenang

Risiko jangka pendek paling signifikan

Jawaban luas mencakup beberapa risiko dengan sitasi bagian

Mengangkat angka survei spesifik 72% (lebih presisi pada yang ini)

Inflasi persisten dan pengetatan kebijakan moneter, disebut oleh 72% responden survei (Kotak 5.1)

Seri, sedikit unggul Vector RAG pada statistik spesifik

Metodologi risiko iklim (rujukan silang)

Mengambil Bagian, tetapi memilih node induk alih-alih Kotak 5.2, sehingga teks metodologi tidak pernah mencapai generasi

Melakukan chunk tepat melalui Kotak 5.2 dan mengembalikan langkah metodologi aktual

Kotak 5.2 menjelaskan penerjemahan risiko fisik dan transisi ke eksposur keuangan melalui analisis skenario

Vector RAG

Interaksi valuasi aset + leverage

Mengambil 18 node lintas Bagian 1 dan 3 dan menjelaskan mekanisme amplifikasi

Menghubungkan kedua bagian dan menjelaskan mekanisme amplifikasi

Valuasi tinggi meningkatkan risiko koreksi harga tajam; leverage memperbesar kerugian saat koreksi menghantam institusi berleverage (Bagian 1 dan 3)

Seri

Menafsirkan hasil

Hasilnya lebih bernuansa daripada kemenangan bersih bagi salah satu sistem, yang membuatnya lebih berguna. 

PageIndex menavigasi ke bagian yang tepat di ketiga kasus. Pada kueri faktual langsung, keduanya menjawab benar, meskipun RAG vektor mengangkat angka 72% spesifik karena chunking datarnya kebetulan menangkap Kotak 5.1 utuh, lebih karena keberuntungan chunking daripada keunggulan struktural. Pada kueri multi-bagian, PageIndex mengambil 18 node yang membentang Bagian 1 dan 3 dan menjelaskan interaksinya, kurang lebih menyamai RAG vektor. Satu kekalahan jelas adalah kueri rujukan silang, dan ini patut dipahami alasannya.

Pada kueri metodologi iklim, PageIndex menalar ke Bagian 5 dan Ringkasan, tetapi metodologinya berada di Kotak 5.2, node anak terpisah di bawah Bagian 5. Pencarian pohon memilih bagian induk, bukan kotak, dan teks induk hanya merujuk kotak tanpa mereproduksinya, jadi langkah generasi tidak pernah melihat metodologi. Vector RAG menang karena melakukan chunk tepat melalui Kotak 5.2. 

Ini adalah masalah granularitas retrieval, bukan kegagalan penalaran: memilih node induk tidak menarik teks anak-anaknya kecuali Anda memperluas seleksi untuk memasukkannya. Anda dapat menutup celah ini dengan memprompt pencarian pohon agar mengembalikan node anak dari setiap bagian relevan, atau dengan memperluas setiap node terpilih agar menyertakan turunannya sebelum generasi.

Keunggulan PageIndex tampak paling jelas pada run pipeline mandiri sebelumnya di tutorial (kueri valuasi aset), di mana ia menalar struktur dokumen dengan benar dan mengembalikan jawaban yang bersitasi baik serta diatribusikan ke bagian. Kueri perbandingan di atas sengaja dipilih untuk stress-test skenario retrieval yang lebih menantang, itulah mengapa mereka menguntungkan RAG vektor untuk dokumen 60 halaman.

Kesimpulan jujur: keunggulan PageIndex atas RAG vektor makin tajam seiring panjang dokumen dan padatnya rujukan silang internal. Pada laporan 60 halaman, selisihnya sedang. Pada 10-K setebal 200 halaman dengan puluhan catatan kaki yang saling merujuk, harapkan selisihnya lebih besar.

Jika Anda ingin mendorong baseline RAG vektor lebih jauh sebelum mengabaikannya, How to Improve RAG Performance membahas lima teknik paling berdampak.

Kapan Menggunakan PageIndex

Perbandingan di atas spesifik untuk satu tipe dan panjang dokumen. Apakah PageIndex pilihan tepat bergantung pada tiga faktor: struktur dokumen, tipe kueri, dan volume.

Saat PageIndex unggul

PageIndex layak dengan overhead-nya pada dokumen profesional yang panjang dan terstruktur di mana jawaban yang salah mahal: 

  • 10-K dan pengajuan keuangan lainnya
  • Kontrak hukum dengan istilah terdefinisi dan rujukan silang
  • Panduan regulasi
  • Manual teknis dengan bagian bernomor 

Ini juga kasus di mana jejak penalaran memberi Anda sesuatu yang konkret untuk diaudit. Sweet spot-nya adalah volume rendah, risiko tinggi: beberapa lusin dokumen per hari di mana setiap kueri harus benar, dan premi latensi serta biaya sepadan.

Saat vector RAG lebih tepat

Vector RAG lebih tepat sebagai default untuk throughput tinggi, latensi rendah, di mana beberapa panggilan LLM PageIndex per kueri menjadi bottleneck dan embedding yang di-cache menangani beban dengan sebagian kecil biaya. 

Ini juga cocok untuk dokumen datar dengan sedikit hierarki untuk dinalar, seperti: 

  • Artikel berita
  • Deskripsi produk
  • Tiket dukungan

Ini juga pilihan tepat untuk kasus di mana jawaban kira-kira sudah cukup. Chatbot dukungan yang menemukan FAQ yang tepat sembilan dari sepuluh kali mungkin sudah memenuhi standar.

Trade-off yang jujur

Biayanya adalah kecepatan dan uang. Setiap kueri PageIndex menjalankan setidaknya dua panggilan LLM (pencarian pohon dan generasi jawaban) plus pemrosesan awal, jadi harapkan tiga hingga delapan detik per kueri pada dokumen 60 halaman dibanding di bawah satu detik untuk RAG vektor dengan indeks FAISS yang di-cache, dan tagihan per kueri berskala sama.

Financial Stability Report adalah kasus uji yang adil untuk PageIndex: terstruktur, hierarkis, berujukan silang. Korpus ribuan tiket dukungan pendek bukan, dan RAG vektor akan mengunggulinya di sana dengan sebagian kecil biaya.

Pemikiran Penutup

Vector RAG memiliki asumsi tersembunyi yang tertanam: bahwa bagian yang paling mirip dengan kueri Anda juga merupakan bagian yang memuat jawaban. Untuk dokumen pendek dan kurang terstruktur, asumsi itu cukup berlaku. Untuk dokumen profesional yang panjang dan terstruktur, asumsi itu runtuh dengan cara yang bisa diprediksi: tabel terbelah, frekuensi istilah yang menyesatkan, dan rujukan silang yang tak terlihat.

PageIndex mengatasi ketiganya dengan memperlakukan retrieval sebagai masalah penalaran atas struktur dokumen alih-alih pencarian kemiripan atas fragmen teks.

Aturan praktisnya sederhana: jika dokumen Anda memiliki hierarki, kueri Anda memerlukan mengikuti rujukan internal, dan mendapatkan jawaban yang benar lebih penting daripada mendapatkannya dengan cepat, PageIndex sepadan dengan tambahan latensi dan biaya. Jika Anda menjalankan pencarian volume tinggi pada dokumen pendek atau datar, RAG vektor tetap menjadi default yang tepat.

Ingin menyelami lebih dalam RAG produksi dan sistem agen? Track AI Engineering with LangChain kami membawa Anda dari fundamental aplikasi hingga retrieval, evaluasi, dan agen pengguna alat.

FAQ PageIndex

Apa itu PageIndex?

PageIndex adalah kerangka kerja RAG open-source dari VectifyAI (September 2025) yang menggantikan pencarian kemiripan vektor dengan penalaran LLM atas indeks pohon hierarkis: tanpa embedding, tanpa basis data vektor, tanpa chunking.

Bagaimana PageIndex berbeda dari RAG standar?

RAG standar membagi dokumen menjadi fragmen, melakukan embedding, dan mengambil chunk yang paling mirip dengan kueri. PageIndex membangun pohon dari struktur alami dokumen dan meminta LLM menalar bagian mana yang kemungkinan memuat jawaban. Langkah retrieval adalah masalah penalaran, bukan pencarian kemiripan.

Apakah PageIndex memerlukan kunci API OpenAI?

Tidak selalu. Repo open-source self-hosted bekerja dengan penyedia apa pun yang didukung LiteLLM. Tutorial ini menggunakan OpenAI untuk penalaran retrieval dan generasi jawaban, jadi jika Anda mengikutinya persis, Anda memerlukan kunci OpenAI plus kunci PageIndex dari dash.pageindex.ai/api-keys.

Apakah PageIndex cocok untuk semua dokumen?

Tidak. PageIndex unggul pada dokumen panjang dan terstruktur dengan hierarki dan rujukan silang. Untuk korpora besar berisi teks pendek dan longgar strukturnya, seperti tiket dukungan atau halaman FAQ, RAG vektor akan lebih cepat dan lebih murah.

Akurasi apa yang dicapai PageIndex pada FinanceBench?

Mafin 2.5, sistem berbasis PageIndex milik VectifyAI, mencapai 98,7%, dibanding sekitar 30–50% untuk RAG berbasis vektor tradisional. FinanceBench mencakup tanya jawab keuangan pada pengajuan SEC, yang memerlukan penalaran multi-langkah dan retrieval numerik tepat. Itulah celah yang dibangun PageIndex untuk ditutup.


Josep Ferrer's photo
Author
Josep Ferrer
LinkedIn
Twitter

Josep adalah Data Scientist freelance yang berfokus pada proyek-proyek Eropa, dengan keahlian dalam penyimpanan data, pemrosesan, analitik lanjutan, dan penyusunan narasi data yang berdampak. 

Sebagai pendidik, ia mengajar Big Data di program Magister di University of Navarra dan berbagi wawasan melalui artikel di platform seperti Medium, KDNuggets, dan DataCamp. Josep juga menulis tentang Data dan Teknologi dalam buletin Databites (databites.tech). 

Ia meraih gelar Sarjana di bidang Fisika Teknik dari Polytechnic University of Catalonia dan gelar Magister di bidang Intelligent Interactive Systems dari Pompeu Fabra University.

Topik

Pelajari RAG bersama DataCamp!

Kursus

Retrieval Augmented Generation (RAG) dengan LangChain

3 Hr
19.1K
Pelajari metode mutakhir untuk mengintegrasikan data eksternal dengan LLM menggunakan Retrieval Augmented Generation (RAG) dengan LangChain.
Lihat DetailRight Arrow
Mulai Kursus
Lihat Lebih BanyakRight Arrow
Terkait

blogs

Tutorial Korelasi di R

Dapatkan pengenalan dasar-dasar korelasi di R: pelajari lebih lanjut tentang koefisien korelasi, matriks korelasi, plotting korelasi, dan sebagainya.
David Woods's photo

David Woods

13 mnt

blogs

40 Pertanyaan Wawancara DBMS Teratas di 2026

Kuasai pertanyaan wawancara basis data, dari konsep SQL dasar hingga skenario desain sistem tingkat lanjut. Panduan mendalam ini mencakup semua yang Anda perlukan untuk sukses di wawancara DBMS dan meraih peran berikutnya.
Dario Radečić's photo

Dario Radečić

15 mnt

blogs

Spaghetti Plot dan Jalur Badai

Temukan alasan mengapa Anda sebaiknya (tidak) menggunakan spaghetti plot untuk menyampaikan ketidakpastian jalur prediksi badai serta dampaknya terhadap interpretasi.
Hugo Bowne-Anderson's photo

Hugo Bowne-Anderson

13 mnt

blogs

12 Alternatif ChatGPT Terbaik yang Bisa Anda Coba pada 2026

Artikel ini menyajikan daftar alternatif ChatGPT yang akan meningkatkan produktivitas Anda.
Javier Canales Luna's photo

Javier Canales Luna

14 mnt

Lihat Lebih BanyakLihat Lebih Banyak