Vectorless RAG з PageIndex: пошук за допомогою міркувань замість векторів
PageIndex замінює ембеддинги та чанкінг на технологію для LLM, яка працює з деревом документа. Короткий приклад на Python та ситуації, коли векторний RAG все ще є кращим вибором.
Джерела: **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.
До цього додаються ще й інші проблеми:
- Розділення на частини руйнує контекст. Розбиття 100-сторінкового документа на фрагменти по 500 токенів призводить до втрати його структури. Речення на кшталт «як зазначено в таблиці 3.2» втрачає зв’язок із цією таблицею, коли вони опиняються в різних частинах, а логіка та послідовність документа ігноруються.
- Вектори не можуть слідувати за переліками посилань. У документах часто зустрічаються вирази на кшталт «див. Додаток G» або «дивіться показники попереднього року у розділі 4», проте алгоритми пошуку за схожістю не мають засобів для відстеження цих посилань. Це просто фрагменти тексту без зв’язку з їхніми цілями.
- Запити виражають намір, а не зміст. Користувач описує, що хоче дізнатися, а не як має виглядати відповідь. Відповідь може містити інший словниковий запас, і алгоритми пошуку за векторами мають труднощі, оскільки порівнюють текст запиту з текстом відповіді.
Пошук за принципом міркування
Зміна підходу полягає у наступному. Замість того, щоб шукати фрагменти, схожі на запит, 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 робить це зі швидкістю комп’ютера.
Окрім останнього завантаження контенту, кожен етап — це процес міркувань моделі. Жоден елемент не знаходиться за допомогою векторів; LLM сам визначає, де шукати.
Проста реалізація
У наступному описі показана спрощена версія цієї ідеї на Python, де в якості LLM використовується 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: ембеддинги, векторні бази даних та чанкінг — Простежте один фрагмент тексту від токенізації через об’єднання даних до пошуку за косинусовою схожістю та дізнайтеся, чому саме чанкінг та моделі ембеддингів визначають якість RAG.
- PageIndex: RAG без векторних баз даних, заснований на міркуваннях та з дерев’яним індексом — PageIndex замінює векторні бази даних на дерев’яний індекс, який керується LLM. Дізнайтеся, як це працює, подивіться приклади коду на Python, результати тестування та коли варто його використовувати.