Векторные базы данных на примере Qdrant: организация семантического поиска

Курс посвящен интеграции высокопроизводительной векторной БД Qdrant в архитектуру ИИ-решений. Вы научитесь эффективно сохранять эмбеддинги, использовать продвинутую фильтрацию по метаданным и настраивать гибридный поиск для RAG-систем.

1. Архитектура Qdrant: коллекции, точки и механизмы индексирования векторов

Архитектура Qdrant: коллекции, точки и механизмы индексирования векторов

В предыдущем курсе мы генерировали 384-мерные эмбеддинги с помощью локальной модели all-MiniLM-L6-v2. Сохранение таких векторов в PostgreSQL через расширение pgvector отлично работает на этапе прототипа. Однако при масштабировании мульти-агентной системы до миллионов документов строгие транзакционные гарантии (ACID) реляционной базы данных становятся узким местом. Семантический поиск требует совершенно иных структур данных в оперативной памяти и на диске. Qdrant жертвует реляционной гибкостью ради ультимативной скорости вычисления расстояний в многомерных пространствах.

Иерархия данных: Коллекции и Точки

В основе Qdrant лежит двухуровневая логическая модель: коллекции (Collections) и точки (Points). Здесь нет таблиц, схем и внешних ключей.

Коллекция в Qdrant — это изолированное пространство имен, объединенное строгими математическими правилами. В отличие от таблиц PostgreSQL, где колонки могут содержать NULL или данные разной длины, коллекция Qdrant требует жесткой фиксации двух параметров при создании: размерности вектора (Vector Size) и метрики расстояния (Distance Metric). Невозможно поместить 384-мерный вектор от Sentence Transformers и 1536-мерный вектор от OpenAI в одну стандартную коллекцию — математика вычисления расстояний для них несовместима.

Базовой единицей данных внутри коллекции является Точка (Point).

!Анатомия Point в Qdrant: связь идентификатора, вектора и метаданных

