Содержание
  1. Требования проекта задают кандидата
  2. Полный набор входов и ручные эталоны
  3. Документированные возможности и неизмеренные свойства
  4. Проверка отрицательного случая и доступности API
  5. Ограничения учебного примера и ручного эталона
  6. Проверка решения перед переносом данных
  7. Частые вопросы
Подборки и сравнения

pgvector или Qdrant: выбор для данных проекта

7 ноября 2026 · 15 минут чтения

pgvector или Qdrant: выбор для данных проекта определяется договором владения записями. Когда источники строк живут в PostgreSQL и нужны JOIN и одна транзакция, кандидат — расширение pgvector. Когда нужен самостоятельный поисковый сервис с отдельным жизненным циклом, кандидат — Qdrant. Порог по задержке на большом корпусе решается замером.

Требования проекта задают кандидата

Сравнение начинается с перечисления требований, которые проект предъявляет к хранению векторов. Универсального рейтинга здесь нет: два маршрута закрывают разные договоры данных, и победитель выводится из конкретного набора условий. Ниже — четыре требования, каждое из которых сразу отсекает один из маршрутов или оставляет оба.

Требование A: источники и владельцы строк уже живут в PostgreSQL, обязательны JOIN и одна транзакция. Здесь кандидат — pgvector. Расширение хранит векторы рядом с остальными данными и даёт доступ к обычным возможностям Postgres: соответствие ACID, восстановление на точку во времени, соединения таблиц. Это документированное свойство расширения, заявленное в его README.

Требование B: нужен самостоятельный поисковый сервис с отдельным жизненным циклом. Здесь кандидат — Qdrant. Документация проекта описывает запуск сервиса отдельным контейнером, собственные REST и gRPC интерфейсы, веб-панель и отдельное хранилище данных. Сервис живёт по своему графику обновлений и масштабирования.

Требование C: порог по задержке на большом корпусе обязателен. Здесь выбор делается после замера, и в этом разборе победителя нет. Ни общее число векторов, ни неподтверждённые скорости, ни объём оперативной памяти не дают основания назвать маршрут заранее. Учебный пример ниже намеренно мал и служит для проверки семантики фильтра.

Требование D: нужна общая авторизация арендаторов. Здесь оба маршрута требуют спроектированного доверенного контроля доступа. Условие tenant внутри запроса само по себе авторизацией не является: фильтр в SQL и payload-фильтр в Qdrant ограничивают выборку строк, а проверку прав пользователя выполняет отдельный слой приложения.

Заполненное решение по каждому требованию выглядит так.

Требование Кандидат Что проверить до внедрения
A: строки в PostgreSQL, JOIN, одна транзакция pgvector Совместимость версии Postgres, план запроса, наличие индекса по колонке фильтра
B: отдельный поисковый сервис Qdrant Схему коллекции, размерность, метрику, индексы payload
C: порог задержки на большом корпусе решается замером Свой корпус, свои фильтры, свой профиль нагрузки
D: общая авторизация арендаторов оба маршрута Доверенный identity и access control вне фильтра

Такой разбор отвечает на вопрос «что выбрать» через договор данных. Читатель получает четыре условия и четыре решения, каждое из которых можно проверить на своём проекте.

Дальше нужен полный набор входов, на котором оба маршрута дают один и тот же ожидаемый ответ. Без него сравнение остаётся на уровне описаний.

Полный набор входов и ручные эталоны

Учебный набор состоит из трёх записей и одного запроса. Идентификаторы записей — числовые 1, 2 и 3; метки id1, id2, id3 ниже служат только для чтения и соответствуют этим числам. В PostgreSQL идентификатор имеет целочисленный тип, в Qdrant точки получают числа 1, 2 и 3.

Полные записи:

  • id1: tenant=A, active=true, vector=[1,0]
  • id2: tenant=B, active=true, vector=[1,0]
  • id3: tenant=A, active=false, vector=[0,1]

Запрос: tenant=A И active=true, ближайший к вектору [1,0]. Ручной эталон для этого запроса — только id1. При tenant=B И active=true ручной эталон — только id2. При tenant=C ручной эталон — пустой результат.

