В новой версии YDB теперь доступны две версии векторного поиска — точный и приближённый. Приближённый поиск может работать с миллиардами векторов, если использовать векторный индекс. Такая технология есть у небольшого количества технологических компаний в мире.

Новый релиз СУБД Яндекса делает векторный поиск доступным для всех. Статья под катом написана по мотивам моего доклада на конференции HighLoad++, с которым я выступил 23 июня в Питере. В ней я расскажу про векторный поиск, индекс, RAG и о том, как эти технологии применяются в Алисе.


Как работает Retrieval-Augmented Generation (RAG)

Если большая языковая модель не была чему‑то обучена, то недостающая информация ищется в интернете или базе данных и добавляется в запрос пользователя. Так, отвечая на запросы вроде «Расскажи мне о расписании кино на завтра», модель способна обращаться к фактам о пользователе, которые она сохранила, и пользоваться информацией из внутренних баз знаний.

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

Векторы и векторные базы данных

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

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

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

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

Точный векторный поиск в YDB

Самая простая версия векторного поиска реализована у нас с помощью функции YQL для расчёта расстояния между двумя векторами (в примере использовано косинусное расстояние, но можно применять и другие). Эта функция получает на вход два вектора и возвращает косинусное расстояние между ними — число, которое определяет, насколько близко друг к другу расположены векторы в многомерном пространстве.

Чтобы воспользоваться таким поиском, достаточно сохранить данные эмбеддингов в столбце типа Bytes, после чего можно делать выборку с сортировкой по косинусному расстоянию:

CREATE TABLE facts (
  id Uint64,
  text String,
  user_id Uint64,
  vector Bytes,
  PRIMARY KEY (id)
)

SELECT id, text FROM facts
WHERE user_id = 1
ORDER BY Knn::CosineDistance(vector, $TargetVector)
LIMIT 10

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

Тем не менее у точного поиска есть свои преимущества:

  • штатная поддержка строгой согласованности транзакций;

  • мгновенная вставка/удаление;

  • поддержка максимально широкого множества реляционных операций.

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

Но с увеличением количества векторов для перебора линейно растёт время поиска. Допустим, общий объём базы по всем пользователям — 300 миллионов векторов. Мы провели тесты для YDB: если на одного пользователя приходится 1000 векторов, то поиск займёт всего 5 миллисекунд. А вот при 100 000 векторов на одного пользователя время поиска увеличится уже до 300 миллисекунд.

Приближённый векторный поиск без индекса

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

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

$BitIds = SELECT id
FROM bit_table
ORDER BY Knn::ManhattanDistance(bitEmbedding, $TargetBitEmbedding)
LIMIT 100

SELECT id, text, embedding
FROM float_table
WHERE id IN $BitIds
ORDER BY Knn::CosineDistance(floatEmbedding, $TargetFloatEmbedding)
LIMIT 10

Такой способ позволяет ускорить поиск в десятки раз, но всё равно остаются области, где нужен полноценный векторный индекс. Например, для поиска по огромным базам знаний, где даже приближённый векторный поиск без индекса может занимать сотни миллисекунд или даже секунды.

Векторный индекс для приближённого поиска

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

Поэтому для YDB решили разработать алгоритм, больше всего похожий на алгоритмы FAISS IVF и ScaNN. При использовании этого алгоритма векторный индекс можно эластично располагать на множестве шардов. Всё это в масштабах Яндекса: миллиарды строк, десятки миллисекунд на поиск внутри дата‑центра. Время создания индекса и занимаемое им место должны линейно зависеть от размера таблицы.

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

Но если векторов миллиарды, то количество кластеров будет большим — и быстро найти нужный кластер не получится. Поэтому для сокращения пространства поиска мы используем алгоритм, похожий на R‑дерево: сначала для входного вектора выбирается наиболее подходящий кластер верхнего уровня, затем наиболее подходящий кластер второго уровня — и так до тех пор, пока поиск не дойдёт до кластера с небольшим количеством векторов, например 100. А их уже можно просто перебрать. Классические R‑деревья хранят в каждом узле ограничивающий прямоугольник и поэтому плохо работают с пространствами большой размерности. Наше дерево в каждом узле хранит список дочерних вершин и не имеет этих проблем:

