Арифметика, с которой начинается проект
Прежде чем выбирать библиотеку индекса, полезно посчитать на салфетке. Пусть эмбеддинг — 768 чисел одинарной точности, это 3 килобайта на вектор. Умножаем на 3,4 миллиарда и получаем больше десяти терабайт только на сырые векторы, без служебных структур индекса.
Такой объем в оперативной памяти — это стойка серверов под задачу, которая должна отвечать за миллисекунды. Отсюда первый и главный вывод: проект не про выбор алгоритма поиска, а про сжатие представления. При сжатии вектора до десятков байт тот же индекс укладывается в сотни гигабайт, и разговор из области фантастики переходит в область бюджета.
Треугольник, из которого не выбраться
| Хотим | Чем платим |
|---|---|
| Выше recall | Больше кандидатов на проверку, выше задержка; либо слабее сжатие, больше памяти |
| Ниже задержка | Меньше кандидатов, ниже recall; либо больше реплик, выше стоимость |
| Меньше памяти | Агрессивнее сжатие, ниже точность расстояний, ниже recall |
Никакая библиотека этот треугольник не отменяет. Работа инженера — выбрать точку в нем осознанно и вместе с заказчиком, а не унаследовать ее от настроек по умолчанию.
Наша точка: 96% recall@10 при 6 мс на девяносто девятом перцентиле. Оставшиеся четыре процента — это случаи, когда один из десяти выданных документов не входит в идеальную десятку полного перебора. Для рекомендаций и подсказок это незаметно, для юридического поиска — неприемлемо, и такие задачи мы бы считали иначе.
Как вообще узнать свой recall
Recall нельзя измерить приближенно тем же приближенным методом. Нужен эталон: на случайной выборке запросов честно считается полный перебор по всей базе. Медленно, дорого, зато это истина, с которой сравнивается быстрый индекс.
Эталон пересчитывается регулярно, потому что база меняется. Проект, где recall измерили один раз на старте и вписали в презентацию, через полгода живет с цифрой, не имеющей отношения к реальности.
Хвост задержки живет в шардах
Индекс разложен по узлам, запрос уходит на все и ждет ответа от всех. Здесь возникает эффект, который убивает распределенный поиск: общая задержка определяется самым медленным узлом. Если у каждого узла редкая тормознутость, то при разлете на десятки узлов эта редкость становится правилом — вероятность попасть хотя бы на один медленный ответ растет с числом шардов.
Что помогло:
- дублирующий запрос к реплике, если основной узел не ответил за долю бюджета времени, — ответ берется от того, кто быстрее;
- ограничение работы на узле по времени: узел отдает лучшее, что успел найти, вместо того чтобы задерживать весь запрос;
- выравнивание размеров шардов — перекошенный шард всегда становится тем самым медленным;
- контроль фоновых операций: слияние сегментов индекса, запущенное в час пик, портит хвост надежнее любой нагрузки.
Индекс, который обновляется на ходу
Данные приходят непрерывно, а перестроение индекса на миллиардах векторов — это часы. Останавливать поиск нельзя.
Поэтому индекс разделен на две части: большой основной, перестраиваемый по расписанию, и небольшой свежий, куда попадают новые векторы и который перестраивается быстро. Запрос идет в обе части, результаты сливаются. Периодически свежий слой вливается в основной. Удаления обрабатываются отметками, физически вычищаются при перестроении.
Схема известная и скучная. Ее главный враг — рост свежего слоя: если он раздувается быстрее, чем успевает вливаться, задержка ползет вверх. За этим следит отдельный сигнал мониторинга.
Фильтры ломают геометрию поиска
Отдельная боль любого промышленного векторного поиска: запрос почти никогда не идет по всей базе. Нужны ближайшие соседи среди активных товаров, среди документов конкретного подразделения, среди записей за последний год. Наивное решение — найти сотню ближайших и отфильтровать после — разваливается, когда фильтр отсекает почти все: из сотни кандидатов остается два, и выдача внезапно пустеет.
Мы разложили частые фильтры в структуру индекса: узкие и стабильные признаки стали отдельными разделами индекса, редкие остались постфильтрацией с автоматическим расширением числа кандидатов. Правило простое: если фильтр отсекает больше девяноста процентов базы, он обязан работать до поиска, иначе задержка и recall уезжают одновременно.
Что мы советуем считать до старта
- Размерность вектора и число объектов — прямо в терабайты, до всяких библиотек.
- Требуемый recall, названный бизнесом в терминах его задачи, а не в процентах ради процентов.
- Бюджет задержки для конечного пользователя, из которого поиску достается лишь часть.
- Темп поступления новых данных и допустимую задержку их появления в поиске.
Четыре числа определяют архитектуру и стоимость. Все остальное — детали реализации.
Направление целиком — векторный поиск; типичный потребитель такого индекса — локальный RAG; эксплуатация под нагрузкой — MLOps.