Strona główna / Artykuły

RAG bez wektorów z PageIndex: wyszukiwanie poprzez rozumowanie zamiast wektorów

PageIndex zamienia embeddingi i dzielenie na fragmenty w przypadku modeli językowych, które przemieszczają się po drzewie dokumentu. Krótki przykład w Pythonie oraz sytuacje, w których vector RAG nadal jest lepszym wyborem.

Wydawca: REACTAPP.TOP9 min czytania2090 słów

Źródła: **I Stopped Using Vector Databases for RAG : PageIndex Vectorless RAG**

Każdy, kto od jakiegoś czasu buduje systemy RAG, zna to irytujące doświadczenie. Spędzasz godziny na przygotowywaniu embeddingów, dostosowywaniu rozmiarów fragmentów tekstu, wyborze bazy danych wektorowej, a system nadal podaje błędną odpowiedź. Znajduje coś podobnego, choć „podobne” i „istotne” to nie to samo.

Raporty finansowe jasno to pokazują. Weźmy na przykład 120-stronicowy dokument typu 10-K. System RAG ciągle wybiera fragmenty z streszczenia zarządu, podczas gdy prawdziwa odpowiedź znajduje się w przypisie na stronie 87. Obie części tekstowe mają wspólne słowa kluczowe i znaczenie, ale tylko jedna zawiera potrzebną liczbę.

PageIndex został stworzony właśnie po to, by rozwiązać ten problem.

Czym jest PageIndex

PageIndex to otwarte oprogramowanie typu RAG stworzone przez zespół VectifyAI. Jego podstawowa idea jest prosta i różni się od większości dostępnych rozwiązań. Zamiast przekształcać dokumenty w wektory i wykonywać wyszukiwanie najbliższego sąsiada, tworzy hierarchiczną strukturę na podstawie dokumentu, a model językowy samodzielnie dochodzi do odpowiedzi.

Pomija on całkowicie embeddingi, magazyny wektorów oraz dzielenie dokumentów na fragmenty.

Projekt nazywa to „vectorless RAG”, a ta nazwa jest dość dokładna. Pojawił się we wrześniu 2025 roku i szybko przyciągnął uwagę, głównie ze względu na jeden podany wynik: 98,7% dokładności na platformie FinanceBench, w porównaniu z około 50% dla tradycyjnego rozwiązania typu vector RAG na tej samej platformie. Trzeba podchodzić z ostrożnością do danych o dokładności podawanych przez producentów i testować rozwiązanie na własnych dokumentach, ale jeśli ta różnica utrzymuje się choć częściowo, oznacza to zupełnie inny poziom jakości.

Gdzie zawodzi rozwiązanie typu vector RAG

Podstawowym założeniem vector RAG jest to, że tekst najbardziej semantycznie podobny do zapytania jest również najbardziej istotny. W praktyce to założenie ciągle się nie sprawdza.

Zadaj pytanie „Jaka była łączna zadłużenie firmy w 2023 roku?”, a fragmenty o najwyższej ocenie mogą to być:

  • list prezesów firmy omawiający strategię zarządzania zadłużeniem,
  • sekcja dotycząca ryzyka z zasadami dotyczącymi zadłużenia,
  • hasło w glosariuszu definiujące długoterminowe zadłużenie.

Wszystkie trzy są bliskie zapytaniu pod względem znaczenia, ale żadne z nich nie stanowi odpowiedzi, która to konkretna kwota z bilansu na stronie 64.

Pojawiają się jeszcze inne problemy:

  1. Dzielenie tekstu na fragmenty niszczy kontekst. Podział 100-stronicowego dokumentu na 500-elementowe części eliminuje jego strukturę. Zdanie takie jak „jak wspomniano w Tabeli 3.2” traci związek z tą tabelą, gdy oba trafiają do różnych fragmentów, a logika i ciągłość dokumentu są ignorowane.
  2. Wektory nie mogą śledzić odnośników. W dokumentach znajdują się informacje typu „zobacz Załącznik G” lub „patrz na dane z poprzedniego roku w Sekcji 4”, ale wyszukiwanie podobieństw nie ma sposobu, by śledzić te odnośniki. Są to jedynie fragmenty tekstu bez żadnego połączenia z docelowymi elementami.
  3. Zapytania wyrażają intencję, a nie treść. Użytkownik opisuje, co chce wiedzieć, a nie jak powinna wyglądać odpowiedź. Odpowiedź może używać innego słownictwa, co utrudnia proces wyszukiwania wektorowego, ponieważ porównuje tekst zapytania z tekstem odpowiedzi.

Wyszukiwanie poprzez rozumowanie

