Гибридный поиск в RAG — это когда кусочки документов для ответа модели ищут сразу двумя способами: по ключевым словам (алгоритм BM25) и по смыслу (векторы-эмбеддинги), а потом сливают два списка результатов в один. Звучит как усложнение, но на практике это одна из самых дешёвых доработок, которая заметно снижает число промахов: векторный поиск перестаёт терять артикулы, коды ошибок и редкие термины, а поиск по словам — синонимы и перефразировки. Разберём, как это устроено, как объединять результаты и как собрать гибридный поиск в Qdrant, PostgreSQL и Elasticsearch.
Если термины RAG и эмбеддинги пока в новинку, начните с объяснений что такое RAG и как текст превращается в числа — дальше будет проще.
Почему одного векторного поиска мало
Классическая схема RAG (Retrieval-Augmented Generation, генерация с подгрузкой найденных данных) выглядит так: документы режут на чанки, каждый превращают в вектор, кладут в векторную базу, а на вопрос пользователя ищут ближайшие по смыслу векторы. Это отлично работает, когда вопрос и ответ сформулированы разными словами: «как вернуть деньги» найдёт абзац про «оформление возврата».
Но у эмбеддингов есть слепая зона — точные строки, которые не несут «смысла» в привычном понимании:
- артикулы и SKU:
AX-4471-BиAX-4471-Cдля модели почти одинаковы, а для покупателя это разные товары; - коды ошибок и номера:
ERR_CONN_RESET,HTTP 429, номер CVE или пункта договора; - имена функций, классов, хуков, названия опций конфигурации;
- редкие термины и внутренний жаргон компании, которых модель эмбеддингов почти не видела при обучении;
- фамилии, названия компаний и продуктов.
В таких запросах вектор «размазывает» точное совпадение по соседям, и нужный чанк оказывается на 15-м месте вместо первого — а в контекст модели попадают только первые пять. Итог — ответ по неправильному документу и та самая галлюцинация, которая на деле была ошибкой поиска.
Что такое BM25 простыми словами
BM25 (Best Matching 25, семейство формул Okapi) — классический алгоритм ранжирования по ключевым словам, на котором десятилетиями работали поисковые движки. Он оценивает документ по трём идеям:
- частота термина — чем чаще слово запроса встречается в документе, тем лучше, но с насыщением: десятое упоминание добавляет меньше, чем второе (за это отвечает параметр
k1, обычно около 1,2); - редкость термина (IDF, inverse document frequency) — совпадение по редкому слову «AX-4471-B» весит гораздо больше, чем по частому «товар»;
- длина документа — длинный текст не должен выигрывать только потому, что в нём больше слов (параметр
b, обычно 0,75).
BM25 не понимает синонимов и перефразировок, зато безошибочно находит точные строки, работает быстро и не требует GPU. Сильные стороны двух методов почти зеркальны — поэтому их и объединяют.
Как устроен гибридный поиск
Схема в общем виде:
- Запрос пользователя отправляется параллельно в два «ретривера» (retriever — компонент, который достаёт кандидатов): векторный и полнотекстовый.
- Каждый возвращает свой топ — например, по 20–50 кандидатов.
- Списки сливаются в один методом фьюжна (fusion, слияние результатов).
- Первые 5–10 результатов идут в промпт модели — или сначала в реранкер, который переоценит их ещё точнее.
Главный вопрос — как именно сливать. Баллы двух методов несравнимы: косинусная близость лежит в пределах от −1 до 1, а оценка BM25 может быть и 3, и 27 в зависимости от коллекции. Сложить их «как есть» нельзя.
Reciprocal Rank Fusion (RRF)
Самый популярный и надёжный способ — RRF, слияние по обратному рангу. Он вообще не смотрит на баллы, только на места в списках. Каждый документ получает сумму по всем спискам, где он встретился:
score(d) = Σ 1 / (k + rank(d))
Здесь rank — место документа в конкретном списке (1, 2, 3…), а k — сглаживающая константа, по умолчанию обычно 60 (так, например, в Elasticsearch). Документ, который стоит высоко в обоих списках, получает почти двойной балл и уверенно поднимается наверх. Документ, найденный только одним методом, но на первом месте, тоже не теряется. Большой k выравнивает вклад мест, маленький — сильнее поощряет лидеров каждого списка.
Весь алгоритм помещается в несколько строк:
def rrf(result_lists, k=60):
scores = {}
for results in result_lists:
for rank, doc_id in enumerate(results, start=1):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)
final = rrf([vector_ids, bm25_ids])[:10]
Слияние по нормализованным баллам
Альтернатива — привести баллы обоих методов к общей шкале (например, 0…1) и сложить с весом: alpha × вектор + (1 − alpha) × BM25. Так работает гибридный поиск Weaviate: параметр alpha = 1 означает чистый векторный поиск, 0 — чистый поиск по словам, а по умолчанию начиная с версии 1.24 используется relative score fusion — слияние по относительным баллам. В Qdrant похожий подход называется DBSF (distribution-based score fusion): баллы нормализуются по среднему и стандартному отклонению.
Практическое правило, которое совпадает с рекомендациями Qdrant: нет набора тестовых вопросов — берите RRF; есть оценочный набор — подбирайте веса и сравнивайте варианты на цифрах.
Насколько это помогает
Хороший ориентир — исследование Anthropic о Contextual Retrieval. На их наборе данных доля случаев, когда нужный фрагмент не попадал в топ-20, была 5,7% для обычных эмбеддингов. Контекстные эмбеддинги снизили её до 3,7%, добавление BM25 — до 2,9% (минус 49% ошибок), а реранкинг поверх гибрида — до 1,9% (минус 67%). Цифры относятся к конкретному эксперименту, но направление характерное: BM25 даёт один из самых заметных приростов за минимальную цену.
Как сделать гибридный поиск на практике
Qdrant: dense + sparse векторы
В Qdrant гибрид строится через Query API (с версии 1.10): в одной коллекции хранятся обычные плотные векторы (dense) и разреженные (sparse) — именно разреженные отвечают за поиск по словам в стиле BM25. Запрос делает два предварительных отбора (prefetch) и сливает их:
client.query_points(
collection_name="docs",
prefetch=[
models.Prefetch(query=sparse_vector, using="sparse", limit=30),
models.Prefetch(query=dense_vector, using="dense", limit=30),
],
query=models.FusionQuery(fusion=models.Fusion.RRF),
limit=10,
)
Константу k в RRF можно настраивать с версии 1.16, а с 1.17 доступен взвешенный RRF — когда одному из методов доверяете больше.
PostgreSQL: pgvector + полнотекстовый поиск
Если данные уже лежат в Postgres, отдельная база не нужна: pgvector отвечает за векторы, встроенный полнотекстовый поиск (tsvector) — за слова, а RRF пишется прямо в SQL:
WITH semantic AS (
SELECT id, RANK() OVER (ORDER BY embedding <=> $1) AS rnk
FROM chunks ORDER BY embedding <=> $1 LIMIT 40
),
keyword AS (
SELECT id, RANK() OVER (ORDER BY ts_rank_cd(tsv, q) DESC) AS rnk
FROM chunks, websearch_to_tsquery('russian', $2) q
WHERE tsv @@ q
ORDER BY ts_rank_cd(tsv, q) DESC LIMIT 40
)
SELECT COALESCE(s.id, k.id) AS id,
COALESCE(1.0 / (60 + s.rnk), 0) +
COALESCE(1.0 / (60 + k.rnk), 0) AS score
FROM semantic s
FULL OUTER JOIN keyword k ON s.id = k.id
ORDER BY score DESC
LIMIT 10;
Нюанс: встроенный ts_rank_cd — не BM25, он не учитывает редкость слов по всей коллекции. Для RRF это не критично (важен порядок, а не баллы), а если нужен настоящий BM25 — есть сторонние расширения для Postgres. Не забудьте GIN-индекс на колонку tsv и конфигурацию russian для стемминга: тогда «возврата» найдётся по запросу «возврат».
Elasticsearch и OpenSearch
Здесь BM25 — поиск по умолчанию, а векторы хранятся в поле типа dense_vector. В Elasticsearch оба запроса объединяются ретривером rrf: внутри — standard (обычный текстовый запрос) и knn (векторный), параметры rank_constant (по умолчанию 60) и rank_window_size задают константу и глубину каждого списка.
Типичные ошибки
- Слишком короткие списки кандидатов. Если каждый метод отдаёт только топ-5, фьюжну нечего объединять. Берите 20–50 из каждого, в модель отдавайте 5–10.
- Сложение сырых баллов. Косинус и BM25 в разных шкалах — без нормализации или RRF один метод просто задавит другой.
- Нет морфологии для русского. BM25 без стемминга или лемматизации не свяжет «договор» и «договора» — половина пользы потеряется.
- Плохая нарезка. Гибрид не спасёт, если нужный факт разрезан между чанками. Как резать документы, разобрано в статье про чанкинг для RAG.
- Настройка «на глаз». Соберите 30–50 реальных вопросов с известными правильными чанками и считайте, в скольких случаях нужный фрагмент попал в топ-N. Без этого любые веса — гадание.
Когда гибридный поиск нужен, а когда нет
Гибрид почти всегда окупается, если в базе есть технические документы, каталог товаров, код, юридические тексты или внутренняя терминология — всё, где пользователь ищет по точным строкам. Если же база — это небольшой набор разговорных текстов (FAQ поддержки, статьи блога), а вопросы задаются свободными формулировками, чистый векторный поиск может справляться не хуже. Проверяется это тем же оценочным набором: включите BM25, сравните долю попаданий — и решение станет очевидным.
Итог
Гибридный поиск — это векторный поиск плюс BM25 и аккуратное слияние результатов, чаще всего через RRF. Он закрывает главную слабость эмбеддингов — точные строки — и почти ничего не стоит: во всех популярных базах есть готовые механизмы, а в Postgres хватает одного SQL-запроса. Следующий шаг после гибрида — реранкер, который переоценивает найденные кандидаты ещё точнее, но начинать стоит именно с BM25 + векторы.
Частые вопросы
Векторный поиск ищет по смыслу через эмбеддинги, гибридный дополнительно ищет по ключевым словам (BM25) и объединяет оба списка. Так находятся и перефразированные вопросы, и точные строки — артикулы, коды ошибок, имена функций.
Reciprocal Rank Fusion — способ слить несколько списков результатов по местам, а не по баллам: каждый документ получает сумму 1/(k + место) по всем спискам, обычно k = 60. Он не требует нормализации несравнимых баллов и хорошо работает без настройки.
Не обязательно. В PostgreSQL достаточно pgvector и встроенного полнотекстового поиска, в Qdrant — dense и sparse векторов в одной коллекции, в Elasticsearch — BM25 и поля dense_vector с ретривером rrf.
Если база небольшая и состоит из разговорных текстов без артикулов, кодов и терминов, чистый векторный поиск может справляться не хуже. Решение стоит принимать по оценочному набору из 30–50 реальных вопросов.
Источники
- 1.Anthropic: Introducing Contextual Retrievalhttps://www.anthropic.com/news/contextual-retrieval
- 2.Qdrant: Hybrid and Multi-Stage Querieshttps://qdrant.tech/documentation/concepts/hybrid-queries/
- 3.pgvector — README, раздел Hybrid Searchhttps://github.com/pgvector/pgvector
- 4.Elasticsearch: Reciprocal rank fusionhttps://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion
- 5.Weaviate: Hybrid searchhttps://docs.weaviate.io/weaviate/search/hybrid



