8(800) 222 32 56
Панель управления
Инфраструктура

pgvector HNSW после UPDATE и DELETE: VACUUM, REINDEX и recall в RAG

pgvector HNSW после UPDATE и DELETE: VACUUM, REINDEX и recall в RAG
Подберите идеальное решение для ваших задач:
в России, США и Нидерландах обеспечат максимальную скорость. Воспользуйтесь всеми преимуществами надежного оборудования. Базовая помощь и техническое обслуживание входят в пакет услуг.
После массового обновления базы знаний RAG продолжает отвечать, но часть top-k запросов начинает возвращать меньше подходящих документов, а обслуживание PostgreSQL занимает непривычно долго. Первая реакция — перестроить HNSW, но это опасный автоматизм: похожий симптом дают фильтрация после approximate scan, слишком маленький search budget и смена embedding-модели. В этой статье разберём отдельный production-сценарий — churn из UPDATE/DELETE, dead tuples и обслуживание HNSW в pgvector — и построим проверяемый цикл baseline → REINDEX CONCURRENTLY → VACUUM → recall gate без обещания, что REINDEX лечит любой RAG.

Готовы перейти на современную серверную инфраструктуру?

В King Servers мы предлагаем серверы как на AMD EPYC, так и на Intel Xeon, с гибкими конфигурациями под любые задачи — от виртуализации и веб-хостинга до S3-хранилищ и кластеров хранения данных.

  • S3-совместимое хранилище для резервных копий
  • Панель управления, API, масштабируемость
  • Поддержку 24/7 и помощь в выборе конфигурации

Создайте аккаунт

Быстрая регистрация для доступа к инфраструктуре

Сначала отделите HNSW maintenance от обычного pgvector tuning

У KingServers уже есть общий материал про pgvector в PostgreSQL для RAG: там важны выбор HNSW/IVFFlat, фильтрация, миграции и базовая эксплуатация. Здесь граница намеренно уже. Мы не выбираем индекс заново и не сравниваем PostgreSQL с отдельной vector DB. Нас интересует конкретный класс инцидентов: таблица с embeddings переживает заметный поток UPDATE/DELETE, после чего меняются maintenance time, количество dead tuples или проверяемый recall.

Это разграничение важно, потому что три независимые проблемы выглядят похоже. Первая — maintenance: PostgreSQL накопил obsolete tuple versions, а HNSW vacuum стал дорогим. Вторая — query budget: approximate scan просмотрел недостаточно кандидатов, особенно при фильтрах. Третья — semantic drift: изменилась embedding-модель или схема chunking, и старый индекс физически исправен, но вектора уже не соответствуют новому retrieval pipeline.

Не начинайте с команды REINDEX. Начните с трёх доказательств: версия pgvector, состояние таблицы/индекса и одинаковый canary-набор запросов для approximate-vs-exact сравнения. Только если эти слои указывают на maintenance path, имеет смысл расходовать I/O на rebuild.

Отдельный случай — смена embedding-модели. Для него у нас есть материал про embedding lifecycle и переиндексацию после смены модели. Обычный HNSW REINDEX не пересчитывает embeddings и не исправляет несовместимое vector space.

UPDATE и DELETE создают churn в PostgreSQL и усложняют обслуживание HNSW-индекса pgvector

Version gate: проверьте pgvector до любого HNSW vacuum runbook

Версия здесь не формальность. В changelog pgvector есть несколько свежих исправлений именно вокруг HNSW vacuum: версия 0.8.3 от 17 июня 2026 года исправляла возможную corruption при vacuum, а 0.8.4 от 30 июня — ошибку hnsw graph not repaired и возможную ошибку при inserts во время HNSW vacuum. В актуальном changelog последняя версия — 0.8.6 от 29 июля 2026 года.

Это не означает, что любой production обязан немедленно прыгнуть на последнюю minor-версию без canary. Но если ваш maintenance-процесс построен на HNSW vacuum и вы всё ещё ниже ветки с этими исправлениями, сначала разберите upgrade path и release notes, а уже затем интерпретируйте странное поведение как «нормальный bloat».

PostgreSQL тоже фиксируйте в evidence bundle. На дату этой статьи current stable branch — PostgreSQL 18.6; при этом runbook ниже применим и к поддерживаемым 17/16/15/14, если соответствующие команды доступны. Не переносите значения memory/autovacuum между major-версиями вслепую.

