Curso
Imagine que você está com o Financial Stability Report do Federal Reserve aberto. Alguém pergunta: Qual porcentagem dos respondentes citou a inflação persistente como o principal risco de curto prazo? Você vai à Seção 5, encontra o Box 5.1 e vê o número em dois segundos. É 72%.
Se você passar essa pergunta para um pipeline RAG padrão, encontrar a resposta vai depender de como o “chunker” dividiu a página. Se o Box 5.1 for fragmentado entre blocos, a busca por similaridade retorna trechos que mencionam inflação em algum lugar, mas deixam passar o número. Essa é a limitação central da busca por similaridade: texto que parece com sua consulta não é a mesma coisa que a seção que responde à pergunta.
Em documentos longos e estruturados, isso pesa muito — e é exatamente o que o PageIndex foi criado para resolver. Neste tutorial, vou construir um sistema de QA de documentos com PageIndex e testá-lo contra um baseline de RAG vetorial nesse mesmo relatório do Fed, para você sair entendendo quando cada abordagem compensa o custo.
Resumo
- PageIndex é um framework de RAG sem vetores que faz recuperação por raciocinar sobre a estrutura do documento.
- Ele resolve pontos fracos do RAG vetorial: tabelas divididas, termos repetidos e referências cruzadas.
- Fazemos um benchmark no relatório de 2023 do Fed: PageIndex vs. um baseline FAISS.
- Trade-off, não vitória absoluta: a vantagem do PageIndex cresce com o tamanho do documento.
O que é o PageIndex?
PageIndex é um framework de recuperação que raciocinas sobre a estrutura do documento em vez de buscar texto que seja parecido com sua consulta. Foi lançado pela VectifyAI em setembro de 2025, criado por Mingtian Zhang e Yu Tang. É open source sob licença MIT e, em julho de 2026, o repositório no GitHub acumula mais de 34.000 estrelas.
PageIndex vs. RAG padrão
A melhor forma de explicar como o PageIndex funciona é contrastá-lo com o RAG tradicional. Se você é novo em RAG, a ideia básica é dar a um LLM acesso a um documento recuperando as partes relevantes no momento da consulta e passando-as como contexto.
É justamente essa etapa de recuperação que o PageIndex repensa. Em vez de comparar sua consulta com cada chunk do documento, o PageIndex primeiro constrói um índice em árvore hierárquico a partir da estrutura do documento. Isso leva a resultados diferentes:
- Busca vetorial encontra texto que parece semelhante à sua consulta.
- Busca em árvore encontra seções com maior probabilidade de conter a resposta.
Em outras palavras, a diferença entre o RAG padrão e o PageIndex é como procurar palavra-chave em cada página de um livro versus usar o sumário para primeiro encontrar o capítulo certo.
Esses dois objetivos convergem para perguntas factuais simples em documentos curtos, mas se distanciam bastante quando os documentos ficam longos, estruturados e cheios de referências internas.
|
Dimensão |
PageIndex |
RAG vetorial |
|
Mecanismo de recuperação |
Raciocínio de LLM sobre índice em árvore |
Similaridade do cosseno sobre embeddings |
|
Custo de setup |
Processamento do documento (uma vez) |
Chunking + embedding (uma vez) |
|
Latência por consulta |
3–8 segundos (múltiplas chamadas de LLM) |
< 1 segundo (consulta ao índice) |
|
Custo por consulta |
Maior (2+ chamadas de LLM) |
Menor (consulta de embedding + uma chamada de LLM) |
|
Acurácia em docs longos e estruturados |
98,7% no FinanceBench (Mafin 2.5) |
30–50% no FinanceBench |
|
Lida com referências cruzadas |
Sim |
Não |
|
Lida com tabelas divididas |
Sim |
Não |
|
Rastreabilidade |
Trilha completa de raciocínio + citações de página |
Scores dos top-k chunks |
|
Melhor para |
Demonstrações financeiras, contratos, manuais |
FAQs, documentação de produto, tickets de suporte |
Como funciona o índice em árvore
O processo acontece em duas etapas.
- Você ingere um documento e o PageIndex gera uma árvore. Cada nó carrega um título, um resumo e um índice de página. A árvore reflete a hierarquia natural do documento (capítulos, seções, subseções, apêndices, o que quer que o documento tenha).
- Quando chega uma consulta, o LLM recebe a estrutura da árvore (sem o texto completo, que estouraria a janela de contexto) e raciocina sobre quais nós têm maior chance de conter a resposta. Ele retorna um conjunto de IDs de nós com uma trilha explicando por que cada um foi selecionado; em seguida, o texto desses nós é extraído e passado para a etapa de geração.

