Гибридный поиск BM25 + эмбеддинги для базы знаний

Чистый BM25 не находит синонимы и парафразы. Чистые эмбеддинги пролетают мимо аббревиатур, артикулов и точных фраз. Гибридный поиск закрывает оба слабых места. Разбираем, как собрать BM25 + векторный поиск с reciprocal rank fusion на PostgreSQL и Elasticsearch.

Базы знаний — техподдержка, внутренняя wiki, корпоративные документы, FAQ магазина — упираются в один и тот же камень: поиск. Чистый BM25 (классический полнотекстовый) теряет синонимы, парафразы, опечатки. Чистый семантический поиск через эмбеддинги — теряет аббревиатуры, артикулы, точные термины и числа.

В 2026 году стандарт де-факто для production-поиска по базе знаний — гибрид: BM25 + векторный, объединённые через reciprocal rank fusion. Разбираем механику и рабочие реализации на PostgreSQL и Elasticsearch.

Где ломается чистый BM25

BM25 — это лучшая эволюция TF-IDF. Считает релевантность через частоту терминов, корректирует по длине документа. Работает по точному совпадению слов и их форм.

Запросы, где BM25 проваливается:

  • «как вернуть телевизор» → не найдёт документ «Порядок возврата электроники».
  • «не запускается ноутбук» → не найдёт «Устранение проблем с включением».
  • «крутится колесо но машина не едет» → не найдёт «Диагностика сцепления».

Семантически похоже — лексически разное.

Где ломается чистый семантический поиск

Эмбеддинги (text-embedding-3-large, multilingual-e5, bge-m3, GigaChat-embeddings) переводят текст в вектор. Сравнение — косинусное расстояние. Хорошо ловит смысл, плохо — точность.

Запросы, где провалится семантика:

  • «артикул 7748-К» → найдёт «инструкция по сборке артикула 5512-К», потому что числа и коды плохо ложатся в эмбеддинги.
  • «статья 152.1» → пропустит точную ссылку, всплывут общие материалы про персданные.
  • «MacBook Pro M4 14 дюймов» → выдаст и M2, и Air, потому что модели близки по семантике.
  • «ИП Иванов» → найдёт «индивидуальное предпринимательство», но не конкретного контрагента.

Что делает гибрид

Идея простая: запустить оба поиска параллельно, объединить результаты, выдать топ.

Проблема — нормализация скоров. BM25 даёт абсолютные числа без верхней границы. Косинусное — от -1 до 1. Линейно складывать нельзя: один поиск задавит другой.

Решение, которое работает в проде в 90% случаев — reciprocal rank fusion (RRF). Складывает не скоры, а ранги.

RRF(d) = sum_over_engines( 1 / (k + rank(d, engine)) )

Где rank — позиция документа в выдаче конкретного движка (1, 2, 3…), k — константа сглаживания, обычно 60.

Документ, попавший в топ-3 обоих движков, получит высокий RRF. Документ из топа BM25, но провалившийся семантически — средний. Документ только из одного движка — ещё ниже.

Минимальная реализация на PostgreSQL

Один Postgres закрывает оба движка: full-text через tsvector, векторный через pgvector.

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE docs (
  id          bigserial PRIMARY KEY,
  title       text NOT NULL,
  body        text NOT NULL,
  body_tsv    tsvector
              GENERATED ALWAYS AS (
                to_tsvector('russian',
                  coalesce(title,'') || ' ' || coalesce(body,'')
                )) STORED,
  embedding   vector(1024)
);

CREATE INDEX docs_tsv_idx ON docs USING gin (body_tsv);
CREATE INDEX docs_embed_idx ON docs
  USING hnsw (embedding vector_cosine_ops);

Гибридный запрос с RRF:

WITH params AS (
  SELECT plainto_tsquery('russian', $1) AS q,
         $2::vector AS qvec,
         60 AS k,
         50 AS topn
),
ft AS (
  SELECT id,
    row_number() OVER (
      ORDER BY ts_rank_cd(body_tsv, params.q) DESC
    ) AS r
  FROM docs, params
  WHERE body_tsv @@ params.q
  LIMIT (SELECT topn FROM params)
),
sem AS (
  SELECT id,
    row_number() OVER (
      ORDER BY embedding <=> params.qvec
    ) AS r
  FROM docs, params
  ORDER BY embedding <=> params.qvec
  LIMIT (SELECT topn FROM params)
),
fused AS (
  SELECT id, sum(score) AS rrf FROM (
    SELECT id, 1.0 / (60 + r) AS score FROM ft
    UNION ALL
    SELECT id, 1.0 / (60 + r) AS score FROM sem
  ) t
  GROUP BY id
)
SELECT d.id, d.title,
       left(d.body, 240) AS snippet,
       f.rrf
FROM fused f
JOIN docs d ON d.id = f.id
ORDER BY f.rrf DESC
LIMIT 10;

$1 — текст запроса для BM25-стиля поиска через ts_rank, $2 — вектор того же запроса от той же модели, что считала эмбеддинги документов. Используем одну и ту же модель и для индексации, и для запроса — иначе расстояния не имеют смысла.