Весь индекс реализован как две скрытые таблицы YDB. В первой из них, Level Table, хранится иерархия центроидов для быстрого поиска по кластерам. Данные в этой таблице организованы в древовидную структуру, и YDB может быстро найти идентификатор кластера нижнего уровня для вектора, по которому происходит поиск.

Вторая таблица, Posting Table, хранит информацию о векторах в каждом кластере нижнего уровня из первой таблицы. По индексу кластера в ней можно получить индексы всех векторов, которые входят в этот кластер. А сами векторы хранятся в основной таблице, Main Table, которая не является частью индекса:

Таблицы в YDB автоматически распределяются по серверам в зависимости от нагрузки, поэтому в результате получается глобальный эластичный индекс.

Предварительные замеры производительности показывают, что поиск по сотням миллионов векторов выполняется за десятки миллисекунд, если серверы кластера находятся в одном дата‑центре.

Для добавления векторного индекса нужно воспользоваться ключевым словом INDEX: база данных создаст векторный индекс под указанным именем для выбранного поля и с заданными настройками:

ALTER TABLE facts
  ADD INDEX idx_vector
  GLOBAL USING vector_kmeans_tree
  ON (vector)
  WITH (
    distance=cosine,
    vector_type="float",
    vector_dimension=512,
    levels=2,
    clusters=128)

Чтобы воспользоваться векторным индексом, достаточно указать его имя после ключевого слова VIEW:

SELECT * FROM facts 
VIEW idx_vector
ORDER BY Knn::CosineDistance(embedding, $Target)
LIMIT $k

Фильтруемый векторный индекс

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

SELECT * FROM facts VIEW idx_vector
  WHERE user_id = $TargetUserId
  ORDER BY Knn::CosineDistance(embedding, $TargetEmbedding)
  LIMIT $k

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

В случае YDB это третья таблица векторного индекса с именем Prefix Table. При использовании вторичного индекса YDB находит в Prefix Table все деревья из Level Table, в которых содержатся данные с указанным значением. А затем использует алгоритм, описанный выше, чтобы найти ближайшие к указанному векторы и быстро их перебрать.

Сейчас наша команда работает над добавлением векторов в векторный индекс: добавляемый вектор спускается по дереву кластеров, смещая их центроиды, после чего записывается в Posting Table. Это позволит автоматически пересчитывать индекс за константное время при добавлении и удалении элементов.

Как векторный поиск YDB используется в Алисе

Для команды AI‑ассистента Алисы важны гарантии ACID при записи в несколько таблиц, возможность выполнять JOIN и эластичное масштабирование, поэтому выбор пал на YDB. Текущая реализация использует точный векторный поиск: пока в базе данных по несколько тысяч векторов для каждого пользователя, время поиска составляет десятки миллисекунд. При этом «из коробки» поддерживается строгая согласованность транзакций, мгновенная вставка/удаление, максимально широкое множество реляционных операций. Но объёмы данных линейно растут, поэтому команда готовится перейти на векторный индекс, который позволит среди сотен тысяч фактов находить нужные за миллисекунды.

Если включена опция «Персонализированное общение», то бэкенд Алисы считает векторы для фраз с учётом контекста: от текущей фразы и предыдущего диалога. Когда пользователь делает запрос, бэкенд находит несколько наиболее подходящих по смыслу сессий с пользователем. Сессии передаются фактовому суммаризатору — отдельной модели, которая обучена суммаризировать и формулировать ключевые факты на основе фраз (сессий):

Факты от суммаризатора добавляются в запрос пользователя и передаются в итоговый суммаризатор — основную нейросеть Алисы, возвращающую ответ. Ответ передаётся пользователю как поток токенов, а когда ответ полностью сформирован, помещается в базу данных персонализации, где в будущем Алиса сможет найти его текст с помощью векторного поиска и использовать для формирования следующих ответов.

Также векторный поиск используется для «открывашек». Когда разговор заходит в тупик, Алиса находит в истории одну из интересных пользователю тем из заранее известного списка (домашние животные, хобби и т. д.). Благодаря этому диалог с голосовым помощником похож на разговор с друзьями, когда есть общий контекст общения.

Какой векторный поиск выбрать для вашего проекта

