Почему мы отказались от отдельной векторной БД?
Поддержка отдельного кластера Qdrant для 5 миллионов векторных эмбеддингов в нашем пайплайне Feedback AI создавала ненужные накладные расходы на эксплуатацию. Qdrant легко отдавал запросы быстрее 10 мс, однако двойная запись между PostgreSQL 16 (где хранились метаданные фидбека) и Qdrant постоянно вылезала рассинхроном. Когда в pgvector 0.7 появились параллельная сборка HNSW-индексов и итеративное сканирование, задержка векторного поиска p95 прямо внутри Postgres составила 34 мс. Консолидация хранилища убрала межсетевые задержки, упростила работу с транзакциями и снизила расходы на инфраструктуру благодаря запуску поиска на имеющихся репликах чтения.
В нашем проекте Feedback AI входящие отзывы пользователей превращаются в 1 536-мерные векторы с помощью модели OpenAI text-embedding-3-small. Изначально мы держали Qdrant v1.8 параллельно с Postgres. К моменту, когда датасет превысил 5 миллионов строк, поддержка отдельных расписаний снапшотов, пайплайнов синхронизации и токенов авторизации для двух разных БД съедала куда больше инженерного времени, чем отладка SQL-запросов.
Перенос эмбеддингов в Postgres 16 позволил работать с векторами как с обычным типом данных. Мы потеряли 20 мс чистой скорости запроса, но получили джойны в рамках одного запроса, честный ACID и ноль дополнительных забот по инфраструктуре.
Двойная запись и сбой синхронизации в три часа ночи
Двойная запись в PostgreSQL и Qdrant ломала атомарность транзакций во время сетевых сбоев. Воркер на Python 3.12 должен был сохранять метаданные в Postgres и отправлять вектор в Qdrant по HTTP. Когда Qdrant упал по таймауту под пиковой нагрузкой 12 ноября 2024 года, 4 120 записей метаданных остались без векторов. Нам пришлось срочно писать скрипт сверки на 140 строк. Перенос векторов в колонки PostgreSQL внутри стандартных блоков BEGIN...COMMIT полностью убрал проблему частичных обновлений.
В pgvector 0.7 вставка вектора происходит в той же транзакции, что и сохранение метаданных. Если обработка вектора падает или клиент отключается, Postgres откатывает все изменения разом.
-- Вставка текста фидбека, метаданных и 1536-d вектора в одной транзакции
BEGIN;
INSERT INTO feedback_items (id, tenant_id, content, created_at)
VALUES ('fb_98231', 'tenant_42', 'Great response times on API', NOW());
INSERT INTO feedback_embeddings (feedback_id, embedding)
VALUES ('fb_98231', '[0.012,-0.043,...0.008]'::vector(1536));
COMMIT;Этот фрагмент гарантирует отсутствиеосиротевших записей в нашей базе. После перехода на транзакционную запись мы удалили 450 строк кода повторных попыток из кодовой базы Celery.
Сравнение производительности Qdrant и pgvector 0.7
Сравнение Qdrant v1.8 и pgvector 0.7 на 5 миллионах 1 536-мерных векторов показывает разницу в расходе ресурсов. Qdrant хранит векторы в RAM или memory-mapped файлах для сверхбыстрого поиска p95 до 12 мс, но требует отдельного кластера. Postgres с HNSW-индексом pgvector потребляет на 28% меньше RAM при настройках m=16, ef_construction=64, обеспечивая p95 задержку до 35 мс и сохраняя данные рядом с реляционными таблицами.
| Метрика | Qdrant v1.8 | pgvector 0.7 (HNSW) |
|---|---|---|
| Задержка запроса p95 | 11,8 мс | 34,2 мс |
| Использование RAM (индекс + данные) | 19,8 ГБ | 14,2 ГБ |
| Время сборки индекса (5 млн векторов) | 26 минут | 42 минуты |
| Задержка поиска с фильтром | 410 мс (холодный индекс payload) | 38 мс (составной B-tree + HNSW) |
| Надёжность транзакций | Eventual (синхронизация двойной записью) | Соответствие ACID |
Как тюнинг HNSW влияет на память и точность поиска (recall)?
Подбор параметров HNSW в pgvector 0.7 напрямую определяет расход памяти и точность поиска при кластеризации отзывов. Значения m=16 и ef_construction=64 дали точность recall@10 в 97,4% на нашей выборке из 5 млн строк, потребовав 14,2 ГБ под векторный индекс. Увеличение hnsw.ef_search с 40 до 100 во время выполнения запросов добавило 11 мс к времени ответа, но подняло точность на сложных семантических крайних случаях. Дефолтные настройки оказались непригодны для высокой размерности.
В процессе миграции Feedback AI мы протестировали три стратегии индексирования. Индексы IVFFlat строились быстрее, но теряли в точности (падали ниже 81%), когда новые эмбеддинги поступали после построения индекса. HNSW удержал высокую точность при постоянной записи.
-- Создание HNSW-индекса для косинусного сходства 1536-d векторов
CREATE INDEX CONCURRENTLY idx_feedback_embeddings_hnsw
ON feedback_embeddings
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);Параллельное создание этого индекса заняло 42 минуты на нашей основной базе данных, не блокируя операции чтения и записи в основную таблицу. В pgvector 0.5 построение индексов полностью блокировало таблицы, не позволяя делать выкатки на живом продакшене.
От 38 секунд к 38 мс: выборка метаданных с векторными фильтрами
Фильтрация векторного поиска по структурированным метаданным в Qdrant работала медленно при поиске по холодным полям payload. Postgres же обрабатывает комбинированные фильтры с помощью обычных составных индексов. В нагрузке Feedback AI выборка векторов с фильтрацией по ID тенанта и диапазону дат требовала прокидывания индексов payload в Qdrant, что проседало при совпадении строгих критериев по нескольким колонкам. Postgres выполняет фильтрованный HNSW-скан благодаря итеративному сканированию индексов, объединяя векторное расстояние и проверки B-tree без костылей.
До версии pgvector 0.7 фильтрация векторных запросов в Postgres часто сваливалась в full table scan, если фильтр возвращал небольшой набор строк. Итеративное сканирование решает эту проблему: оно обходит граф HNSW до тех пор, пока не наберётся нужное количество строк, соответствующих условиям WHERE.
Наш типовой продакшен-запрос фильтрует данные по организации и дате перед ранжированием по семантике:
SET hnsw.ef_search = 64;
SELECT f.id, f.content, e.embedding <=> :query_vector AS distance
FROM feedback_items f
JOIN feedback_embeddings e ON f.id = e.feedback_id
WHERE f.tenant_id = 'tenant_99'
AND f.created_at >= '2024-01-01'
ORDER BY distance ASC
LIMIT 10;Запрос выполняется за 38 мс на 5 миллионах записей. В Qdrant непроиндексированные поля payload стабильно выбивали таймауты в 400 мс при мультитенантной нагрузке.
Что ломается при масштабировании pgvector свыше 5 млн векторов?
Рост pgvector свыше 5 миллионов векторов упирается в лимиты RAM и долгое время сборки индексов на стандартных облачных инстансах. Построение HNSW-индекса на 5 млн 1 536-мерных векторов заняло 42 минуты на AWS RDS db.r6g.xlarge с 32 ГБ RAM, выкрутив CPU на 98%. Если shared_buffers меньше суммарного размера таблицы и HNSW-индекса, задержка поиска вырастает с 34 мс до 280 мс из-за ухода Postgres в диск I/O.
Нам пришлось явно менять конфигурацию PostgreSQL под векторные нагрузки. Параметр maintenance_work_mem = 4GB стал обязательным: без этого воркеры падали по OOM во время параллельной сборки индексов.
Если объем векторов превысит 20 миллионов строк, требования Postgres к RAM резко вырастут. На таком масштабе понадобится партиционирование таблиц по ID тенанта или датам, чтобы удержать отдельные HNSW-индексы в пределах оперативной памяти.