sqlversion-gate.sql
SELECT version();

SELECT extname, extversion
FROM pg_extension
WHERE extname = 'vector';

SELECT current_setting('server_version_num') AS server_version_num;
Версии PostgreSQL и pgvector перед maintenance

Что делать с version gate?

Перед разбором HNSW vacuum-инцидента приоритетно проверьте upgrade path: в 0.8.3/0.8.4 были исправления корректности и ошибок vacuum.
Вы уже на ветке с ключевыми vacuum fixes, но всё равно сверяйте changelog и воспроизводите symptom на canary.
Версия сама по себе не доказывает, что нужен REINDEX. Переходите к evidence: dead tuples, plan, maintenance time и recall.

Что UPDATE и DELETE оставляют в PostgreSQL — и при чём здесь HNSW

PostgreSQL использует MVCC: удалённая строка или старая версия строки после UPDATE не исчезает физически мгновенно. Она остаётся как dead tuple, пока VACUUM не сможет её обработать. Поэтому write-heavy RAG corpus после массовой замены документов может иметь гораздо больше физической работы, чем подсказывает простое COUNT(*).

Для HNSW есть дополнительная практическая деталь: официальный pgvector README прямо предупреждает, что vacuum HNSW-индексов может занимать заметное время и рекомендует перед ним reindex. Это важнее, чем пытаться вывести универсальное правило вроде «при N% dead tuples всегда REINDEX»: такого порога документация не задаёт, а допустимый churn зависит от размера таблицы, storage I/O, окна обслуживания и запроса.

Нужно различать table dead tuples, index physical size и retrieval behavior. Большой n_dead_tup — сигнал к расследованию, но сам по себе не доказывает semantic recall regression. И наоборот, мало результатов в HNSW-запросе не обязательно означает bloat: при фильтрах pgvector применяет filter после approximate scan, поэтому search budget может закончиться раньше, чем будет найден нужный top-k.

Схема диагностики HNSW после churn: dead tuples, query tuning, REINDEX CONCURRENTLY, VACUUM и recall gate

Baseline до REINDEX: dead tuples, index size, plan и recall

Evidence bundle собирайте до первого maintenance action. В нём должны быть stable table/index identity, версия extension, пример реального retrieval query, статистика таблицы и размер HNSW. Для application layer сохраните небольшой набор canary queries: не случайные synthetic vectors, а те типы запросов, на которых команда замечает проблему.

pg_stat_user_tables даёт оценочные n_live_tup/n_dead_tup и время последнего vacuum/autovacuum. Размер индекса удобно фиксировать через pg_relation_size. План нужно смотреть на реальном WHERE и том же операторе distance, который использует приложение.

sqlhnsw-baseline.sql
SELECT
  schemaname,
  relname,
  n_live_tup,
  n_dead_tup,
  last_vacuum,
  last_autovacuum,
  n_mod_since_analyze
FROM pg_stat_user_tables
WHERE schemaname = 'public'
  AND relname = 'rag_documents';

SELECT
  pg_size_pretty(pg_relation_size('public.rag_documents')) AS table_size,
  pg_size_pretty(pg_relation_size('public.rag_documents_embedding_hnsw_idx')) AS hnsw_size;

EXPLAIN (ANALYZE, BUFFERS)
SELECT id
FROM public.rag_documents
WHERE tenant_id = :tenant_id
ORDER BY embedding <=> :query_embedding
LIMIT 20;
Минимальный baseline перед HNSW maintenance

Baseline достаточно полный?

Baseline для pgvector HNSW сравнивает dead tuples, размер индекса, query plan и recall

Почему pgvector рекомендует REINDEX CONCURRENTLY перед VACUUM

Для HNSW официальный pgvector README даёт необычно конкретную рекомендацию: если vacuum идёт долго, сначала выполните REINDEX INDEX CONCURRENTLY, затем VACUUM таблицы. Это не общий закон PostgreSQL для любого index type, а pgvector-specific operational hint.

REINDEX строит новую копию индекса из данных таблицы. В production вариант CONCURRENTLY позволяет продолжать обычные INSERT/UPDATE/DELETE, тогда как обычный REINDEX блокирует writes на время rebuild. Цена concurrency — больше общей работы, дополнительные scans и ожидание транзакций.

