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, алерты, проблемы с отказоустойчивостью. Я презираю подход, когда новую базу тащат в проект только потому, что разраб прочитал хайповую статью. Векторная СУБД — это ещё один узел, который обязательно отвалится в три часа ночи в субботу.
Когда мы в Morana Labs пилили движок визуального поиска для огромного ритейл-каталога с бешеным потоком обновлений инвентаря, мы честно пытались удержать всё в одной базе. Потому что реальный векторный поиск — это почти всегда гибридный поиск. У вас есть вектор изображения товара, и у вас есть жёсткая бизнес-логика. Вы не просто ищете «похожие кроссовки». Вам нужны похожие кроссовки, которые прямо сейчас есть в наличии на складе в конкретном городе, на которые не действует глобальная скидка, и которые доступны пользователю с премиум-аккаунтом.
Попытка разнести эти данные по двум системам порождает распределённые транзакции. Товар купили — нужно обновить флаг наличия в Постгресе и синхронно обновить 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 спайки, но настоящая инженерия заключается в том, чтобы менять архитектуру ровно за шаг до того, как система начнёт пожирать инфраструктурный бюджет и нервы команды.