Гибридный поиск для RAG: почему одних эмбеддингов мало

Векторный поиск хорошо понимает смысл и плохо — точные совпадения. Разбираем, как совместить его с классическим BM25 и зачем после этого нужен реранкер.

2 минуты чтения≈ 880 токенов#RAG#поиск

Типичный RAG-конвейер выглядит так: документы режут на фрагменты, каждый фрагмент превращают в вектор-эмбеддинг, а на запрос пользователя ищут ближайшие векторы. Для вопросов вроде «как оформить возврат» это работает отлично — модель эмбеддингов понимает, что «вернуть товар» и «оформить возврат» об одном и том же.

Проблемы начинаются, когда в запросе есть что-то точное.

Где эмбеддинги промахиваются #

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

  • артикулы, коды и номера документов: ТУ 14-3-1128, A7-2240;
  • числа и единицы измерения — «16 мм» и «18 мм» для модели почти неотличимы;
  • редкие термины и названия, которых модель эмбеддингов почти не видела;
  • короткие запросы из одного-двух слов, где просто мало смысла для сравнения.

Классический лексический поиск вроде BM25 устроен наоборот: он ничего не понимает о смысле, зато точное совпадение редкого слова даёт ему большой вес.

Как совместить два ранжирования #

Самый простой и устойчивый способ — Reciprocal Rank Fusion. Каждый поиск возвращает свой список, а итоговая оценка документа складывается из его позиций во всех списках:

score(d) = Σ 1 / (k + rank_i(d))

Константу k обычно берут около 60. Метод не требует сводить разные шкалы оценок к одной — используются только места в списках, поэтому его легко добавить к любой паре поисковиков.

def rrf(rankings, k=60):
    scores = {}
    for ranking in rankings:
        for rank, doc_id in enumerate(ranking, start=1):
            scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
    return sorted(scores, key=scores.get, reverse=True)


fused = rrf([bm25_top50, vector_top50])[:20]

Реранкер как последний шаг #

После слияния остаётся несколько десятков кандидатов. Их можно передать кросс-энкодеру — модели, которая смотрит на запрос и документ одновременно и выставляет оценку релевантности. Он заметно точнее сравнения эмбеддингов, но и медленнее, поэтому его запускают только на коротком списке.

Что проверить перед запуском #

  1. Нормализуйте текст одинаково при индексации и при поиске: регистр, «ё» и «е», пробелы внутри чисел, запись единиц измерения.
  2. Храните коды и артикулы в отдельном поле и ищите по нему точным совпадением.
  3. Соберите хотя бы сотню размеченных запросов и измеряйте recall@k для каждого поиска отдельно и для их комбинации.
  4. Смотрите на ошибки глазами: заметная часть проблем RAG обычно прячется в нарезке документов, а не в модели.

Гибридный поиск не делает систему умнее. Он делает её менее хрупкой: запросы, на которых проваливается один метод, часто вытягивает второй.