Kursus
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.
- 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).
- 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:
- Python 3.10+ terpasang
- Pemahaman dasar konsep RAG. Jika Anda baru pada retrieval-augmented generation, bacalah What is Retrieval Augmented Generation (RAG)? terlebih dahulu
- Kunci API OpenAI dari platform.openai.com/api-keys
- Kunci API PageIndex dari dash.pageindex.ai/api-keys (tier gratis cukup untuk tutorial ini)

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 queued → processing → completed.
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 dinode_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 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.