Zmiana w sposobie myślenia polega na tym. Zamiast pytać, które fragmenty przypominają zapytanie, PageIndex pyta, gdzie ekspert człowiek poszukałby odpowiedzi w tym dokumencie. To inna strategia wyszukiwania, która znacznie lepiej pasuje do zstrukturyzowanych, złożonych dokumentów.

Jak to działa

Istnieją dwie fazy.

Faza 1: tworzenie indeksu drzewa. Dla danego dokumentu PageIndex nie dzieli go na fragmenty. Czyta go i konstruuje hierarchię, która odzwierciedla naturalną strukturę dokumentu, podobną do inteligentnego, głęboko zagnieżdżonego spisu treści. Każdy węzeł reprezentuje sekcję lub podsekcję, zawierającą tytuł oraz streszczenie jej treści. LLM pisze te streszczenia podczas tworzenia indeksu, a one później stają się sygnałem do wyszukiwania.

Dla dokumentu typu 10-K indeks drzewa może wyglądać w ten sposób:

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

Dokument nie jest już płaski. Ma strukturę, a LLM może przeglądać go w taki sam sposób, w jaki osoba przewraca strony książki.

Faza 2: wyszukiwanie drzewem poprzez rozumowanie. Gdy przychodzi zapytanie, nic nie trafia do bazy danych wektorowej. LLM otrzymuje zapytanie oraz najwyższy poziom drzewa i jest proszony o rozważenie, gdzie może znajdować się odpowiedź. Może dojść do wniosku, że pytanie dotyczące długu długoterminowego należy do sekcji Sprawozdań Finansowych, prawdopodobnie do Uwag, i zwrócić identyfikator węzła.

Następnie PageIndex rozwija ten węzeł, pokazując kolejny poziom szczegółów, a LLM kontynuuje poszukiwania, aż dotrze do treści, która odpowiada na pytanie.

Człowiek-analizator działa w ten sam sposób. Nikt nie czyta 200-stronicowego raportu od początku do końca. Sprawdza się spis treści, znajduje odpowiednią sekcję i idzie prosto tam. PageIndex robi to z prędkością maszyny.

Oprócz ostatniego pobierania treści, każdy etap to rozumowanie modelu. Nic nie jest dopasowywane za pomocą wektorów; LLM sam decyduje, gdzie szukać.

Niewielka implementacja

Poniższy przegląd przedstawia uproszczoną wersję tej koncepcji w Pythonie, z Gemini jako modelem językowym.

Krok 1: analiza dokumentu

Najpierw należy wydobywać surowy tekst strona po stronie. PyMuPDF sprawnie obsługuje pliki 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

Następnie grupuj kolejne strony w sekcje. W odróżnieniu od dowolnego dzielenia, metoda ta uwzględnia granice stron i trzyma ze sobą powiązane treści.

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

Krok 2: tworzenie indeksu drzewa za pomocą LLM

Tutaj znajduje się sedno tej metody. Pomija się embeddingi, a model czyta fragment każdego rozdziału i tworzy ustrukturyzowany streszczenie. Wynikiem jest zagnieżdżone drzewo JSON, w którym każdy węzeł zawiera tytuł, krótkie streszczenie oraz listę kluczowych tematów.

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

Zastosuj to do wszystkich sekcji, a cały drzewo zostanie zapisane jako plik 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)

Krok 3: wyszukiwanie w drzewie oparte na rozumowaniu

W tym momencie podejście całkowicie odbiega od modelu RAG opartego na wektorach. Pojawia się pytanie i zamiast wyszukiwać w bazie danych, podaje się modelowi LLM całe drzewo i pyta się, które sekcje najprawdopodobniej zawierają odpowiedź. Działa ono jak analityk przeglądający spis treści.

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

Pole uzasadnienia w odpowiedzi służy czemuś więcej niż tylko dekoracji. Zapisuje ono powód, dla którego model wybrał te sekcje, co właśnie oznacza tu wyjaśnialność. Można w nim przejrzeć decyzję dotyczącą wyboru treści, czego nie może zapewnić wyszukiwanie wektorowe.

Krok 4: pobranie treści

Mając już ID wybranych sekcji, pobierz surowy tekst tych sekcji. Bez wyszukiwania wektorowego i bez obliczania embeddingów – tylko wyszukiwanie w słowniku sekcji, które już przetworzyłeś.

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)

Krok 5: generowanie odpowiedzi

Zdobyty tekst trafia do Gemini wraz z oryginalnym pytaniem, a model tworzy dokładną odpowiedź z odniesieniami na poziomie stron.

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 kontra vector RAG

