Запросы в духе ILIKE '%красные кроссовки%' перестают работать в тот момент, когда пользователь пишет «кеды для бега». Смысл один — результат нулевой. Переезжать из PostgreSQL в ElasticSearch ради гибридного поиска хочется не всегда. Хорошая новость: скрестить два расширения прямо внутри базы — вполне рабочая стратегия. В этом разборе вы увидите, как именно это делается.
Два столпа гибридного поиска
Гибридный поиск решает слепые зоны каждого подхода по отдельности. Разберём обе части.
Лексический поиск (BM25) ищет документы по словам запроса, но считает не просто совпадения — он оценивает, насколько термин важен для документа и насколько он редок в коллекции. Это позволяет точнее ранжировать результаты, чем встроенный ts_rank. Аббревиатуры, инвентарные номера и редкие фамилии лексика отрабатывает безотказно.
Семантический (векторный) поиск переводит текст запроса и документы в векторы — числовые представления смысла — и ищет ближайшие. Такой поиск понимает синонимы и общий контекст, но может упустить чёткое упоминание конкретного слова, если модель его «замылила». Вместе они закрывают пробелы друг друга.
Ключевые расширения PostgreSQL
pgvector для векторного поиска
pgvector добавляет в базу тип vector и операторы расстояния. Индексные методы HNSW и IVFFlat позволяют выполнять приближённый поиск ближайших соседей за почти реальное время. Качество и скорость управляются параметрами hnsw.ef_search для HNSW и ivfflat.probes для IVFFlat. Без индекса поиск точный, но на больших таблицах он будет медленным.
Установка выполняется через CREATE EXTENSION pgvector;. Создавать колонку можно так: embedding vector(384). Для приближённого поиска после загрузки данных создаётся индекс, например:
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);
Запросы выглядят как SELECT … ORDER BY embedding <=> query_embedding LIMIT 20;, где <=> — оператор косинусного расстояния (доступны и другие).
pg_textsearch для BM25
Встроенный полнотекстовый поиск PostgreSQL (tsvector, ts_query, оператор @@) — это не BM25. За настоящий BM25 отвечает отдельное расширение pg_textsearch. Оно добавляет оператор <@>, который ранжирует документы по релевантности согласно модели BM25 и используется в ORDER BY.
Установка требует прописать расширение в shared_preload_libraries и выполнить CREATE EXTENSION pg_textsearch;. Далее можно настроить параметры BM25 (k1 и b) на уровне сессии или индекса. Для быстрых top-k запросов с LIMIT расширение автоматически применяет оптимизацию Block-Max WAND, поэтому лимит стоит задавать всегда — отсутствие LIMIT приведёт к ранжированию всех документов до внутреннего значения default_limit, что может ударить по производительности.
Как объединить результаты: Reciprocal Rank Fusion (RRF)
После того как отдельные поиски вернули списки id документов с весами, нужно слить их в один рейтинг. Самый простой и прозрачный метод — Reciprocal Rank Fusion. Формула простая: документ получает балл, равный сумме обратных рангов в каждом источнике:
RRF_score = Σ (1 / (k + rank_i)), где k — константа (обычно 60).
Документ, высоко поднявшийся в обоих поисках, окажется наверху. RRF не требует нормализации и легко реализуется на чистом SQL.
Альтернативный подход — пропустить небольшой топ результатов через cross-encoder. Это более тяжёлая модель, которая переоценивает пары «запрос-документ» и даёт более точный итоговый порядок. Но для большинства приложений RRF достаточно.
Практическая реализация: пошаговое руководство
Подготовка инфраструктуры
Предположим, у вас уже установлены pgvector и pg_textsearch по инструкциям их репозиториев. Создайте таблицу для документов:
CREATE TABLE docs (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title text,
content text,
embedding vector(384) -- размер под вашу модель
);
Заполните поле embedding с помощью внешнего API эмбеддингов или библиотеки на Python, передающей векторы в базу.
Лексический поиск с BM25
Создайте индекс BM25 по столбцу, по которому будете искать:
CREATE INDEX docs_content_idx ON docs USING bm25 (content);
Теперь запрос на поиск выглядит так:
SELECT id, title
FROM docs
ORDER BY content <@> 'поисковой запрос'
LIMIT 20;
Расширение само преобразует текстовую строку в tsquery с учётом языковой конфигурации по умолчанию. Если нужно управлять параметрами k1 и b, установите их через SET pg_textsearch.k1 = 1.2; перед запросом.
Векторный поиск с pgvector
Создаём индекс для приближённого поиска:
CREATE INDEX docs_embedding_hnsw_idx ON docs USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
После этого поиск делается так:
SELECT id, title, 1 - (embedding <=> '[-0.23, 0.15, …, 0.04]') AS similarity
FROM docs
ORDER BY embedding <=> '[-0.23, 0.15, …, 0.04]'
LIMIT 20;
Помните, что приближённый поиск может слегка варьировать результаты, а настройка hnsw.ef_search влияет на баланс скорости и точности.
Гибридный запрос с RRF
Соберём всё в одном SQL. Каждый подзапрос возвращает ранжированный топ, а затем по id вычисляется RRF-оценка:
WITH lexical AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY content <@> 'умные часы для бега') AS rank
FROM docs
ORDER BY content <@> 'умные часы для бега'
LIMIT 50
),
semantic AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> vector_query) AS rank
FROM docs
ORDER BY embedding <=> vector_query
LIMIT 50
)
SELECT d.id, d.title,
COALESCE(1.0 / (60 + l.rank), 0) +
COALESCE(1.0 / (60 + s.rank), 0) AS rrf_score
FROM docs d
LEFT JOIN lexical l ON d.id = l.id
LEFT JOIN semantic s ON d.id = s.id
WHERE l.id IS NOT NULL OR s.id IS NOT NULL
ORDER BY rrf_score DESC
LIMIT 20;
Значение vector_query должно быть подставлено клиентским кодом или обёрткой в виде массива чисел. Такой запрос можно упаковать в функцию или пользоваться им напрямую.
Если после RRF нужно поднять точность, отберите топ-30 и пропустите их через cross-encoder уже на стороне приложения — модель сравнит заголовок+текст с запросом и вернёт окончательный порядок.
Резюме и следующие шаги
Комбинация pg_textsearch + pgvector + RRF даёт полноценный гибридный поиск без внешних систем. Документы хранятся в одном месте, транзакционная целостность не страдает, а оба индекса живут прямо в PostgreSQL.
Куда смотреть дальше:
- Официальная документация
pgvector(особенно раздел Hybrid Search) иpg_textsearch— там описаны все параметры. - Мониторинг качества выдачи: проверяйте долю кликов на первых позициях, чтобы вовремя подкрутить
kв RRF или обновить эмбеддинги. - При росте объёмов задумайтесь о переранжировании cross-encoder на топе и о периодическом перестроении векторного индекса.
FAQ
Обязательно ли использовать именно pg_textsearch для BM25?
Нет. Можно работать и со встроенным полнотекстовым поиском PostgreSQL — он тоже даёт лексический сигнал. Но его внутренняя модель ранжирования — это не BM25. Если для вас важен именно BM25, pg_textsearch — прямой кандидат.
Замедляет ли pgvector работу PostgreSQL?
Сам по себе тип vector и индекс не замедляют запросы к другим таблицам. Приближённый поиск с HNSW работает быстро, но требует оперативной памяти под индекс. При точном поиске без индекса или на очень больших таблицах нагрузка может возрасти.
Когда гибридный поиск имеет смысл, а когда можно обойтись чем-то одним?
Если в запросах часто встречаются чёткие термины, каталожные номера или редкие фамилии — одного вектора мало. Если пользователи формулируют запросы описательно, а документы написаны разным языком — одного лексического поиска не хватит. Гибрид незаменим, когда оба сценария смешаны.
Какой метод слияния результатов выбрать: RRF или cross-encoder?
Начните с RRF: он понятен, не требует дополнительного железа и легко реализуется в SQL. Если после запуска видите, что целевой релевантности не хватает, добавьте переранжирование кросс-энкодером поверх топ-30/50 кандидатов.
Можно ли обойтись без приближённого поиска в pgvector?
Да, не создавайте индекс или используйте поиск без индекса — будет точный nearest neighbor. Но на сотнях тысяч векторов запрос без индекса может занять несколько секунд.
Нужно ли перестраивать индексы?
Индекс BM25 в pg_textsearch обновляется при изменении данных автоматически. Векторные индексы HNSW и IVFFlat также поддерживают вставки, но если вы вставили много новых векторов и скорость упала, периодически делайте REINDEX. Для IVFFlat иногда советуют перестроение после больших загрузок.
Совместимы ли эти расширения с облачными PostgreSQL?
Если облачный провайдер разрешает устанавливать расширения из shared_preload_libraries, то да. Уточните список поддерживаемых расширений в вашем провайдере — pgvector встречается чаще, pg_textsearch может потребовать отдельного согласования.




.svg.webp)


