Inicio / Artículos

RAG sin vectores con PageIndex: recuperación mediante razonamiento en lugar de vectores

PageIndex intercambia los embeddings y el particionamiento para un LLM que recorre un árbol de documentos. Un breve ejemplo en Python, y los casos en los que RAG vectorial sigue siendo la mejor opción.

Editor: REACTAPP.TOP9 min de lectura2090 palabras

Fuentes: **I Stopped Using Vector Databases for RAG : PageIndex Vectorless RAG**

Cualquiera que haya desarrollado sistemas RAG durante algún tiempo conoce esa frustración. Pasas horas trabajando en los embeddings, ajustando los tamaños de los fragmentos, eligiendo una base de datos vectorial, y el sistema sigue dando la respuesta incorrecta. Encuentra algo similar, pero “similar” y “relevante” no son lo mismo.

Los informes financieros lo ilustran claramente. Toma un documento 10-K de 120 páginas: una configuración RAG vectorial sigue extrayendo fragmentos del resumen ejecutivo, mientras que la respuesta real se encuentra en una nota al pie de la página 87. Ambos pasajes comparten palabras clave y significado, pero solo uno contiene el número que necesitas.

PageIndex fue creado precisamente para abordar ese problema.

Qué es PageIndex

PageIndex es un framework RAG de código abierto del equipo de VectifyAI. Su idea central es sencilla y diferente a la mayoría de las soluciones existentes. En lugar de convertir un documento en vectores y realizar una búsqueda de vecino más cercano, construye un árbol jerárquico a partir del documento y permite que un LLM encuentre la respuesta por sí mismo.

Omite por completo los embeddings, los almacenes de vectores y el procesamiento en fragmentos.

El proyecto denomina esto “RAG sin vectores”, y esa denominación es bastante precisa. Apareció en septiembre de 2025 y atrajo rápidamente atención, sobre todo por una cifra presentada: un 98,7% de precisión en FinanceBench, frente a aproximadamente un 50% para los métodos RAG tradicionales con vectores en el mismo benchmark. Hay que tratar con cautela las cifras de rendimiento reportadas por los proveedores y probarlos con los propios documentos; pero si la diferencia se mantiene, aunque sea en parte, se trata de una categoría completamente distinta.

Donde falla el RAG con vectores

La suposición fundamental del RAG vectorial es que el texto semánticamente más similar a la consulta es también el más relevante. En la práctica, esa suposición falla constantemente.

Si se pregunta “¿Cuál fue la deuda total de la empresa en 2023?”, los fragmentos con mayor puntuación pueden ser:

  • la carta del CEO en la que se discute una estrategia de gestión de deudas,
  • una sección de riesgos sobre los acuerdos de deuda,
  • una entrada de glosario que define la deuda a largo plazo.

Todos los tres son similares en significado a la consulta, pero ninguno es la respuesta, que es una cifra específica del balance general en la página 64.

Se suman otros problemas:

  1. El fragmentado destruye el contexto. Dividir un documento de 100 páginas en fragmentos de 500 tokens elimina su estructura. Una oración como “como se menciona en la Tabla 3.2” queda separada de dicha tabla una vez que ambas terminan en fragmentos diferentes, y se ignora la lógica y el flujo del documento.
  2. Los vectores no pueden seguir referencias cruzadas. Los documentos indican “ver el Apéndice G” o “consultar las cifras del año anterior en la Sección 4”, pero la búsqueda por similitud no tiene forma de seguir esas indicaciones. Son simplemente fragmentos de texto sin conexión con sus objetivos.
  3. Las consultas expresan la intención, no el contenido. El usuario describe lo que quiere saber y no cómo debe ser la respuesta. La respuesta puede utilizar un vocabulario diferente, y la recuperación vectorial tiene dificultades porque compara el texto de la pregunta con el texto de la respuesta.

Recuperación mediante razonamiento

El cambio en la forma de pensar es el siguiente: en lugar de preguntarse qué fragmentos se parecen a la consulta, PageIndex busca dónde un experto humano buscaría en este documento para responderla. Se trata de una estrategia de recuperación diferente que se adapta mucho mejor a documentos estructurados y complejos.

