Curso
Imagina que tienes delante el Financial Stability Report de la Reserva Federal. Alguien pregunta: ¿Qué porcentaje de encuestados señaló la inflación persistente como el principal riesgo a corto plazo? Vas a la sección 5, buscas el recuadro 5.1 y ves la cifra en dos segundos: 72%.
Si pasas esa misma pregunta por un pipeline RAG estándar, encontrar la respuesta dependerá de cómo el "chunker" haya troceado la página. Si el recuadro 5.1 queda fragmentado en varios trozos, la búsqueda por similitud devolverá pasajes donde aparece la palabra inflación, pero omitirá la cifra. Ese es el límite clave de la búsqueda por similitud: texto que se parece a tu consulta no es lo mismo que la sección que realmente la responde.
En documentos largos y estructurados, esto importa mucho, y justo para eso se creó PageIndex. En este tutorial, construiré un sistema de preguntas y respuestas sobre documentos con PageIndex y lo pondré a prueba frente a un RAG vectorial de referencia con ese mismo informe de la Fed, para que salgas sabiendo cuándo compensa cada enfoque.
TL;DR
- PageIndex es un framework RAG sin vectores que recupera información razonando sobre la estructura del documento.
- Resuelve los puntos débiles del RAG vectorial: tablas partidas, términos repetidos y referencias cruzadas.
- Comparamos ambos con el informe de 2023 de la Fed: PageIndex vs. un baseline FAISS.
- Un intercambio, no una victoria aplastante: la ventaja de PageIndex crece con la longitud del documento.
¿Qué es PageIndex?
PageIndex es un framework de recuperación que razona sobre la estructura del documento en lugar de buscar texto que se parezca a tu consulta. Lo lanzó VectifyAI en septiembre de 2025, creado por Mingtian Zhang y Yu Tang. Es de código abierto bajo licencia MIT y, en julio de 2026, el repositorio de GitHub ha acumulado más de 34.000 estrellas.
PageIndex vs RAG estándar
La mejor forma de explicar cómo funciona PageIndex es contrastarlo con el RAG estándar. Si eres nuevo en RAG, la idea básica es dar a un LLM acceso a un documento recuperando las partes relevantes en tiempo de consulta y pasándolas como contexto.
Justo ese paso de recuperación es lo que PageIndex reimagina. En lugar de comparar tu consulta con cada fragmento del documento, PageIndex primero construye un índice en árbol jerárquico a partir de la estructura del documento. Esto conduce a resultados distintos:
- Búsqueda vectorial encuentra texto que se parece a tu consulta.
- Búsqueda en árbol encuentra secciones con alta probabilidad de contener la respuesta.
Dicho de otro modo, la diferencia entre RAG estándar y PageIndex es como entre escanear cada página de un libro buscando palabras clave y leer primero el índice para dar con el capítulo adecuado.
Ambos objetivos convergen en consultas factuales simples sobre documentos cortos, pero divergen claramente cuando los documentos son largos, estructurados y están llenos de referencias internas.
|
Dimensión |
PageIndex |
RAG vectorial |
|
Mecanismo de recuperación |
Razonamiento del LLM sobre un índice en árbol |
Similitud coseno sobre embeddings |
|
Coste de configuración |
Procesamiento del documento (una sola vez) |
Chunking + embedding (una sola vez) |
|
Latencia por consulta |
3–8 segundos (varias llamadas al LLM) |
< 1 segundo (consulta al índice) |
|
Coste por consulta |
Más alto (2+ llamadas al LLM) |
Más bajo (consulta de embeddings + una llamada al LLM) |
|
Precisión en documentos largos y estructurados |
98,7% en FinanceBench (Mafin 2.5) |
30–50% en FinanceBench |
|
Gestiona referencias cruzadas |
Sí |
No |
|
Gestiona tablas partidas |
Sí |
No |
|
Trazabilidad |
Rastro completo de razonamiento + citas de páginas |
Puntuaciones de los top-k chunks |
|
Ideal para |
Estados financieros, contratos, manuales |
FAQs, documentación de producto, tickets de soporte |
Cómo funciona el índice en árbol
El proceso ocurre en dos pasos.
- Ingestas un documento y PageIndex genera un árbol. Cada nodo incluye un título, un resumen y un índice de página. El árbol refleja la jerarquía natural del documento (capítulos, secciones, subsecciones, anexos; lo que contenga el documento).
- Cuando llega una consulta, el LLM recibe la estructura del árbol (sin el texto completo, que desbordaría la ventana de contexto) y razona qué nodos probablemente contienen la respuesta. Devuelve un conjunto de IDs de nodo con un rastro de razonamiento explicando por qué se seleccionó cada uno; luego se extrae el texto de esos nodos y se pasa a la fase de generación.

