Strona główna / Artykuły

Wnętrze baz danych wektorowych dla RAG: przechowywanie fragmentów, HNSW, IVF i ponowne rankowanie

Jak bazy danych wektorowych RAG przechowują fragmenty danych, wyszukują miliony wektorów za pomocą HNSW i IVF, dodają hybrydowe metody wyszukiwania i ponownego rankowania oraz porównują miary podobieństwa.

Wydawca: REACTAPP.TOP9 min czytania1968 słów

Źródła: Inside Vector Databases for RAG: From Chunk Storage to HNSW & IVF Search

System generacji wzbogaconej o wyszukiwanie musi odpowiedzieć na jedno z kluczowych pytań praktycznych: gdzie znajdują się fragmenty tekstu? Najczęstszą odpowiedzią, którą stosują większość systemów produkcyjnych, jest umieszczanie każdego fragmentu bezpośrednio w bazie danych wektorowej obok jego wektora embeddingu.

Jak wygląda zapisana rekord

W rozwiązaniach opartych na FAISS, Pinecone lub Weaviate każda rekord zawiera trzy elementy: wektor embeddingu, tekst fragmentu oraz pewne metadane.

{
        "embedding": [0.123, 0.456, ...],
        "text": "Transformer models are powerful...",
        "metadata": {
        "doc_id": "doc1",
        "page": 5
        }
}

Zachowanie razem fragmentu tekstu i jego wektora przynosi trzy korzyści. Szukanie jest szybkie, ponieważ tekst pasujący pojawia się w tej samej odpowiedzi. Architektura pozostaje prosta, bez konieczności korzystania z dodatkowego magazynu do wyszukiwania. Ponadto opóźnienie jest niskie, co ma znaczenie, gdy model językowy czeka na wynik w czasie rzeczywistym.

Proces wyodrębniania informacji odbywa się w pięciu krokach. Użytkownik zadaje pytanie, które jest przekształcane na wektor embedding, baza danych znajduje najbliższe przechowywane embeddingi, tekst pasującego fragmentu jest bezpośrednio zwracany, a model językowy pisze swoją odpowiedź na podstawie tego tekstu. Nie ma żadnych dodatkowych operacji wyszukiwania.

Dlaczego szukanie pozostaje szybkie

Powszechny opis w jednym zdaniu brzmi tak, że baza danych wektorowa znajduje podobne embeddingi przy użyciu podobieństwa kosinowego, iloczynu skalarnego lub odległości euklidesowej. To jest dokładne, ale pomija ciekawe aspekty inżynieryjne. Jak to możliwe, że przy milionach lub miliardach wektorów wynik pojawia się w ułamku sekundy?

Prostym podejściem jest siła bruta: porównuje się zapytanie z każdym przechowywanym wektorem. Daje to doskonałą dokładność, ale strasznie wolną prędkość. Milion wektorów oznacza milion porównań dla każdego zapytania, a to nie jest skalowalne.

Rzeczywiste systemy wykorzystują szukanie w przybliżeniu najbliższego sąsiada (ANN). Idea polega na najpierw zmniejszeniu przestrzeni poszukiwań, a następnie sprawdzeniu tylko małej podgrupy wektorów, akceptując niewielką stratę dokładności w zamian za znaczne przyspieszenie. SZAN jest realizowane za pomocą specjalistycznych struktur indeksowych. NAPRZYKŁAD FAISS dostarcza takie indeksy, a w nim dostępne są zarówno HNSW, jak i IVF.

HNSW – warstwowy graf

HNSW to skrót od Hierarchical Navigable Small World. Opiera się na grafach, a Pinecone i Weaviate używają go w swoich systemach. Każdy wektor staje się węzłem, a każdy węzeł łączy się z podobnymi sąsiadami. Poszukiwanie polega na przemierzaniu tego grafu.

Graf składa się z kilku warstw. Górna warstwa zawiera bardzo niewiele węzłów i dostarcza ogólny obraz, natomiast dolna warstwa przedstawia każdy węzeł w szczegółach. Dobrym przykładem jest porównanie autostrad z lokalnymi ulicami.

