Гибридный поиск: retrievers и RRF
В предыдущих статьях разобрали два противоположных подхода: BM25 ищет по точным словам, а Векторный поиск: поле dense_vector и запрос kNN и ELSER и разреженные векторы для семантики — по смыслу. Каждый подход точен в одной ситуации и слеп в другой.
Гибридный поиск объединяет оба — и почти всегда выигрывает у каждого из них по отдельности.
Почему одного подхода мало
Возьмём каталог товаров.
Запрос: «iPhone 15 Pro» — точный артикул.
- BM25 справится отлично: найдёт документы с этими словами.
- Семантика может вернуть «Samsung Galaxy S24» как «похожий по смыслу» — это провал.
Запрос: «чехол защищает телефон при падении» — смысловой запрос.
- BM25 буквально ищет слово «падении» — пропустит товары с описанием «ударопрочный» или «защита корпуса».
- Семантика поймёт намерение и найдёт нужное.
Гибрид одновременно покрывает оба сценария.
Reciprocal Rank Fusion: слияние без общей шкалы
Главная проблема при объединении двух ранжированных списков — несовместимость баллов. _score от BM25 и косинусное сходство kNN живут на разных числовых шкалах: складывать их напрямую бессмысленно.
RRF не смотрит на баллы вообще. Он работает только с позициями:
Где:
- $R$ — набор ранжированных списков (BM25, kNN и т. д.),
- $\text{rank}_r(d)$ — позиция документа $d$ в списке $r$ (нумерация с 1),
- $k$ — константа сглаживания, по умолчанию $k = 60$.
Числовой пример при $k = 60$ и двух списках:
| Документ | Ранг BM25 | Ранг kNN | RRF-балл |
|---|---|---|---|
| A | 1 | 1 | $\frac{1}{61} + \frac{1}{61} \approx 0{,}033$ |
| B | 2 | 4 | $\frac{1}{62} + \frac{1}{64} \approx 0{,}032$ |
| C | 1 | — | $\frac{1}{61} \approx 0{,}016$ |
| D | — | 2 | $\frac{1}{62} \approx 0{,}016$ |
Документ A — первый в обоих списках — получает вдвое больший балл, чем C или D, которые первые только в одном. Это и есть главная идея RRF: консенсус нескольких независимых методов даёт бонус к финальному рангу.
Константа $k = 60$ взята из оригинальной статьи Cormack, Clarke и Buettcher (SIGIR 2009) и хорошо работает без дополнительного тюнинга.
flowchart TD
Q[Поисковый запрос] --> ST[standard retriever]
Q --> KN[knn retriever]
ST --> BL[Список BM25 — rank_window_size кандидатов]
KN --> KL[Список kNN — rank_window_size кандидатов]
BL --> RRF[RRF — суммирует 1 делить на k+rank по каждому документу]
KL --> RRF
RRF --> RES[Топ-size финальных результатов]Синтаксис retrievers (ES 8.14+, GA с 8.16)
Вместо привычного ключа query в запросе появляется retriever. Доступны три основных типа:
| Тип | Что делает |
|---|---|
standard | Оборачивает любой Query DSL запрос |
knn | kNN-поиск (те же параметры, что у поля knn в _search) |
rrf | Объединяет несколько retrievers через RRF |
Минимальная структура гибридного запроса:
GET /index/_search
{
"retriever": {
"rrf": {
"retrievers": [
{ "standard": { "query": { "match": { "..." : "..." } } } },
{ "knn": { "field": "embedding", "query_vector": ["..."], "k": 10, "num_candidates": 100 } }
],
"rank_window_size": 50,
"rank_constant": 60
}
},
"size": 10
}rank_window_size — сколько кандидатов запрашивает каждый sub-retriever перед финальным слиянием. Значение должно быть не меньше size. Для size: 10 рекомендуется ставить 50–100: больший пул кандидатов улучшает качество финального ранжирования.
Полный пример: BM25 + kNN
Создадим индекс с текстовым и векторным полями:
PUT /catalog
{
"mappings": {
"properties": {
"title": { "type": "text" },
"description": { "type": "text" },
"embedding": {
"type": "dense_vector",
"dims": 384,
"index": true,
"similarity": "cosine"
}
}
}
}Гибридный запрос (вектор запроса — заглушка-пример; в реальном проекте его генерирует embedding-модель перед отправкой запроса):
GET /catalog/_search
{
"retriever": {
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"multi_match": {
"query": "ударопрочный чехол смартфон",
"fields": ["title^2", "description"]
}
}
}
},
{
"knn": {
"field": "embedding",
"query_vector": [0.12, -0.45, 0.33],
"num_candidates": 100,
"k": 10
}
}
],
"rank_window_size": 100,
"rank_constant": 60
}
},
"size": 10
}Вариант с semantic_text — без своих векторов
Если вы используете Поле semantic_text и inference-эндпоинты, векторизовать запрос вручную не нужно — semantic-запрос делает это сам. Оба retriever'а будут типа standard, потому что Query DSL уже содержит всю логику:
GET /articles/_search
{
"retriever": {
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"match": {
"title": "наушники для путешествий"
}
}
}
},
{
"standard": {
"query": {
"semantic": {
"field": "content",
"query": "наушники для путешествий"
}
}
}
}
],
"rank_window_size": 50
}
},
"size": 5
}knn-retriever здесь не нужен — semantic-запрос сам выполняет векторный поиск через ELSER внутри ES.
Общий фильтр через rrf
Когда одно и то же условие нужно применить сразу к обоим путям, filter на уровне rrf избавляет от повторений:
{
"retriever": {
"rrf": {
"retrievers": [
{ "standard": { "query": { "match": { "title": "чехол" } } } },
{ "knn": { "field": "embedding", "query_vector": ["..."], "k": 10, "num_candidates": 100 } }
],
"rank_window_size": 50,
"filter": [
{ "term": { "in_stock": true } },
{ "range": { "price": { "lte": 2000 } } }
]
}
},
"size": 10
}Фильтр применяется до слияния — оба списка кандидатов уже содержат только документы, прошедшие условие.
Когда гибрид оправдан
Гибридный поиск добавляет сложность: нужно хранить два поля, вычислять эмбеддинги при индексации и при каждом запросе. Это не всегда нужно.
Оставайтесь на BM25, если:
- Пользователи ищут по артикулам, именам, кодам — точные термины важнее смысла.
- Корпус маленький или хорошо структурированный.
Чистый семантический поиск, если:
- Запросы — развёрнутые вопросы на естественном языке, ключевые слова непредсказуемы.
- Нет требований к точным совпадениям.
Гибрид BM25 + kNN/ELSER, если:
- Часть запросов — точные термины, часть — смысловые вопросы (типичная поисковая строка на сайте).
- Нужна максимальная полнота охвата.
- Качество поиска приоритетнее экономии ресурсов.
На практике для продуктовых каталогов, баз знаний и статейных порталов гибридный поиск почти всегда обгоняет любой из подходов по отдельности — именно поэтому он стал стандартной рекомендацией Elastic для production-сценариев.
См. также
- Векторный поиск: поле dense_vector и запрос kNN — kNN-поиск как один из компонентов гибрида
- ELSER и разреженные векторы для семантики — разреженные эмбеддинги, семантическая ветка гибрида
- Поле semantic_text и inference-эндпоинты — упрощённый путь к семантике в паре с BM25
- Релевантность и BM25: как считается _score — лексический алгоритм ранжирования, первый компонент гибрида
- Бустинг полей и function_score: управление ранжированием — дополнительный контроль над весами полей внутри standard-retriever
- Составные запросы: bool и комбинирование условий — фильтры и bool-запросы внутри retrievers