Отдельно фиксируется проверка без фильтра с лимитом 2. Векторы id1 и id2 равны [1,0] и равноудалены от запроса, поэтому результат может вернуть id1 и id2 в любом порядке. При лимите 1 требовать возврата обоих идентификаторов нельзя: запрос просит одну ближайшую запись.

Ручной эталон — это ожидание, записанное до запуска. Он не является результатом выполнения и не подтверждает поведение продукта. Его роль — дать точку сверки: если маршрут вернул не тот идентификатор, дальше разбирается причина расхождения.

Полный набор входов для PostgreSQL. Схема объявляет колонку с размерностью 2, затем идут три вставки. Это учебный точный пример, обязательный ANN-индекс в нём не нужен.

SQL13 строк
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE items (
    id bigserial PRIMARY KEY,
    tenant text NOT NULL,
    active boolean NOT NULL,
    embedding vector(2)
);

INSERT INTO items (tenant, active, embedding) VALUES
    ('A', true,  '[1,0]'),
    ('B', true,  '[1,0]'),
    ('A', false, '[0,1]');

Запрос с фильтром и лимитом 1:

SQL5 строк
SELECT id
FROM items
WHERE tenant = 'A' AND active = true
ORDER BY embedding <=> '[1,0]'::vector
LIMIT 1;

Оператор <=> — косинусная дистанция, документированная в README расширения вместе с L2, внутренним произведением, L1, Хэмминговым и Жаккаровым расстояниями. Для воспроизводимости важны все три вставки: без id2 проверка tenant=B не имеет входа, без id3 не проверяется условие active=false.

Полный набор входов для Qdrant. Коллекция имеет размерность 2 и метрику Cosine, векторы и метаданные совпадают с таблицей выше.

PYTHON18 строк
from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.create_collection(
    collection_name="items",
    vectors_config=models.VectorParams(size=2, distance=models.Distance.COSINE),
)

client.upsert(
    collection_name="items",
    wait=True,
    points=[
        models.PointStruct(id=1, vector=[1.0, 0.0], payload={"tenant": "A", "active": True}),
        models.PointStruct(id=2, vector=[1.0, 0.0], payload={"tenant": "B", "active": True}),
        models.PointStruct(id=3, vector=[0.0, 1.0], payload={"tenant": "A", "active": False}),
    ],
)

Соответствующий payload-фильтр: must=[tenant==A, active==true]. Верный результат обоих маршрутов — id1. Идентификаторы сравниваются по содержанию, порядок тай-брейка в сверке не участвует.

Набор собран, эталоны записаны. Следующий шаг — разделить то, что уже документировано, и то, что ещё предстоит измерить.

Документированные возможности и неизмеренные свойства

Документированные возможности подтверждаются первичными страницами проектов. Неизмеренные свойства требуют собственного замера и в готовый вывод не переносятся.

По pgvector README расширения фиксирует следующее. Поддерживаются точный и приближённый поиск ближайших соседей. Доступны типы single-precision, half-precision, binary и sparse векторов. Расширение работает с любым языком, для которого есть клиент Postgres. Индексы HNSW и IVFFlat создаются для каждой используемой функции расстояния. Тип vector индексируется до 2000 измерений, halfvec — до 4000, bit — до 64000, sparsevec — до 1000 ненулевых элементов. Хранение векторов допускает до 16000 измерений, и это отдельное число от предела индексирования.

По умолчанию pgvector выполняет точный поиск ближайших соседей и даёт полную полноту. Приближённый индекс меняет этот режим: после его добавления запросы начинают возвращать другие результаты. Параметры HNSW — m со значением 16 по умолчанию и ef_construction со значением 64 по умолчанию; параметр поиска hnsw.ef_search по умолчанию равен 40. Для IVFFlat число списков подбирается от объёма таблицы, а число проб по умолчанию равно 1.