Cómo funciona

Hay dos fases.

Fase 1: crear el índice en forma de árbol. Dado un documento, PageIndex no lo divide en fragmentos. Lo lee y construye una jerarquía que refleja la estructura natural del documento, similar a un índice inteligente y profundamente anidado. Cada nodo representa una sección o subsección, con un título y un resumen de su contenido. El LLM escribe esos resúmenes durante el proceso de indexación, y más tarde se convierten en la señal de recuperación.

En el caso de un informe 10-K, el árbol podría verse así:

Annual Report 2023
├── Business Overview
│   ├── Products and Services
│   └── Market Position
├── Risk Factors
│   ├── Financial Risks
│   └── Operational Risks
├── Financial Statements
│   ├── Balance Sheet
│   │   ├── Assets
│   │   └── Liabilities
│   └── Income Statement
└── Notes to Financial Statements
    ├── Note 1: Accounting Policies
    └── Note 12: Long-term Debt

El documento ya no es plano. Tiene estructura, y el LLM puede navegar por él de la misma manera que una persona hojea un libro.

Fase 2: búsqueda en árbol mediante razonamiento. Cuando llega una consulta, nada se envía a una base de datos vectorial. El LLM recibe la consulta y el nivel más alto del árbol, y se le pide que razone sobre dónde podría encontrarse la respuesta. Puede concluir que una pregunta sobre deuda a largo plazo pertenece a la sección de Estados Financieros, probablemente a las notas, y devolver un ID de nodo.

PageIndex luego expande ese nodo, mostrando el siguiente nivel de detalle, y el LLM sigue explorando hasta llegar al contenido que responde a la pregunta.

Un analista humano trabaja de la misma manera. Nadie lee un informe de 200 páginas de principio a fin. Se revisan los índices, se encuentra la sección adecuada y se va directamente allí. PageIndex hace eso a velocidad de máquina.

Excepto en la última recuperación de contenido, cada etapa corresponde al razonamiento del modelo. No hay coincidencias mediante vectores; el LLM determina dónde buscar.

Una pequeña implementación

La explicación a continuación crea una versión simplificada de esta idea en Python, utilizando a Gemini como LLM.

Paso 1: analizar el documento

Comience extrayendo texto en bruto página por página. PyMuPDF maneja los PDF de manera eficaz.

import fitz  # pip install pymupdf

def parse_pdf(pdf_path: str) -> list[dict]:
    doc = fitz.open(pdf_path)
    pages = []
    for i, page in enumerate(doc):
        text = page.get_text().strip()
        if text:
            pages.append({"page_num": i + 1, "text": text})
    doc.close()
    return pages

Luego, agrupe las páginas consecutivas en secciones. A diferencia del particionamiento arbitrario, esto respeta los límites de las páginas y mantiene junto al contenido relacionado.

def group_pages_into_sections(pages, per_section=3):
    sections = []
    for i in range(0, len(pages), per_section):
        batch = pages[i : i + per_section]
        section_id = f"S{str(i // per_section + 1).zfill(3)}"
        combined_text = "\n\n".join(p["text"] for p in batch)
        sections.append({
            "section_id": section_id,
            "start_page": batch[0]["page_num"],
            "end_page": batch[-1]["page_num"],
            "text": combined_text,
        })
    return sections

Paso 2: crear el índice en forma de árbol con un LLM

Aquí reside el núcleo del método. Se omiten los embeddings, y el modelo lee un resumen preliminar de cada sección y escribe un resumen estructurado. El resultado es un árbol JSON anidado, donde cada nodo contiene un título, un breve resumen y una lista de temas clave.

from google import genai
from google.genai import types