Te dwa podejścia różnią się pod kilkoma względami. Vector RAG jest szybki, skaluje się na dużych zbiorkach danych i wymaga embeddingów oraz indeksu. PageIndex nie potrzebuje nic z tego, ale wykonuje kilka sekwencyjnych wywołań modeli LLM na zapytanie, co z kolei zapewnia wyższą dokładność przy dokumentach strukturalnych oraz pełny ślad rozumowania.

Kiedy wybrać PageIndex

To nie jest uniwersalna alternatywa dla vector RAG. To inny narzędzie do innego zadań.

Wybierz PageIndex, gdy:

  • Pracujesz z jednym długim, strukturalnym dokumentem, gdzie dokładność jest ważniejsza niż szybkość: dokumenty finansowe, umowy prawnicze, akty regulacyjne, podręczniki techniczne, prace naukowe. Wszystko to, co ma sensowną hierarchię, gdzie „dość bliskie” nie wystarcza.
  • Potrzebna jest wyjaśnialność, ponieważ użytkownicy lub interesariusze muszą dokładnie widzieć, skąd pochodzi odpowiedź. Otrzymuje się ślad rozumowania z odniesieniami do sekcji i numerami stron.
  • Buduje się rozwiązania dla sektorów regulowanych, takich jak finanse, prawo i opieka zdrowotna, gdzie błędna odpowiedź ma rzeczywiste konsekwencje, a podana dokładność ma znaczenie.

Wybierz vector RAG, gdy:

  • Przeszukujesz dużą kolekcję i musisz określić, które dokumenty są istotne. Vector RAG doskonale sobie z tym radzi, podczas gdy PageIndex skupia się na jednym dokumencie i nie jest przeznaczony do wyboru właściwego z tysiąca.
  • Dla ciebie ważne są przepustowość i opóźnienie. Kilka kolejnych wywołań modeli LLM na jedno zapytanie staje się kosztowne i powolne wraz ze wzrostem ruchu.
  • Materiał jest krótki, słabo zorganizowany lub ma charakter rozmowy. Silną stroną jest nawigacja w hierarchii, a bez niej przewaga znika.

Połącz je ze sobą. Użyj wyszukiwania wektorowego, aby znaleźć odpowiedni dokument w swojej kolekcji, a następnie zastosuj PageIndex, by uzyskać dokładną odpowiedź bezpośrednio z niego. Taki hybrydowy podejście łączy najlepsze cechy obu metod.

Ważniejsza koncepcja

PageIndex traktuje proces wyszukiwania jako problem rozumowania, a nie problem podobieństwa. Ta zmiana ma znaczenie nie tylko w przypadku dokumentów. Doniesienia wskazują, że Claude Code przestał polegać na wyszukiwaniu opartym na embeddingach do znajdowania kodu i teraz korzysta z metod opartych na rozumowaniu. Wzorzec staje się coraz wyraźniejszy: gdy zadanie wymaga zrozumienia struktury i śledzenia logiki, rozumowanie jest skuteczniejsze niż podobieństwo.

Bazy danych wektorowych nie znikną. Nadal są szybkie, skalowalne i idealne do wyszukiwania semantycznego w dużych zbiorach danych. Jednak w przypadku trudnego zadania znalezienia dokładnie odpowiednich informacji w złożonym dokumencie podejście oparte na rozumowaniu wydaje się lepszą opcją.

PageIndex należy do lepszych implementacji tej koncepcji obecnie. Jest otwarty kodowo, posiada przejrzystą API, a projekt oferuje usługę chmurową oraz serwer MCP umożliwiający integrację z Claude lub innymi systemami typu agentic bez konieczności pisania kodu wyszukiwania od zera. Przykłady z oficjalnego przewodnika stanowią rozsądny punkt wyjścia i pozwalają uruchomić działający system w około pół godziny. Jeśli twoi użytkownicy zadają precyzyjne pytania dotyczące 200-stronicowych raportów, warto to wypróbować.

Literatura pokrewna

  • Wnętrze baz danych wektorowych do RAG: przechowywanie fragmentów, HNSW, IVF i ponowna sortacja — Jak bazy danych wektorowych RAG przechowują fragmenty, wyszukują miliony wektorów za pomocą HNSW i IVF, dodają hybrydowe wyszukiwanie i ponowną sortację oraz porównują miary podobieństwa.
  • Hybrydowe wyszukiwanie w RAG: BM25, fuzja rankingu i ponowna sortacja przewyższają metody wektorowe — Dlaczego czyste wyszukiwanie wektorowe pomija dokładne terminy w RAG oraz jak BM25 w połączeniu z wyszukiwaniem semantycznym, fuzją wzajemnego rankingu i ponownym sorterem poprzez kodera krzyżowego zwiększyło dokładność wyszukiwania do 91%.