Гибридный поиск 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, не на весь индекс.
Подготовка данных
Гибрид не вытащит то, чего нет в индексе. Что обязательно:
- Чанкование. Длинные документы режут на куски 300–800 токенов с overlap 50–100. Иначе эмбеддинг «размазан» по теме всего документа.
- Заголовки + body вместе. Эмбеддить только заголовок — мало контекста. Только body — теряем «о чём вообще документ».
- Метаданные. Артикул, дата, версия, продукт — отдельными полями. Через них фильтрация (
WHERE product_id = X) до поиска. - Нормализация текста. Убираем лишние пробелы, нормализуем регистр, разворачиваем сокращения для BM25 (
«ИП»→«ИП индивидуальный предприниматель»).
Оценка качества
Чтобы понять, гибрид лучше или хуже одиночного движка — нужна оценка. Минимум:
- Собрать 50–200 реальных запросов от пользователей.
- Для каждого — ручная разметка топ-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 — категорически нет в продуктах с артикулами, кодами и точными ссылками.