Главная / Статьи

RAG без векторов с PageIndex: поиск по логике вместо векторов

PageIndex заменяет использование эмбеддингов и чанкинга на подход для LLM, который обрабатывает дерево документа. Краткий пример на Python и случаи, когда векторный RAG по-прежнему является лучшим выбором.

Издатель: REACTAPP.TOP9 мин чтения2090 слов

Источники: **I Stopped Using Vector Databases for RAG : PageIndex Vectorless RAG**

Каждый, кто уже некоторое время работает над системами RAG, знает эту раздражающую проблему. Вы тратите часы на подготовку эмбеддингов, настройку размеров фрагментов текста, выбор векторной базы данных, но система всё равно дает неверный ответ. Она находит что-то похожее, однако «похожее» и «релевантное» — не одно и то же.

Финансовые отчёты наглядно показывают это. Возьмём 120-страничный отчёт типа 10-K. Система RAG продолжает брать фрагменты из резюме для руководства, в то время как настоящий ответ находится в примечании на странице 87. Оба фрагмента содержат схожие ключевые слова и смысл, но только в одном из них указано необходимое число.

PageIndex был создан именно для решения этой проблемы.

Что такое PageIndex

PageIndex — это фреймворк RAG с открытым исходным кодом от команды VectifyAI. Его основная идея проста и отличается от большинства существующих решений. Вместо преобразования документа в векторы и выполнения поиска по ближайшему соседу он создаёт иерархическое дерево на основе документа, позволяя большой языковой модели самостоятельно найти ответ.

Он полностью исключает использование эмбеддингов, хранилищ векторов и их разбиения на части.

Проект называет это «vectorless RAG», и такое обозначение довольно точно отражает суть. Он появился в сентябре 2025 года и быстро привлёк внимание, в основном благодаря одному указанному показателю: точность 98,7% на тестовой платформе FinanceBench по сравнению примерно с 50% у традиционных решений типа vector RAG на той же платформе. Необходимо с осторожностью относиться к показателям, сообщаемым производителями, и тестировать решение на собственных документах, но если этот разрыв сохраняется хотя бы частично, это совершенно другой уровень.

Где терпит неудачу vector RAG

Основное предположение векторного RAG заключается в том, что текст, наиболее семантически похожий на запрос, является также наиболее релевантным. На практике это предположение постоянно не справляется со своей задачей.

Если задать вопрос «Какова была общая сумма долга компании в 2023 году?», то фрагменты с наивысшими оценками могут быть следующими:

  • письмо генерального директора о стратегии управления долгом,
  • раздел о рисках, касающийся условий долговых обязательств,
  • статья в глоссарии, определяющая понятие долгосрочного долга.

Все три фрагмента близки по смыслу к запросу, но ни один из них не является ответом — а ответом должна быть конкретная цифра из баланса на странице 64.

К этим проблемам добавляются и другие.

  1. Разбиение на фрагменты разрушает контекст. Разделение 100-страничного документа на части по 500 токенов приводит к потере его структуры. Фраза вроде «как указано в таблице 3.2» теряет связь с этой таблицей, когда они оказываются в разных фрагментах, и логика и последовательность документа игнорируются.
  2. Векторы не могут следовать за переходами. В документах часто встречаются указания вроде «см. Приложение G» или «смотрите показатели прошлых лет в разделе 4», но алгоритмы поиска по сходству не имеют возможности следовать за этими указаниями. Это просто фрагменты текста без связи с их целями.
  3. Запросы выражают намерение, а не содержимое. Пользователь описывает, что хочет узнать, а не как будет выглядеть ответ. Ответ может использовать другую лексику, и алгоритмы поиска по векторам сталкиваются с трудностями, так как сопоставляют текст вопроса с текстом ответа.

Поиск с использованием логических рассуждений

Изменение в подходе заключается в следующем. Вместо того чтобы искать те фрагменты документа, которые похожи на запрос, PageIndex определяет, где человек-эксперт искал бы ответ в этом документе. Это другая стратегия поиска, которая гораздо лучше подходит для структурированных, сложных документов.

Как это работает

Существует два этапа.

Этап 1: создание деревянного индекса. Получив документ, PageIndex не делил его на фрагменты. Он читает его и создает иерархию, отражающую естественную структуру документа, подобную интеллектуальному, глубоко вложенному оглавлению. Каждый узел представляет собой раздел или подраздел с названием и кратким описанием его содержимого. Языковая модель генерирует эти описания во время создания индекса, и позже они становятся сигналами для поиска.

Для отчета формата 10-K дерево индекса может выглядеть так:

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

Документ больше не является плоским. У него есть структура, и ЯЗЫК может перемещаться по ней так же, как человек листает книгу.

Этап 2: поиск по дереву с использованием логических рассуждений. Когда поступает запрос, он не попадает в векторную базу данных. ЯЗЫК получает запрос и верхний уровень дерева, после чего ему предлагается разобраться, где может находиться ответ. Он может прийти к выводу, что вопрос о долгосрочном долге относится к разделу «Финансовая отчетность», вероятно, к примечаниям, и вернуть ID узла.

Затем PageIndex раскрывает этот узел, показывая следующий уровень деталей, и ЯЗЫК продолжает исследование до тех пор, пока не найдет содержимое, отвечающее на вопрос.

Человек-аналитик работает так же. Никто не читает 200-страничный отчет с начала до конца. Сначала проверяются содержание, находится соответствующий раздел, и переходят прямо туда. PageIndex делает это с машинной скоростью.

Помимо последнего получения контента, каждый этап представляет собой процесс рассуждения модели. Ничто не совпадает по вектору; большая языковая модель сама определяет, где искать.

Простая реализация

В следующем описании показана упрощённая версия этой идеи на Python, где в качестве большой языковой модели используется Gemini.