Jego budowa przebiega w następujący sposób: gdy wprowadza się embedding, przypisuje mu się losowy poziom, dzięki czemu większość węzłów znajduje się tylko na dolnej warstwie, a jedynie nieliczne docierają na górę. Na każdym poziomie węzeł łączy się ze swoimi najbliższymi sąsiadami, mierząc to za pomocą kosinusa, iloczynu skalarnego lub odległości euklidesowej. W rezultacie powstaje sieć, w której wektory bliskie siebie są ściśle połączone, natomiast te dalekie – jedynie luźno.

Poszukiwania rozpoczynają się od najwyższej warstwy. Zapytanie znajduje tam najlepiej pasujący węzeł, przechodzi do następnej warstwy od tego węzła i powtarza ten proces. Trwa to aż do najniższej warstwy. W górnych i środkowych warstwach poszukiwania są ściśle oparte na zasadzie maksymalizacji korzyści: przechodzi się do najlepszego sąsiada i zatrzymuje się, gdy nie ma już możliwości poprawy wyniku, po czym schodzi niżej. Najniższa warstwa ma charakter bardziej eksploracyjny. Kolejka priorytetowa kandydatów zaczyna się od węzła wejściowego, a poszukiwania stale dodają do niej odpowiednie wyniki i jednocześnie sprawdzają kilku sąsiadów.

W praktyce istotne są dwa parametry:

  • M to liczba połączeń na węzeł. Większa wartość M zwiększa dokładność odnalezienia, ale wymaga więcej pamięci i czasu na przetwarzanie.
  • efSearch określa, ilu kandydatów jest sprawdzanych podczas jednego zapytania. Wyższa wartość poprawia dokładność, ale powoduje wyższą opóźnioność.

Prościej mówiąc, M wpływa na jakość indeksu, natomiast efSearch poświęca dokładność na rzecz szybkości podczas wyszukiwania.

IVF, najpierw klasterowanie

IVF (odwrócony indeks plików) jest powszechny w FAISS i stosuje zupełnie inny podejście. Nie tworzy grafu – zamiast tego grupuje podobne wektory w klasterach i wyszukuje tylko te istotne grupy. Dobrym przykładem może być biblioteka: książki są sortowane według kategorii, więc nikt nie przegląda każdej półki.

Podczas dodawania danych algorytm klasterowania, taki jak k-means, dzieli wektory na N klastrów. Każdy klaster ma środek, a każdy wektor jest przypisywany do najbliższego z nich. Podczas wyszukiwania embedding jest porównywany ze wszystkimi środkami, wybierany jest najbliższy klaster, a następnie oceniane są wszystkie wektory w tym klasterze, aby uzyskać najlepsze wyniki.

Obie metody różnią się pod względem zużycia pamięci. IVF przechowuje mniej połączeń, więc zużywa mniej zasobów. HNSW przechowuje graf z wieloma krawędziami na każdy węzeł, więc zużywa więcej pamięci.

Gdzie czyste wyszukiwanie wektorowe zawodzi

Embeddingi odzwierciedlają znaczenie, kontekst oraz bliskość koncepcyjną. Są mniej skuteczne przy dokładnych słowach kluczowych, liczbach i identyfikatorach. Jeśli wyszukasz „kafka error code 504”, indeks wektorowy może zwrócić wyniki takie jak „kafka network issues” lub „timeouts in distributed systems”, pomijając dokument, który faktycznie zawiera informację o kodzie 504.

Systemy RAG w produkcji radzą sobie z tym za pomocą wyszukiwania hybrydowego, które łączy semantyczne wyszukiwanie (embeddingi w połączeniu z algorytmami typu ANN, takimi jak HNSW lub IVF) z wyszukiwaniem według słów kluczowych dla dokładnych dopasowań przy użyciu BM25. W typowej architekturze oba rodzaje wyszukiwania odbywają się na oddzielnych systemach, a ich wyniki są łączone i ponownie sortowane.