Por que isso importa em documentos estruturados
Já construí pipelines de RAG suficientes para saber que o chunking é onde muitos sistemas em produção falham silenciosamente. A estratégia é linda no papel, mas no momento em que seu documento tem uma tabela que cruza páginas ou uma nota de rodapé que define um termo usado três seções antes, as rachaduras aparecem.
O RAG vetorial tem três fragilidades estruturais que se repetem em documentos financeiros e jurídicos.
- Conteúdo dividido: um balanço patrimonial quebrado em dois chunks perde a relação entre as linhas e ambas as metades recebem baixa relevância, porque nenhuma faz sentido sozinha. (Late chunking ajuda aqui, mas não resolve referências cruzadas.)
- Termos repetidos: um relatório anual menciona "revenue" dezenas de vezes; uma consulta sobre o crescimento de receita de uma divisão puxa chunks de todas as menções, ranqueados quase igual, sem como diferenciar o que realmente importa.
- Referências cruzadas: quando a Seção 4.3 diz "veja o Apêndice G para a reconciliação completa", uma busca por similaridade não tem mecanismo para seguir esse apontamento. A resposta está no Apêndice G e o recuperador não tem como saber.
O PageIndex resolve os três pontos porque recupera sobre a estrutura, não sobre fragmentos. A árvore mantém o documento inteiro, a etapa de raciocínio pode seguir uma referência cruzada ou comparar seções e, como cada nó tem índice de página e resumo, o recuperador navega a "geografia" do documento em vez de similaridade superficial de texto.
Sendo sincero: quando vi pela primeira vez a taxa de 98,7% para o Mafin 2.5 (sistema da VectifyAI baseado em PageIndex) no FinanceBench, minha reação foi cética — um gap de quase 50 pontos sobre o RAG padrão parece benchmark escolhido a dedo. Mas o FinanceBench testa exatamente esses modos de falha: raciocínio em múltiplas etapas sobre arquivos da SEC com respostas numéricas precisas.
Primeiros passos com PageIndex
Este tutorial exige alguns pacotes e duas chaves de API. Aqui está tudo o que você precisa antes de escrever uma linha de código.
Você pode conferir todo o código usado ao longo do tutorial no meu repositório no GitHub.
Para acompanhar este tutorial, você vai precisar de:
- Python 3.10+ instalado
- Familiaridade básica com conceitos de RAG. Se você está começando com retrieval-augmented generation, leia O que é Retrieval Augmented Generation (RAG)? antes de continuar
- Uma chave de API da OpenAI pelo platform.openai.com/api-keys
- Uma chave de API do PageIndex pelo dash.pageindex.ai/api-keys (o plano gratuito é suficiente para este tutorial)