client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
def index_section(section: dict) -> dict:
    preview = section["text"][:1500]
    prompt = f"""Read this section from a document and summarize it.
Section pages: {section['start_page']} to {section['end_page']}
Text:
{preview}
Respond with ONLY valid JSON:
{{
  "title": "short descriptive title (5-8 words)",
  "summary": "2-3 sentence summary of what this section covers",
  "key_topics": ["topic1", "topic2", "topic3"]
}}"""
    response = client.models.generate_content(
        model="gemini-2.0-flash",
        contents=prompt,
        config=types.GenerateContentConfig(temperature=0.0),
    )
    parsed = json.loads(response.text.strip())
    return {
        "node_id": section["section_id"],
        "title": parsed["title"],
        "pages": f"{section['start_page']}-{section['end_page']}",
        "summary": parsed["summary"],
        "key_topics": parsed["key_topics"],
    }

Ejecútelo en todas las secciones y el árbol completo se guardará como un archivo JSON.

def build_tree_index(sections):
    nodes = [index_section(s) for s in sections]
    return {
        "title": "Your Document Title",
        "total_sections": len(nodes),
        "nodes": nodes,
    }
# Save for reuse
with open("tree.json", "w") as f:
    json.dump(tree, f, indent=2)

Paso 3: búsqueda en árbol impulsada por el razonamiento

En este punto, el enfoque se separa por completo del RAG vectorial. Llega una pregunta y, en lugar de buscar en una base de datos, se le proporciona al LLM todo el árbol y se le pregunta qué secciones tienen más probabilidades de contener la respuesta. Se comporta como un analista que examina un índice.

def retrieve_sections(tree: dict, query: str) -> dict:
    # Build a compact text representation of the tree
    tree_text = f"Document: {tree['title']}\n\n"
    for node in tree["nodes"]:
        tree_text += f"[{node['node_id']}] Pages {node['pages']} | {node['title']}\n"
        tree_text += f"  Summary: {node['summary']}\n"
        tree_text += f"  Topics: {', '.join(node['key_topics'])}\n\n"
        prompt = f"""You are a document retrieval expert.
        Given this document tree, identify which sections most likely answer the question.
        Think step by step about where a human expert would look.
        {tree_text}
        QUESTION: {query}
        Respond with ONLY valid JSON:
        {{
          "reasoning": "your step-by-step reasoning about where to look",
          "selected_ids": ["S001", "S004"],
          "confidence": "high/medium/low"
        }}"""
            response = client.models.generate_content(
                model="gemini-2.0-flash",
                contents=prompt,
                config=types.GenerateContentConfig(temperature=0.0),
            )
            return json.loads(response.text.strip())

El campo de razonamiento en la respuesta no es solo una decoración. Registra por qué el modelo seleccionó esas secciones, y eso es lo que significa la explicabilidad en este contexto. Puedes examinar la decisión de recuperación, algo que la búsqueda vectorial no puede ofrecer.

Paso 4: obtener el contenido

Con los IDs de las secciones elegidas en mano, obtén el texto sin procesar de esas secciones. Sin búsquedas vectoriales ni cálculos de embeddings, solo una búsqueda en un diccionario de las secciones que ya has analizado.

def retrieve_content(selected_ids: list, sections: list) -> str:
    section_map = {s["section_id"]: s for s in sections}
    context_parts = []
    for sid in selected_ids:
            if sid in section_map:
                sec = section_map[sid]
                context_parts.append(
                    f"--- Pages {sec['start_page']}-{sec['end_page']} ---\n"
                    + sec["text"][:3000]
                )
        return "\n\n".join(context_parts)

Paso 5: generar la respuesta

El texto recuperado se envía a Gemini junto con la pregunta original, y el modelo elabora una respuesta precisa con citas a nivel de página.

def generate_answer(query: str, context: str) -> str:
    prompt = f"""Answer the question using only the provided context.
Be specific. Include exact numbers, technical terms, and cite page numbers.CONTEXT:
{context}
QUESTION: {query}
ANSWER:"""
    response = client.models.generate_content(
        model="gemini-2.0-flash",
        contents=prompt,
        config=types.GenerateContentConfig(temperature=0.1),
    )
    return response.text.strip()

PageIndex frente a vector RAG

