Ir al contenido principal

Tutorial de PageIndex: RAG basado en razonamiento sin vectores

Aprende a crear un sistema de preguntas y respuestas sobre documentos en Python con PageIndex y a compararlo, en un informe real de la Reserva Federal, con un RAG vectorial usando FAISS.
Actualizado 6 ago 2026  · 8 min leer

Explorar con IA

Abrir en ChatGPTAbrir en ClaudeAbrir en Perplexity

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

No

Gestiona tablas partidas

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.

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

Getting pageindex API key

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 queuedprocessingcompleted

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 el node_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 Ferrer's photo
Author
Josep Ferrer
LinkedIn
Twitter

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.

Temas

¡Aprende RAG con DataCamp!

Curso

Retrieval Augmented Generation (RAG) con LangChain

3 h
19.1K
Aprende novedosos métodos para integrar datos externos con LLM utilizando la generación aumentada por recuperación (RAG) con LangChain.
Ver detallesRight Arrow
Iniciar Curso
Ver másRight Arrow
Relacionado

Tutorial

RankGPT como Agente de Re-Ranking para RAG (Tutorial)

RankGPT es un método que utiliza LLMs como ChatGPT para reordenar los documentos recuperados en sistemas RAG, mejorando la calidad del resultado al priorizar la información más relevante.
Ryan Ong's photo

Ryan Ong

Tutorial

Tutorial FLAN-T5: Guía y puesta a punto

Una guía completa para afinar un modelo FLAN-T5 para una tarea de respuesta a preguntas utilizando la biblioteca de transformadores, y ejecutando la inferencia optmizada en un escenario del mundo real.
Zoumana Keita 's photo

Zoumana Keita

Tutorial

RAG Con Llama 3.1 8B, Ollama y Langchain: Tutorial

Aprende a crear una aplicación RAG con Llama 3.1 8B utilizando Ollama y Langchain, configurando el entorno, procesando documentos, creando incrustaciones e integrando un recuperador.
Ryan Ong's photo

Ryan Ong

Tutorial

Tutorial de pgvector: Integrar la búsqueda vectorial en PostgreSQL

Descubre cómo mejorar PostgreSQL con capacidades de búsqueda vectorial utilizando pgvector. Este tutorial te guía a través de la instalación, las operaciones básicas y la integración con herramientas de IA.
Moez Ali's photo

Moez Ali

Tutorial

Tutorial de Generación de nubes de palabras en Python

Aprende a realizar Análisis exploratorios de datos para el Procesamiento del lenguaje natural utilizando WordCloud en Python.
Duong Vu's photo

Duong Vu

Tutorial

Ajuste fino de GPT-3 mediante la API OpenAI y Python

Libere todo el potencial de GPT-3 mediante el ajuste fino. Aprenda a utilizar la API de OpenAI y Python para mejorar este modelo de red neuronal avanzado para su caso de uso específico.
Zoumana Keita 's photo

Zoumana Keita

Ver MásVer Más