Фильтрация в pgvector устроена так: при приближённом индексе условие применяется после сканирования индекса. Если условие отбирает 10% строк, при HNSW и значении hnsw.ef_search 40 в среднем совпадут 4 строки. Для большего числа строк README предлагает итеративные сканирования индекса, включаемые параметром hnsw.iterative_scan. Мультиарендность описана отдельно: общий приближённый индекс между арендаторами означает, что векторы одного арендатора влияют на полноту и скорость для других. Для изоляции арендаторов README предлагает секционирование по списку или отдельные таблицы.

По Qdrant документация quickstart фиксирует следующее. Сервис запускается контейнером, данные по умолчанию лежат в каталоге ./qdrant_storage. Доступны REST API на порту 6333, веб-панель на 6333/dashboard и gRPC API на порту 6334. Клиентские библиотеки существуют для Python, JavaScript, Rust, Java, C#, Go и других языков. Коллекция создаётся с указанием размерности и метрики. В документации отмечено, что по умолчанию Qdrant стартует без шифрования и аутентификации, и любой, у кого есть сетевой доступ к машине, получает доступ к контейнеру. Qdrant может работать встроенно внутри приложения; Qdrant Edge — облегчённый поисковый движок для Python и Rust, работающий в процессе.

По Qdrant filtering документация фиксирует следующее. Условия задаются на payload и на id точки. Для производительной фильтрации создаются индексы payload, и документация рекомендует создавать их до загрузки данных. Клаузы must, should, must_not соответствуют логическим AND, OR и NOT, клаузы вкладываются друг в друга. Условие match применяется к keyword, integer, bool и UUID payload. Условие range работает с float и integer, отдельный datetime range поддерживает формат RFC 3339. Геоусловия охватывают прямоугольник, радиус и полигон. Вложенные ключи используют точечную нотацию и [] для элементов массива. Вложенный фильтр проверяет каждый элемент массива отдельно, и для ускорения нужны поэлементные индексы.

Неизмеренные свойства перечисляются отдельно, чтобы их не смешивать с документацией. К ним относятся: фактическая задержка на вашем корпусе, потребление памяти под индексы, полнота приближённого поиска при вашем профиле фильтрации, поведение при вашем объёме данных и вашем соотношении tenant. Эти величины определяются замером на вашем наборе.

Свойство Статус Источник
Типы векторов и функции расстояния pgvector документировано README расширения
Пределы измерений по типам pgvector документировано README расширения
Применение фильтра после сканирования индекса документировано README расширения
Изоляция арендаторов через секционирование документировано README расширения
Порты и отсутствие аутентификации по умолчанию в Qdrant документировано quickstart проекта
Клаузы фильтрации и типы условий Qdrant документировано раздел filtering
Задержка на вашем корпусе требует замера ваш стенд
Полнота ANN при вашей фильтрации требует замера ваш стенд

Разделение важно для честного вывода. Документированное свойство можно планировать. Неизмеренную величину можно только измерить.

Отдельно стоит сказать о чужих числах. Бенчмарки, тарифы и значения по умолчанию из сторонних обзоров в этот разбор не переносятся: они сняты на другом корпусе, другом железе и другом профиле запросов. Перенос такого числа в решение о своём проекте даёт ложную опору.

Проверка отрицательного случая и доступности API

Отрицательный случай проверяется на том же наборе. Запрос tenant=C И active=true обязан вернуть пустой результат в обоих маршрутах. Пустой ответ — это ожидаемый исход, и он подтверждает, что фильтр действительно ограничивает выборку.

Порядок проверки для PostgreSQL:

  1. Выполнить запрос с tenant=A И active=true и сверить результат с эталоном id1.
  2. Выполнить запрос с tenant=B И active=true и сверить результат с эталоном id2.
  3. Выполнить запрос с tenant=C И active=true и убедиться, что строк нет.
  4. Выполнить запрос без фильтра с лимитом 2 и убедиться, что вернулись id1 и id2 в любом порядке.
  5. Выполнить запрос без фильтра с лимитом 1 и убедиться, что вернулась одна запись.