Los dos enfoques difieren en varios aspectos. Vector RAG es rápido, se escala en grandes colecciones y necesita embeddings e un índice. PageIndex no necesita nada de eso, pero realiza varias llamadas secuenciales al LLM por consulta, y a cambio ofrece mayor precisión en documentos estructurados y un rastro completo de razonamiento.

Cuándo elegir PageIndex

No es un reemplazo universal para vector RAG. Es una herramienta diferente para una tarea distinta.

Utilice PageIndex cuando:

  • Trabaja con un único documento largo y estructurado donde la precisión es más importante que la velocidad: documentos financieros, contratos legales, documentos regulatorios, manuales técnicos, artículos académicos. Cualquier cosa con una jerarquía significativa donde “lo suficientemente cercano” no es suficiente.
  • Se necesita explicabilidad, ya que los usuarios o interesados deben ver con exactitud de dónde proviene una respuesta. Se obtiene un rastro de razonamiento con referencias a secciones y números de página.
  • Se diseña para sectores regulados como las finanzas, el derecho y la salud, donde una respuesta incorrecta tiene consecuencias reales y la precisión reportada es importante.

Manténgase con vector RAG cuando:

  • Busca en una gran colección y necesita determinar qué documentos son relevantes. Vector RAG destaca en esto, mientras que PageIndex se enfoca en un solo documento y no está diseñado para seleccionar el adecuado entre miles.
  • La capacidad de procesamiento y la latencia son importantes para usted. Realizar varias llamadas a LLM seguidas para cada consulta se vuelve costoso y lento a medida que aumenta el tráfico.
  • El material es breve, está organizado de forma poco estructurada o es conversacional. La ventaja radica en la navegación por jerarquías, y sin ellas esa ventaja desaparece.

Combínalos. Utiliza la búsqueda vectorial para encontrar el documento adecuado en tu colección y luego emplea PageIndex para obtener la respuesta precisa de su interior. Un enfoque híbrido como este aprovecha lo mejor de ambos métodos.

La idea principal

Lo que realmente hace PageIndex es tratar la recuperación de información como un problema de razonamiento en lugar de uno de similitud. Este cambio tiene importancia más allá de los documentos. Según algunos informes, Claude Code dejó de depender de la búsqueda por embeddings para encontrar código y ahora se basa en búsquedas impulsadas por el razonamiento. El patrón se vuelve cada vez más claro: cuando una tarea requiere comprender la estructura y seguir la lógica, el razonamiento supera a la similitud.

Las bases de datos vectoriales no desaparecerán. Siguen siendo rápidas, escalables e ideales para la búsqueda semántica en grandes colecciones. Pero para el difícil problema de encontrar la información exacta dentro de un documento complejo, el enfoque basado en el razonamiento parece ser la opción más sólida.

PageIndex se encuentra entre las mejores implementaciones de esa idea en la actualidad. Es de código abierto, tiene una API limpia, y el proyecto publica un servicio en la nube y un servidor MCP para integrarlo en Claude u otros flujos de trabajo agentes sin tener que escribir código de recuperación desde cero. Los ejemplos oficiales del manual de uso son un buen punto de partida y permiten poner en marcha un sistema funcional en aproximadamente media hora. Si sus usuarios hacen preguntas precisas sobre informes de 200 páginas, vale la pena probarlo.

Leer más

  • Dentro de las bases de datos vectoriales para RAG: Almacenamiento en trozos, HNSW, IVF y reclasificación — Cómo las bases de datos vectoriales de RAG almacenan los trozos, buscan millones de vectores con HNSW e IVF, añaden búsqueda híbrida y reclasificación, y comparan medidas de similitud.
  • Búsqueda híbrida para RAG: BM25, fusión de rangos y reclasificación superan a los métodos vectoriales — Por qué la búsqueda puramente vectorial no encuentra términos exactos en RAG, y cómo BM25 combinado con búsqueda semántica, fusión de rangos recíprocos y un reclasificador con codificador cruzado aumentó la precisión de la búsqueda al 91%.