Point состоит из трех обязательных компонентов:

  • ID (Идентификатор). Qdrant поддерживает только два типа идентификаторов: 64-битные беззнаковые целые числа (Unsigned Integer) и UUID. Это архитектурное ограничение идеально ложится на паттерн Claim Check: мы используем тот же самый UUIDv7, который был сгенерирован в PostgreSQL для записи документа, в качестве ID для вектора в Qdrant. Это обеспечивает связь между эпизодической (реляционной) и семантической (векторной) памятью агента без необходимости хранить сложные составные ключи.
  • Vector (Вектор). Массив чисел с плавающей точкой (Float32), длина которого строго равна размерности коллекции.
  • Payload (Полезная нагрузка). Свободный JSON-объект с метаданными. В отличие от JSONB в PostgreSQL, Payload в Qdrant не предназначен для хранения тяжелых текстов. Его главная задача — хранить легковесные теги (ID пользователя, дата создания, категория) для жесткой пре-фильтрации перед векторным поиском.
  • Физическое хранение: сегменты и оптимизатор

    Прямая вставка новых векторов в глобальный граф HNSW (Hierarchical Navigable Small World) невозможна с точки зрения производительности. Перестроение связей в графе при каждой записи полностью заблокировало бы базу данных. Qdrant решает эту проблему через архитектуру независимых сегментов.

    Сегмент — это физический контейнер на диске и в памяти, хранящий определенное подмножество точек коллекции. Каждый сегмент функционирует как миниатюрная независимая векторная база данных со своим собственным индексом.

    При поступлении нового запроса на запись (Upsert), данные попадают в добавляемый (appendable) сегмент. На этом этапе векторы просто складываются в линейный массив в оперативной памяти без построения HNSW-графа. Поиск по такому сегменту выполняется полным сканированием (Sequential Scan). Поскольку добавляемый сегмент мал, линейный поиск по нему происходит за доли миллисекунды и не влияет на общую задержку.

    По мере накопления данных в работу включается фоновый процесс — Оптимизатор (Optimizer).

    !Процесс работы оптимизатора: слияние мелких сегментов и построение HNSW-графа

    Оптимизатор выполняет две критические задачи:

  • Берет несколько небольших добавляемых сегментов и сливает их в один большой неизменяемый (immutable) сегмент.
  • В процессе слияния вычисляет и строит полноценный HNSW-индекс для нового большого сегмента.
  • Как только новый проиндексированный сегмент готов, старые мелкие сегменты удаляются. Эта архитектура напоминает механизм LSM-деревьев (Log-Structured Merge-Tree), но адаптирована под специфику многомерных графов. При выполнении поискового запроса Qdrant параллельно опрашивает все существующие сегменты (и проиндексированные, и линейные), а затем сливает результаты, выдавая итоговый топ-K ответов.

    Механизмы хранения: InMemory против Mmap

    В предыдущем курсе при разборе формата GGUF для локальных LLM мы познакомились с системным вызовом mmap (Memory-mapped file). Qdrant использует ровно ту же технологию операционной системы для управления памятью, предлагая два режима хранения векторов:

  • InMemory (В оперативной памяти). Векторы и HNSW-граф хранятся непосредственно в RAM. Это обеспечивает максимальную пропускную способность, но ограничивает размер базы данных физическим объемом памяти сервера. Если RAM закончится, процесс Qdrant будет убит OOM Killer'ом операционной системы.
  • Mmap (Отображение в память). Данные хранятся на SSD-накопителе, но виртуально проецируются в адресное пространство RAM. Операционная система сама решает, какие страницы памяти (обычно по 4 КБ) держать в быстром кэше (Page Cache), а какие вытеснять на диск.
  • Для HNSW-индекса режим Mmap критически важен. Навигация по графу HNSW требует случайного доступа к памяти (Random Access). Современные NVMe SSD обладают достаточной скоростью случайного чтения, чтобы Qdrant мог эффективно обходить граф, физически лежащий на диске, загружая в RAM только те узлы, которые пересекаются поисковым запросом. Это позволяет хранить миллиарды векторов на дешевых дисках, используя RAM только для кэширования горячих путей поиска.

    Оптимизация метрик расстояния

    При создании коллекции мы обязаны указать метрику расстояния. Векторные модели, такие как all-MiniLM-L6-v2, обучаются с использованием косинусного расстояния (Cosine Distance), которое измеряет угол между векторами, игнорируя их длину.

    Формула косинусного сходства:

    Где — скалярное произведение (Dot Product) векторов, а и — их длины (L2-нормы). Операция вычисления длины вектора и последующего деления требует значительных процессорных ресурсов (извлечение квадратного корня), особенно когда она выполняется миллионы раз в секунду при обходе HNSW-графа.

    Здесь кроется важная архитектурная оптимизация. Библиотека sentence-transformers по умолчанию выполняет L2-нормализацию векторов перед их выдачей. Это означает, что длина каждого сгенерированного вектора принудительно приравнивается к единице: и .

    Если подставить единицы в знаменатель, формула схлопывается:

    Для нормализованных векторов косинусное сходство математически эквивалентно скалярному произведению. Поэтому при проектировании коллекций в Qdrant для моделей Sentence Transformers архитектурно правильно указывать метрику Dot (Dot Product), а не Cosine. Qdrant пропустит этап вычисления длин векторов на аппаратном уровне ядра процессора, что повысит общую пропускную способность (TPS) векторного поиска на 15-20% без единой потери в качестве ранжирования.

    Параметры HNSW на уровне коллекции

    В отличие от PostgreSQL, где параметры индексов задаются глобально или при создании конкретного индекса через DDL-запросы, Qdrant инкапсулирует конфигурацию HNSW внутри настроек коллекции. Это позволяет агентам иметь разные стратегии поиска.

    Ключевые параметры, которые мы передаем при инициализации:

  • m (по умолчанию 16) — количество связей (ребер), которые создаются для каждого нового узла (вектора) на нижнем слое графа. Увеличение m до 32 или 64 имеет смысл для высокомерных векторов (например, 1536 от OpenAI), так как это повышает вероятность нахождения правильного пути в сложном пространстве, но линейно увеличивает потребление памяти.
  • ef_construct (по умолчанию 100) — размер динамического списка кандидатов при вставке нового элемента. Чем выше значение, тем дольше Оптимизатор строит сегмент, но тем качественнее (ближе к идеальному) получается структура графа.
  • Разделение хранилища на мелкие линейные сегменты и крупные HNSW-сегменты, использование mmap для обхода ограничений RAM и математическая редукция косинусного расстояния до скалярного произведения делают Qdrant высокопроизводительным движком. Векторная математика работает быстро, когда она изолирована от реляционных блокировок.

    2. Метаданные и Payload: реализация жесткой фильтрации в семантическом поиске

    Метаданные и Payload: реализация жесткой фильтрации в семантическом поиске

    Семантический поиск на основе эмбеддингов превосходно улавливает концептуальное сходство, но абсолютно слеп к жестким логическим границам. Если пользователь запрашивает «финансовый отчет за 3 квартал 2024 года», языковая модель Sentence Transformers сгенерирует вектор, который окажется математически близок к «финансовому отчету за 3 квартал 2023 года». Разница между этими документами составляет всего один токен, их косинусное расстояние будет стремиться к единице, но с точки зрения бизнес-логики прошлогодний отчет является критической фактической ошибкой (галлюцинацией контекста).

    Чтобы ИИ-система могла оперировать детерминированными правилами (правами доступа, временными рамками, категориями), векторные базы данных совмещают поиск по сходству с фильтрацией по метаданным. В Qdrant этот механизм реализован через объект Payload.

    Анатомия Payload и декларативная фильтрация

    В Qdrant каждая точка (Point) помимо уникального идентификатора (UUID) и самого вектора может содержать Payload — JSON-объект произвольной вложенности. В отличие от реляционных баз данных, где схема жестко задана колонками, Payload позволяет хранить гетерогенные данные для разных типов документов в рамках одной коллекции.

    Логика фильтрации в Qdrant строится на основе декларативного синтаксиса, напоминающего запросы в Elasticsearch или MongoDB. Условия объединяются в иерархические структуры с помощью трех основных логических операторов:

  • must — логическое И (AND). Точка обязана удовлетворять всем условиям в массиве.
  • should — логическое ИЛИ (OR). Точка должна удовлетворять хотя бы одному условию.
  • must_not — логическое НЕ (NOT). Точка исключается из выдачи, если совпадает с условием.
  • Этот синтаксис позволяет транслировать бизнес-правила (например, изоляцию данных в multi-tenant SaaS-приложениях) напрямую в ядро векторного движка. Однако сложность заключается не в описании фильтров, а в том, на каком этапе поиска они применяются.

    Архитектурная дилемма: Pre-filtering против Post-filtering

    Интеграция точного совпадения с приближенным поиском ближайших соседей (ANN) порождает фундаментальную алгоритмическую проблему. Существует два базовых подхода к фильтрации, каждый из которых имеет критические уязвимости.

    Post-filtering (Пост-фильтрация)

    Алгоритм сначала выполняет векторный поиск и находит ближайших соседей, а затем отбрасывает из этого списка результаты, не соответствующие условиям Payload.

    Главный недостаток этого подхода — непредсказуемость размера итоговой выдачи. Допустим, в коллекции из 1 000 000 векторов пользователь ищет документы по маркетингу, но устанавливает жесткий фильтр: department = "HR". База данных извлекает топ-100 документов, наиболее близких к вектору запроса. Поскольку запрос связан с маркетингом, все 100 документов принадлежат отделу маркетинга. На этапе пост-фильтрации база применяет условие department == "HR" и отбрасывает все 100 результатов. Пользователь получает пустой ответ, хотя в коллекции существуют маркетинговые регламенты для HR-отдела, просто их векторное расстояние поместило их на 500-ю позицию.

    Pre-filtering (Пре-фильтрация)

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

    С точки зрения бизнес-логики это идеальный вариант, гарантирующий возврат ровно релевантных результатов. Но с точки зрения алгоритма HNSW (Hierarchical Navigable Small World), на котором базируется Qdrant, пре-фильтрация разрушительна.

    HNSW строит многослойный граф, где поиск осуществляется путем перехода от узла к узлу. Если фильтр tenant_id = "org_771" отсекает 99% точек в коллекции, граф HNSW «рассыпается». Большинство связей между оставшимися валидными узлами оказываются разорванными отфильтрованными соседями. Навигация по графу становится невозможной, алгоритм заходит в тупики и возвращает неточные результаты.

    Решение Qdrant: Динамическая оценка кардинальности

    Для преодоления ограничений обоих подходов Qdrant использует механизм Cardinality Estimation (оценка кардинальности). Перед выполнением запроса оптимизатор анализирует условия фильтра и предсказывает, какое количество точек останется после его применения. На основе этой оценки Qdrant на лету выбирает оптимальную стратегию выполнения:

  • Точный поиск (Flat Search). Если фильтр очень строгий (например, оставляет всего 500 точек из миллиона), Qdrant полностью игнорирует HNSW-индекс. Он извлекает все 500 валидных векторов и выполняет алгоритмически простое вычисление скалярного произведения (Dot Product) для каждого из них. Для малых выборок вычисления на современных процессорах с SIMD-инструкциями происходят быстрее, чем обход сложного графа.
  • Фильтрация при обходе графа (In-Graph Filtering). Если фильтр широкий (например, отсекает только 10% архивированных документов), Qdrant использует HNSW. При переходе от узла к узлу алгоритм проверяет Payload текущей точки. Если точка не проходит фильтр, она не может быть добавлена в финальную выдачу, но алгоритм все равно использует ее векторные связи для дальнейшей навигации по графу. Это сохраняет связность HNSW и обеспечивает логарифмическую сложность .
  • Индексирование Payload: оптимизация

    Механизм оценки кардинальности и быстрого переключения стратегий невозможен без индексов. Если метаданные не проиндексированы, Qdrant вынужден десериализовать JSON-объект каждой точки для проверки условий, что приводит к катастрофической деградации производительности (CPU-bound нагрузка).

    Qdrant позволяет создавать специализированные индексы для конкретных ключей внутри Payload. Выбор типа индекса зависит от характера данных:

  • Keyword Index. Применяется для строковых значений, требующих точного совпадения (идентификаторы пользователей, статусы, категории). Работает аналогично хэш-таблицам, обеспечивая проверку за .
  • Integer / Float Index. Используется для числовых данных (цены, таймстемпы, счетчики). Позволяет эффективно выполнять запросы по диапазонам (Range Queries), например: created_at >= 1704067200.
  • Geo Index. Специализированный индекс для координат (широта и долгота), позволяющий фильтровать точки по радиусу от заданного центра или внутри полигона.
  • Text Index. Базовый полнотекстовый индекс с токенизацией. Важно понимать, что он не заменяет FTS в PostgreSQL и не поддерживает стемминг или сложные словари. Он предназначен исключительно для простой фильтрации по подстрокам или тегам внутри векторной БД.
  • Создание индекса на поле tenant_id является обязательным архитектурным требованием при проектировании RAG-систем для B2B-сегмента. Отсутствие этого индекса при выполнении пре-фильтрации приведет к полному сканированию коллекции (Sequential Scan), нивелируя все преимущества векторного движка.

    Синхронизация состояний и минимизация Payload

    В архитектуре двойной памяти, где PostgreSQL выступает надежным хранилищем (эпизодическая память), а Qdrant — семантическим индексом, возникает вопрос разделения данных. Что именно следует хранить в Payload?

    Согласно паттерну Claim Check, векторная база данных не должна быть основным хранилищем текстов или тяжелых метаданных. В Payload Qdrant следует дублировать только те атрибуты, по которым будет осуществляться фильтрация на этапе поиска.

    Если документ содержит 50 полей метаданных (имя автора, количество просмотров, ссылки на изображения, история редактирования), но поиск ограничивается только проверкой прав доступа (group_id) и датой публикации (timestamp), то в Payload Qdrant переносятся только эти два поля.

    Раздувание Payload неиспользуемыми данными ведет к двум проблемам:

  • Перерасход оперативной памяти. Даже при использовании режима Mmap (хранение на диске), часто запрашиваемые страницы Payload оседают в Page Cache операционной системы, вытесняя оттуда критически важные векторы.
  • Накладные расходы на сериализацию. При возврате результатов Qdrant тратит процессорное время на упаковку больших JSON-объектов в HTTP/gRPC ответы.
  • Жизненный цикл метаданных требует строгой синхронизации. Если в PostgreSQL статус документа меняется на deleted, это изменение должно быть асинхронно, но гарантированно доставлено в Qdrant для обновления Payload. Использование паттерна Transactional Outbox гарантирует, что векторный индекс не рассинхронизируется с реляционной базой, предотвращая ситуации, когда семантический поиск находит и возвращает удаленные или заблокированные документы.

    Детерминированная фильтрация через Payload превращает векторное хранилище из вероятностного рекомендательного движка в строгий инструмент извлечения данных. Комбинация математического сходства эмбеддингов с логическими границами метаданных формирует фундамент для безопасных и предсказуемых корпоративных ИИ-систем, где галлюцинации контекста блокируются на уровне архитектуры базы данных.

    3. Оптимизация производительности: HNSW-индексы, квантование и асинхронный Batching

    Оптимизация производительности: HNSW-индексы, квантование и асинхронный Batching

    Хранение 10 миллионов векторов размерностью 768 (стандарт для легковесных моделей вроде all-MiniLM-L6-v2) в формате float32 требует около 30 ГБ оперативной памяти только для самих данных. Графовый индекс HNSW добавит к этому объему еще 10–15 ГБ накладных расходов на хранение указателей. Если сервер располагает лишь 16 ГБ RAM, операционная система начнет агрессивно использовать файл подкачки (swapping), и задержка семантического поиска мгновенно возрастет с 5 миллисекунд до нескольких секунд. Масштабирование векторной базы данных в production-среде требует жесткого управления потреблением памяти, балансировки параметров графа и оптимизации сетевого взаимодействия.

    Тонкая настройка HNSW: баланс между памятью и точностью

    В основе векторного поиска Qdrant лежит алгоритм HNSW. Параметры построения графа m (количество связей на узел) и ef_construct (размер пула кандидатов при вставке) управляют качеством самого индекса и временем его создания. Однако в высоконагруженных системах критическую роль играет параметр этапа поиска — ef (эффективность поиска).

    Параметр ef определяет, сколько узлов графа алгоритм будет удерживать в памяти и оценивать в процессе навигации к ближайшему соседу при выполнении конкретного запроса.

    Если пользователь запрашивает ближайших документов, значение ef должно быть строго . Увеличение ef (например, до 64 или 128) расширяет область исследования графа, позволяя алгоритму находить пути к векторам, которые могли быть пропущены при жадном поиске. Это повышает метрику Recall@K (полноту), но линейно увеличивает количество вычислений косинусного расстояния, снижая общую пропускную способность системы (TPS).

    Влияние параметра m на потребление оперативной памяти часто недооценивают. Каждая двунаправленная связь в графе требует хранения указателей. Для коллекции из точек объем памяти, занимаемый исключительно структурой HNSW, можно аппроксимировать формулой:

    где — константа накладных расходов (обычно от 8 до 12 байт на связь). Для векторов высокой размерности (например, 1536 от OpenAI) значение m часто повышают со стандартных 16 до 32 или 64 для сохранения связности пространства, что приводит к кратному росту потребления RAM.

    Решением проблемы разрастания индекса является перенос графа на диск. Qdrant позволяет хранить структуру HNSW в режиме mmap, оставляя в оперативной памяти только самые верхние (разреженные) слои графа. Это радикально снижает требования к RAM, но требует использования высокоскоростных NVMe-накопителей, так как случайное чтение узлов графа с HDD или медленных SSD полностью уничтожит производительность поиска.

    Векторное квантование: радикальное сжатие данных

    Векторное квантование в базах данных решает задачу минимизации объема памяти, необходимого для хранения самих эмбеддингов, при сохранении приемлемой точности вычисления расстояний. В отличие от квантования весов LLM, здесь сжимаются данные пользователей.

    Скалярное квантование (Scalar Quantization)

    Скалярное квантование (SQ) преобразует каждое число с плавающей запятой (float32, 4 байта) в целое число (int8, 1 байт). Это обеспечивает фиксированное сжатие данных в 4 раза. Алгоритм находит минимальное и максимальное значения по каждому измерению во всей коллекции и линейно отображает этот диапазон на сетку от 0 до 255.

    Поскольку SQ вносит ошибку округления, вычисленное расстояние между векторами в пространстве int8 будет незначительно отличаться от реального расстояния в пространстве float32. Для компенсации этой ошибки Qdrant использует архитектурный паттерн Oversampling (передискретизация) в связке с механизмом Re-scoring (повторное ранжирование).

    Механика Re-scoring работает следующим образом:

  • Исходные векторы в float32 сохраняются на диске (через mmap), не занимая дефицитную оперативную память.
  • Квантованные векторы в int8 загружаются в RAM.
  • При поиске топ- результатов (например, ) алгоритм извлекает из графа расширенную выборку: . Если фактор равен 3, Qdrant найдет 30 ближайших соседей, используя быстрые вычисления над int8 в оперативной памяти.
  • Для найденных 30 кандидатов база данных выполняет точечное чтение с диска, извлекает оригинальные float32 векторы и пересчитывает точные косинусные расстояния.
  • Финальные 10 результатов возвращаются клиенту.
  • Этот подход позволяет получить точность поиска, близкую к 99% от оригинальной, при этом сократив потребление RAM в 4 раза. Чтение 30 векторов с диска для пересчета занимает доли миллисекунды и не становится узким местом.

    Продуктовое квантование (Product Quantization)

    Продуктовое квантование (PQ) применяется для экстремального сжатия (в 32–64 раза), когда коллекция исчисляется сотнями миллионов точек.

    Вместо квантования каждого числа по отдельности, PQ разбивает многомерный вектор на подмножества (chunks). Например, вектор размерностью 768 разбивается на 32 блока по 24 измерения в каждом. Фоновый процесс Оптимизатора Qdrant анализирует всю коллекцию и находит для каждого блока 256 типичных паттернов (центроидов) с помощью алгоритма кластеризации K-Means.

    После обучения центроидов каждый блок из 24 чисел (float32) заменяется всего одним байтом — индексом ближайшего центроида (от 0 до 255). Исходный объем: байта. Сжатый объем: байта.

    При поиске Qdrant заранее вычисляет расстояния от вектора запроса до всех возможных центроидов и сохраняет их в небольшую таблицу (Look-up Table). Расстояние между вектором запроса и сжатым вектором в базе вычисляется как сумма предвычисленных расстояний из таблицы. PQ дает феноменальную экономию памяти, но требует вычислительных ресурсов на этапе кластеризации и слегка увеличивает задержку поиска из-за необходимости постоянных обращений к таблице центроидов.

    Асинхронный Batching и мультиплексирование gRPC

    Даже при идеально настроенном графе HNSW и квантованных векторах система может демонстрировать низкую пропускную способность из-за накладных расходов сетевого протокола. Взаимодействие с векторной базой данных через стандартный REST API (HTTP/1.1) для каждой отдельной операции создает критическое узкое место.

    Переход на gRPC

    Qdrant нативно поддерживает протокол gRPC, построенный поверх HTTP/2. В отличие от REST, где каждый запрос требует парсинга текстового JSON, gRPC использует бинарный формат сериализации Protobuf. Это исключает затраты CPU на кодирование и декодирование массивов чисел с плавающей запятой. Более того, мультиплексирование HTTP/2 позволяет отправлять тысячи параллельных запросов через одно TCP-соединение, устраняя проблему Head-of-line blocking.

    В Python-клиенте qdrant-client использование gRPC активируется автоматически при указании соответствующего порта (по умолчанию 6334) и использовании класса AsyncQdrantClient.

    Пакетная вставка (Upsert Batching)

    Вставка 100 000 векторов по одному запросу убьет производительность сети задержками на RTT (Round Trip Time). Операции upsert должны выполняться пакетами (батчами). Оптимальный размер пакета зависит от размерности векторов и объема Payload, но на практике составляет от 500 до 2000 точек за один вызов.

    При использовании AsyncQdrantClient пакетная вставка комбинируется с конкурентным выполнением корутин:

    Такой подход позволяет утилизировать пропускную способность сети на 100% и передать контроль над распределением I/O-нагрузки внутренним механизмам Qdrant.

    Пакетный поиск (Search Batching)

    Оптимизация требуется не только при записи, но и при чтении. В современных RAG-системах часто применяется паттерн Multi-Query Retrieval: исходный запрос пользователя перефразируется LLM в 3–5 альтернативных вариантов для повышения вероятности нахождения релевантного контекста.

    Вместо того чтобы выполнять 5 последовательных вызовов search, клиент должен использовать метод search_batch. Этот метод принимает массив векторов запросов и отправляет их в базу данных единым сетевым пакетом. Qdrant распараллеливает выполнение этих поисковых запросов на уровне своих внутренних потоков (worker threads) и возвращает массив результатов. Это снижает сетевую задержку в 5 раз и позволяет планировщику СУБД эффективнее использовать кэш процессора при обходе графа HNSW.

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

    4. Гибридный поиск и интеграция: связка Qdrant с FastAPI и LangChain

    Гибридный поиск и интеграция: связка Qdrant с FastAPI и LangChain

    Пользователь корпоративной базы знаний вводит запрос: «лимиты на командировки в 2023 году». Классический семантический поиск с высокой вероятностью вернет регламент 2024 года. Для нейросети тексты «лимиты на командировки в 2023 году» и «лимиты на командировки в 2024 году» семантически почти идентичны — векторы лягут вплотную друг к другу. Семантика прекрасно улавливает смысл, но слепа к точным фактам, артикулам, датам и идентификаторам. Чтобы система не галлюцинировала контекстом, архитектура должна объединять понимание смысла с жесткой лексической и метаданной фильтрацией.

    Архитектура гибридного поиска: плотные и разреженные векторы

    В контексте векторных баз данных под гибридным поиском понимают одновременное использование двух математических пространств: плотных векторов (Dense Vectors) и разреженных векторов (Sparse Vectors), часто в сочетании с жесткой фильтрацией по метаданным (Payload).

    Плотные векторы, генерируемые моделями вроде Sentence Transformers, представляют собой массивы фиксированной длины (например, 384 или 768 измерений), где каждое число — это абстрактный вес, не имеющий прямого человеческого смысла. Они отвечают за синонимию и контекст.

    Разреженные векторы (обычно генерируемые алгоритмами семейства BM25 или нейросетевыми моделями вроде SPLADE) имеют размерность, равную размеру всего словаря языка (десятки тысяч измерений). Однако для любого конкретного текста 99% этих измерений равны нулю. Ненулевые значения соответствуют конкретным словам (токенам) из текста, а их вес отражает важность слова (TF-IDF). Разреженные векторы решают проблему поиска по точным совпадениям: именам собственным, аббревиатурам и специфичным терминам.

    Qdrant поддерживает работу с обоими типами векторов в рамках одной коллекции через механизм именованных векторов (Named Vectors). При инициализации коллекции мы явно указываем, какие пространства в ней будут существовать:

    Каждая точка (Point) в такой коллекции будет содержать два независимых вектора. При выполнении запроса Qdrant должен вычислить расстояния в обоих пространствах, а затем объединить результаты.

    Механизм слияния результатов: Score Fusion и Prefetch API

    Когда мы выполняем поиск по двум разным векторным пространствам, мы получаем два независимых списка кандидатов со своими оценками (scores). Оценка косинусного расстояния плотного вектора лежит в диапазоне от -1 до 1, а оценка BM25 для разреженного вектора может быть любым положительным числом (например, 15.4 или 42.1). Их нельзя просто сложить.

    Для решения этой задачи применяется алгоритм Reciprocal Rank Fusion (RRF), который игнорирует абсолютные значения оценок и опирается только на позицию (ранг) документа в каждой из выдач. Формула слияния выглядит так: , где константа (обычно 60) смягчает влияние экстремально высоких позиций.

    В Qdrant этот процесс инкапсулирован на уровне движка базы данных через Prefetch API. Вместо того чтобы делать два отдельных запроса из Python-приложения и сливать их в оперативной памяти (что убивает производительность и увеличивает сетевой трафик), мы отправляем единый декларативный запрос.

    Prefetch API позволяет определить несколько подзапросов, которые выполнятся параллельно внутри Qdrant, после чего их результаты будут объединены указанным методом:

    В этом сценарии Qdrant самостоятельно извлекает по 20 лучших кандидатов из каждого пространства, применяет к ним жесткий фильтр по метаданным (например, department="HR"), вычисляет RRF и возвращает 10 итоговых документов. Это радикально снижает задержку (latency) гибридного поиска.

    Интеграция AsyncQdrantClient в жизненный цикл FastAPI

    Для обеспечения высокой пропускной способности API взаимодействие с Qdrant должно быть строго асинхронным и использовать мультиплексирование соединений gRPC. Инициализация клиента выносится в контекстный менеджер lifespan приложения FastAPI, чтобы избежать накладных расходов на установку TCP-соединений при каждом запросе.

    Использование prefer_grpc=True критически важно при передаче массивов чисел (векторов). В отличие от REST (JSON), где каждое число с плавающей запятой сериализуется в строку, gRPC использует бинарный формат Protobuf, что уменьшает объем передаваемых данных в несколько раз и ускоряет парсинг на стороне базы данных.

    Связка с LangChain: трансляция HTTP-параметров в Retriever

    Библиотека LangChain предоставляет высокоуровневую абстракцию QdrantVectorStore, которая скрывает под капотом логику векторизации запроса и обращения к БД. Однако в реальном API мы редко делаем «чистый» поиск. Обычно HTTP-запрос содержит как текстовый запрос, так и набор фильтров, пришедших от фронтенда или ИИ-агента.

    Рассмотрим Pydantic-модель входящего запроса:

    Главная архитектурная задача на слое FastAPI — динамически транслировать поля этой модели во внутренние структуры фильтров Qdrant, а затем передать их в абстракцию LangChain.

    LangChain позволяет передавать специфичные для векторной БД параметры через аргумент search_kwargs при создании объекта Retriever.

    В этом процессе QdrantVectorStore берет на себя вызов модели Sentence Transformers для векторизации request.query, а затем формирует финальный запрос к Qdrant, внедряя в него наш собранный qdrant_filter.

    Важный нюанс: LangChain ожидает, что фильтр, передаваемый в search_kwargs, является нативным объектом клиента Qdrant (models.Filter), а не словарем Python. Это позволяет использовать всю мощь декларативного языка запросов Qdrant (вложенные условия, гео-фильтры, диапазоны дат), не дожидаясь, пока разработчики LangChain реализуют абстракцию для каждого специфичного оператора.

    Для гибридного поиска LangChain предоставляет расширенный класс QdrantSparseVectorStore, который требует передачи двух моделей: одной для плотных эмбеддингов (например, HuggingFace) и одной для разреженных (например, локальной реализации алгоритма SPLADE). Логика проброса фильтров через search_kwargs при этом остается неизменной.

    Архитектурно такая интеграция четко разделяет зоны ответственности. FastAPI отвечает за валидацию входящих данных и управление соединениями. Qdrant берет на себя математику векторных пространств и слияние результатов через Prefetch API. LangChain выступает в роли клея, предоставляя унифицированный интерфейс Retriever, который в дальнейшем можно бесшовно встроить в сложные графы рассуждений и RAG-цепочки, не меняя бизнес-логику самого поиска.