Исправление Conjunction Shrinkage в BM25: байесовский подход | 2026 | AiManual
AiManual Logo Ai / Manual.
08 Фев 2026 Гайд

Тот самый баг в гибридном поиске, который все игнорируют: как Log-Odds Conjunction убивает ваши RAG-системы

Глубокий разбор фундаментальной ошибки гибридного поиска в RAG. Практическое исправление Conjunction Shrinkage через Bayesian BM25 с логарифмическими шансами.

Почему ваш "гибридный" поиск на самом деле хуже обычного

Вы настраиваете RAG-систему. Берёте гибридный поиск, складываете BM25 и векторные эмбеддинги, ждёте синергии. А получаете... странные результаты. Документы с одним релевантным термином вылезают выше, чем документы, где есть ВСЕ термины запроса. Система словно наказывает документы за то, что они слишком хорошо соответствуют запросу.

Знакомо? Поздравляю, вы столкнулись с Conjunction Shrinkage — фундаментальным дефектом вероятностных моделей ранжирования. И да, эта проблема живёт в вашем коде прямо сейчас.

Простой тест: Запрос "машинное обучение нейросети". Документ А содержит только "машинное". Документ Б содержит все три слова. При классическом BM25 документ А часто получает более высокий скор. Абсурд? Да. Реальность? К сожалению.

Корень зла: почему умножение вероятностей не работает

Традиционный BM25 основан на вероятностной модели независимости терминов. Формула оценки документа D для запроса Q:

score(D, Q) = Σ IDF(q_i) * TF(q_i, D) * (k1 + 1) / (TF(q_i, D) + k1 * (1 - b + b * |D| / avgdl))

Проблема в этой самой сумме. Представьте: у вас есть два термина с вероятностями релевантности p1 = 0.8 и p2 = 0.8. Логично ожидать, что документ, содержащий ОБА термина, должен быть релевантнее, чем документ с любым одним. Но в вероятностной модели независимости совместная вероятность p1 * p2 = 0.64 — МЕНЬШЕ, чем каждая из отдельных вероятностей.

Это математический парадокс, который десятилетиями игнорировали. До тех пор, пока RAG-системы не стали критически важными для бизнеса.

💡
Conjunction Shrinkage — это не баг реализации, а фундаментальное ограничение вероятностных моделей, основанных на предположении независимости терминов. Чем больше терминов в запросе, тем сильнее "штрафуются" документы, содержащие все термины.

Решение, которое не придумали 40 лет: логарифмические шансы

Выход из тупика предложили только в 2024 году, но массовое внедрение началось как раз к 2026-му. Вместо работы с вероятностями p мы переходим к логарифмическим шансам (log-odds).

Преобразование простое: log-odds = log(p / (1 - p)). Магия в том, что логарифмические шансы СКЛАДЫВАЮТСЯ при условии независимости событий.

1 Переписываем BM25 на языке шансов

Классический BM25 вычисляет скор как сумму вкладов терминов. Bayesian BM25 переосмысливает это как произведение шансов:

# Традиционный BM25 (проблемный)
def bm25_score(doc_terms, query_terms):
    score = 0
    for term in query_terms:
        if term in doc_terms:
            score += idf[term] * tf_component(term, doc_terms)
    return score

# Bayesian BM25 через log-odds
def bayesian_bm25_score(doc_terms, query_terms):
    log_odds = 0
    for term in query_terms:
        if term in doc_terms:
            p = sigmoid(idf[term] * tf_component(term, doc_terms))
            log_odds += math.log(p / (1 - p))
    return log_odds

Кажется, мелочь? Но это меняет всё. Теперь добавление нового релевантного термина УВЕЛИЧИВАЕТ общий скор, а не уменьшает его.

2 Интеграция с гибридным поиском без боли

Главное преимущество подхода — совместимость с существующими системами. Вам не нужно переписывать весь поисковый движок. Достаточно заменить функцию подсчёта скора.

Вот как это выглядит в реальной RAG-системе:

class BayesianHybridSearch:
    def __init__(self, bm25_index, vector_index, alpha=0.5):
        self.bm25 = bm25_index
        self.vector = vector_index
        self.alpha = alpha  # Вес BM25 относительно векторов
        
    def search(self, query, k=10):
        # Получаем результаты от обоих методов
        bm25_results = self.bm25.search(query, k=k*2)
        vector_results = self.vector.search(query, k=k*2)
        
        # Конвертируем BM25 скора в log-odds
        bm25_scores = {doc_id: self._bm25_to_log_odds(score) 
                       for doc_id, score in bm25_results}
        
        # Нормализуем векторные скора (они уже в косинусной близости)
        vector_scores = self._normalize_scores(vector_results)
        
        # Комбинируем с учётом весов
        combined = {}
        all_docs = set(bm25_scores.keys()) | set(vector_scores.keys())
        
        for doc_id in all_docs:
            bm25_score = bm25_scores.get(doc_id, MIN_LOG_ODDS)
            vector_score = vector_scores.get(doc_id, 0)
            
            # Линейная комбинация в пространстве log-odds
            combined[doc_id] = self.alpha * bm25_score + (1 - self.alpha) * vector_score
        
        return sorted(combined.items(), key=lambda x: x[1], reverse=True)[:k]
    
    def _bm25_to_log_odds(self, bm25_score):
        """Конвертирует BM25 скор в log-odds через сигмоиду"""
        # Эмпирически подобранные параметры
        scaled = (bm25_score + 10) / 20  # Приводим к диапазону ~[0, 1]
        p = 1 / (1 + math.exp(-scaled))
        return math.log(p / (1 - p))

