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

Внутренняя структура векторных баз данных для RAG: хранение фрагментов, алгоритмы HNSW и IVF, а также процесс переранжирования.

Как векторные базы данных RAG хранят фрагменты, выполняют поиск среди миллионов векторов с использованием алгоритмов HNSW и IVF, внедряют гибридный поиск и переранжирование, а также сравнивают метрики сходства.

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

Источники: Inside Vector Databases for RAG: From Chunk Storage to HNSW & IVF Search

Система генерации с усилением с помощью поиска должна сразу решить практический вопрос: где хранятся фрагменты текста? Наиболее распространенным ответом, который используют большинство производственных систем, является размещение каждого фрагмента прямо в векторной базе данных рядом с его вектором представления.

Как выглядит сохраненная запись

В системах, построенных на FAISS, Pinecone или Weaviate, каждая запись включает три элемента: вектор представления, текст фрагмента и некоторые метаданные.

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

Сохранение фрагмента текста и его вектора вместе имеет три преимущества. Поиск происходит быстро, поскольку соответствующий текст возвращается в том же ответе. Архитектура остается простой, так как нет необходимости обращаться к дополнительному хранилищу. Кроме того, задержка остается низкой, что важно, когда большая языковая модель ожидает результата в реальном времени.

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

Почему поиск остаётся быстрым

Обычное однострочное описание гласит, что векторная база данных находит похожие векторные представления с использованием косинусного сходства, скалярного произведения или евклидова расстояния. Это верно, но оно упускает важные технические аспекты. Как при наличии миллионов или миллиардов векторов запрос может возвращаться за доли секунды?

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

В реальных системах используется приближённый поиск ближайшего соседа (ANN). Суть заключается сначала в сужении пространства поиска, а затем в проверке лишь небольшой подмножества векторов, при этом допускается небольшая потеря точности в обмен на значительное ускорение. Поиск ближайшего соседа реализуется с помощью специализированных структур индексации. Например, FAISS предоставляет индексы такого типа, а также там доступны HNSW и IVF.

HNSW — слоистая графическая структура

HNSW — это аббревиатура от Hierarchical Navigable Small World. Это графическая структура, которую внутренне используют Pinecone и Weaviate. Каждый вектор становится узлом, а каждый узел связан с похожими соседями. Поиск представляет собой прогулку по этому графу.

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

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

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

На практике важны два параметра:

  • M — это количество связей на узел. Большее значение M повышает точность восстановления данных, но требует больше памяти и времени на обработку.
  • efSearch — это количество кандидатов, которые исследуются во время запроса. Большее значение улучшает точность восстановления данных, но приводит к более высокой задержке.

Проще говоря, M определяет качество индекса, в то время как efSearch идет на компромисс между точностью и скоростью при выполнении запросов.

IVF: сначала кластеризация

IVF (обратный файловый индекс) широко используется в FAISS и применяет совершенно иной подход. Он не создает граф, а выполняет кластеризацию, объединяя похожие векторы и ища только в соответствующих группах. Хорошим примером может служить библиотека: книги сортируются по категориям, поэтому не нужно просматривать каждую полку.

При вставке векторов алгоритм кластеризации, такой как k-means, делят их на N кластеров. Каждый кластер имеет центроид, и каждому вектору присваивается ближайший к нему центроид. При выполнении запроса эмбеддинг сравнивается со всеми центроидами, выбирается ближайший кластер, после чего для получения лучших результатов оценивается соответствие каждому вектору в этом кластере.

Эти два метода отличаются по использованию памяти. IVF хранит меньше связей, поэтому потребляет меньше ресурсов. HNSW сохраняет граф с большим количеством рёбер на узел, поэтому требует больше памяти.

Где чистый векторный поиск оказывается недостаточным

Эмбеддинги передают смысл, контекст и концептуальную близость. Они менее эффективны при поиске точных ключевых слов, чисел и идентификаторов. При поиске по запросу «kafka error code 504» векторный индекс может вернуть результаты вроде «kafka network issues» или «timeouts in distributed systems», упустив документ, в котором действительно присутствует код 504.

Системы RAG в производственных условиях решают эту проблему с помощью гибридного поиска, сочетающего семантический поиск (эмбеддинги плюс алгоритмы типа HNSW или IVF) с поиском по ключевым словам для точных совпадений с использованием алгоритма BM25. В типичной архитектуре оба вида поиска выполняются на отдельных системах, а их результаты объединяются и переранжируются.

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