Instale os pacotes necessários:
pip install pageindex openai requests faiss-cpu pymupdf
Defina as chaves de API como variáveis de ambiente em vez de fixá-las no código:
export PAGEINDEX_API_KEY="your_pageindex_key_here"
export OPENAI_API_KEY="your_openai_key_here"
Com as duas chaves configuradas, inicialize os clientes:
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)
Esse é todo o setup: nove imports, duas variáveis de ambiente, dois clientes.
Ingerindo um documento e construindo a árvore do PageIndex
Para este tutorial, vamos usar o Financial Stability Report de outubro de 2023 do Federal Reserve. É um PDF público estruturado como um documento real: seções formais, capítulos numerados, apêndices e referências internas.
Baixando o documento
O Fed publica seus relatórios como PDFs acessíveis em uma URL estável. A verificação if not os.path.exists() garante que você só faça o download uma vez, o que importa quando está iterando no restante do código e não quer baixar um PDF de 60 páginas a cada execução.
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}")
Enviando o documento
O envio é uma única chamada de API. submit_document() faz o upload do arquivo e retorna um doc_id que você vai usar em todas as operações seguintes com esse documento. Guarde-o, porque se você reiniciar a sessão, pode pular o envio e ir direto ao get_tree() usando o mesmo ID.
print("Submitting document to PageIndex...")
submit_result = pi_client.submit_document(PDF_PATH)
doc_id = submit_result["doc_id"]
print(f"Document ID: {doc_id}")
O PageIndex processa o documento de forma assíncrona, então você precisa fazer polling até a árvore ficar pronta:
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.")
Processar um PDF de 60 páginas leva, em geral, de dois a quatro minutos — e esse custo é pago apenas uma vez por documento. Você verá o status passar por queued → processing → completed.
Submitting document to PageIndex...
Document ID: pi-cmq2bp4ok00rx01qxmym7tnd1
Waiting for tree generation...
Status: queued
Status: processing
Status: completed
Processing done.
Inspecionando a árvore
Quando o processamento termina, get_tree() retorna a estrutura hierárquica completa como uma lista aninhada de nós. O helper utils.create_node_mapping() achata isso em um dicionário simples indexado por ID do nó, deixando a extração de texto muito mais rápida do que percorrer a árvore recursivamente a cada consulta.
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()
Executar isso no Financial Stability Report dá o esqueleto do documento de relance:
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
…
A árvore é JSON puro, com cada nó trazendo um node_id, title, summary, page_index e uma lista nodes para eventuais subseções. Diferente de índices vetoriais, não há um espaço de embedding opaco nem arquivo binário exigindo leitor especial. É só uma lista aninhada que você pode imprimir, inspecionar e sobre a qual pode raciocinar diretamente.
Essa transparência é muito útil na prática: se o recuperador retorna a seção errada, você consegue olhar a árvore e entender o porquê — algo impossível com um embedding de 768 dimensões.
Consultando o PageIndex com busca em árvore via LLM
A etapa de recuperação envia a estrutura da árvore (sem o texto completo, que estouraria a janela de contexto) para um LLM e pede que ele identifique quais nós são relevantes para a consulta.
A função de busca na árvore
O prompt passa a árvore “slim” (apenas títulos e resumos, sem o texto integral das seções) para o LLM e pede que retorne um JSON contendo uma trilha de raciocínio e uma lista de IDs de nós. Duas escolhas de design valem nota.
Primeiro, utils.remove_fields() remove o conteúdo real de cada nó antes de serializar, mantendo o prompt dentro dos limites de contexto mesmo em documentos longos. A chamada copy.deepcopy() é necessária porque remove_fields muta in place, e você precisa da árvore original intacta para a extração de texto depois.
Segundo, response_format={"type": "json_object"} força saída estruturada, então o parsing é confiável, não frágil. A temperatura fica em 0 porque essa é uma tarefa de raciocínio.
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
Observação: as chamadas await abaixo assumem um ambiente Jupyter ou iPython com suporte a await no topo. Se rodar como um script .py simples, envolva as chamadas async dentro de uma função async def main() e execute com asyncio.run(main()).
Executando uma consulta
Comece com uma pergunta que exige navegar por várias seções. Uma consulta sobre vulnerabilidades de valuation de ativos precisa de material do overview, do capítulo de valuations e de suas subseções sobre mercados específicos.
Essas são seções que o RAG vetorial pode até trazer, mas raramente juntas com a hierarquia correta. É justamente o tipo de consulta em que a busca em árvore mostra seu valor.
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"])
A trilha de raciocínio é a parte para observar com mais atenção. Aqui está um exemplo típico de saída:
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']
Você vê exatamente por que cada seção foi selecionada — um nível de rastreabilidade que você simplesmente não tem em um ranking por cosseno.
Gerando respostas a partir do contexto recuperado
Depois de ter os IDs dos nós relevantes, o próximo passo é extrair o texto correspondente e passá-lo a um modelo de geração.
Extraindo contexto e gerando respostas
Duas funções cuidam da etapa de geração.
-
generate_answer()procura cada nó selecionado nonode_map, antepõe um cabeçalho com o título da seção e o índice de página (é isso que dá as citações auditáveis na resposta final) e passa o contexto montado para um modelo gerador. -
pageindex_pipeline()encapsula a busca em árvore e a geração em uma única chamada, para você não precisar encadear manualmente toda vez.
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
}
Execute o pipeline completo:
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']}")
A resposta vem com citações no nível da seção e números de página. Essa é a rastreabilidade que o PageIndex oferece por padrão.
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").
Comparando PageIndex com RAG vetorial
Esta é a seção que realmente traz algo útil. Vamos construir um baseline de RAG vetorial no mesmo documento e rodar ambos os pipelines nas mesmas consultas.
Construindo o baseline de RAG vetorial
O baseline segue o padrão: extrair texto bruto do PDF com PyMuPDF, dividir em chunks sobrepostos de 500 palavras, gerar embeddings de cada chunk com text-embedding-3-small e carregar tudo em um índice FAISS em memória para busca por similaridade de cosseno.
Se quiser mais contexto sobre por que as escolhas de tamanho e sobreposição dos chunks afetam a qualidade da recuperação, nosso artigo Chunking Strategies vale a leitura em paralelo, e o explicativo O que é FAISS? também ajuda se os passos de construção do índice não forem familiares.
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
Com as funções auxiliares definidas, construa o índice. Em um documento de 60 páginas, a etapa de embedding é a parte mais lenta: espere de um a dois minutos, dependendo da sua conexão e da vazão da 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")
Rodando a comparação
Um relatório de 60 páginas gera cerca de 47 chunks com 500 palavras e sobreposição de 50 palavras.
Extracting text from PDF...
Chunking text...
47 chunks created
Embedding chunks (takes 1-2 min)...
Embeddings shape: (47, 1536)
Building FAISS index...
Done.
Três consultas, cada uma desenhada para expor um desafio de recuperação diferente:
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?"
]
Com os dois pipelines prontos, o loop abaixo envia cada consulta por ambos os sistemas e coleta os resultados. O corte [:500] em cada resposta apenas mantém o console legível; as respostas completas ficam em results para inspeção posterior.
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)
Comparação dos resultados
Aqui está o tipo de resultado que você verá nos três tipos de consulta:
|
Consulta |
PageIndex |
RAG vetorial |
Resposta correta |
Vencedor |
|
Risco mais significativo de curto prazo |
Resposta ampla cobrindo múltiplos riscos com citações de seção |
Trouxe o número específico da pesquisa (72% — mais preciso neste caso) |
Inflação persistente e política monetária mais restritiva, citadas por 72% dos respondentes (Box 5.1) |
Empate, leve vantagem do RAG vetorial pelo dado específico |
|
Metodologia de risco climático (referência cruzada) |
Recuperou a Seção, mas selecionou o nó pai em vez do Box 5.2; o texto da metodologia não chegou à geração |
Fez chunking passando pelo Box 5.2 e retornou os passos da metodologia |
O Box 5.2 descreve traduzir riscos físicos e de transição em exposições financeiras via análise de cenários |
RAG vetorial |
|
Interação valuations + alavancagem |
Recuperou 18 nós nas Seções 1 e 3 e explicou o mecanismo de amplificação |
Conectou as duas seções e explicou o mecanismo de amplificação |
Valuations elevados aumentam o risco de correções bruscas; a alavancagem amplia perdas quando as correções atingem instituições alavancadas (Seções 1 e 3) |
Empate |
Interpretando os resultados
Os resultados são mais sutis do que uma vitória limpa de qualquer lado — e por isso mesmo mais úteis.
O PageIndex navegou para as seções certas nas três situações. Na consulta factual direta, ambos responderam corretamente, embora o RAG vetorial tenha trazido o número específico de 72% porque o chunking plano capturou o Box 5.1 inteiro — mais sorte de chunking do que vantagem estrutural. Na consulta de múltiplas seções, o PageIndex recuperou 18 nós cobrindo as Seções 1 e 3 e explicou a interação, ficando próximo ao RAG vetorial. A perda clara foi na consulta de referência cruzada, e vale entender o motivo.
Na consulta sobre metodologia climática, o PageIndex raciocinou até a Seção 5 e o Overview, mas a metodologia está no Box 5.2, um nó filho específico sob a Seção 5. A busca em árvore selecionou a seção pai, não o box, e o texto da seção faz referência ao box sem reproduzi-lo — então a geração nunca viu a metodologia. O RAG vetorial venceu por fazer chunking passando direto pelo Box 5.2.
Isso é um problema de granularidade de recuperação, não de raciocínio: selecionar um nó pai não puxa o texto dos filhos, a menos que você expanda a seleção. Dá para fechar essa lacuna pedindo ao prompt de busca na árvore que retorne nós filhos de qualquer seção relevante, ou expandindo cada nó selecionado para incluir seus descendentes antes da geração.
A vantagem do PageIndex ficou mais clara na execução isolada do pipeline mais acima (a consulta sobre valuations), em que ele raciocinou bem sobre a estrutura e retornou respostas bem citadas e atribuídas à seção. As consultas da comparação foram escolhidas para estressar cenários mais desafiadores de recuperação — por isso favorecem o RAG vetorial em um documento de 60 páginas.
Em resumo: a vantagem do PageIndex sobre o RAG vetorial aumenta com o tamanho do documento e a densidade de referências internas. Em um relatório de 60 páginas, o gap é moderado. Em um 10-K de 200 páginas com dezenas de notas de rodapé se referenciando, espere algo bem maior.
Se quiser ir além no baseline de RAG vetorial antes de descartá-lo, Como melhorar a performance de RAG coberte as cinco técnicas de maior impacto.
Quando usar PageIndex
A comparação acima é específica para um tipo e tamanho de documento. Se o PageIndex é a escolha certa depende de três fatores: estrutura do documento, tipo de consulta e volume.
Quando o PageIndex vence
O PageIndex paga seu overhead em documentos profissionais longos e estruturados, onde errar custa caro:
- 10-Ks e outras demonstrações/arquivos financeiros
- Contratos jurídicos com termos definidos e referências cruzadas
- Orientações regulatórias
- Manuais técnicos com seções numeradas
São também os casos em que as trilhas de raciocínio dão algo concreto para auditar. O sweet spot é baixo volume e alto impacto: algumas dezenas de documentos por dia, em que cada consulta precisa estar certa — e o prêmio de latência e custo vale a pena.
Quando o RAG vetorial é melhor
O RAG vetorial é a escolha padrão para cargas de alto throughput e baixa latência, em que as múltiplas chamadas de LLM do PageIndex por consulta viram gargalo e embeddings em cache seguram a barra por uma fração do custo.
Ele também se encaixa em documentos planos com pouca hierarquia para raciocinar, como:
- Notícias
- Descrições de produto
- Tickets de suporte
Também é a escolha certa em casos em que respostas aproximadas bastam. Um chatbot de suporte que acerta a FAQ certa nove em cada dez vezes provavelmente cumpre o papel.
Os trade-offs reais
O custo é em velocidade e dinheiro. Cada consulta no PageIndex faz pelo menos duas chamadas de LLM (busca em árvore e geração de resposta) além do processamento inicial, então espere de três a oito segundos por consulta em um documento de 60 páginas, contra menos de um segundo no RAG vetorial com FAISS em cache — e a conta por consulta segue a mesma lógica.
O Financial Stability Report é um caso de teste justo para o PageIndex: estruturado, hierárquico, com referências internas. Um corpus de milhares de tickets curtos de suporte não é — e o RAG vetorial vai vencer ali por uma fração do custo.
Considerações finais
O RAG vetorial embute uma suposição: de que o trecho mais parecido com sua consulta é também o trecho que contém a resposta. Em documentos curtos e pouco estruturados, essa suposição se sustenta bem. Em documentos profissionais longos e estruturados, ela quebra de formas previsíveis: tabelas divididas, frequência de termos enganosa e referências cruzadas invisíveis.
O PageIndex resolve os três pontos ao tratar a recuperação como um problema de raciocínio sobre a estrutura do documento — não como uma busca por similaridade entre fragmentos de texto.
A regra prática é simples: se seus documentos têm hierarquia, suas consultas exigem seguir referências internas e acertar a resposta importa mais do que responder rápido, o PageIndex vale a latência e o custo extras. Se você roda buscas em alto volume sobre documentos curtos ou planos, o RAG vetorial continua sendo o padrão certo.
Quer se aprofundar em RAG de produção e sistemas agentic? Nossa trilha AI Engineering with LangChain leva você dos fundamentos de aplicação a retrieval, avaliação e agentes que usam ferramentas.
FAQs sobre PageIndex
O que é o PageIndex?
PageIndex é um framework de RAG open source da VectifyAI (setembro de 2025) que substitui a busca por similaridade vetorial por raciocínio de LLM sobre um índice hierárquico em árvore: sem embeddings, sem banco de vetores, sem chunking.
Como o PageIndex difere do RAG padrão?
O RAG padrão divide um documento em fragmentos, gera embeddings e recupera os chunks mais semelhantes à consulta. O PageIndex constrói uma árvore a partir da estrutura natural do documento e pede a um LLM para raciocinar sobre quais seções provavelmente contêm a resposta. A recuperação vira um problema de raciocínio, não de similaridade.
O PageIndex exige uma chave de API da OpenAI?
Não necessariamente. O repositório open source self-hosted funciona com qualquer provedor suportado pelo LiteLLM. Este tutorial usa OpenAI para o raciocínio de recuperação e a geração de respostas; então, se você seguir ao pé da letra, vai precisar de uma chave da OpenAI e de uma chave do PageIndex em dash.pageindex.ai/api-keys.
O PageIndex é bom para todos os documentos?
Não. O PageIndex brilha em documentos longos e estruturados, com hierarquia e referências cruzadas. Para grandes corpora de textos curtos e pouco estruturados, como tickets de suporte ou páginas de FAQ, o RAG vetorial será mais rápido e barato.
Qual acurácia o PageIndex atingiu no FinanceBench?
O Mafin 2.5, sistema da VectifyAI baseado em PageIndex, alcançou 98,7%, contra cerca de 30–50% para RAG vetorial tradicional. O FinanceBench cobre QA financeiro em arquivos da SEC, o que exige raciocínio em várias etapas e recuperação numérica exata. É justamente essa lacuna que o PageIndex foi criado para fechar.
Josep é cientista de dados e gerente de projetos no Conselho de Turismo da Catalunha, usando dados para melhorar a experiência dos turistas na Catalunha. Sua experiência inclui o gerenciamento de armazenamento e processamento de dados, juntamente com análises avançadas e a comunicação eficaz de insights de dados.
Ele também é um educador dedicado, lecionando no programa de mestrado em Big Data da Universidade de Navarra e contribuindo regularmente com artigos perspicazes sobre ciência de dados para o Medium e o KDNuggets.
Ele é bacharel em Engenharia Física pela Universidade Politécnica da Catalunha e mestre em Sistemas Interativos Inteligentes pela Universidade Pompeu Fabra.
Atualmente, ele está empenhado em tornar as tecnologias relacionadas a dados mais acessíveis a um público mais amplo por meio da publicação ForCode'Sake no Medium.

