CREATE EXTENSION IF NOT EXISTS vector;
ALTER SYSTEM SET shared_buffers = '16GB';
ALTER SYSTEM SET max_parallel_workers_per_gather = 4;
CREATE INDEX ON catalog_items USING hnsw (embedding vector_cosine_ops);Смотришь на этот кусок SQL и понимаешь: разработчик по ту сторону экрана искренне верит, что закрыл задачу векторного поиска. Поднял расширение, накинул памяти, повесил индекс — можно идти в бар. До первого реального спайка нагрузки.
Тема «Qdrant против pgvector на проде: где проходит граница в 10 млн векторов» всплывает на каждом втором аудите архитектуры. Заказчики приносят ее как некий священный грааль: зачем нам отдельная векторная база, если слоны умеют все? Спойлер: слоны лягут, если не понимать физику работы с памятью в обеих системах.
Qdrant против pgvector на проде: где проходит граница в 10 млн векторов
Начнем с базы. На объемах до миллиона векторов вообще плевать, что вы используете. HNSW (Hierarchical Navigable Small World) — это математический граф, его механика неизменна. Замеряем латентность в тепличных условиях: Qdrant выдает на чтение порядка 4мс по p50, pgvector отдает результат за 11мс. Кого волнует разница в семь миллисекунд? Никого. Если ваш поиск — это часть пайплайна RAG, то следующая за ним генерация ответа большой языковой моделью все равно сожрет свои секунды. Накладные расходы на инференс перекроют любую разницу в БД.
Но миллион векторов — это песочница для пет-проектов. Мы идем в хардкорный продакшен, где графики рисуют совершенно иную картину.
Десять миллионов векторов. Конкурентная нагрузка на чтение. Параллельно в фоне льется поток апдейтов индексов. Что происходит под капотом?
Pgvector живет внутри PostgreSQL. Постгрес — это монолит, спроектированный для транзакционной целостности. Его святая святых — shared_buffers и кэш операционной системы. Поиск по графу HNSW — это серия псевдослучайных прыжков по узлам в оперативной памяти. Когда весь индекс (а для 10 миллионов 768-мерных эмбеддингов это десятки гигабайт) помещается в RAM — все сносно. Как только индекс раздувается и перестает влезать в выделенные буферы, начинается ад.
Постгрес запускает алгоритмы вытеснения страниц. Страницы графа улетают на диск. Следующий запрос на поиск требует именно этих узлов графа. Случайное чтение с диска. Cache-miss. И тут ваши 11мс моментально превращаются в 250мс, а p99 улетает за секунду. График латентности начинает напоминать кардиограмму висячего моста в шторм. SSD-диск захлебывается в IOPS, потому что чтение абсолютно хаотичное — граф не лежит на диске линейно. Векторный поиск по своей природе ломает все паттерны упреждающего чтения (read-ahead), на которые так полагается ядро Linux.
И тут я всегда задаю один вопрос: вы готовы тюнить eviction-политики Постгреса ради одной таблицы с векторами, ломая производительность остальных транзакционных данных? Будете крутить random_page_cost? Это костыли. В итоге вы просто побежите накидывать RAM в облаке, раздувая бюджет.
А что Qdrant? Это движок на Rust, написанный инженерами, которые понимают, как работает железо. Он не пытается дублировать кэширование, как это делает PostgreSQL со связкой shared_buffers и OS Cache. Qdrant мапит файлы напрямую в память через mmap. Он делегирует управление страницами ядру, но структура хранения самих векторов и графа оптимизирована так, чтобы минимизировать фрагментацию. Плюс квантование. Скалярное (Scalar Quantization) или Product Quantization в Qdrant сжимают размер графа в памяти в разы, практически не теряя в полноте (recall). Постгресу с pgvector такие агрессивные оптимизации памяти даются гораздо тяжелее, потому что он скован рамками архитектуры таблиц и WAL.
На одном нормальном узле (скажем, 64GB RAM и быстрые NVMe) Qdrant спокойно держит до 30 миллионов векторов. Латентность остается плоской. Никакой магии — просто специализированный инструмент делает ровно то, для чего создан.
Давайте сделаем резкий поворот. Если Qdrant такой крутой, почему мы вообще обсуждаем pgvector?
Потому что есть суровая реальность эксплуатации. Добавление любого stateful-сервиса в инфраструктуру — это боль. Это новые скрипты бэкапов, дашборды в Grafana, алерты, проблемы с отказоустойчивостью. Я презираю подход, когда новую базу тащат в проект только потому, что разраб прочитал хайповую статью. Векторная СУБД — это еще один узел, который обязательно отвалится в три часа ночи в субботу.
Когда мы в MoranaLabs пилили движок визуального поиска для огромного ритейл-каталога с бешеным потоком обновлений инвентаря, мы честно пытались удержать все в одной базе. Потому что реальный векторный поиск — это почти всегда гибридный поиск. У вас есть вектор изображения товара, и у вас есть жесткая бизнес-логика. Вы не просто ищете «похожие кроссовки». Вам нужны похожие кроссовки, которые прямо сейчас есть в наличии на складе в конкретном городе, на которые не действует глобальная скидка, и которые доступны пользователю с премиум-аккаунтом.
Попытка разнести эти данные по двум системам порождает распределенные транзакции. Товар купили — нужно обновить флаг наличия в Постгресе и синхронно обновить payload (метаданные) в Qdrant. А сеть моргнула. Постгрес закоммитил, а до Qdrant апдейт не долетел. Рассинхрон стейта. Пользователь кликает на товар из выдачи, а его нет в наличии. Бизнес теряет деньги.
И вот тут наступает момент истины. Выбор инструмента — это всегда честный трейд-офф:
- Pgvector забирает победу, когда вектор — это лишь один из атрибутов транзакционной сущности. Если вам критически важны ACID-гарантии, если вы делаете тяжелые JOIN-ы векторов с таблицами прав доступа, биллингом или динамическими ценами, разделять эти данные — архитектурное самоубийство. В таком сценарии вы сжимаете зубы, терпите деградацию скорости, скейлите Постгрес вертикально, заливаете сервер оперативной памятью и молитесь, чтобы индекс не выпал из кэша.
- Qdrant доминирует, когда векторный поиск — это самостоятельный высоконагруженный сервис, а метаданные для фильтрации меняются редко или их рассинхрон не фатален. Когда объемы переваливают за те самые 10 миллионов, и бизнес требует железных SLA на latency, которые монолит больше не может обеспечить.
Анатомия распределенного тупика
Что происходит, когда вы пробиваете потолок вертикального масштабирования в PostgreSQL? Железо конечно. Рано или поздно вы упретесь в лимиты материнской платы по памяти. И тут шардинг стучится в дверь.
Шардировать векторные данные в реляционной БД — задача для сильных духом. Стандартное декларативное партиционирование в Постгресе прекрасно работает для логов или временных рядов. Но оно отвратительно ложится на графовые индексы. Векторы не бьются ровно по хэшам так, чтобы поиск оставался эффективным. Запрос на поиск ближайших соседей (k-NN) придется отправлять на все партиции, а потом сливать результаты.
Вы приходите к тому, что пишете кастомную логику на уровне приложения. Ваш бэкенд начинает маршрутизировать запросы, размазывать векторы по независимым инстансам PostgreSQL, а потом агрегировать результаты через Scatter-Gather прямо в коде. По факту, вы за деньги бизнеса пишете свою собственную распределенную СУБД на костылях, перенося сложность консенсуса и маршрутизации на слой приложения. Ошибка в логике ребалансировки — и вы теряете данные или ломаете recall.
С другой стороны баррикад все скучно и предсказуемо. Qdrant — это распределенная система из коробки. Там под капотом работает протокол Raft для консенсуса. Уперлись в лимиты ноды? Подняли новый сервер, добавили в кластер. Система сама перебалансирует шарды, индексы плавно перестроятся в фоне. Вы просто пьете кофе и наблюдаете за метриками сети. Он создан для того, чтобы масштабироваться по горизонтали, скрывая всю эту боль от разработчика.
Еще один невидимый убийца производительности на этих объемах — время построения индекса и WAL amplification. Попробуйте перестроить гигантский HNSW-индекс в Постгресе. Вы выкручиваете maintenance_work_mem до предела, база уходит в глубокую задумчивость, а если в этот момент падает процесс — начинаете все сначала. Параллельно с этим любой апдейт вектора генерирует гигантские объемы бинарного мусора в Write-Ahead Log. Если у вас настроена потоковая репликация, эти гигабайты летят по сети на реплики. Интенсивная заливка эмбеддингов заставит read-only реплики отставать на минуты.
В Qdrant векторные данные иммутабельны в контексте конкретного сегмента графа. При обновлениях он пишет в новые сегменты и фоном делает merge (аналог LSM-деревьев), а внутренний лог оптимизирован исключительно под многомерные массивы. Это радикально снижает нагрузку на диск и сеть при кластеризации.
Граница в 10 миллионов векторов — это не теоретический предел расширения pgvector. Математически он переварит и больше. Это граница архитектурного здравого смысла. Рубеж, после которого стоимость владения монолитом начинает экспоненциально превышать затраты на внедрение профильного векторного движка. Можно бесконечно заливать проблему железом в облаке, игнорируя cache-miss спайки, но настоящая инженерия заключается в том, чтобы менять архитектуру ровно за шаг до того, как система начнет пожирать инфраструктурный бюджет и нервы команды.