Порядок проверки для Qdrant:

  1. Выполнить поиск с фильтром must=[tenant==A, active==true] и сверить результат с эталоном id1.
  2. Выполнить поиск с фильтром must=[tenant==B, active==true] и сверить результат с эталоном id2.
  3. Выполнить поиск с фильтром must=[tenant==C, active==true] и убедиться, что точек нет.
  4. Выполнить поиск без фильтра с лимитом 2 и убедиться, что вернулись id1 и id2 в любом порядке.
  5. Выполнить поиск без фильтра с лимитом 1 и убедиться, что вернулась одна точка.

Проверка доступности требуемого API идёт до этих шагов. Для pgvector нужно убедиться, что расширение установлено в целевой базе и что команда CREATE EXTENSION vector выполняется без ошибки. README перечисляет способы установки: сборка из исходников, Docker, Homebrew, PGXN, APT, Yum, pkg, APK, conda-forge, а также предустановка в Postgres.app и у ряда хостинг-провайдеров. Для Qdrant нужно убедиться, что сервис отвечает на выбранном интерфейсе: REST на 6333, gRPC на 6334. Клиент инициализируется адресом сервиса.

Актуальность продуктового пути проверяется отдельно. Для pgvector README описывает обновление расширения командой ALTER EXTENSION vector UPDATE и проверку установленной версии запросом к pg_extension. Для Qdrant документация ведёт от локального запуска к облачному развёртыванию и отдельно упоминает production checklist для проверки безопасности и оптимизации перед выкладкой.

Частая ошибка на этом шаге — считать, что фильтр в запросе закрывает доступ между арендаторами. Условие tenant ограничивает набор строк, которые попадут в результат. Права пользователя на эти строки проверяет отдельный слой. В обоих маршрутах проектируется доверенный identity и access control, и фильтр его дополняет.

Вторая частая ошибка — сверять порядок тай-брейка. Две записи с одинаковым вектором равноудалены от запроса, и требовать конкретного порядка между ними нельзя. Сверка идёт по содержанию результата.

Третья ошибка — проверять только положительный случай. Запрос с tenant=C проверяет, что фильтр действительно отсекает всё лишнее. Без этой проверки ошибка в условии остаётся незамеченной.

Ограничения учебного примера и ручного эталона

Учебный пример мал намеренно. Три записи и один запрос проверяют семантику фильтра: как условие tenant и active сочетается с поиском ближайшего соседа. Этот набор не измеряет скорость, не показывает поведение приближённого индекса на большом корпусе и не даёт основания для вывода о победителе по объёму.

Ручной эталон — это ожидание, записанное до запуска. Он отвечает на вопрос «какой идентификатор должен вернуться». Он не отвечает на вопрос «сколько времени это заняло» и «какую полноту дал приближённый индекс». Смешивать эти роли нельзя: эталон проверяет правильность, замер проверяет производительность.

Полнота приближённого поиска при фильтрации оценивается отдельным замером. README pgvector описывает механику: при приближённом индексе условие применяется после сканирования, и при узком условии число совпавших строк ограничено размером динамического списка кандидатов. Итеративные сканирования расширяют область поиска, и их поведение настраивается параметрами hnsw.max_scan_tuples и hnsw.scan_mem_multiplier. Как эти параметры поведут себя на вашем корпусе, показывает замер.

Разные настройки планировщика запросов и приближённого индекса не подменяются предсказанным сбоем. Если запрос вернул не тот идентификатор, причины разбираются по порядку: проверяется условие фильтра, проверяется наличие индекса по колонке фильтра, проверяется план запроса. README pgvector отдельно описывает случай, когда запрос не использует индекс: для этого нужны ORDER BY и LIMIT, и ORDER BY должен быть результатом оператора расстояния в возрастающем порядке. Выражение вместо оператора индекс не задействует.

Для Qdrant аналогичный разбор начинается с индексов payload. Документация рекомендует создавать их до загрузки данных и отмечает, что без индекса условие всё равно работает, но проверяется по каждой точке. Для вложенных фильтров нужны поэлементные индексы: обычный индекс по полю массива только сужает набор кандидатов, после чего Qdrant читает payload каждой оставшейся точки.

Ограничение по авторизации остаётся в силе для обоих маршрутов. Условие tenant в SQL и payload-фильтр в Qdrant ограничивают выборку. Проверку прав выполняет доверенный слой identity и access control, спроектированный отдельно.