Шаг 1: парсинг документа

Начните с извлечения необработанного текста по страницам. PyMuPDF эффективно обрабатывает PDF-файлы.

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

Затем объедините последовательные страницы в разделы. В отличие от произвольного разбиения, это учитывает границы страниц и сохраняет связанный контент вместе.

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

Шаг 2: создание индекса в виде дерева с использованием LLM

Здесь находится суть метода. Этап генерации эмбеддингов опускается — модель читает краткий просмотр каждого раздела и формирует структурированный краткий обзор. В результате получается вложенное дерево в формате JSON, где каждый узел содержит заголовок, краткое описание и список ключевых тем.

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"],
    }

Выполните его для всех разделов, и полная структура будет сохранена в виде файла 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)

Шаг 3: поиск в дереве на основе логических рассуждений

На этом этапе подход полностью отличается от векторного RAG. Приходит вопрос, и вместо поиска в базе данных вы передаёте большую модель языка всё дерево и спрашиваете, какие его разделы с наибольшей вероятностью содержат ответ. Модель ведёт себя как аналитик, просматривающий оглавление.

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())

Поле обоснования в ответе — это не просто декоративный элемент. Оно фиксирует, почему модель выбрала именно эти разделы, и именно это и означает здесь объяснимость. Вы можете изучить решение о выборе материала, чего не может предложить векторный поиск.

Шаг 4: загрузка контента

Имея в руках идентификаторы выбранных разделов, загрузите их необработанный текст. Никаких векторных поисков и вычислений эмбеддингов — только поиск в словаре среди уже обработанных разделов.

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)

Шаг 5: генерация ответа

Загруженный текст передается в Gemini вместе с исходным вопросом, и модель формирует точный ответ с цитатами на уровне страницы.

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 против векторного RAG

Эти два подхода отличаются в нескольких аспектах. Векторный RAG работает быстро, масштабируется на большие коллекции и требует использования эмбеддингов и индекса. PageIndex не нуждается ни в чем из этого, но выполняет несколько последовательных вызовов LLM на каждый запрос; в результате он обеспечивает более высокую точность при обработке структурированных документов и полную возможность отслеживания логики рассуждений.

Когда выбирать PageIndex

Это не универсальная замена векторному RAG — это разные инструменты для разных задач.

Используйте PageIndex, когда:

  • Вы работаете с одним длинным структурированным документом, где точность важнее скорости: финансовая отчетность, юридические контракты, нормативные документы, технические руководства, научные статьи. Любые документы с четкой иерархией, где «примерно достаточно» недопустимо.
  • Вам необходима объяснимость, поскольку пользователи или заинтересованные стороны должны точно знать, откуда взялся ответ. Вы получаете трекировку логики с ссылками на разделы и номерами страниц.
  • Вы разрабатываете решения для регулируемых секторов, таких как финансы, юриспруденция и здравоохранение, где неверный ответ имеет реальные последствия, и важна указанная точность.

Используйте vector RAG в следующих случаях:

  • Вы проводите поиск в большом наборе данных и должны определить, какие документы являются релевантными. Vector RAG отлично справляется с этой задачей, в то время как PageIndex анализирует только один документ и не предназначен для выбора правильного из тысячи.
  • Для вас важны пропускная способность и задержка. Множество вызовов LLM подряд для каждого запроса становится дорогостоящим и медленным при увеличении объема работы.
  • Материал краткий, слабо структурирован или имеет разговорный стиль. Сильная сторона метода — навигация по иерархии, и без нее преимущество исчезает.

Сочетайте их. Используйте векторный поиск, чтобы найти нужный документ в вашей коллекции, затем примените PageIndex, чтобы получить точный ответ изнутри него. Такой гибридный подход объединяет лучшие стороны обоих методов.

Более общая идея

PageIndex на самом деле рассматривает процесс поиска как задачу рассуждения, а не проблему сходства. Это изменение имеет значение не только для документов. Согласно отчетам, Claude Code перестал полагаться на поиск с использованием векторных представлений для поиска кода и теперь использует поиск, основанный на рассуждениях. Образец становится все более очевидным: когда задача требует понимания структуры и следования логике, рассуждения эффективнее сходства.

Базы данных векторов не исчезнут. Они остаются быстрыми, масштабируемыми и идеальными для семантического поиска в больших коллекциях. Однако для решения сложной задачи поиска точно нужной информации внутри сложного документа подход, основанный на логических рассуждениях, кажется более эффективным вариантом.

PageIndex — одна из лучших реализаций этой идеи в настоящее время. Это проект с открытым исходным кодом, имеющий чистый API; кроме того, проект предоставляет облачный сервис и сервер MCP, позволяющие интегрировать его в Claude или другие агентные рабочие процессы без необходимости писать код поиска с нуля. Примеры из официального руководства — хорошая отправная точка, которая позволяет запустить рабочую систему примерно за полчаса. Если ваши пользователи задают точные вопросы о 200-страничных отчетах, стоит попробовать этот инструмент.

Связанная литература

  • Внутреннее устройство векторных баз данных для RAG: хранение фрагментов, алгоритмы HNSW, IVF и процесс переранжирования — Как векторные базы данных RAG хранят фрагменты данных, выполняют поиск среди миллионов векторов с использованием алгоритмов HNSW и IVF, внедряют гибридные методы поиска и переранжирования, а также сравнивают методы измерения сходства.
  • Гибридные методы поиска для RAG: BM25, объединение рангов и процесс переранжирования превосходят векторные методы — Почему чистый векторный поиск упускает точные термины в системах RAG, и как использование BM25 в сочетании с семантическим поиском, методом взаимного объединения рангов и кросс-кодером для переранжирования позволило повысить точность поиска до 91%.