Nowocześniejsze bazy danych wektorowych obsługują wewnętrznie wyszukiwanie hybrydowe, więc nie jest potrzebny dodatkowy system. Weaviate i Pinecone obsługują je w sposób natywny. Szybko staje się to standardem w środowiskach produkcyjnych, ponieważ łączy zrozumienie znaczenia z dopasowywaniem dokładnych intencji.

Czy hybrydowe rozwiązanie oznacza przechowywanie wszystkiego dwa razy?

W przypadku rozwiązania zewnętrznego – tak: te same fragmenty danych znajdują się często zarówno w bazie danych wektorowych, jak i w silniku kluczów słów, takim jak Elasticsearch. Jest to celowe. Baza danych wektorowych przechowuje indeks HNSW lub IVF do porównywania podobieństwa semantycznego, natomiast silnik kluczów słów przechowuje indeks odwrotny do dokładnego dopasowywania. Każdy indeks jest tworzony do realizacji swojego celu, więc nie jest to redundancja.

Zespoły zazwyczaj utrzymują jedno źródło prawdy, np. bazę danych lub data lake, i synchronizują je z oboma systemami za pomocą pipeline’ów. Dzięki temu oba systemy pozostają spójne, a proces wyszukiwania pozostaje szybki i dokładny.

Ponowna klasyfikacja najlepszych wyników

Wielu inżynierów zakłada, że praca jest zakończona, gdy z bazy danych wektorowej otrzymano wyniki top-K. To nieprawda. Te wyniki są jedynie przybliżenie pod względem istotności, nie są idealnie uporządkowane, a drobne różnice w stopniu istotności mogą zostać przeoczone.

Weźmy zapytanie „kafka error 504 fix”. Wyniki top-K mogą zawierać informacje o „problemach z czasem oczekiwania w Kafka”, następnie o „błędach w systemach rozproszonych”, a potem o „kodzie błędu 504 gateway timeout”. Trzeci wynik powinien być oczywiście pierwszy, ale wyszukiwanie wektorowe nie zawsze to zapewnia.

Ponowna klasyfikacja polega na ponownym uporządkowaniu uzyskanych wyników na podstawie głębszej analizy zapytania. Bazy danych wektorowych zazwyczaj same tego nie robią, więc ta logika znajduje się w warstwie aplikacji. Dominują dwa podejścia:

  1. Ponowna klasyfikacja oparta na LLM. Wysyłaj zapytanie oraz fragmenty tekstu do modelu LLM i poproś go o ustalenie ich kolejności według stopnia istotności. Jest to proste w implementacji, a model dobrze rozumie kontekst. Wadą jest wolniejsza praca ze względu na dodatkową próbę obliczeń oraz wyższe koszty w skali większych zbiórów danych.
  2. Kros-encoder. Jest to standard dla wysokiej jakości ponownej klasyfikacji. Model przyjmuje zapytanie wraz z jednym fragmentem tekstu jako parę i wyprowadza wynik dotyczący stopnia istotności. Proces ten powtarza się dla wszystkich kandydatów z listy top-K, a następnie dokonuje się sortowania według tych wyników. Metoda ta jest deterministyczna i znacznie dokładniejsza.

Przykład kompaktowego kros-encodera z użyciem LangChain:

"""Super-simple CrossEncoder reranking example.

Steps:
1. Define a query and a few short documents.
2. Build (query, doc) pairs.
3. Use a CrossEncoder to get a relevance score for each pair.
4. Print raw scores, then print documents sorted by score.
"""

from sentence_transformers import CrossEncoder


def main() -> None:
        # 1. Create model
model_name = "cross-encoder/ms-marco-MiniLM-L-6-v2"
print(f"Loading CrossEncoder model: {model_name}\n")
model = CrossEncoder(model_name)

    # 2. Query and documents
        query = "What is a vector database?"
documents = [
        "A vector database stores embeddings and allows similarity search.",
        "Relational databases store structured data in tables.",
        "FAISS is a library for efficient similarity search of vectors.",
        "Vector databases are used in AI applications like RAG.",
        ]

        # 3. Build (query, doc) pairs
pairs = [(query, doc) for doc in documents]

        # 4. Get scores
scores = model.predict(pairs)

