← Production RAG: retrieval systems that do not hallucinate
02 / 07 · 55 min

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

Запрос «ошибка ERR_QUIC_PROTOCOL_ERROR в v2.14» стабильно не находит нужный документ при чистом dense-поиске. Почему?

01Запрос «ошибка ERR_QUIC_PROTOCOL_ERROR в v2.14» стабильно не находит нужный документ при чистом dense-поиске. Почему?

Зачем в формуле RRF константа k=60?

02Зачем в формуле RRF константа k=60?

Чем cross-encoder точнее bi-encoder’а и почему его нельзя применить ко всей базе?

03Чем cross-encoder точнее bi-encoder’а и почему его нельзя применить ко всей базе?

Retrieval возвращает в top-8 пять почти одинаковых чанков одного факта. Какой механизм это чинит?

04Retrieval возвращает в top-8 пять почти одинаковых чанков одного факта. Какой механизм это чинит?

Eval показывает: recall@50 первой ступени 0.92, precision@5 после неё 0.35. Что добавить в первую очередь?

05Eval показывает: recall@50 первой ступени 0.92, precision@5 после неё 0.35. Что добавить в первую очередь?

Sources

Embeddings, hybrid search, and rerankers · Production RAG: retrieval systems that do not hallucinate · VORCL