Почему это работает там, где другие методы терпят неудачу

Log-Odds Conjunction решает три фундаментальные проблемы одновременно:

  • Аддитивность: Log-odds складываются, что соответствует интуиции «больше релевантных терминов = выше релевантность»
  • Калибровка: Шансы автоматически калиброваны — разница в 1 log-odds означает увеличение шансов в e ≈ 2.72 раза
  • Гибридность: Log-odds из BM25 можно напрямую комбинировать с логарифмическими шансами из векторной модели
Метод Conjunction Shrinkage Калибровка скора Совместимость с векторами
Классический BM25 ❌ Есть ❌ Нет ⚠️ Через нормализацию
Bayesian BM25 (log-odds) ✅ Решена ✅ Автоматическая ✅ Прямая

Ошибки, которые вы сделаете при внедрении (и как их избежать)

Ошибка 1: Слепая замена BM25 на Bayesian версию

Нельзя просто взять и заменить функцию подсчёта скора. Классический BM25 оптимизирован под определённые распределения скора. Bayesian BM25 требует калибровки параметров под ваш корпус документов.

Что делать: Запустите A/B тест на части трафика. Сравните не только точность (precision@k), но и пользовательские метрики — кликабельность, время чтения, удовлетворённость.

Ошибка 2: Игнорирование эффекта длинных запросов

Log-odds подход особенно выигрывает на длинных запросах (3+ термина). Но на коротких запросах (1-2 термина) разница может быть минимальной. Если у вас в основном короткие запросы — возможно, игра не стоит свеч.

Ошибка 3: Неправильная интеграция с векторным поиском

Самая частая ошибка — попытка сложить log-odds из BM25 с косинусным расстоянием от векторной модели. Это как складывать метры с килограммами. Нужно либо конвертировать векторные скора в log-odds, либо нормализовать оба скора к одному распределению.

Практический рецепт: внедряем за один день

  1. Берём существующую реализацию BM25 (из Elasticsearch, Vespa или собственный индекс)
  2. Добавляем преобразование скора в log-odds через сигмоиду с подобранными параметрами
  3. Модифицируем функцию комбинирования с векторным поиском (используем взвешенную сумму log-odds)
  4. Тестируем на контрольной выборке запросов с ground truth релевантности
  5. Запускаем canary-деплой на 1% трафика, мониторим метрики

Если вы используете Elasticsearch 8.12+ (актуально на февраль 2026), там уже есть экспериментальная поддержка Bayesian BM25 через script_score функции. Но готовьтесь к 20-30% падению производительности — преобразование в log-odds на лету не бесплатно.

Когда это действительно нужно, а когда — overkill

Bayesian BM25 с log-odds conjunction — не серебряная пуля. Вот когда оно того стоит:

  • У вас длинные поисковые запросы (3+ ключевых слова)
  • Точность поиска критична для бизнеса (медицина, финансы, юриспруденция)
  • Вы уже упираетесь в математический потолок RAG и ищете даже небольшие улучшения
  • Готовы пожертвовать 10-20% производительности ради качества

А вот когда можно обойтись классическим BM25:

  • Запросы в основном однословные
  • У вас жесткие ограничения по latency
  • Точность и так на приемлемом уровне
  • Нет ресурсов на калибровку и тестирование

Что дальше? Будущее гибридного поиска после исправления Conjunction Shrinkage

Исправление фундаментального дефекта — это не финал, а новый старт. Теперь можно думать о более сложных схемах комбинирования:

  • Динамическое взвешивание — больше веса BM25 для конкретных запросов, больше веса векторам для других
  • Нейросетевые ранжировщики, обученные на log-odds признаках от обоих методов
  • Мультимодальный поиск, где log-odds становятся универсальной валютой для сравнения разных модальностей

Самая интересная возможность — использование Bayesian BM25 как features для детекторов дрейфа RAG-систем. Резкое изменение распределения log-odds для типовых запросов — ранний сигнал о проблемах с индексом.

💡
К 2027 году я ожидаю, что Bayesian BM25 станет стандартом де-факто для production RAG-систем. Прямо сейчас это competitive advantage. Через год это будет baseline.

Финальный совет: начните с малого, но начните сейчас

Conjunction Shrinkage — не теоретическая проблема. Она прямо сейчас снижает качество вашего поиска на 5-15%. В мире RAG, где каждый процент точности конвертируется в бизнес-метрики, это недопустимо.

Попробуйте реализовать преобразование в log-odds хотя бы для 10 самых проблемных запросов. Увидите разницу сразу. Потом масштабируйте на всю систему. И не забудьте поделиться результатами — сообществу нужны real-world данные, а не только теоретические выкладки.

P.S. Если вы думаете, что ваша RAG-система и так работает нормально — попробуйте специально протестировать её на запросах с 3-4 терминами. Сравните топ-1 результат классического BM25 и Bayesian версии. Уверен, сюрприз будет.