Заметки

Гибридный поиск для RAG: BM25 + векторы

M
Markabus
·
Две стопки карточек — кремовая и зелёная — сливаются в одну общую колонку, сверху лежит латунная лупа: иллюстрация к статье о гибридном поиске для RAG

Гибридный поиск в 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. Сильные стороны двух методов почти зеркальны — поэтому их и объединяют.

Как устроен гибридный поиск

Схема в общем виде:

  1. Запрос пользователя отправляется параллельно в два «ретривера» (retriever — компонент, который достаёт кандидатов): векторный и полнотекстовый.
  2. Каждый возвращает свой топ — например, по 20–50 кандидатов.
  3. Списки сливаются в один методом фьюжна (fusion, слияние результатов).
  4. Первые 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 + векторы.

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

Q.Чем гибридный поиск отличается от векторного?

Векторный поиск ищет по смыслу через эмбеддинги, гибридный дополнительно ищет по ключевым словам (BM25) и объединяет оба списка. Так находятся и перефразированные вопросы, и точные строки — артикулы, коды ошибок, имена функций.

Q.Что такое RRF и почему его используют чаще всего?

Reciprocal Rank Fusion — способ слить несколько списков результатов по местам, а не по баллам: каждый документ получает сумму 1/(k + место) по всем спискам, обычно k = 60. Он не требует нормализации несравнимых баллов и хорошо работает без настройки.

Q.Нужна ли отдельная база для гибридного поиска?

Не обязательно. В PostgreSQL достаточно pgvector и встроенного полнотекстового поиска, в Qdrant — dense и sparse векторов в одной коллекции, в Elasticsearch — BM25 и поля dense_vector с ретривером rrf.

Q.Когда гибридный поиск не нужен?

Если база небольшая и состоит из разговорных текстов без артикулов, кодов и терминов, чистый векторный поиск может справляться не хуже. Решение стоит принимать по оценочному набору из 30–50 реальных вопросов.

Источники

←
Предыдущая
Thinking-модели: когда включать режим рассуждений

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

Фотореалистичный кадр рабочего стола сбоку: на переднем плане крупная поворотная ручка на металлическом пульте с пятью метками растущего размера, повёрнутая почти до упора, рядом горит янтарный индикатор и лежит секундомер, за ними в расфокусе монитор с чатом, где под вопросом длинный серый блок размышлений и внизу короткий ответ — иллюстрация к статье о режиме рассуждений у thinking-моделей.Заметки
24 сентября 2026 г.

Thinking-модели: когда включать режим рассуждений

У современных моделей есть переключатель глубины рассуждений — effort, thinking_level, reasoning_effort. Разбираем, что он реально меняет, сколько стоит и на каких задачах его стоит поднимать, а на каких он только жжёт токены.

Читать →
Тёмный рабочий стол разработчика крупным планом: ноутбук с абстрактной облачной консолью на экране, рядом аппаратный ключ безопасности и мини-сервер со светодиодами, на фоне в расфокусе второй монитор с кодомЗаметки
23 сентября 2026 г.

Vertex AI: когда он нужен вместо AI Studio

Модели одинаковые, а обвязка разная: ключ из AI Studio против проекта в Google Cloud с сервисными аккаунтами и выбором региона. Разбираем, где проходит граница, сколько это стоит и как переехать за полчаса — с поправкой на переименование Vertex AI в Gemini Enterprise Agent Platform.

Читать →
Фотореалистичный кадр на рабочем столе: большая пробковая доска с сотнями цветных булавок, собранных в кучки по цветам, от одной красной булавки натянуты пять красных нитей к пяти ближайшим соседям, слева выдвинутый ящик деревянного библиотечного каталога с карточками — иллюстрация к статье о векторном поиске ближайших соседей в PostgreSQL с расширением pgvector.Заметки
22 сентября 2026 г.

pgvector: векторный поиск прямо в PostgreSQL

Расширение pgvector добавляет в PostgreSQL векторный тип, операторы расстояния и ANN-индексы. Разбираем установку, выбор между HNSW и IVFFlat, фильтры по метаданным, экономию памяти и границу, за которой нужен отдельный векторный движок.

Читать →