
Привет, Хабр! Меня зовут Александр Зевайкин, и мы с командой делаем YDB (СУБД Яндекса). В конце прошлого года Яндекс представил специализированного ИИ‑помощника — Нейроюриста, для которого обучил языковую модель на основе Alice AI LLM. Сервис работает на базе RAG, под капотом у которого находится YDB c миллионами различных юридических документов.
Под катом — история о том, как команда разработки Нейроюриста сделала семейство векторных индексов, чтобы находить нужное количество документов при любых параметрах фильтрации. Я кратко расскажу про архитектуру векторного индекса, покажу, как выбирать правильные настройки, и продемонстрирую бенчмарки получившегося решения.
Как работает векторный поиск с индексом
Получив запрос от пользователя, Нейроюрист ищет в базе подходящие по смыслу документы и добавляет их в промпт, на основе которого языковая модель формулирует ответ. Такой механизм известен как retrieval augmented generation (RAG), а используемый поиск называется векторным поиском.
С помощью индекса векторный поиск можно сделать быстрым. А настройки индекса позволяют всегда находить нужное для RAG количество документов. Но Нейроюрист использует фильтры, и одинаковые настройки индекса применяются к выборкам с количеством документов, отличающимся на порядки.
При неудачной комбинации фильтров поиск может найти меньше документов, чем нужно. Я расскажу, какой способ применили разработчики Нейроюриста для решения проблемы. И начну с того, как работает векторный поиск с индексом.
Вообще, векторный поиск можно проводить и без индекса. Сначала нужно вычислить вектор‑эмбеддинг для запроса пользователя и векторные расстояния между ним и эмбеддингами всех текстов в базе. После чего — использовать нужное количество наиболее релевантных вектору запроса текстов. Для нашего RAG мы используем топ-50, чтобы у модели всегда было достаточно контекста для ответа.
У точного векторного поиска без индекса есть всего один недостаток: чем больше эмбеддингов в базе, тем больше времени занимает такой поиск.
К примеру, точный векторный поиск без индекса для 1000 векторов занимает всего 5 мс (мы писали на Хабре про векторный поиск и приводили цифры). Но уже для 100 тысяч векторов это время увеличивается до 300 мс и дальше растёт линейно. А у нас в базе миллионы векторов‑эмбеддингов!
Когда количество данных увеличиваетcя, для быстрого поиска можно использовать разные подходы. Например, квантование или векторный индекс. Векторный индекс обеспечивает логарифмическое время поиска за счёт небольшого падения полноты, поэтому разумно использовать именно его. Найденные векторы будут релевантны вектору запроса, но могут быть не самыми близкими, что допустимо для подавляющего большинства ситуаций, особенно для RAG. Такой поиск называют приближённым.
Приближённый векторный поиск по индексу работает следующим образом. Все векторы в базе группируются в некоторое количество кластеров, для каждого из которых рассчитывается вектор‑центроид. После этого каждый кластер разбивается на такое же количество кластеров второго уровня, для которых считаются свои центроиды, и процесс повторяется указанное при создании индекса количество раз. В результате получается дерево кластеров с некоторым количеством уровней.
Во время поиска вектор запроса сначала сравнивается со всеми центроидами кластеров первого уровня, и среди них выбирается тот кластер, центроид которого ближе всего к образцу. Затем вектор запроса сравнивается с кластерами второго уровня, дочерними по отношению к найденному кластеру. Процесс повторяется, пока не будет найден кластер на самом низком уровне, после чего вектор запроса сравнивается уже с векторами, входящими в этот кластер, и среди них отбирается нужное количество ближайших.