Por qué importa en documentos estructurados
He construido suficientes pipelines RAG como para saber que el chunking es donde la mayoría de sistemas en producción fallan en silencio. La estrategia se ve limpia sobre el papel, pero en cuanto tu documento tiene una tabla que ocupa dos páginas, o una nota al pie que define un término usado tres secciones antes, aparecen las grietas.
El RAG vectorial tiene tres debilidades estructurales que se repiten en documentos financieros y legales.
- Contenido partido: un balance dividido en dos chunks pierde la relación entre sus partidas, y ambas mitades puntúan como baja relevancia porque ninguna tiene sentido por sí sola. (Late chunking ayuda aquí, pero no resuelve las referencias cruzadas.)
- Términos repetidos: un informe anual menciona "revenue" decenas de veces, así que una consulta sobre el crecimiento de ingresos de una división arrastra chunks de todas las menciones, con rangos similares, sin forma de distinguir el relevante.
- Referencias cruzadas: cuando la sección 4.3 dice "ver el apéndice G para la conciliación completa", una búsqueda por similitud no tiene forma de seguir esa indicación. La respuesta está en el apéndice G y el recuperador no tiene cómo saberlo.
PageIndex gestiona las tres porque recupera sobre la estructura y no sobre fragmentos. El árbol mantiene íntegro el documento, el paso de razonamiento puede seguir una referencia cruzada o comparar entre secciones y, como cada nodo lleva un índice de página y un resumen, el recuperador navega por la geografía del documento en lugar de por similitud superficial de texto.
Seré sincero: cuando vi por primera vez la afirmación de 98,7% de precisión para Mafin 2.5 (el sistema de VectifyAI basado en PageIndex) en FinanceBench, mi reacción fue escepticismo: una ventaja de casi 50 puntos sobre el RAG estándar suena a benchmark hecho a medida. Pero FinanceBench prueba justo estos fallos: razonamiento multi-paso sobre informes de la SEC con respuestas numéricas precisas.
Primeros pasos con PageIndex
Este tutorial requiere algunos paquetes y dos claves de API. Aquí tienes todo lo necesario antes de escribir una línea de código.
Puedes consultar todo el código usado a lo largo del tutorial en mi repo de GitHub de acompañamiento.
Para seguir el tutorial, necesitarás:
- Python 3.10+ instalado
- Familiaridad básica con conceptos de RAG. Si eres nuevo en retrieval-augmented generation, lee What is Retrieval Augmented Generation (RAG)? antes de continuar
- Una clave de API de OpenAI de platform.openai.com/api-keys
- Una clave de API de PageIndex de dash.pageindex.ai/api-keys (el plan gratuito es suficiente para este tutorial)

