Как понять, когда нужен rebuild векторной базы
Разбираем, когда нужен rebuild векторной базы: tombstones, фрагментация, churn обновлений, пороги обслуживания и безопасная перестройка.

Удаление в векторной базе редко означает, что место и граф освободились в ту же секунду. Для приложения точка уже исчезла: поиск не должен ее вернуть. Для индекса она часто остается tombstone-записью, старой версией при upsert или частью сегмента, которую движок уберет позже. Именно в этом разрыве между логическим и физическим удалением появляются лишняя задержка, рост памяти, мелкие сегменты и, в плохих случаях, падение качества выдачи.
Rebuild не должен быть еженедельным ритуалом и не должен запускаться после каждого batch delete. Нужен измеримый режим обслуживания: отдельно считать накопленные удаления, отдельно видеть темп изменений, проверять качество на контрольных запросах и учитывать, успевает ли фоновая компакция. Один процент мусора в коллекции, которую почти не трогают, не интересен. Те же несколько процентов, которые прибавляются каждый час в активно обновляемом HNSW-индексе, уже требуют решения.
Удаленная точка может остаться в индексе
Логическое удаление скрывает точку из результата, физическое удаление освобождает структуры хранения и перестраивает индекс без нее. Смешивать эти два события опасно: команда видит успешный ответ на delete и делает вывод, что коллекция стала меньше и быстрее. Обычно это неверно.
У HNSW граф хранит связи между вершинами. Если движок помечает точку как удаленную, он может исключить ее из финальной выдачи, но обход графа еще встретит эту вершину. Поиск тратит вычисления на кандидата, который потом отбросит. При одном удалении это незаметно. При длинной серии замен документов обход начинает делать лишнюю работу на каждом запросе.
Сегментные хранилища добавляют второй слой. Новые точки могут попасть в свежий или растущий сегмент, старые остаться в запечатанном сегменте, а полезные записи оказаться размазаны по многим небольшим кускам. Запросу приходится искать по нескольким индексам, собирать частичные кандидаты и затем объединять результат. Количество живых векторов при этом может не меняться вовсе.
Документация Qdrant прямо описывает удаления как soft delete через битовую маску: индекс не перестраивается после каждого delete. Она же уточняет неприятную деталь, которую часто пропускают при проектировании обновлений: upsert существующей точки с теми же данными помечает предыдущую версию удаленной и вставляет новую копию. Значит, пайплайн, который каждую ночь без проверки переписывает весь корпус, создает технический долг даже при неизменном количестве документов.
Это не аргумент против upsert. Idempotent-загрузка нужна, особенно когда очередь повторяет сообщения. Но idempotent для бизнес-логики не означает бесплатный для физического индекса. Если вы не знаете, была ли у документа новая версия текста, новая модель embedding или только повтор доставки, не отправляйте вектор заново по привычке.
Есть и третья категория, которую полезно отделить от удаления: смена представления. Если вы заменили embedding-модель, поменяли размерность, метрику расстояния или метод нормализации, старые векторы больше не являются прошлой версией тех же данных. Их нельзя «вылечить» компакцией. Для такого изменения нужна параллельная коллекция и полная переиндексация исходных документов.
Доля удалений сама по себе не решает вопрос
Доля tombstone-записей полезна, но она не говорит, с какой скоростью коллекция зарастает мусором и насколько этот мусор мешает запросам. Для решения о rebuild нужны минимум четыре наблюдения: физическое количество записей, количество живых записей, темп замен и поведение поиска.
Ведите два расчетных показателя. Первый показывает, сколько места индекса занято неактуальными точками:
removed_ratio = deleted_or_superseded / physical_points
physical_points = live_points + deleted_or_superseded
Второй показывает churn за окно наблюдения. Он отвечает не на вопрос «сколько мусора накопилось», а на вопрос «успеет ли уборка за записью»:
update_churn_7d = updated_or_reinserted_points_7d / live_points
Считать churn только по явным delete нельзя. В него входят замены вектора тем же ID, повторная обработка документов, пересчет embeddings после изменения chunking, истечение TTL и перенос документов между tenant-ами. Если за неделю вы переписали 30% живого корпуса, а фоновые задачи вернули deleted ratio с 18% до 12%, база не стала здоровее. Она просто проигрывает гонку медленнее.
Добавьте к этим счетчикам три эксплуатационные метрики:
- p50, p95 и p99 latency для основного search-запроса с обычными фильтрами;
- recall@k или nDCG@k на фиксированном наборе запросов и размеченных релевантных документов;
- число сегментов и объем неиндексированных либо растущих сегментов, если движок раскрывает эти данные.
Средняя задержка здесь почти бесполезна. Сегментная фрагментация и уборка tombstones часто сначала бьют по хвосту распределения: один запрос попал в перегруженный шард, другой задел сегмент, который сейчас перестраивается, третий прошел через слишком много малых сегментов. Среднее еще выглядит спокойно, а пользователь уже ждет ответ в несколько раз дольше обычного.
У качества поиска тоже два разных сбоя. Первый виден, когда движок не может быстро найти достаточно живых кандидатов в поврежденной или переполненной структуре. Второй появляется при политике, которая исключает неготовые сегменты из поиска ради latency. В FAQ Qdrant есть прямое предупреждение: при поиске по неиндексированным сегментам задержка растет, а при включении indexed_only или prevent_unoptimized результаты могут стать неполными. Это осознанный обмен полноты выдачи на предсказуемость ответа, а не «оптимизация без цены».
Пороги обслуживания должны учитывать размер и churn
Для рабочей коллекции я бы не утверждал один магический процент для всех случаев. Я бы ввел три зоны и привязал каждую к наблюдаемому состоянию поиска. Эти границы дают команде общий язык, а не заменяют измерения.
Зеленая зона: до 10%
При доле удаленных или замененных точек до 10% обычно достаточно наблюдения. Не запускайте ручной rebuild только ради красивого счетчика. Проверьте, что p95 и recall держатся у базовой линии, а фоновые процессы действительно уменьшают старые записи после крупных batch-операций.
Исключение есть для очень маленьких сегментов. В сегменте на 500 точек 50 удалений дают те же 10%, но накладные расходы на метаданные и fan-out могут быть заметнее, чем подсказал бы общий процент по коллекции. Поэтому смотрите распределение по сегментам и шардам, а не только агрегат.
Желтая зона: от 10% до 20%
В этой зоне планируйте компакцию, особенно если churn высокий или p95 начинает ползти вверх. Не ждите, пока пользователь заметит проблему. Сначала убедитесь, что фоновая оптимизация запускается и получает достаточно CPU, памяти и I/O. Если она работает, но не сокращает долю удалений после периода затишья, проверьте ее очереди, лимиты конкуренции и свободное пространство.
20% не взято с потолка. В примере конфигурации Vacuum Optimizer Qdrant параметр deleted_threshold равен 0.2, а сегмент должен иметь как минимум vacuum_min_vector_number точек, чтобы попасть под эту оптимизацию. Документация называет это примером критериев, а не обязательной нормой. Используйте его как разумную исходную точку, затем скорректируйте по собственным замерам.
Красная зона: выше 20%, или деградация безотносительно процента
При значении выше 20% создайте конкретный план уборки и назначьте окно. При 30-35% я обычно отношусь к этому как к обязательной работе, если только коллекция не находится накануне полного вывода из эксплуатации. В активной базе такой объем мертвых записей редко исчезает сам, когда поток обновлений продолжается.
Но красная зона начинается и при меньшей доле, если выполняется хотя бы одно условие: p95 или p99 стабильно ухудшились относительно базовой линии, recall на контрольном наборе просел, число сегментов растет после каждого batch-upsert, либо optimizer не успевает разобрать очередь. Это важнее круглого числа. Коллекция с 8% tombstones и обновлением 25% корпуса в сутки может быть в худшем состоянии, чем почти статичный индекс с 22%.
Не путайте этот режим с полной переиндексацией. Если векторы и параметры индекса не менялись, сначала нужна компакция или rebuild физических сегментов. Полная перезаливка из исходных данных оправдана только когда вы хотите изменить сам индекс, исправить исторический дефект пайплайна или не доверяете целостности текущих данных.
Фрагментация проявляется не только tombstone-записями
Фрагментация означает, что живые данные лежат в слишком большом числе физических кусков или графовых структур. Tombstones часто ее усиливают, но не исчерпывают проблему. Можно иметь почти нулевую долю удалений и все равно платить за поиск по десяткам небольших сегментов после частых микрозагрузок.
Milvus хорошо показывает эту механику на запечатанных сегментах. Его команда описывает Force Merge как объединение небольших sealed segments в более крупные. Каждый запрос иначе ищет в каждом таком сегменте и объединяет частичные результаты. В опубликованном ими контролируемом тесте на статичной коллекции из миллиона 768-мерных векторов с HNSW объединение сегментов заметно подняло QPS и снизило p99. Это не обещание для вашего кластера, но причина эффекта универсальна: fan-out стоит денег на каждом запросе.
Поэтому добавьте в мониторинг не только removed_ratio, но и такие признаки:
- растет число сегментов при неизменном live count;
- после ночной загрузки p99 ухудшается, хотя deleted ratio почти не изменился;
- одни шарды отвечают заметно медленнее других;
- маленькие сегменты остаются маленькими неделями, хотя запись уже закончилась;
- при фильтрации по tenant-у или типу документа кандидаты часто отбрасываются после поиска.
Последний пункт выглядит как проблема фильтра, но он связан с физической организацией данных. Если один шард хранит точки многих tenant-ов и запрос берет только один tenant, HNSW может пройти большое число неподходящих кандидатов. Rebuild сам по себе не исправит плохую стратегию шардирования. Он лишь временно уменьшит лишнюю работу.
Не пытайтесь оценивать фрагментацию по объему диска в одиночку. Квантизация, payload, журналы, репликация и файловая система искажают картину. Объем важен для емкости rebuild, но решение о поисковой оптимизации должно опираться на число сегментов, рабочую задержку и качество.
Частые обновления нужно отделить от массовой переиндексации
Самая частая ошибка в RAG-системах выглядит безобидно: crawler раз в сутки получает набор документов и отправляет upsert для каждого чанка, потому что так проще обеспечить актуальность. Если из 2 миллионов чанков реально изменились 40 тысяч, вы создали почти 2 миллиона потенциальных старых версий ради 40 тысяч полезных обновлений.
Правильный путь начинается до векторной базы. Для каждого документа храните контентный хеш, версию схемы chunking, имя embedding-модели и версию промпта или препроцессора, если он влияет на текст. Новый вектор нужен, когда изменилось хоть одно из этих условий. Повтор доставки того же документа не должен идти в embedding и запись.
Практический минимальный контракт может выглядеть так:
{
"id": "policy-431:chunk-07",
"content_sha256": "...",
"chunking_version": "2026-06",
"embedding_model": "text-model-v4",
"embedding_version": "v4.2",
"source_updated_at": "2026-07-21T03:14:00Z"
}
Перед генерацией нового вектора сравните сохраненные поля с входным документом. Совпали хеш и версии, пропустите запись. Изменился только metadata-параметр, который ваш движок умеет обновлять без замены вектора, обновите только payload. Изменился текст, chunking или embedding-модель, создайте новый вектор и сознательно учтите его в churn.
Это разделение дает еще одну пользу: вы можете отличить нормальную ежедневную жизнь индекса от миграции. Смена embedding_version для всего корпуса должна запускать отдельный проект с параллельной коллекцией, оценкой качества и переключением читателей. Ее нельзя прятать внутри обычного job, который «обновляет документы». Иначе вы проснетесь с 80% замененных точек, забитой очередью optimizer и без возможности понять, какие результаты относятся к старому пространству векторов.
Для крупных загрузок лучше писать батчами и дать движку закончить индексацию после загрузки, а не заставлять его перестраивать структуры после каждой тысячи точек. Qdrant в документации optimizer отдельно советует на время начальной массовой загрузки отключать индексацию, затем включать ее, чтобы не тратить вычисления на повторные rebuild. Этот прием подходит для контролируемой заливки, но не для постоянно доступной коллекции, где запросы обязаны видеть свежие данные.
Rebuild запускают по двум сигналам, а не по одному счетчику
Решение стоит формализовать как правило из состояния данных и состояния сервиса. Один счетчик удалений запускает лишние работы. Одна задержка заставляет лечить симптомы. Вместе они дают понятный триггер.
Для каждой критичной коллекции зафиксируйте базовую линию после успешной загрузки и оптимизации. Это не «идеальные» цифры из тестового ноутбука, а реальные p50, p95, p99, recall@10 или recall@20, размер индекса, число сегментов и доля удалений при обычной нагрузке. Затем создайте правило:
Запланировать компакцию, если:
removed_ratio >= 0.20
и update_churn_7d >= 0.05
Запустить ускоренную диагностику, если:
p95_latency > baseline_p95 * 1.25
или recall@10 ниже baseline более чем на согласованный допуск
или очередь optimizer растет два интервала наблюдения подряд
Провести rebuild в новой коллекции, если:
изменились embedding_model, размерность, distance metric
или параметры HNSW/квантизации требуют нового физического индекса
Множитель 1,25 не является законом физики. Он полезен как стартовая граница, если вы еще не настроили SLO. Для сервиса с жестким пользовательским SLA допустимая деградация может быть меньше. Для ночного аналитического процесса допустима и больше. Важно, чтобы правило сравнивало коллекцию с ее собственным прошлым, а не с чужим benchmark.
Проверяйте recall на наборе, который отражает ваш поиск. Для поддержки это должны быть вопросы клиентов с ожидаемыми статьями. Для каталога товаров, реальные поисковые формулировки и клики после ручной проверки. Для внутреннего поиска документов, вопросы сотрудников и документы, которые они действительно считают правильным ответом. Синтетические запросы, сделанные из тех же текстов, что лежат в базе, обычно слишком добры к индексу.
Не подменяйте оценку качества ростом ef или nprobe. Эти параметры иногда дают хороший аварийный рычаг: вы покупаете recall ценой latency. Но если команда месяцами компенсирует фрагментацию увеличением search breadth, она платит за мусор на каждом запросе. Сначала разберите физическое состояние коллекции, затем подбирайте параметры поиска.
Безопасный rebuild требует второго пути чтения
Полная перестройка опасна, когда команда перезаписывает единственную рабочую коллекцию и надеется, что фоновые задачи успеют раньше трафика. Безопаснее создать новую версию, загрузить ее из воспроизводимого источника и переключить читателей только после проверки.
Последовательность выглядит так:
- Зафиксируйте входной срез: источник документов, правила chunking, версию embedding-модели, метрику, параметры индекса и схему payload.
- Создайте новую коллекцию с версионным именем. Не меняйте старую во время валидации.
- Загрузите векторы батчами, дождитесь построения индекса и снимите метрики размера, сегментов и времени индексации.
- Прогоните контрольный набор запросов по старой и новой коллекциям. Сравните не только latency, но и документы в top-k, фильтры и бизнес-правила дедупликации.
- Переключите чтение через alias, конфигурацию приложения или слой маршрутизации. Старую коллекцию оставьте на согласованный период отката, затем удалите.
Вам понадобится запас емкости. Во время rebuild одновременно существуют исходные векторы, новая коллекция, промежуточные сегменты и иногда копии во время репликации. Если у вас едва хватает диска для текущего индекса, не начинайте перестройку в надежде, что удаление старой версии освободит место вовремя. Именно она будет удалена последней.
Qdrant описывает rebuild сегмента через copy-on-write: старый сегмент остается читаемым, а изменения попадают в приоритетный слой, пока идет перестройка. Это удобная модель для онлайн-оптимизации, но она не отменяет нагрузку на ресурсы и не заменяет тест переключения при полной миграции данных.
Проверьте еще один неприятный случай: запись приходит во время rebuild. Если новая коллекция наполняется одним batch-процессом, а старую продолжают обновлять пользователи, в момент переключения вы потеряете хвост изменений. Решение выбирают заранее: короткая пауза записи, двойная запись в обе коллекции или журнал изменений, который догоняет новую версию перед cutover. Для высоконагруженного сервиса я предпочитаю журнал и явный watermark. «Мы быстро переключимся» не является стратегией согласованности.
У разных движков разные механизмы уборки
Термин rebuild скрывает разные операции. Нельзя переносить настройку одного движка в другой по совпадению слов в документации.
В Qdrant вакуумный optimizer выбирает сегменты по доле удаленных векторов и минимальному числу векторов. Он также объединяет сегменты и строит индексы. Для него полезно следить за тем, не отстает ли optimizer от входящего потока. Если вы используете квантизацию, сохраните исходные full precision vectors: документация Qdrant говорит, что они нужны для пересчета квантованных представлений при Vacuum Optimizer. Удалить исходные векторы ради экономии места, а потом ожидать корректный rebuild, не получится.
В Milvus компакция работает с sealed segments и очищает сущности, удаленные за пределами удерживаемого времени Time Travel. Его конфигурация также отражает связь между размером сегментов и качеством эксплуатации: маленькие сегменты объединяют, а допустимый размер результата регулирует dataCoord.segment.expansionRate. Не оценивайте Milvus только по проценту delete. Посмотрите, сколько sealed segments реально обслуживает каждый запрос.
В Weaviate HNSW-индекс использует tombstones и периодическую очистку. Документация предлагает наблюдать метрики числа tombstones, хода cleanup-цикла и числа потоков очистки. Для больших индексов там же предусмотрены верхняя и нижняя границы удалений за цикл, чтобы уборка не захватила все CPU и не растянулась бесконечно. Это хороший пример правильного компромисса: агрессивная очистка не всегда делает сервис быстрее, если она забирает ресурсы у поиска.
В hnswlib удаление по mark_deleted помечает элемент, а библиотека отдельно допускает повторное использование удаленных слотов через allow_replace_deleted и replace_deleted=True. Это полезно для встроенных или локальных индексов, но не стоит принимать повторное использование слота за полноценную профилактику структуры графа. Если рабочий набор постоянно меняется, все равно измеряйте latency и recall, а не только capacity.
Общий вывод простой: сначала выясните, как конкретный движок хранит старые версии, когда запускает очистку, что происходит с чтением во время оптимизации и какие метрики раскрывает. Потом выбирайте пороги. Конфигурация, которая хорошо работает для сегментной базы, может ничего не значить для одного in-memory HNSW-файла.
Архитектура индекса определяет объем будущей уборки
Обслуживание становится намного проще, когда жизненный цикл данных виден в самой схеме. Не складывайте в одну коллекцию документы с разной скоростью старения только потому, что у них одинаковая размерность embedding.
Отделите почти неизменяемый корпус политик, инструкций и архивов от быстро меняющихся карточек товаров, сессий, статусов заказов или сообщений. Статичная коллекция может жить месяцами без заметной фрагментации. Коллекция оперативных данных нуждается в другом пороге, меньшем размере сегментов и отдельном бюджете на компакцию. Если смешать их, частые обновления будут заставлять обслуживать и холодный корпус.
Для multi-tenant поиска не создавайте коллекцию на каждого пользователя без очень веской причины. Это раздувает число индексов и усложняет обслуживание. Но и один общий индекс без продуманного shard key не спасает. Выберите гранулярность по размеру tenant-а, частоте обновления и характеру фильтров. Крупный tenant, который переписывает каталог каждые часы, не должен постоянно фрагментировать данные тысяч небольших клиентов.
Храните исходный текст или надежную ссылку на него вне векторной базы. Rebuild не должен зависеть от того, сохранились ли случайно старые payload-поля в удаляемой коллекции. Вам нужны воспроизводимые документы, deterministic chunking и версионные параметры embedding. Без этого любая перестройка превращается в раскопки: команда восстанавливает, каким кодом был создан индекс, вместо того чтобы проверить новую версию.
AI Router уместен в этой схеме как единый OpenAI-совместимый путь к моделям и как вариант размещения open-weight моделей на инфраструктуре в Казахстане, когда для команды важны residency и задержка. Но он не отменяет дисциплину данных: выбор момента для rebuild остается задачей метрик коллекции и контролируемого жизненного цикла embeddings.
Плохая привычка дешевле исправляется до первой аварии
Не ждите, пока индекс станет явно медленным. Введите baseline сразу после первой загрузки, измеряйте tombstones и churn раздельно, а массовые изменения оформляйте как миграции. Тогда rebuild перестает быть тревожной операцией, которую проводят ночью по графику p99, и становится обычной работой по правилам.
Если у вас уже есть коллекция с растущей задержкой, не начинайте с удаления и повторной загрузки. Снимите live count, физический count, долю удалений, количество сегментов, p95, p99 и recall на одном наборе запросов. Через сутки после остановки массовых обновлений повторите измерение. Если optimizer не сдвинул ситуацию, у вас есть основание для компакции или новой версии коллекции. Если сдвинул, вы нашли не «проблему векторной базы», а слишком быстрый поток изменений, который надо исправлять в ingestion-пайплайне.
Часто задаваемые вопросы
Нужно ли делать rebuild после каждого удаления в векторной базе?
Не обязательно. Небольшая доля tombstone-записей почти всегда дешевле постоянной перестройки. Планируйте компакцию, когда доля удалений растет, а latency, расход памяти или recall на контрольном наборе начинают уходить от базовой линии.
Как правильно посчитать долю удаленных векторов?
Для рабочей политики удобно считать от физического числа записей: удаленные записи делятся на живые плюс удаленные. Если база показывает только live count, сохраняйте снимки счетчиков до и после массовых замен, иначе вы не увидите накопленный мусор.
Считается ли 20% удаленных векторов критичным порогом?
Ориентир в 20% полезен как повод поставить задачу в план, потому что такое значение используется и в примере настройки Vacuum Optimizer Qdrant. Но это не универсальный аварийный порог: маленькие сегменты, высокий churn и рост tail latency могут требовать действий раньше.
Создает ли upsert фрагментацию, если идентификатор документа не меняется?
Нет. Upsert меняет логическое содержимое по тому же идентификатору, но многие движки сохраняют старую физическую версию до уборки. Частые переиндексации одинакового набора документов поэтому могут создавать столько же мусора, сколько явные delete-запросы.
Может ли фрагментация снизить recall, если удаленные точки скрыты из выдачи?
Нет. Потеря качества возникает, когда поиск тратит часть обхода на мертвые вершины, когда фильтры отбрасывают много кандидатов или когда движок временно ищет по неиндексированным сегментам. Проверяйте recall@k на фиксированном наборе, а не только ответ API и среднюю задержку.
Какие метрики смотреть перед rebuild?
Сначала посмотрите на p95 и p99 latency, recall@k, число сегментов, долю удалений и очередь фоновых задач. Если ухудшилась только средняя задержка, причина часто в нагрузке или фильтрах; rebuild без этой проверки легко проведет вас мимо проблемы.
Можно ли перестраивать индекс без остановки поиска?
В большинстве продакшен-систем сначала выбирают онлайн-компакцию, если движок умеет читать старый сегмент во время построения нового. Полный rebuild в отдельную коллекцию нужен при смене embeddings, метрики расстояния, параметров индекса или когда фоновая уборка не успевает за потоком обновлений.
Сколько свободного места нужно для rebuild векторной коллекции?
Оцените размер исходных векторов, временный второй индекс, payload и репликацию, а затем добавьте запас на фоновые операции. Не рассчитывайте емкость только по размеру итоговой коллекции: во время перестройки старые и новые структуры часто существуют одновременно.
Чем tombstone отличается от физического удаления вектора?
Смысл в том, что движок сохраняет логическое состояние удаления и очищает физические структуры позже. В HNSW это особенно заметно: старая вершина может остаться в графе, а поисковый обход все еще платит за встречу с ней, хотя в выдачу она не попадет.
Поможет ли AI Router решить проблему фрагментации векторной базы?
Да, но не как универсальная команда на каждый рост счетчика. AI Router может маршрутизировать рабочий трафик через единый OpenAI-совместимый API, а обслуживание векторного хранилища все равно должно опираться на метрики конкретной коллекции, ее churn и целевой recall.