Главное достоинство приближённого векторного поиска — скорость работы. Для поиска в миллиардах векторов достаточно:
найти ближайший кластер среди нескольких сотен;
затем повторить такой поиск несколько раз, спускаясь вниз по уровням дерева;
и, наконец, найти нужное количество ближайших векторов среди тех нескольких сотен, которые входят в последний найденный кластер.
Проблема несбалансированности данных и её решение
Чтобы в последнем найденном кластере было достаточно векторов, нужно настроить векторный индекс под ожидаемый объём данных.
Нейроюрист использует фильтры по несбалансированным юридическим данным, поэтому нельзя выбрать хорошие настройки индекса и обеспечить нужную полноту поиска. Настройки задаются на индекс целиком, и для каких‑то значений фильтра в последнем кластере будет слишком много векторов, что замедлит поиск. А для других значений в последнем кластере будет недостаточно векторов, что сделает поиск неполным.
Получается, чтобы разработать сервис, который отвечает на правовые вопросы, как профессиональный юрист, и даёт заключения со ссылками на актуальные нормы права и судебную практику, нужно решить проблему быстрого векторного поиска по несбалансированным данным — чтобы нужное для RAG количество векторов находилось при любой комбинации фильтров.
Как находить не меньше векторов, чем нужно для RAG
Если объём данных заранее известен, то можно подобрать количество уровней в индексе (за них отвечает параметр levels) и количество кластеров на уровне (за них отвечает параметр clusters).
С настройками по умолчанию поиск по векторному индексу находит один ближайший кластер на первом уровне дерева, затем один на втором и так далее — пока не найдёт последний кластер и не начнёт искать нужное количество входящих в него векторов.
С помощью параметра KMeansTreeSearchTopSize можно указать, сколько ближайших кластеров искать на каждом уровне. В таком случае придётся делать больше сравнений, зато можно указывать этот параметр для каждого поиска и получать нужное количество векторов без необходимости обновлять векторный индекс.
Оба этих способа хорошо работают без фильтров или если данные для фильтрации сбалансированы.
Как выглядят данные Нейроюриста?
Особенности юриспруденции: несбалансированное количество данных
Нейроюрист работает с обширной базой юридических данных: Конституцией, федеральными законами, подзаконными актами, приказами ведомств, письмами министерств и федеральных служб, постановлениями и распоряжениями Правительства РФ.
Все эти данные разделены на сферы, например «трудовое право» или «защита прав потребителей». А каждая сфера, в свою очередь, разделена на категории: законодательство, кодексы, судебная практика, комментарии юристов и всё остальное.
Задавая Нейроюристу вопросы, клиенты хотят выбирать сферу права и категорию:

Но количество документов в разных сферах для разных категорий права отличается на порядки. Судебная практика по защите прав потребителей — это 2,5 миллиона текстов. А в законодательстве по корпоративному праву их всего 6 тысяч. Чтобы реализовать такую фичу, нужно выполнять векторный поиск с фильтрацией и получать не менее 50 результатов вне зависимости от того, какой фильтр выбрал пользователь.
Фильтрующий векторный индекс и его одинаковые настройки для всех фильтров
Как отфильтровать результаты векторного поиска с использованием индекса? Фильтровать до поиска нельзя: дерево векторного индекса строится один раз для всех данных, и его нельзя использовать для произвольного набора строк базы данных. Если же фильтровать после поиска, то подходящих результатов может остаться меньше, чем нужно, и качество поиска упадёт.
В YDB фильтрация интегрирована прямо в структуру и алгоритм векторного индекса, так что неподходящие по условиям объекты сразу исключаются из рассмотрения. Как это сделано?
Индекс делится на несколько частей. Вначале строится вторичный векторный индекс для нужных колонок. В случае Нейроюриста это колонки «Сфера права» и «Категория». После чего для каждого уникального элемента вторичного индекса строится отдельное дерево векторного индекса.
В Нейроюристе на момент написания этой статьи 7 сфер права и 5 категорий, так что будет построено 35 деревьев. При поиске с фильтрацией вначале будет выбрано одно из этих деревьев, после чего в выбранном дереве будут найдены нужные для RAG топ-50 векторов, ближайших к вектору запроса. Каждый найденный вектор соответствует какому‑то документу. Нейроюрист забирает эти документы из базы, добавляет в промпт и даёт языковой модели нужный для работы контекст.
Фильтрующий векторный индекс строит все деревья с одинаковыми настройками количества уровней и кластеров. Если выбрать усреднённые параметры в 3 уровня и 40 кластеров на уровень, то даже при расширении полноты поиска (с помощью KMeansTreeSearchTopSize) не для каждой комбинации сферы права и категории удастся найти 50 векторов:
Сфера права + категория |
Количество текстов |
Найдено векторов |
|
6022 |
? 26 |
|
76 490 |
? 36 |
|
116 436 |
? 38 |
|
1 202 156 |
? 50 |
|
2 506 787 |
? 50 |
Семейство векторных индексов
Команда разработки Нейроюриста воспользовалась тем, что в YDB можно создать больше одного векторного индекса для таблицы и при поиске выбирать, какой из индексов применить. В коде выбор индекса выглядит вот так:
PRAGMA ydb.KMeansTreeSearchTopSize = "<значение>"; SELECT id, metadata, Knn::CosineDistance(embedding, $query_vector) as distance FROM embeddings VIEW <имя_векторного_индекса> WHERE index_id = $index_id ORDER BY distance LIMIT 50;
Для таблицы с данными разработчики построили несколько векторных индексов с разными параметрами. Каждый индекс отвечает за определённый диапазон и используется для поиска в тех комбинациях сфер прав и категорий, которые содержат подходящее количество векторов.
Например, в комбинации «корпоративное право» и «законодательство» 6022 вектора. Поэтому для поиска по этой комбинации используется индекс tiny с 1 уровнем в дереве и 32 кластерами на этом уровне. А в комбинации «защита прав потребителей» и «судебная практика» 2 506 787 векторов. Поэтому будет использован индекс xxxlarge с 2 уровнями и 128 кластерами:
Диапазон векторов |
Название индекса |
Levels |
Clusters |
Количество векторов в последнем кластере |
Количество кластеров, участвующих в поиске |
1.5M — 2.5M |
|
2 |
128 |
90–150 |
10 |
500k — 1.5M |
|
2 |
100 |
50–150 |
10 |
200k — 500k |
|
2 |
80 |
30–80 |
10 |
100k — 200k |
|
2 |
64 |
25–50 |
10 |
40k — 100k |
|
2 |
48 |
18–45 |
10 |
15k — 40k |
|
2 |
32 |
15–40 |
8 |
5k — 15k |
|
1 |
32 |
150–470 |
6 |
< 5k |
brute force |
Количество уровней и векторов на уровень было выбрано так, чтобы в последнем кластере было примерно от 50 до 150 векторов. Такое количество оптимально: не нужно перебирать много векторов в кластере для поиска 50 ближайших к образцу, и не нужно перебирать много кластеров с небольшим количеством векторов в каждом.
Что получилось
В этой статье мы выяснили, что специфика разрабатываемого продукта может потребовать более сложных решений, чем векторный поиск с индексом.
Возможность создавать в YDB несколько векторных индексов с разными параметрами, ограничивать их предикатами и явно указывать нужный индекс в запросе для поиска позволила разработчикам Нейроюриста использовать стратегию индексации с семейством индексов: каждый векторный индекс оптимален для работы в некотором диапазоне размера подвыборки и позволяет оптимальным образом отвечать на запросы пользователей.
Семейство векторных индексов позволяет быстро и качественно находить нужное для RAG количество векторов в выборках, отличающихся по объёму на порядки: от тысяч до миллионов векторов:
время векторного поиска топ-50 векторов на стороне YDB — около 15 мс для p50 и до 200 мс для p99;
качество поиска по топ-50 кандидатов сохраняется и на больших, и на маленьких подвыборках.
YDB (СУБД Яндекса) доступна как опенсорс‑проект и как коммерческая сборка с открытым ядром. Вы можете запустить её на своих серверах или воспользоваться нашим managed‑решением в Yandex Cloud.
Комментарии (11)