Команды не объединяйте в одну большую transaction. PostgreSQL не разрешает REINDEX CONCURRENTLY внутри transaction block; VACUUM тоже запускается отдельно. И не превращайте последовательность ниже в cron «после каждого batch»: сначала symptom и evidence, затем maintenance.

sqlhnsw-maintenance.sql
REINDEX INDEX CONCURRENTLY public.rag_documents_embedding_hnsw_idx;

VACUUM public.rag_documents;

-- Если после большого churn нужна свежая planner statistics:
ANALYZE public.rag_documents;
Pgvector-specific последовательность rebuild и vacuum

Какой следующий шаг оправдан evidence?

Безопасная последовательность обслуживания HNSW: REINDEX CONCURRENTLY, progress, VACUUM и recall canary

CONCURRENTLY не блокирует writes, но требует отдельного resource budget

Слово CONCURRENTLY иногда ошибочно читают как «дёшево и незаметно». PostgreSQL описывает обратный trade-off: concurrent rebuild делает больше работы, выполняет два scans таблицы и ждёт завершения транзакций, которые могут мешать переключению индекса. Кроме времени, вы платите CPU, memory и storage I/O; именно поэтому rebuild способен ухудшить application latency даже без lock, который блокирует writers.

Для крупной RAG-таблицы maintenance budget должен жить рядом с пользовательским SLO. До запуска определите stop condition: например, рост database queue/latency выше внутреннего canary threshold, replication lag выше вашего допустимого окна или saturation storage. Числа здесь нельзя копировать из статьи — они зависят от дисков, размера HNSW и текущего трафика.

PostgreSQL также ограничивает concurrent index work: на одной таблице одновременно может идти только один concurrent index build. Длинные транзакции способны затянуть фазы ожидания. Поэтому перед окном rebuild полезно посмотреть не только CPU, но и transaction age.

sqlreindex-progress.sql
SELECT
  pid,
  datname,
  relid::regclass AS table_name,
  index_relid::regclass AS index_name,
  command,
  phase,
  lockers_total,
  lockers_done,
  blocks_total,
  blocks_done,
  tuples_total,
  tuples_done
FROM pg_stat_progress_create_index
WHERE relid = 'public.rag_documents'::regclass;
Наблюдение за concurrent rebuild

Если вы хотите оценить resource envelope заранее, делайте rebuild на свежем production-sized clone, а не на маленькой demo-базе. Это не докажет точную длительность в production, зато покажет порядок I/O и memory pressure без риска для live traffic. Ценность выделенной инфраструктуры в этом сценарии — не обещание «pgvector станет быстрее», а управляемый запас IOPS/CPU и возможность запланировать maintenance без соседнего noisy workload.

REINDEX CONCURRENTLY требует запаса CPU, памяти и storage I/O при продолжающемся RAG-трафике

VACUUM после rebuild: что он освобождает и чего не делает

Обычный VACUUM обрабатывает dead tuples и делает освободившееся место доступным для повторного использования. В normal PostgreSQL operation это routine maintenance, который обычно может идти параллельно с чтениями и записями. При этом freed space чаще остаётся внутри relation для будущих tuples — ожидать, что файл на диске сразу резко уменьшится, неправильно.

VACUUM FULL — другая операция: она переписывает таблицу, может вернуть больше места операционной системе, но требует ACCESS EXCLUSIVE и значительно тяжелее. Не используйте FULL как автоматическое продолжение HNSW runbook только потому, что pg_relation_size выглядит большим.

В PostgreSQL 18 параметр INDEX_CLEANUP у VACUUM по умолчанию AUTO: движок может пропустить index vacuuming, если dead tuples очень мало и стоимость обхода индексов выше пользы. Это ещё одна причина не сводить политику к одному percentage threshold.

После завершения сравните тот же evidence bundle: dead tuples, длительность следующего vacuum, plan и application canary. Если изменился только storage counter, а retrieval SLO/recall остались прежними, называйте это доказательством maintenance mechanism — не «ускорением RAG».

Recall gate: approximate HNSW сравнивайте с exact search на том же snapshot

pgvector предлагает простой способ мониторить recall: сравнивать результаты approximate search с exact search, временно отключая index scan. Для production-проверки этого мало сделать один раз случайным vector. Нужен фиксированный набор запросов, одинаковые filters, одинаковый top-k и по возможности один database snapshot, чтобы concurrent writes не подменили причину различий.

