Embeddings, hybrid search, and rerankers
Combine dense and lexical retrieval with RRF and cross-encoder reranking, then tune the two-stage funnel against a golden dataset.
Lesson content is currently available in Russian.
What you will learn
- →Объяснять слепые зоны dense-поиска и BM25 и почему гибрид почти всегда выигрывает у каждого по отдельности.
- →Реализовывать слияние результатов через RRF и настраивать веса компонент.
- →Встраивать cross-encoder reranker второй ступенью и оценивать его вклад в precision.
Слепые зоны: где ломается dense и где ломается BM25
Dense-поиск (эмбеддинги + косинусная близость) ловит перефразировки: «как вернуть деньги» найдёт документ про «процедуру возврата средств». Но он систематически слеп к точным идентификаторам: артикул «KX-450B», код ошибки «ERR_QUIC_PROTOCOL_ERROR», фамилия, номер договора. Эмбеддинг-модель видела такие токены редко, вектор запроса с опечаткой или редким ID оказывается далеко от вектора документа — и точное совпадение проигрывает семантически «похожим», но неверным чанкам.
BM25 — обратная картина. Это лексический скоринг по совпадению термов с учётом их редкости (IDF) и длины документа: точный артикул он находит идеально, но «возврат денег» и «refund policy» для него — непересекающиеся множества токенов. Плюс BM25 не требует ML-инфраструктуры: это инвертированный индекс, который считается на CPU за миллисекунды.
Вывод, устоявшийся в индустрии: в продакшене нужны оба. Типичные доли трафика, где выигрывает каждый метод, зависят от домена: в технической поддержке с кодами ошибок и версиями лексика решает до трети запросов; в базе знаний с разговорными формулировками доминирует семантика. Гибрид покрывает оба хвоста, и почти во всех публичных бенчмарках (BEIR, внутренние замеры Qdrant/Weaviate/Elastic) hybrid стабильно даёт +5–15 пунктов recall против лучшей одиночной компоненты. Отдельный практический пункт — эмбеддинг-модель выбирайте по MTEB-лидерборду с фильтром на ваш язык и длину текстов, а не «ту же, что у всех»: разница между соседями лидерборда на конкретном домене бывает больше, чем между поколениями моделей.
Reciprocal Rank Fusion: слияние без настройки шкал
BM25 отдаёт скоры в одной шкале, косинусная близость — в другой; напрямую складывать их нельзя. Reciprocal Rank Fusion обходит проблему, используя только позиции документов в каждом списке:
RRF(d) = Σ_r 1 / (k + rank_r(d)), k = 60
где rank_r(d) — позиция документа d в списке ранкера r (1, 2, 3…), а k=60 — константа сглаживания из оригинальной статьи Cormack et al.: она гасит чрезмерное влияние первых позиций одного ранкера. Документ, стоящий 3-м в BM25 и 5-м в dense, получает 1/63 + 1/65 ≈ 0.0313 и почти наверняка обгонит документ, который один ранкер поставил 1-м (1/61 ≈ 0.0164), а другой не нашёл вовсе.
def rrf(result_lists: list[list[str]], k: int = 60) -> list[str]:
scores: dict[str, float] = {}
for results in result_lists:
for rank, doc_id in enumerate(results, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)
RRF не требует нормализации скоров и обучения весов — поэтому он стал дефолтом в Qdrant (Query API prefetch+fusion), Weaviate (hybrid с параметром alpha), OpenSearch и Azure AI Search. Альтернатива — взвешенная сумма нормализованных скоров (в Weaviate alpha = доля dense: 0.5 — паритет, 0.75 — перекос в семантику); она даёт больше контроля, но требует калибровки на eval-наборе. Начинайте с RRF, переходите на веса, только если eval показывает систематический перекос.
Rerankers: вторая ступень точности
Bi-encoder (обычные эмбеддинги) кодирует запрос и документ независимо — поэтому индекс можно посчитать заранее, но взаимодействие «запрос ↔ документ» модель не видит. Cross-encoder читает пару (запрос, документ) целиком одним проходом трансформера и выдаёт скор релевантности — на порядок точнее, но и на порядки дороже: прогнать миллион документов через cross-encoder на каждый запрос невозможно.
Отсюда каноническая двухступенчатая архитектура:
- Ступень 1 (recall): гибридный поиск быстро отбирает top-50…150 кандидатов из миллионов.
- Ступень 2 (precision): reranker пересортировывает кандидатов, в промпт уходит top-5…10.
candidates = hybrid_search(query, limit=100) # ~50-100 мс
reranked = reranker.rank(query, candidates) # +100-300 мс
context = reranked[:8]
Практические варианты: Cohere Rerank и Voyage rerank как managed API; BGE-reranker (v2-m3) и Jina reranker — открытые веса для self-host; ColBERT — компромисс late interaction, быстрее cross-encoder’а при близком качестве. Цена вопроса — латентность (плюс 100–300 мс на 100 кандидатов) и деньги за вызов API, выгода — рост precision@5 обычно на 10–25 пунктов и заметное снижение галлюцинаций, потому что в контекст перестаёт попадать «почти релевантный» шум. Правило: reranker — первое, что стоит добавить, когда retrieval «почти работает».
MMR: разнообразие вместо восьми копий одного факта
Топ выдачи часто заполняют почти одинаковые чанки — один и тот же факт из разных версий документа. Контекстное окно тратится на дубли, а второй нужный факт не влезает. Maximal Marginal Relevance выбирает документы итеративно, штрафуя похожесть на уже выбранное:
MMR = argmax_{d ∈ R\S} [ λ · sim(d, q) − (1 − λ) · max_{s ∈ S} sim(d, s) ]
где R — кандидаты, S — уже выбранные, λ — баланс релевантности и разнообразия. λ=1.0 — чистая релевантность, λ=0.5 — сильная диверсификация; стартовая точка в проде — λ≈0.7. MMR встроен в большинство фреймворков (search_type='mmr' в LangChain-ретриверах) и почти бесплатен по латентности, так как работает на уже посчитанных векторах кандидатов.
Итоговый конвейер зрелого retrieval выглядит так: гибрид (BM25 + dense) → RRF → cross-encoder reranker → MMR/дедупликация → top-k в промпт. Каждая ступень включается по показаниям eval, а не «потому что можем»: если recall@50 первой ступени низкий, reranker нечего пересортировывать — сначала чините гибрид и chunking; если recall@50 высокий, а precision@5 низкий — это ровно случай для reranker’а.
Practice
Гибрид против одиночных ранкеров
На своей коллекции (или открытом датасете) соберите три конфигурации: только BM25, только dense, гибрид с RRF (k=60). Составьте 20 запросов: 10 разговорных перефразировок и 10 с точными идентификаторами (коды, названия, версии). Замерьте recall@10 каждой конфигурации, затем добавьте reranker поверх гибрида и замерьте precision@5.
Deliverable
Таблица recall@10 по трём конфигурациям с разбивкой «разговорные / лексические» запросы + дельта precision@5 от reranker’а, с выводом о том, какие запросы спасает каждая компонента.
Checkpoint
Спроектируйте вслух retrieval для базы техподдержки с кодами ошибок и разговорными вопросами: какие компоненты, в каком порядке, как сливаются результаты и где здесь reranker.
- ✓ Назван гибрид: BM25 для точных идентификаторов, dense для перефразировок, слияние через RRF с объяснением формулы 1/(k+rank).
- ✓ Описана двухступенчатая схема: широкий top-100 кандидатов, затем cross-encoder reranker до top-5…10.
- ✓ Сформулировано, когда reranker бесполезен (низкий recall первой ступени) и чем MMR спасает контекст от дублей.
Check yourself
Sources