Тот же подход на Elasticsearch

В Elasticsearch с 8.9+ RRF встроен как ретривер:

GET /docs/_search
{
  "retriever": {
    "rrf": {
      "retrievers": [
        { "standard": {
            "query": { "match": { "body": "как вернуть телевизор" } }
        }},
        { "knn": {
            "field": "embedding",
            "query_vector": [...],
            "k": 50,
            "num_candidates": 200
        }}
      ],
      "rank_window_size": 50,
      "rank_constant": 60
    }
  },
  "size": 10
}

Тот же RRF, но без ручной сборки.

Какие эмбеддинги брать

В РФ-проекте 2026 живые варианты:

  • multilingual-e5-large (open source, MIT) — 1024 dim, бесплатный self-host, отлично работает на русском.
  • bge-m3 (open source) — мультиязык + dense + sparse в одной модели. Можно поднять в Docker.
  • GigaChat Embeddings — облако от Сбера, русский в приоритете, оплата за токены.
  • YandexGPT Embeddings — облако от Яндекса.
  • OpenAI text-embedding-3-large — лидер по качеству, но из РФ напрямую нельзя.

На внутреннюю базу знаний 80% задач закрывает self-host e5-large или bge-m3 — нет утечки данных, нет регулярных расходов.

Когда добавлять реранкер

RRF — быстрый и грубый объединитель. На 50–100 кандидатов работает хорошо. Для качественной выдачи топ-3-5 поверх RRF ставят cross-encoder реранкер: модель, которая берёт пару (запрос, документ) и оценивает релевантность.

Популярные:

  • bge-reranker-v2-m3 — мультиязычный, open source.
  • mxbai-rerank-large-v1 — англоязычный, но компактный.
  • Cohere rerank — облако.

Реранкер — медленный (50–200 мс на пару). Запускать на топ-20 из RRF, не на весь индекс.

Подготовка данных

Гибрид не вытащит то, чего нет в индексе. Что обязательно:

  1. Чанкование. Длинные документы режут на куски 300–800 токенов с overlap 50–100. Иначе эмбеддинг «размазан» по теме всего документа.
  2. Заголовки + body вместе. Эмбеддить только заголовок — мало контекста. Только body — теряем «о чём вообще документ».
  3. Метаданные. Артикул, дата, версия, продукт — отдельными полями. Через них фильтрация (WHERE product_id = X) до поиска.
  4. Нормализация текста. Убираем лишние пробелы, нормализуем регистр, разворачиваем сокращения для BM25 («ИП»«ИП индивидуальный предприниматель»).

Оценка качества

Чтобы понять, гибрид лучше или хуже одиночного движка — нужна оценка. Минимум:

  1. Собрать 50–200 реальных запросов от пользователей.
  2. Для каждого — ручная разметка топ-3 «правильных» документов.
  3. Метрики: Recall@10 (есть ли правильный в топ-10), MRR (на каком месте первый правильный), NDCG@10.

В типовом проекте на русскоязычной базе знаний:

  • Чистый BM25 — Recall@10 ~60-72%.
  • Чистые эмбеддинги — Recall@10 ~64-78%.
  • Гибрид через RRF — Recall@10 ~82-90%.
  • Гибрид + cross-encoder реранкер — Recall@10 ~88-94%.

Грабли

  • Разные модели для индексации и запроса. Если эмбеддили базу через bge-m3, а запрос через e5 — расстояния бессмысленны.
  • Малая константа k в RRF. k=1 даёт сильный буст топ-1 каждого движка, k=60 — сглаживает. Слишком мало — задавит шум, слишком много — потеряет различия.
  • Поиск только по эмбеддингам в продукте с артикулами. Категорическая ошибка для интернет-магазина и техподдержки оборудования. BM25 обязателен.
  • HNSW без правильных параметров (m, ef_construction). Recall падает на низких значениях. По умолчанию в pgvector часто мало.
  • Кэширование эмбеддингов запросов. «Как вернуть товар» спрашивают часто. Считать эмбеддинг каждый раз — лишние вызовы API. Простой Redis-кэш на популярные запросы экономит много.

Когда хватает чистого BM25

Не каждой базе знаний нужен гибрид. Хватает BM25, если:

  • Корпус однородный (юридические документы со строгой терминологией).
  • Запросы пользователей точные и используют термины из документов.
  • Бюджет на инфраструктуру и эмбеддинги — минимум.
  • Recall@10 на чистом BM25 уже >80%.

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

Вывод

Гибрид BM25 + эмбеддинги + RRF — стандартная отправная точка для поиска по базе знаний в 2026. На PostgreSQL это собирается за день на pgvector. На Elasticsearch — встроенный rrf ретривер. Качество выдачи прыгает с 65% до 85% Recall@10. Дальше — cross-encoder реранкер на топ-20. Чистая семантика без BM25 — категорически нет в продуктах с артикулами, кодами и точными ссылками.