Exact control отвечает на узкий вопрос: насколько approximate index path воспроизводит ближайших соседей, которые получил бы exact search. Он не измеряет качество RAG целиком: если embedding-модель плохо кодирует предметную область, exact top-k тоже может быть семантически плохим. Поэтому index recall и answer quality держите раздельно.

sqlrecall-canary.sql
BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;

-- 1) approximate HNSW path
SELECT id
FROM public.rag_documents
WHERE tenant_id = :tenant_id
ORDER BY embedding <=> :query_embedding
LIMIT 20;

-- 2) exact control, как рекомендует pgvector для recall monitoring
SET LOCAL enable_indexscan = off;

SELECT id
FROM public.rag_documents
WHERE tenant_id = :tenant_id
ORDER BY embedding <=> :query_embedding
LIMIT 20;

ROLLBACK;
Approximate и exact control на одном snapshot

Сравнивайте IDs в тестовом harness: top-k overlap, попадание в заранее размеченный relevant set и стабильность на нескольких категориях запросов. Если exact и approximate расходятся только при фильтре, переходите к query-budget диагностике. Если расхождение коррелирует с churn и меняется после controlled maintenance, это уже гораздо сильнее как attribution.

Iterative scans лечат filter starvation, а не заменяют maintenance

У HNSW есть другой источник «пропавших» результатов. В approximate search pgvector применяет обычный WHERE-filter после index scan. Если условие пропускает небольшую долю rows, из первоначального candidate set может остаться меньше строк, чем запрошено через LIMIT. Это не обязательно повреждение индекса.

Начиная с pgvector 0.8.0 доступны iterative index scans. Режим strict_order продолжает обход, сохраняя строгий порядок distance; relaxed_order может дать лучшую recall ценой слегка relaxed ordering. Обход ограничивается, в частности, hnsw.max_scan_tuples; default в текущем source — 20 000, а hnsw.scan_mem_multiplier — 1. Эти default не надо автоматически увеличивать: больший search budget покупает recall дополнительной работой.

sqliterative-scan-canary.sql
BEGIN;

SHOW hnsw.ef_search;
SHOW hnsw.max_scan_tuples;
SHOW hnsw.scan_mem_multiplier;

SET LOCAL hnsw.iterative_scan = strict_order;

SELECT id
FROM public.rag_documents
WHERE tenant_id = :tenant_id
ORDER BY embedding <=> :query_embedding
LIMIT 20;

ROLLBACK;
Scoped canary без изменения глобальных defaults

Что именно меняет результат?

Если iterative scan/ef_search восстанавливает нужный top-k на тех же данных, сначала оптимизируйте query path и filters.
Если symptom связан с churn, dead tuples и тяжёлым HNSW vacuum, проверяйте controlled REINDEX → VACUUM.
Если изменились модель, dimension, normalization или chunking, старый index maintenance не заменяет re-embed.
Decision tree отличает HNSW maintenance от проблем фильтрации, ef_search и смены embedding-модели

Autovacuum: настраивайте trigger под churn, но не превращайте его в магию

Routine vacuum должен оставаться автоматизированным. PostgreSQL запускает autovacuum, когда число obsolete tuples превышает threshold, который зависит от базового значения, scale factor и размера таблицы. Для очень большой RAG table даже стандартный scale factor может означать большой абсолютный объём изменений до следующего vacuum; для маленькой таблицы слишком агрессивный trigger создаст лишнюю фоновую работу.

Поэтому сначала смотрите фактический churn и table-specific settings. Не копируйте из чужого runbook autovacuum_vacuum_scale_factor=0.01 или другое красивое число без бюджета: частота vacuum должна соответствовать write rate, storage bandwidth и допустимому background load.

sqlautovacuum-evidence.sql
SHOW autovacuum;
SHOW autovacuum_vacuum_threshold;
SHOW autovacuum_vacuum_scale_factor;

SELECT
  n_live_tup,
  n_dead_tup,
  last_autovacuum,
  autovacuum_count
FROM pg_stat_user_tables
WHERE relid = 'public.rag_documents'::regclass;

SELECT reloptions
FROM pg_class
WHERE oid = 'public.rag_documents'::regclass;
Проверка global и table-specific autovacuum policy

