pgvector 0.6 и HNSW: добавляем векторный поиск в PostgreSQL 16 без отдельной СУБД
pgvector 0.6 добавил нормальные HNSW-индексы в PostgreSQL - пробуем их на корпоративном RAG-пайплайне с 1M векторов и смотрим, стоит ли отказываться от Qdrant.
Релиз pgvector 0.6 с поддержкой HNSW-индексов для ускорения векторного поиска в PostgreSQL, ноябрь 2023
В ноябре вышел pgvector 0.6, и главная новость в нём - параллельное построение HNSW-индексов. HNSW появился ещё в 0.5 (май 2023), но строился в один поток, что на больших объёмах превращалось в многочасовое ожидание. До 0.5 pgvector умел только IVFFlat, который при больших объёмах давал не самые весёлые результаты по задержке. Поэтому в нашем сравнении векторных хранилищ мы в итоге выбрали Qdrant для клиента с полумиллионом фрагментов - там HNSW был с самого начала. Теперь pgvector обновился, и стало интересно: пересматривать ли этот выбор на другом, более крупном проекте?
Контекст: ещё один корпоративный RAG
Параллельно с тем проектом идёт другой: промышленный холдинг, RAG поверх технических паспортов оборудования, регламентов и архивной переписки по согласованиям. Объём после чанкования - около миллиона векторов размерностью 1024 (используем multilingual-e5-large с увеличенным измерением под специфику технических текстов). Клиент - консервативный: PostgreSQL 16 уже стоит, уже настроен, уже резервируется. Вторую СУБД в инфраструктуру добавлять никто не хочет категорически.
Вариантов три: IVFFlat в старом pgvector, HNSW в новом pgvector 0.6, или всё же Qdrant рядом. Обновление pgvector до 0.6 сделали через ALTER EXTENSION pgvector UPDATE - пара минут, нет даунтайма.
Что изменилось в HNSW у pgvector 0.6
IVFFlat работает иначе: сначала кластеризация, потом поиск в ближайших кластерах. При nprobes=1 быстро, но recall падает; при nprobes=10 recall лучше, но задержка растёт. Настройка - постоянный компромисс, и при изменении данных индекс надо перестраивать.
HNSW (Hierarchical Navigable Small World) строит граф навигации по векторному пространству. Основные параметры при создании индекса:
m- количество связей на узел графа. Больше m - лучше recall, больше памяти, дольше построение.ef_construction- ширина поиска при построении графа. Влияет на качество индекса, не на скорость запроса.ef_search- ширина поиска при запросе (задаётся черезSET hnsw.ef_search). Это основной рычаг компромисса recall/latency в runtime.
Мы взяли m=16, ef_construction=64 как базовую конфигурацию - стандартные значения, с которых обычно начинают. Построение индекса на 1M векторов размерностью 1024 на NVMe-железе заняло несколько часов. Это нормально, индекс строится один раз; важно понимать, что на добавлении новых векторов поточно HNSW не тормозит так, как перестройка IVFFlat.
Что получилось с latency
На одиночных запросах (top-10, косинусное расстояние) с hnsw.ef_search=40 задержка укладывается в sub-50ms даже при холодном кеше. При прогретом кеше - ещё лучше. Это принципиально отличается от того, что мы наблюдали с IVFFlat на том же объёме: там 50ms были на пределе при оптимальной настройке nprobes, и нестабильно.
Под параллельной нагрузкой, когда десятки запросов идут одновременно, latency растёт - PostgreSQL разделяет пул соединений между векторными и обычными транзакциями. Здесь у Qdrant как изолированного процесса структурное преимущество, и оно никуда не делось. Но для профиля нагрузки этого клиента (несколько активных пользователей, не десятки одновременных запросов) pgvector справляется.
Операционная сторона
Добавление pgvector 0.6 в существующий PostgreSQL 16 - это буквально:
CREATE EXTENSION IF NOT EXISTS vector;
ALTER TABLE documents ADD COLUMN embedding vector(1024);
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
Бэкапы, мониторинг, репликация - всё работает как обычно, потому что это обычный PostgreSQL. pg_dump забирает векторы вместе с остальными данными. Zabbix мониторит как раньше. Никакого нового сервиса, никаких отдельных снапшотов, никакой дополнительной операционной нагрузки на команду.
Это не мнимое преимущество. У клиента небольшая инфраструктурная команда, и каждый новый сервис в продакшне - это реальные расходы внимания на мониторинг, обновления, инциденты.
Фильтрация - ограничение, которое остаётся
Главный изъян pgvector 0.6 при работе с RAG - та же история, что мы фиксировали раньше. HNSW-индекс не поддерживает pre-filtering: сначала обходится граф, потом применяется WHERE. Если фильтр избирательный - отсеивает, скажем, 90% корпуса - pgvector вернёт из графа topK результатов, потом отфильтрует большинство из них, и реальный top-10 будет сильно деградированным по качеству.
Для этого клиента фильтрация по метаданным пока не критична - ищут по всему корпусу, права доступа не гранулярные. Если это изменится, вопрос о Qdrant встанет снова.
Итог на текущий момент
Для аналитической инфраструктуры с консервативным стеком pgvector 0.6 стал вполне рабочим выбором. HNSW закрыл главный операционный аргумент против pgvector на больших объёмах - задержка поиска. Sub-50ms на 1M векторов без отдельной векторной СУБД - это достаточно хороший результат для корпоративного RAG с умеренной нагрузкой.
Мы всё ещё считаем, что у Qdrant есть сценарии, где он объективно лучше: высокая параллельная нагрузка, сложная фильтрация по метаданным, необходимость изоляции нагрузки от OLTP. Но «PostgreSQL для всего» - это не отговорка, если PostgreSQL справляется. Здесь он справляется.