krokhmalyuk
11.03.2026 12:40А есть возможность задавать несколько векторных индексов для таблицы и выбирать нужный при запросе в других базах?

ShuraZ Автор
11.03.2026 12:40Да, можно строить несколько векторных индексов, а при выборке SELECT указывать нужный через VIEW index_name
Например:DECLARE $query_vector AS List<Uint8>; SELECT user, data FROM my_table VIEW my_index ORDER BY Knn::CosineSimilarity(embedding, $query_vector) DESC LIMIT 10;
https://ydb.tech/docs/ru/dev/vector-indexes?version=v25.4#select

matveeva-sn
11.03.2026 12:40До этого, еще на стадии MVP Нейроюриста, мы пробовали PostgreSQL, в нем есть другой тип векторных индексов — HNSW. Cам постгрес позволяет строить несколько векторных индексов тоже с фильтрацией, однако управлять тем, в какой из них ходить для поиска - нельзя, этот выбор делает планировщик запросов постгреса.
Чтобы планировщик решил, что запрос будет использовать нужный вам векторный индекс, приходится изворачиваться с самим поисковым запросом: условие WHERE при поиске должно точно совпадать с условием, которое было использовано для построения векторного индекса. На практике же, реальных условий для фильтрации при поиске бывает больше, чем тех, по которым был построен векторный индекс. Тут кажется логичным вопрос: почему бы не использовать постфильтры? Да, можно, но это уже негативно сказывается на полноте поиска. Даже если векторным поиском искать в 10 раз больше документов, чем требуется, нет гарантий, что постфильтры оставят нам нужное для ретрива количество.
В YDB же это не является проблемой: не нужно колдовать, чтобы планировщик решил ходить в нужный индекс, при этом всегда обеспечивается нужная полнота

vagon333
11.03.2026 12:40Из доки:
Индекс
vector_kmeans_treeреализует иерархическую кластеризацию данных.
...
Параметры:levels: число уровней в дереве, задает глубину поиска (рекомендуется 1-3);
Если вы эмпирически уже вычислили условия определения количества уровней и кластеров, то может задавать эти параметры автоматически?
При создании многоуровневого векторного индекса разработчику сложно разобраться с правильными параметрами - новая область.
ShuraZ Автор
11.03.2026 12:40Хорошее замечание, спасибо.
У нас в планах есть автоматическое определение параметров векторного индекса.
Это будет работать для случаев добавления индекса в существующую таблицу, когда есть возможность проанализировать подборку векторов и вычислить нужные параметры вроде числа уровней в дереве.

8643794
11.03.2026 12:40Была нужда найти ответы на свои вопросы (юридического плана). Использовал бесплатную версию ИИ Яндекса и такой же бесплатный аналог ИИ Гугла. По итогу заявляю, адекватные ответы получил только от Гугла, у ИИ Яндекса все ответы были размазаны по проблеме, собрать мозайку не представляется возможным. Вопросы задавал одни и те же.
shtock
точно ли для этой задачи надо векторый поиск, а не GraphRAG?
headliner1985
Соглашусь, тут нужен Graph RAG с ранжированием, и хватило бы обычного Postgres.
ShuraZ Автор
В данной статье решается задача: как всегда быстро достать нужные 50 документов, даже когда пользователь включил фильтры и в одной категории осталось 6 тысяч текстов, а в другой — 2,5 млн. Это именно задача retrieval (поиска кандидатов) - и векторный поиск для этого самый рабочий инструмент.
GraphRAG тут не "вместо", а скорее "поверх": он может помочь связать найденные куски в более понятную аргументацию, но чтобы вообще было из чего строить граф и ранжировать, кандидатов всё равно нужно сначала вытащить