Если autovacuum явно не успевает за predictable churn, меняйте одну table-level policy за раз и наблюдайте несколько циклов. Но не путайте более частый routine VACUUM с pgvector-specific рекомендацией reindex-before-vacuum для тяжёлого HNSW: это разные уровни maintenance.

Rollout: canary, stop condition и восстановление без легенды про «нулевой риск»

На production не начинайте с самого большого HNSW. Сначала проверьте процедуру на production-sized clone или на менее критичной таблице с той же схемой. На live primary используйте REINDEX CONCURRENTLY только когда нужен write availability и у вас есть запас ресурсов; если сервис можно полностью остановить в maintenance window, обычный REINDEX дешевле по общей работе.

У rollout должны быть три gate. Infrastructure gate: rebuild завершён, index valid, VACUUM не завис, storage и replication вернулись к baseline. Retrieval gate: canary approximate-vs-exact не ухудшился и production filters возвращают ожидаемый top-k. Application gate: RAG endpoint отвечает в вашем SLO и не переносит bottleneck, например, с database I/O на CPU или embedding service.

Rollback здесь не означает, что после каждого REINDEX вы обязаны восстанавливать базу из backup. REINDEX перестраивает индекс из данных таблицы; если data layer не менялся, при неудачном application canary чаще нужно остановить дальнейший rollout, вернуть query settings и расследовать planner/filter path. А PITR остаётся независимым data-protection control на случай ошибочных data changes, а не штатной кнопкой отмены index maintenance.

Maintenance можно считать завершённым?

Canary проверяет recall нового HNSW-индекса до переключения production RAG и сохраняет rollback

Decision tree: когда ничего не делать, когда tuning, а когда REINDEX + VACUUM

Главный вывод — HNSW maintenance должен начинаться не с команды, а с классификации причины. Если таблица почти append-only, autovacuum справляется, а canary recall стабилен, периодический REINDEX «для профилактики» только создаст ненужный I/O. Если меньше результатов возникает при selective filters, проверяйте ef_search, iterative scans и filter indexes. Если изменилась embedding-модель, запускайте полноценный re-embed/migration lifecycle. И только когда churn, dead tuples, HNSW maintenance symptom и canary указывают в одну сторону, применяйте pgvector-specific последовательность REINDEX INDEX CONCURRENTLYVACUUM.

Для инфраструктуры решение тоже практическое. Dedicated database node полезен здесь не потому, что сам по себе повышает recall, а потому что даёт контролируемый storage/CPU envelope, понятный maintenance window и возможность воспроизвести один hardware profile. Если у вас embeddings занимают значимую долю PostgreSQL и вы ещё выбираете границу системы, сначала сверитесь со сравнением отдельной vector DB и PostgreSQL. А если retrieval cost начинает доминировать, отдельно проверьте semantic cache — это другой слой и он не должен маскировать больную database maintenance.

Практический следующий шаг: сохраните один baseline SQL bundle и 20–50 canary queries, прогоните их до и после следующего реального churn-window и только затем решайте, нужен ли recurring maintenance policy. Так у команды появится причинная связь между изменением базы, состоянием HNSW и пользовательским retrieval, а не набор периодических команд «потому что так принято».

Что поставить в maintenance policy?

Proxmox VE на выделенном сервере: как собрать частное облако для бизнеса в 2026 году
Proxmox

Proxmox VE на выделенном сервере: как собрать частное облако для бизнеса в 2026 году

Практический гид по Proxmox VE на выделенном сервере: выбор железа, ZFS и Ceph, сеть, кластер, HA, backup и безопасность.

NVMe/TCP TLS и DH-HMAC-CHAP: защищаем хранилище весов LLM
AI

NVMe/TCP TLS и DH-HMAC-CHAP: защищаем хранилище весов LLM

Production-runbook по защите удалённых весов LLM: NVMe/TCP TLS, DH-HMAC-CHAP, kernel keyring, canary, negative controls, vLLM gate и rollback.

I/O-изоляция LLM-узла: cgroup v2 и systemd против штормов NVMe
AI

I/O-изоляция LLM-узла: cgroup v2 и systemd против штормов NVMe

Практический runbook по I/O-изоляции LLM-узла: cgroup v2 io.max, systemd slices, PSI, два gate, canary rollout и rollback.