print("Query:\n  " + query + "\n")
print("Raw scores (higher = more relevant):")
    for doc, score in zip(documents, scores):
print(f"  score={score:.4f}  |  doc={doc}")

    # 5. Sort by score (descending)
ranked = sorted(zip(documents, scores), key=lambda x: x[1], reverse=True)

print("\nDocuments sorted by cross-encoder score:\n")
    for rank, (doc, score) in enumerate(ranked, start=1):
print(f"Rank {rank}: score={score:.4f}")
print(f"  {doc}\n")


if __name__ == "__main__":
main()

Output
****************************************************
Query:
  What is a vector database?

Raw scores (higher = more relevant):
  score=8.4591  |  doc=A vector database stores embeddings and allows similarity search.
  score=-7.1590  |  doc=Relational databases store structured data in tables.
  score=1.0106  |  doc=FAISS is a library for efficient similarity search of vectors.
  score=7.0736  |  doc=Vector databases are used in AI applications like RAG.

Documents sorted by cross-encoder score:

Rank 1: score=8.4591
  A vector database stores embeddings and allows similarity search.

Rank 2: score=7.0736
  Vector databases are used in AI applications like RAG.

Rank 3: score=1.0106
  FAISS is a library for efficient similarity search of vectors.

Rank 4: score=-7.1590
  Relational databases store structured data in tables.

Mierzenie podobieństwa

Jakość wyszukiwania w dużej mierze zależy od sposobu obliczania bliskości wektorów. Trzy metryki obejmują praktycznie wszystko.

Podobieństwo kosinowe bierze pod uwagę kąt pomiędzy dwoma wektorami, innymi słowy tylko kierunek. Dwie strzały skierowane w tym samym kierunku są bardzo podobne, natomiast przeciwne strzały są zupełnie różne, niezależnie od długości. Wzór brzmi cos(θ) = A·B / (‖A‖‖B‖). Dla zapytania „Błąd Kafka 504” dokument o „obsłudze błędów w Kafka” uzyska wysoką ocenę (około 0,9), natomiast dokument o „indeksowaniu bazy danych” niską (około 0,2). Metoda ta nadaje się do embeddingów i jest standardowym wyborem w RAG, ponieważ ważne jest znaczenie, a nie wielkość. Spróbuj zapytania = [1, 1], docA = [2, 2] oraz docB = [1, 0]: docA skierowany jest w tym samym kierunku co zapytanie, więc jego wartość kosinowa wskazuje na doskonałe dopasowanie, mimo że jest dłuższy.

Iloczyn skalarny łączy kierunek i wielkość – A·B = |A| × |B| × cos(θ). Wektory o większej wartości są uważane za ważniejsze, a wektory skierowane w tym samym kierunku są uważane za bardziej podobne.

Odległość euklidesowa mierzy odległość w linii prostej pomiędzy punktami. Im mniejsza odległość, tym większa podobieństwo. Jest używana głównie do rozwiązywania problemów klastrowania.

Jak wszystko się łączy

Dobry pipeline RAG to coś więcej niż baza wektorowa przymocowana do modelu językowego. Kierunek we wektorze embeddingu niesie znaczenie, a jego wielkość wskazuje na siłę sygnału. FAISS, Pinecone i Weaviate wykorzystują algorytmy HNSW i IVF do efektywnego wyszukiwania na dużą skalę. Po tym etapie jakość zależy od kilku dodatkowych decyzji:

  • starannie dzielić dokumenty na fragmenty, aby zachować kontekst
  • używać wyszukiwania hybrydowego, aby uwzględnić zarówno dopasowania semantyczne, jak i oparte na słowach kluczowych
  • przeprowadzać ponowną sortację za pomocą kodera krzyżowego lub modelu językowego
  • pisz silne instrukcje, aby kierować modelem i ograniczyć halucynacje
  • dostosuj progi top-K i oceny
  • Cała ścieżka wygląda tak: dane, dzielenie na fragmenty, embeddingi, indeksowanie, wyszukiwanie, ponowne sortowanie, instrukcje, LLM, odpowiedź.