Если у вас уже работает PostgreSQL, отдельная векторная база может не понадобиться. Расширение pgvector добавляет в Postgres новый тип данных, операторы расстояния и приближённые индексы — и семантический поиск начинает жить обычным SELECT, рядом с вашими таблицами, транзакциями и бэкапами. Ниже — как это устроено на практике, какой индекс выбрать, где pgvector упирается в потолок и когда всё-таки стоит поднимать отдельный движок.
Что делает pgvector
pgvector — это расширение PostgreSQL, которое умеет три вещи: хранить эмбеддинги (векторы чисел, в которые модель превращает текст) в отдельном типе колонки, считать между ними расстояние специальными операторами и строить по этим колонкам ANN-индексы. ANN (approximate nearest neighbor, приближённый поиск ближайших соседей) — это поиск, который находит почти самых похожих соседей, но за доли миллисекунды вместо перебора всей таблицы.
Ключевое отличие от отдельных движков вроде Qdrant: вектор лежит в той же строке, что и остальные данные. Значит, фильтровать можно обычным WHERE, связывать JOIN, писать в одной транзакции с бизнес-таблицами и забирать одним бэкапом. Для типичного RAG-сценария это заметно упрощает эксплуатацию.
На момент написания актуальная версия — 0.8.6, вышла 29 июля 2026. Важные вехи: половинная точность (halfvec), разреженные векторы (sparsevec) и индексация битовых векторов появились в 0.7.0; итеративные сканы индекса — в 0.8.0; поддержка Postgres 18 — в 0.8.1.
Установка и первая таблица
На своём сервере расширение ставится пакетом (APT, Yum, Homebrew, PGXN) или собирается из исходников, а в контейнере проще взять готовый образ pgvector/pgvector:pg17. Дальше — одна команда в нужной базе:
CREATE EXTENSION vector;
В управляемых облаках расширение обычно уже есть в списке доступных. В Yandex Managed Service for PostgreSQL, например, pgvector включается для версий СУБД с 14-й по 18-ю — но версия самого расширения отстаёт от апстрима (на момент проверки для PostgreSQL 16–18 предлагалась 0.8.0). Это стоит учитывать: часть свежих возможностей в облаке может быть недоступна.
Минимальная рабочая таблица для поиска по документам выглядит так:
CREATE TABLE docs (
id bigserial PRIMARY KEY,
url text NOT NULL,
lang text NOT NULL,
chunk text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now(),
embedding vector(1536)
);
Размерность фиксируется прямо в типе. Сменить модель эмбеддингов «на лету» не получится: у другой модели другая размерность и другое пространство, так что колонку придётся пересчитывать целиком. Сразу продумайте и размер кусков текста — про это есть отдельный разбор чанкинга документов.
Операторы расстояния
Расстояние считается операторами, а не функциями — это принципиально для работы индекса:
<->— евклидово расстояние (L2);<=>— косинусное расстояние, самый частый выбор для текстовых эмбеддингов;<#>— отрицательное скалярное произведение;<+>— манхэттенское расстояние (L1);<~>и<%>— Хэмминг и Жаккар для битовых векторов.
Сам запрос предельно скучный, и это хорошо:
SELECT id, url, chunk
FROM docs
ORDER BY embedding <=> $1
LIMIT 5;
Индексы: HNSW или IVFFlat
Без индекса Postgres честно переберёт все строки: точность стопроцентная, скорость падает линейно с ростом таблицы. На нескольких тысячах строк это нормально, на сотнях тысяч — уже нет.
HNSW строит многослойный граф ближайших соседей. Даёт лучшее соотношение «скорость/полнота», работает на пустой таблице, но строится дольше и ест больше памяти:
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
SET hnsw.ef_search = 100; -- по умолчанию 40
Параметр m (по умолчанию 16) — число связей на слой, ef_construction (по умолчанию 64) — размер списка кандидатов при построении. Оба повышают полноту ценой времени сборки. hnsw.ef_search крутится уже на запросах: больше — точнее и медленнее.
IVFFlat делит пространство на кластеры и просматривает только ближайшие. Строится в разы быстрее и требует меньше памяти, но проигрывает HNSW по качеству поиска. Важная деталь: его нужно создавать после загрузки данных, иначе кластеры не на чем обучить.
CREATE INDEX ON docs USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 1000);
SET ivfflat.probes = 32; -- по умолчанию 1
Официальная рекомендация по lists — rows / 1000 для таблиц до миллиона строк и sqrt(rows) для более крупных; стартовое значение probes — sqrt(lists). Дефолтная единица в probes почти всегда даёт слишком низкую полноту, поменять её — первое, что стоит сделать.
Сборку тяжёлого HNSW-индекса ускоряют два параметра сессии: индекс строится заметно быстрее, когда граф помещается в maintenance_work_mem, и когда ему разрешено больше параллельных воркеров (по умолчанию их два).
SET maintenance_work_mem = '8GB';
SET max_parallel_maintenance_workers = 7; -- плюс сам лидер
И главная ловушка новичка: индекс подхватывается, только если в запросе есть ORDER BY по оператору расстояния (именно по оператору, не по выражению вокруг него) и LIMIT, и сортировка идёт по возрастанию. Проверяется это привычным способом — если вы настраивали индексы в MySQL, логика знакомая:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM docs ORDER BY embedding <=> $1 LIMIT 5;
Потолок в 2000 измерений
Тип vector хранит до 16 000 измерений, но проиндексировать можно только до 2000. Современные модели легко выдают 3072 — и тут выручает halfvec (16-битные числа вместо 32-битных), который индексируется до 4000 измерений. Индекс строится по приведению типа, и точно такое же приведение должно быть в запросе:
CREATE INDEX ON docs USING hnsw ((embedding::halfvec(3072)) halfvec_cosine_ops);
SELECT * FROM docs
ORDER BY embedding::halfvec(3072) <=> $1::halfvec(3072)
LIMIT 5;
Половинная точность заодно вдвое сокращает объём хранения и индекса, а качество поиска на текстовых эмбеддингах теряет считаные проценты.
Фильтры по метаданным и пустые результаты
Ради этого pgvector чаще всего и выбирают: фильтр — это обычный WHERE по вашим же колонкам.
SELECT id, url, chunk
FROM docs
WHERE lang = 'ru' AND updated_at > now() - interval '180 days'
ORDER BY embedding <=> $1
LIMIT 5;
Но здесь скрыта неприятность. ANN-индекс сначала достаёт свою порцию ближайших векторов, и только потом применяется фильтр. Если условие строгое, до фильтра доживёт меньше строк, чем просит LIMIT — и запрос молча вернёт два результата вместо пяти. Для этого в 0.8.0 и сделали итеративные сканы: индекс продолжает досматривать данные, пока не наберёт нужное количество подходящих строк.
SET LOCAL hnsw.iterative_scan = 'relaxed_order';
-- строгий порядок сортировки: 'strict_order'
-- для IVFFlat: SET LOCAL ivfflat.iterative_scan = 'relaxed_order';
Досмотр ограничен сверху: hnsw.max_scan_tuples (по умолчанию 20 000) и ivfflat.max_probes не дают запросу выродиться в полный перебор. Если же фильтр у вас постоянный и предсказуемый — дешевле построить частичный индекс с тем же условием в WHERE.
Когда места жалко: бинарная квантизация
Для действительно больших коллекций есть приём: индексировать не сами векторы, а их бинарные «отпечатки», а затем переранжировать небольшую выборку по точным значениям.
CREATE INDEX ON docs
USING hnsw ((binary_quantize(embedding)::bit(1536)) bit_hamming_ops);
SELECT * FROM (
SELECT * FROM docs
ORDER BY binary_quantize(embedding)::bit(1536) <~> binary_quantize($1)
LIMIT 100
) t
ORDER BY embedding <=> $1
LIMIT 5;
Индекс получается в разы компактнее и строится быстрее, а финальную точность возвращает второй проход по сотне кандидатов.
pgvector или отдельная векторная база
Практическая граница проходит примерно так. pgvector стоит брать, когда PostgreSQL уже в проекте, векторов — до нескольких миллионов, фильтрация по метаданным важна, а команда не хочет администрировать ещё один сервис: резервные копии, репликация, права доступа и мониторинг остаются теми же, что и для остальных данных.
Отдельный движок оправдан, когда векторов десятки и сотни миллионов, нужен шардинг и независимое масштабирование поиска, требуется встроенный гибридный поиск и квантизация «из коробки» или когда нагрузка на поиск начинает мешать основной OLTP-базе. Подробное сравнение вариантов — в обзоре векторных баз данных.
Отдельно держите в голове память: HNSW-индекс должен помещаться в RAM, иначе поиск упрётся в диск. Прикиньте размер заранее — это самая частая причина, по которой «всё работало на тесте и встало на проде».
Чек-лист перед продом
- Зафиксировали модель эмбеддингов и размерность — миграция стоит полного пересчёта.
- Выбрали индекс: HNSW по умолчанию, IVFFlat — если критично время сборки.
- Строите индекс после массовой загрузки, с поднятыми
maintenance_work_memи параллельными воркерами. - Подобрали
hnsw.ef_search/ivfflat.probesпо замеру полноты на своём наборе запросов, а не по умолчанию. - Проверили через
EXPLAIN (ANALYZE, BUFFERS), что индекс реально используется. - Включили итеративные сканы там, где есть строгие фильтры.
- Оценили размер индекса относительно доступной оперативной памяти.
В сумме pgvector закрывает потребности большинства проектов, где семантический поиск — важная, но не единственная часть системы. Начать можно с одной команды CREATE EXTENSION, а переезжать на отдельный движок — только когда цифры действительно этого потребуют.
Частые вопросы
В большинстве случаев нет. До нескольких миллионов векторов pgvector справляется, а вы получаете фильтрацию обычным WHERE, JOIN с бизнес-таблицами, транзакции и общий бэкап. Отдельный движок имеет смысл при десятках миллионов векторов, потребности в шардинге или когда поиск начинает мешать основной OLTP-нагрузке.
Индекс подхватывается, только если в запросе есть ORDER BY по оператору расстояния (именно по оператору, а не по выражению вокруг него) и LIMIT, а сортировка идёт по возрастанию. Проверьте план через EXPLAIN (ANALYZE, BUFFERS). Если индекс строился по приведению типа — например, к halfvec, — точно такое же приведение должно быть и в запросе.
Тип vector хранит до 16 000 измерений, но индексируется только до 2000. Используйте halfvec: он индексируется до 4000 измерений, вдвое экономит место и почти не теряет в качестве поиска. Индекс создаётся по выражению embedding::halfvec(3072), и то же приведение повторяется в запросе.
ANN-индекс сначала достаёт свою порцию ближайших векторов, и только потом применяется WHERE. При строгом фильтре до него доживает меньше строк, чем нужно. Решение — итеративные сканы: SET LOCAL hnsw.iterative_scan = 'relaxed_order' (или strict_order). Для постоянных условий выгоднее частичный индекс.
Источники
- 1.pgvector — официальный репозиторий и документацияhttps://github.com/pgvector/pgvector
- 2.pgvector CHANGELOG — история версийhttps://github.com/pgvector/pgvector/blob/master/CHANGELOG.md
- 3.The pgvector extension — Neon Docshttps://neon.com/docs/extensions/pgvector
- 4.pgvector, a guide for DBA — Part 2: Indexes (dbi services, март 2026)https://www.dbi-services.com/blog/pgvector-a-guide-for-dba-part-2-indexes-update-march-2026/
- 5.Управление расширениями — Yandex Managed Service for PostgreSQLhttps://yandex.cloud/ru/docs/managed-postgresql/operations/extensions/cluster-extensions