YDB предлагает на выбор три механизма векторного поиска: точный поиск, приближённый поиск без индекса и приближённый поиск с использованием векторного индекса.

  • Если поиск ограничен десятками тысяч элементов, то прекрасно справится точный векторный поиск. Достаточно добавить в таблицу колонку для хранения векторов и воспользоваться функцией расстояния для сравнения элементов. Это уже доступно во всех кластерах YDB и работает в продакшн‑окружении.

  • Приближённый векторный поиск без индекса позволит с помощью нескольких строк YQL‑запроса увеличить скорость на порядок, причём без применения индексов. Это тоже доступно во всех кластерах YDB.

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

База данных YDB доступна как опенсорс‑проект и как коммерческая сборка с открытым ядром. Вы можете запустить её на своих серверах или воспользоваться нашим managed‑решением в Yandex Cloud.

Мы общаемся с нашими пользователями в Telegram и на Хабре. Пишите комментарии к этой статье: мне, как разработчику базы данных, будет интересно поговорить с теми, кто базами данных пользуется!

А ещё мы проводим вебинары для разработчиков, где рассказываем про возможности YDB и отвечаем на вопросы. Ближайший вебинар — как раз по векторному поиску — пройдёт 14 августа 2025 года, записаться на него можно здесь.

Комментарии (18)


  1. alexhu
    15.07.2025 06:52

    квантованная версия может использовать 8 бит или даже 1 бит.

    Как понимать 1 бит? - вектор существует / вектор не существует?


    1. Miceh
      15.07.2025 06:52

      скорее всего речь про количество бит на элемент вектора: [0.1, 123, 34, 155] -> [0, 1, 0, 1]


      1. alexhu
        15.07.2025 06:52

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


        1. ShuraZ Автор
          15.07.2025 06:52

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


      1. alexhu
        15.07.2025 06:52

        скорее всего речь про количество бит на элемент вектора

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


    1. ShuraZ Автор
      15.07.2025 06:52

      Изначально вектор использует 32 бита для каждого числа (размерности), можно применять статическое квантование и использовать 1 бит на размерность.


      1. alexhu
        15.07.2025 06:52

        1 бит на размерность

        Как раз такое кодирование используется в One Hot encoding, сейчас (давно уже) предпочитают ембединг, как раз такой способ уменьшает размерность и убирает разреженность матриц. 32 бита используется в tensorflow и PyTourch по умолчанию, только можно сразу инициировать другие размерности не дожидаясь квантования. Использовать 1 бит не имеет смысла, это просто потеря информации, или у вас более сложная модель которая читает данные в другой нейронке или уже в готовых данных, конкатенирует эти значения и выдаёт результат.


        1. ShuraZ Автор
          15.07.2025 06:52

          Да, согласен. Просто так использовать 1 бит на размерность нет смысла, слишком низкое качество. Но с реранкингом все гораздо лучше, как по скорости, так и полноте.


    1. U235U235
      15.07.2025 06:52

      Для векторов логично использовать не обычное, а векторное квантование. Или использовать плотные n-мерные решетки..


  1. Aleksey999
    15.07.2025 06:52

    Большое спасибо за статью! я так понимаю для RAG важно вытащить правильные документы из базы, что бы потом скормить их ЛЛМ для контекста с промнтом. И тут важна гибридная модель, когда можно и полнотекстовый поиск (желательно с возможностью задавать коэффициенты - ну например встречается в заголовке 0.9, встречается в теле 0.5 ).

    Вопросы: 1. Полнотекстовый поиск поддерживается в YDB?
    2. Гибридная модель поиска как скажем в Milvus есть в YDB?


    1. ShuraZ Автор
      15.07.2025 06:52

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


  1. SabMakc
    15.07.2025 06:52

    RAG позволяет найти похожие тексты. Т.е. мы должны заранее знать, о чем идет речь (хотя бы примерно).

    Но, например, если нам доступно только название фильма и мы просим проанализировать отзывы?
    Он найдет все отзывы, добавит в контекст? Или найдет ближайшие к названию фильма? А если это просто общеупотребительное слово или есть одноименные фильмы / книги / сериалы?

    Как RAG дает именно то, что надо? Или просто накидываем в контекст все, что есть, а там модель сама разберется?

    Грубо говоря я не понимаю, как RAG позволит организовать что-то вроде толкового словаря или той же википедии, чтобы по слову вытаскивать целую статью.


    1. ShuraZ Автор
      15.07.2025 06:52

      RAG ищет тексты, близкие по смыслу к запросу, но если название неоднозначное, то результаты могут быть неточными. Чтобы избежать мусора в ответах, нужна фильтрация по метаданным (тип, год) или гибридный поиск (векторный + полнотекстовый).
      Плюс никто не мешает строить такие запросы к LLM, чтобы она могла запросить дополнительные детали.


      1. SabMakc
        15.07.2025 06:52

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

        Просто я недавно поработал с WIT-описанием для WebAssembly Component Model. И допустил ошибку в описании - описал resource в world. И самое интересное, что кодогенерация по описанию работала, но невозможно было использовать сгенерированные классы. Да, в спецификациях сказано, что в world допустимы только импорт/экспорт функций и интерфейсов. Но упустить этот нюанс не сложно (особенно если спецификацию не наизусть знаешь).

        Когда уже разобрался, в чем проблема - мне стало интересно, а ИИ найдет эту ошибку в WIT (про решение исходной проблемы, с кодом, даже не говорю)? Топовые модели не справляются (как минимум deepseek-r1, grok-4, kimi-k2, gemini-2.5-pro, gpt-4.1, может что еще пробовал), хотя и знают что это WIT, Component Model и т.д. (к слову, это знали все опробованные модели, в том числе и локальные).

        Но если в контекст добавить спецификации WIT (благо, что это просто один относительно объемный файл), то локальный Qwen3-30A-A3B успешно справляется с нахождением проблемы.

        Пробовал в RAG загрузить (в OpenWebUI, параметры по умолчанию, кроме модели эмбеддинга - использовал nomic-embed-text) - он находит какие-то похожие куски (3 штуки, как задано в параметрах по умолчанию), но нужной фразы туда не попадает - и он не находит ничего полезного.

        Вот я и думаю - а как вообще можно решать подобные кейсы, чтобы спецификации руками не кидать каждый раз. Хорошо, если модель явно несет ерунду - сразу видно, что спецификации не помешают. Но что делать, если ответ более качественный или ты в вопросе совсем не разбираешься (чтобы понять, что в ответе ерунда)?

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


  1. ResearcherNo1
    15.07.2025 06:52

    А как же HNSW? Вроде он лучше R-деревьев будет для приближённого поиска


    1. ShuraZ Автор
      15.07.2025 06:52

      Мы смотрели в сторону разных алгоритмов.
      Конкретно HNSW не подошел именно для нас.

      Произвольный доступ к памяти – замедление дискового IO.
      Увеличенное потребление памяти 2x–8x в зависимости от числа слоёв.
      Сложно сделать граф эластичным, перешардироваться на лету.

      Ну и наконец, мы целимся в миллиарды векторов.


      1. Ogoun
        15.07.2025 06:52

        В HNSW нет никакого 2x-8x, вы храните центроиды в отдельной базе, а в HNSW используется настраиваемый аналог списков с пропусками, которые менее 100% займут от количества векторов.

        Собственно само ваше описание это уже и есть тот же аналог HNSW:

        в первой из них, Level Table, хранится иерархия центроидов для быстрого поиска по кластерам. Данные в этой таблице организованы в древовидную структуру, и YDB может быстро найти идентификатор кластера нижнего уровня для вектора, по которому происходит поиск.

        В том же Qdrant справились с перешардированием - https://qdrant.tech/documentation/cloud/cluster-scaling/

        А сама структура HNSW не ограничивает количество векторов. Миллиарды правда еще не пробовал, но на 60 миллионах векторов поиск занимает миллисекунды.


        1. ShuraZ Автор
          15.07.2025 06:52

          Спасибо за уточнение. Действительно, в ряде реализаций HNSW вектора хранятся прямо в графе, а в ряде реализаций - вне графа. Тут уже баланс между объемом памяти и временем поиска.

          А по поводу перешардирование Qdrant - у них небольшая приписка: "Resharding is exclusively available on multi-node clusters across our cloud offering".
          В случае YDB - немного иначе. СУБД, изначально рожденная в Яндексе, открыта в публичный доступ в OpenSource.