Ограничение по чужим числам тоже остаётся. Пороги по миллионам векторов, объёму памяти и измерения из сторонних обзоров в решение не переносятся. Свой порог определяется своим замером на своём корпусе.

Проверка решения перед переносом данных

Зафиксируйте выбор в карточке проекта. Для варианта с PostgreSQL укажите, какие исходные строки должны изменяться вместе с векторами и какие запросы используют связи с другими таблицами. Для отдельного поискового сервиса укажите владельца загрузки и момент, когда обновлённая запись становится доступна поиску. Это требования приложения. В учебном примере состояния заданы заранее, поэтому он проверяет только смысл фильтра на неизменном наборе.

Перед первым запросом сверьте числовой ID, tenant, active и вектор. Для отдельной проверки active выполните дополнительный запрос tenant=A И active=true с лимитом 2: в SQL замените LIMIT 1 на LIMIT 2, в Qdrant задайте limit=2. Ручной эталон — только ID 1. Затем в этом дополнительном запросе удалите только active, сохранив tenant=A и лимит 2: ручной эталон — ID 1 и ID 3. Так пропущенный active становится видимым. В исходном запросе с лимитом 1 запись 3 дальше от запроса, поэтому её отсутствие не проверяет это условие. Эти ответы заданы как ручные эталоны; запуски продуктов здесь не выполнялись.

В журнале запишите оба условия и множество ожидаемых ID до запуска. После запуска сохраните фактически возвращённые ID. Для этого набора при tenant A и active true ожидается только 1, при tenant B — только 2, при tenant C — пустое множество. Различие типов ID между клиентами нельзя скрывать приведением всех значений к произвольному виду: здесь заранее выбран числовой договор, и именно его сохраняют при загрузке.

Результат маленького примера подтверждает согласованность фильтра с заданным вводом. Решение о задержке и поведении индекса на большом корпусе потребует собственного измерения. До такого измерения карточка содержит незаполненное требование по скорости. Это позволяет принять архитектурное решение по известным условиям и сохранить видимой часть работы, для которой данных пока нет.

Частые вопросы

pgvector vs qdrant — что выбрать для проекта на PostgreSQL?

Когда источники строк уже живут в PostgreSQL и нужны JOIN и одна транзакция, кандидат — расширение pgvector: векторы хранятся рядом с остальными данными, а расширение даёт доступ к обычным возможностям Postgres, включая соответствие ACID и восстановление на точку во времени. Когда нужен самостоятельный поисковый сервис с отдельным жизненным циклом, кандидат — Qdrant. Решение выводится из договора владения записями.

Как проверить, что фильтр работает одинаково в обоих маршрутах?

Соберите полный набор входов: три записи с полями tenant, active и вектором, затем запрос с условием tenant=A И active=true. Ручной эталон для этого запроса — одна конкретная запись. Дополнительно проверьте tenant=B, tenant=C с пустым результатом и запрос без фильтра с лимитом 2, где две равноудалённые записи возвращаются в любом порядке.

Почему нельзя требовать конкретный порядок при равных векторах?

Две записи с одинаковым вектором равноудалены от запроса, и расстояние у них совпадает. Порядок между ними определяется внутренним правилом тай-брейка, и он не является частью ожидаемого результата. Сверка сопоставляет множества вернувшихся идентификаторов.

Заменяет ли условие tenant в запросе авторизацию арендатора?

Нет. Условие tenant в SQL и payload-фильтр в Qdrant ограничивают набор строк, которые попадут в результат поиска. Права пользователя на эти строки проверяет отдельный слой приложения. В обоих маршрутах проектируется доверенный identity и access control, и фильтр его дополняет.

Что нужно измерить перед выбором по скорости?

Замер делается на своём корпусе, своём профиле фильтрации и своём железе. Общее число векторов, чужие бенчмарки и неподтверждённые значения памяти основанием для выбора не служат. Полнота приближённого поиска при фильтрации оценивается отдельным замером, поскольку условие применяется после сканирования индекса.

Читайте также

3 материала