Instala los paquetes necesarios:
pip install pageindex openai requests faiss-cpu pymupdf
Define las claves de API como variables de entorno en lugar de hardcodearlas:
export PAGEINDEX_API_KEY="your_pageindex_key_here"
export OPENAI_API_KEY="your_openai_key_here"
Con ambas claves listas, inicializa los 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)
Y ya está: nueve imports, dos variables de entorno, dos clientes.
Ingestar un documento y construir el árbol de PageIndex
Para este tutorial, usaremos el Financial Stability Report de octubre de 2023 de la Reserva Federal. Es un PDF público estructurado como un documento real en producción: secciones formales, capítulos numerados, anexos y referencias internas.
Descargar el documento
La Fed publica sus informes como PDFs accesibles en una URL estable. La comprobación if not os.path.exists() hace que solo lo descargues una vez, lo cual importa cuando iteras sobre el resto del código y no quieres volver a bajar un PDF de 60 páginas en cada ejecución.
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}")
Enviar el documento
El envío es una sola llamada de API. submit_document() sube el archivo y devuelve un doc_id que usarás en todas las operaciones posteriores sobre ese documento. Guárdalo, porque si reinicias la sesión puedes saltarte este paso e ir directo a get_tree() usando el mismo 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}")
PageIndex procesa el documento de forma asíncrona, así que tienes que hacer polling hasta que el árbol esté listo:
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.")
Procesar un PDF de 60 páginas suele tardar entre dos y cuatro minutos, y este coste se paga una sola vez por documento. Verás el estado pasar por queued → processing → completed.
Submitting document to PageIndex...
Document ID: pi-cmq2bp4ok00rx01qxmym7tnd1
Waiting for tree generation...
Status: queued
Status: processing
Status: completed
Processing done.
Inspeccionar el árbol
Cuando el procesamiento termina, get_tree() devuelve toda la estructura jerárquica como una lista anidada de nodos. El helper utils.create_node_mapping() la aplana en un diccionario simple indexado por ID de nodo, lo que hace que extraer texto más tarde sea mucho más rápido que recorrer el árbol de forma recursiva en 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()
Ejecutarlo sobre el Financial Stability Report te da de un vistazo el esqueleto del documento:
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
…
El árbol es JSON puro: cada nodo lleva un node_id, un title, un summary, un page_index y una lista nodes para las subsecciones que contenga. A diferencia de los índices vectoriales, no hay un espacio de embeddings opaco ni un archivo binario que requiera un lector especial. Es solo una lista anidada que puedes imprimir, inspeccionar y sobre la que puedes razonar directamente.
Esa transparencia es muy útil en la práctica: si el recuperador devuelve la sección equivocada, puedes mirar el árbol y entender por qué, algo que simplemente no puedes hacer con un embedding de 768 dimensiones.
Consultar PageIndex con búsqueda en árbol mediante LLM
El paso de recuperación envía la estructura del árbol (sin el texto completo, que desbordaría la ventana de contexto) a un LLM y le pide identificar qué nodos son relevantes para la consulta.
La función de búsqueda en árbol
El prompt pasa el árbol reducido (solo títulos y resúmenes, sin el texto completo) al LLM y le pide que devuelva un objeto JSON con un rastro de razonamiento y una lista de IDs de nodo. Dos decisiones de diseño a tener en cuenta.
Primero, utils.remove_fields() elimina el contenido de cada nodo antes de serializarlo, manteniendo el prompt dentro de los límites de contexto incluso en documentos largos. La llamada a copy.deepcopy() es necesaria porque remove_fields muta in situ, y necesitas el árbol original intacto para la extracción de texto posterior.
Segundo, response_format={"type": "json_object"} fuerza salida estructurada, de modo que el parseo sea fiable y no frágil. La temperatura se queda en 0 porque es una tarea de razonamiento.
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
Nota: las llamadas await siguientes suponen un entorno Jupyter o iPython donde se admite await a nivel superior. Si ejecutas esto como un script .py plano, envuelve las llamadas async en una función async def main() y ejecútala con asyncio.run(main()).
Ejecutar una consulta
Empieza con una pregunta que requiera navegar por varias secciones. Una consulta sobre vulnerabilidades en valoraciones de activos necesita material del resumen, del capítulo de valoraciones de activos y de sus subsecciones sobre mercados concretos.
Esas son secciones que un RAG vectorial puede sacar por separado, pero rara vez juntas y con la jerarquía correcta. Es justo el tipo de consulta donde la búsqueda en árbol marca la diferencia.
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"])
El rastro de razonamiento es la parte a la que más atención debes prestar. Un resultado típico tiene este aspecto:
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']
Puedes ver exactamente por qué se seleccionó cada sección, un nivel de trazabilidad que no obtienes con un ranking por coseno de similitud.
Generar respuestas a partir del contexto recuperado
Una vez tienes los IDs de los nodos relevantes, el siguiente paso es extraer el texto correspondiente y pasarlo a un modelo de generación.
Extraer contexto y generar respuestas
Dos funciones gestionan la fase de generación.
-
generate_answer()busca cada nodo seleccionado en elnode_map, añade un encabezado con el título de la sección y el índice de página (esto te da citas auditables en la respuesta final) y pasa el contexto ensamblado a un modelo generativo. -
pageindex_pipeline()envuelve la búsqueda en árbol y la generación en una sola llamada, para no encadenarlas manualmente cada 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
}
Ejecuta el 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']}")
La respuesta llega con citas a nivel de sección y números de página. Esa es la trazabilidad que PageIndex te da por defecto.
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").
Comparar PageIndex frente a RAG vectorial
Aquí es donde viene lo útil. Construyamos un baseline de RAG vectorial sobre el mismo documento y ejecutemos ambos pipelines con las mismas consultas.
Construir el baseline de RAG vectorial
El baseline sigue el patrón estándar: extraer texto bruto del PDF con PyMuPDF, dividirlo en chunks solapados de 500 palabras, generar embeddings de cada chunk con text-embedding-3-small y cargarlo todo en un índice FAISS en memoria para búsquedas por coseno.
Si quieres más contexto sobre cómo el tamaño de chunk y el solapamiento afectan a la calidad de recuperación, nuestro artículo Chunking Strategies es una buena lectura complementaria, y el ¿Qué es FAISS? también es útil si los pasos de construcción del índice no te suenan.
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
Con las funciones auxiliares definidas, construye el índice. En un documento de 60 páginas, el paso de embeddings es lo más lento: espera uno o dos minutos, según tu conexión y el throughput de 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")
Ejecutar la comparación
Un informe de 60 páginas produce alrededor de 47 chunks con 500 palabras y 50 de solapamiento.
Extracting text from PDF...
Chunking text...
47 chunks created
Embedding chunks (takes 1-2 min)...
Embeddings shape: (47, 1536)
Building FAISS index...
Done.
Tres consultas, cada una diseñada para exponer un reto de recuperación distinto:
queries = [
# Búsqueda factual directa
"What does the Fed consider the most significant near-term risk to financial stability as of October 2023?",
# Pregunta con referencia cruzada (el recuadro 5.2 se referencia desde la sección 5 y requiere seguir un puntero interno)
"What methodology does the Fed use to assess climate-related financial risks, and where in the report is it described?",
# Razonamiento multi-sección (conecta la sección 1 y la 3)
"How do elevated asset valuations interact with leverage in the financial sector to amplify systemic risk, according to the report?"
]
Con ambos pipelines listos, el bucle siguiente envía cada consulta por los dos sistemas y recoge los resultados. El corte [:500] en cada respuesta mantiene la salida legible por consola; las respuestas completas se guardan en results para revisarlas después.
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)
Comparativa de resultados
Este es el tipo de resultado que verás en los tres tipos de consulta:
|
Consulta |
PageIndex |
RAG vectorial |
Respuesta correcta |
Ganador |
|
Riesgo más relevante a corto plazo |
Respuesta amplia que cubre múltiples riesgos con citas de secciones |
Encontró el dato específico del 72% de la encuesta (más preciso en esta) |
Inflación persistente y política monetaria más restrictiva, citadas por el 72% de los encuestados (recuadro 5.1) |
Empate, ligera ventaja para RAG vectorial por la cifra |
|
Metodología de riesgo climático (referencia cruzada) |
Recuperó la sección, pero seleccionó el nodo padre en lugar del recuadro 5.2, así que la metodología no llegó a la generación |
Hizo chunk justo a través del recuadro 5.2 y devolvió los pasos de la metodología |
El recuadro 5.2 describe traducir riesgos físicos y de transición a exposiciones financieras mediante análisis de escenarios |
RAG vectorial |
|
Interacción valoraciones + apalancamiento |
Recuperó 18 nodos de las secciones 1 y 3 y explicó el mecanismo de amplificación |
Conectó ambas secciones y explicó el mecanismo de amplificación |
Valoraciones elevadas aumentan el riesgo de caídas bruscas; el apalancamiento amplifica pérdidas cuando afectan a instituciones apalancadas (secciones 1 y 3) |
Empate |
Cómo interpretar los resultados
Los resultados son más sutiles que una victoria clara de uno u otro sistema, y por eso son más útiles.
PageIndex llegó a las secciones correctas en los tres casos. En la consulta factual directa, ambos respondieron bien, aunque el RAG vectorial sacó el 72% porque su troceado plano capturó el recuadro 5.1 entero: más suerte de chunking que ventaja estructural. En la consulta multi-sección, PageIndex recuperó 18 nodos de las secciones 1 y 3 y explicó la interacción, quedando a la par del RAG vectorial. La única derrota clara fue la consulta con referencia cruzada, y conviene entenderla.
En la consulta de metodología climática, PageIndex razonó hasta la sección 5 y el resumen, pero la metodología está en el recuadro 5.2, un nodo hijo bajo la sección 5. La búsqueda en árbol seleccionó la sección padre, no el recuadro, y el texto de la sección hace referencia al recuadro sin reproducirlo, así que la fase de generación nunca vio la metodología. RAG vectorial ganó por trocear directo a través del recuadro 5.2.
Esto es un problema de granularidad de recuperación, no de razonamiento: seleccionar un nodo padre no arrastra el texto de sus hijos a menos que amplíes la selección para incluirlos. Puedes cerrar la brecha pidiendo en el prompt que la búsqueda en árbol devuelva los nodos hijo de cualquier sección relevante o ampliando cada nodo seleccionado para incluir sus descendientes antes de generar.
La ventaja de PageIndex se ve más claramente en la ejecución independiente anterior (consulta de valoraciones de activos), donde razonó bien sobre la estructura y devolvió respuestas bien citadas y atribuidas por sección. Las consultas comparativas de arriba se eligieron para estresar escenarios de recuperación más complejos, por eso favorecen al RAG vectorial en un documento de 60 páginas.
Conclusión honesta: la ventaja de PageIndex sobre el RAG vectorial se acentúa con la longitud del documento y la densidad de referencias internas. En un informe de 60 páginas, la brecha es moderada. En un 10-K de 200 páginas con docenas de notas al pie referenciándose, espera algo mayor.
Si quieres exprimir más el baseline vectorial antes de descartarlo, How to Improve RAG Performance cubre las cinco técnicas con mayor impacto.
Cuándo usar PageIndex
La comparación anterior es específica de un tipo y longitud de documento. Que PageIndex sea la opción adecuada depende de tres factores: estructura del documento, tipo de consulta y volumen.
Cuándo gana PageIndex
PageIndex compensa su sobrecarga en documentos profesionales largos y estructurados donde equivocarse sale caro:
- 10-Ks y otros estados financieros
- Contratos legales con términos definidos y referencias cruzadas
- Guías regulatorias
- Manuales técnicos con secciones numeradas
También son los casos donde los rastros de razonamiento te dan algo concreto que auditar. El punto óptimo es bajo volumen y alta criticidad: unos pocos documentos al día donde cada consulta debe ser correcta y merece la pena la prima de latencia y coste.
Cuándo es mejor el RAG vectorial
RAG vectorial es la mejor opción por defecto para cargas de alto rendimiento y baja latencia, donde las múltiples llamadas al LLM por consulta de PageIndex suponen un cuello de botella y los embeddings en caché aguantan la carga por una fracción del coste.
También encaja con documentos planos con poca jerarquía sobre la que razonar, como:
- Artículos de noticias
- Descripciones de producto
- Tickets de soporte
También es la opción adecuada cuando bastan respuestas aproximadas. Un chatbot de soporte que acierta la FAQ correcta nueve de cada diez veces probablemente cumple.
Los trade-offs reales
El coste está en la velocidad y el dinero. Cada consulta en PageIndex hace al menos dos llamadas al LLM (búsqueda en árbol y generación de respuesta) más el procesamiento inicial, así que espera de tres a ocho segundos por consulta en un documento de 60 páginas, frente a menos de un segundo con RAG vectorial y un índice FAISS en caché; la factura por consulta escala en la misma dirección.
El Financial Stability Report es una prueba justa para PageIndex: estructurado, jerárquico y con referencias cruzadas. Un corpus de miles de tickets cortos no lo es, y ahí RAG vectorial ganará a una fracción del coste.
Reflexiones finales
El RAG vectorial lleva una asunción oculta: que el pasaje más similar a tu consulta es también el que contiene la respuesta. En documentos cortos y poco estructurados, suele cumplirse. En documentos profesionales largos y estructurados, se rompe de formas previsibles: tablas partidas, frecuencia de términos que confunde y referencias cruzadas invisibles.
PageIndex aborda las tres al tratar la recuperación como un problema de razonamiento sobre la estructura del documento, en lugar de una búsqueda por similitud sobre fragmentos de texto.
La regla práctica es sencilla: si tus documentos tienen jerarquía, tus consultas requieren seguir referencias internas y prefieres acertar a ir rápido, PageIndex compensa la latencia y el coste extra. Si haces búsquedas a gran escala sobre documentos cortos o planos, RAG vectorial sigue siendo el estándar adecuado.
¿Quieres profundizar en RAG en producción y sistemas agénticos? Nuestro itinerario de AI Engineering with LangChain te lleva desde los fundamentos de aplicación hasta la recuperación, la evaluación y agentes con herramientas.
Preguntas frecuentes sobre PageIndex
¿Qué es PageIndex?
PageIndex es un framework RAG de código abierto de VectifyAI (septiembre de 2025) que sustituye la búsqueda por similitud vectorial por razonamiento de LLM sobre un índice jerárquico en árbol: sin embeddings, sin base de datos vectorial, sin chunking.
¿En qué se diferencia PageIndex del RAG estándar?
El RAG estándar trocea un documento en fragmentos, los convierte en embeddings y recupera los chunks más similares a la consulta. PageIndex construye un árbol a partir de la estructura natural del documento y pide a un LLM que razone sobre qué secciones probablemente contienen la respuesta. La recuperación es un problema de razonamiento, no de similitud.
¿PageIndex requiere una clave de API de OpenAI?
No necesariamente. El repositorio open source autoalojado funciona con cualquier proveedor compatible con LiteLLM. Este tutorial usa OpenAI para el razonamiento de recuperación y la generación de respuestas; si lo sigues tal cual, necesitarás una clave de OpenAI y otra de PageIndex desde dash.pageindex.ai/api-keys.
¿PageIndex es bueno para todo tipo de documentos?
No. PageIndex brilla en documentos largos y estructurados con jerarquía y referencias cruzadas. Para grandes corpus de textos cortos y poco estructurados, como tickets de soporte o páginas de FAQs, el RAG vectorial será más rápido y barato.
¿Qué precisión logró PageIndex en FinanceBench?
Mafin 2.5, el sistema de VectifyAI basado en PageIndex, logró un 98,7%, frente a aproximadamente 30–50% de los RAG tradicionales basados en vectores. FinanceBench evalúa preguntas financieras sobre informes de la SEC, que requieren razonamiento multi-paso y recuperación numérica exacta. Justo esa brecha es la que PageIndex busca cerrar.
Josep es Científico de Datos y Gestor de Proyectos en la Agencia Catalana de Turismo, utilizando datos para mejorar la experiencia de los turistas en Cataluña. Su experiencia incluye la gestión del almacenamiento y procesamiento de datos, junto con la analítica avanzada y la comunicación eficaz de las perspectivas de los datos.
También es un dedicado educador, que imparte clases en el Máster de Big Data de la Universidad de Navarra, y contribuye regularmente con artículos perspicaces sobre ciencia de datos en Medium y KDNuggets.
Es Licenciado en Ingeniería Física por la Universidad Politécnica de Cataluña y Máster en Sistemas Interactivos Inteligentes por la Universidad Pompeu Fabra.
En la actualidad, se dedica con pasión a hacer que las tecnologías relacionadas con los datos sean más accesibles a un público más amplio a través de la publicación de Medium ForCode'Sake.