Разве гибридный подход не означает хранение всего дважды?

При внешней настройке — да: одни и те же данные часто хранятся как в векторной базе данных, так и в движке поиска по ключевым словам, таком как Elasticsearch. Это сделано намеренно. Векторная база хранит индекс HNSW или IVF для оценки семантического сходства, в то время как движок поиска по ключевым словам использует обратный индекс для точного поиска. Каждый индекс создан для своей цели, поэтому это не является дублированием.

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

Переранжирование лучших результатов

Многие инженеры считают, что работа завершена, как только из векторной базы данных возвращаются результаты top-K. Это не так. Эти результаты имеют лишь приблизительную релевантность, не являются идеально отсортированными, и может ускользнуть более тонкий уровень релевантности.

Возьмём запрос «kafka error 504 fix». Результаты top-K могут включать «Проблемы с тайм-аутом Kafka», затем «Ошибки распределённых систем» и наконец «Код ошибки 504 — тайм-аут шлюза». Третий результат должен быть на первом месте, но векторный поиск не всегда способен это обеспечить.

При переранжировании берутся полученные результаты и переупорядочиваются с учётом более глубокого анализа запроса. Векторные базы данных обычно не выполняют эту операцию самостоятельно, поэтому логика реализуется на уровне приложения. Существует два основных подхода:

  1. Переупорядочивание с использованием LLM. Отправляйте запрос и фрагменты в LLM с просьбой отсортировать их по степени релевантности. Этот метод легко реализовать, и модель хорошо понимает контекст. Однако он медленный из-за дополнительных вызовов, а при масштабировании становится дорогостоящим.
  2. Кросс-энкодер. Это стандарт для качественного переупорядочивания. Модель принимает запрос и один фрагмент вместе как пару и выдает оценку релевантности. Эту процедуру повторяют для всех кандидатов из списка top-K, после чего сортируют их по оценкам. Такой метод детерминистичен и значительно точнее.

Пример компактного кросс-энкодера с использованием 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.

Определение сходства

Качество поиска во многом зависит от способа расчёта близости векторов. Три показателя покрывают практически все случаи.

Косинусное сходство определяется по углу между двумя векторами, то есть учитывается только направление. Два стрелка, указывающие в одном направлении, считаются высоко схожими, а противоположные — совершенно разными, независимо от длины. Формула: cos(θ) = A·B / (‖A‖‖B‖). Для запроса «Oshibka Kafka 504» документ о «Обработке ошибок Kafka» получит высокий балл (около 0,9), а документ о «Индексации баз данных» — низкий (около 0,2). Этот метод подходит для эмбеддингов и является стандартным выбором в технологиях RAG, поскольку важен смысл, а не величина. Попробуйте запрос = [1, 1], docA = [2, 2] и docB = [1, 0]: docA указывает в том же направлении, что и запрос, поэтому его косинусное сходство будет равно единице, несмотря на большую длину.

Скалярное произведение сочетает направление и величину: A·B = |A| × |B| × cos(θ). Векторы большей длины считаются более значимыми, а векторы, направленные в одну сторону, — более похожими.

Евклидово расстояние измеряет расстояние по прямой между точками. Чем меньше расстояние, тем выше степень сходства. Оно в основном используется для решения задач кластеризации.

Как все соединяется

Хорошая система RAG — это не просто векторная база данных, присоединенная к большой языковой модели. Направление в эмбеддинге несет смысл, а величина — интенсивность сигнала. FAISS, Pinecone и Weaviate используют алгоритмы HNSW и IVF для поиска в больших объемах данных. После этого качество зависит от еще нескольких решений:

  • аккуратно разбивайте документы на части, чтобы сохранить контекст
  • используйте гибридный поиск для обнаружения как семантических, так и ключевых совпадений
  • переориентируйте результаты с помощью кросс-кодера или большой языковой модели
  • Пишите четкие промпты для управления моделью и снижения галлюцинаций
  • Настройте пороги top-K и оценки
  • Весь процесс включает: данные, разбиение на фрагменты, эмбеддинги, индексацию, поиск, переранжирование, промпты, LLM, ответ.