Реализовать dense retrieval failure detection с fallback на BM25

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать dense retrieval failure detection с fallback на BM25

1. Цель задачи

Разработать детектор отказов dense retrieval в production RAG-системе, который автоматически переключается на BM25 при низком сходстве векторов запроса и найденных документов. Система должна сохранять recall на редких запросах (rare queries) на уровне не ниже baseline, одновременно не увеличивая задержку более чем на 10%.

Ключевой результат Работающий fallback-механизм, при котором recall на rare queries не падает ниже 0.85 от исходного BM25-only, а latency укладывается в <200ms (p99).


2. Исходные данные

Что нужноОткуда взять
RAG-система с dense retrieval (dense + optional BM25)Собранный pet-проект или готовый пример (например, SentenceTransformers + Qdrant + Elasticsearch)
Датасет запросов (mix common + rare)Открытые датасеты: MS MARCO, Natural Questions, или сгенерировать синтетически
Векторная БД с индексамиQdrant / Weaviate / FAISS
BM25 индексElasticsearch / Apache Lucene / Whoosh
Embedding модельsentence-transformers/all-MiniLM-L6-v2 или подобная
Метрики оценкиRAGAS (hit rate, MRR, recall@k)

Если нет реального инструмента — симулируем:

  1. Установить faiss-cpu для dense поиска и whoosh/rank-bm25 для BM25.
  2. Собрать небольшой корпус документов (500–2000) из открытых источников (википедия, новости).
  3. Сгенерировать 100 запросов: 80 common (похожи на средние из корпуса) и 20 rare (с низким cosine similarity к ближайшим документам).
  4. Запустить оба поиска. Записать для каждого запроса: список document_id, score, similarity.

3. Технологический стек

КомпонентИнструментыНазначение
ЯзыкPython 3.9+Разработка детектора
Dense retrievalsentence-transformers + faissВекторный поиск
BM25rank-bm25 / whooshТекстовый поиск
Fallback логикаPythonПринятие решения переключения
ОценкаRAGAS / scikit-learnМетрики recall, precision, f1
КешRedis / LRU (опционально)Ускорение повторных запросов
МониторингPrometheus + Grafana / просто логиОтслеживание fallback-ов
ТестированиеpytestUnit-тесты детектора

4. Этапы выполнения

Этап 1: Подготовка окружения и данных (1–2 часа)

Действия

  1. Установить зависимости
    pip install faiss-cpu sentence-transformers rank-bm25 datasets pandas pytest
    
  2. Загрузить или сгенерировать корпус документов
    • Использовать datasets.load_dataset("ag_news", split="train") (первые 2000 записей как документы).
    • Каждый документ: {"text": "...", "id": idx}
    • Создать список documents и doc_ids.
  3. Сгенерировать тестовые запросы
    • Common: взять 80 случайных документов, вырезать первое предложение.
    • Rare: написать 20 вопросов не по теме (например, «квантовая физика» для новостей AG).
    • Сохранить в queries_meta.csv с колонками query, is_rare.
  4. Построить dense и BM25 индексы
    • Model: SentenceTransformer('all-MiniLM-L6-v2').
    • Embed all docs → faiss index IndexFlatIP.
    • BM25: rank_bm25.BM25Okapi(docs).
    • Записать общее время построения.

Ожидаемый результат этапа

  • Корпус документов (2000), файл запросов (100 строк), готовые индексы.
  • Возможность выполнить поиск по каждому методу и получить троечку документов.

Этап 2: Разработка детектора (2–3 часа)

Действия

  1. Определить порог (threshold)

    • Прогнать common запросы через dense: для каждого запроса получить max_sim (максимальное косинусное сходство с топ-1 документом).
    • Построить гистограмму. Выбрать порог на уровне 0.3 (или по 5-му перцентилю common запросов).
    • Для rare запросов: max_sim будет ниже порога. Сохранить threshold в конфиг.
  2. Написать класс FallbackRetriever

    class FallbackRetriever:
        def __init__(self, dense, bm25, threshold=0.3, top_k=5):
            ...
        def retrieve(self, query):
            docs_dense, sims = self.dense.search(query)
            if max(sims) >= self.threshold:
                return docs_dense
            else:
                return self.bm25.search(query)
    
  3. Реализовать логирование fallback-ов

    • Счётчик total_queries, fallback_count, query_text, timestamp.
    • Выводить в консоль (затем в prometheus).
  4. Добавить опцию “force_fallback” для тестов

    • Метод set_force(mode: bool) – принудительно включает BM25 для оценки baseline.

Ожидаемый результат этапа

  • Python модуль retriever.py с классом FallbackRetriever.
  • Возможность запустить на всех 100 запросах и получить словарь результатов с указанием источника (dense / bm25).

Этап 3: Оценка качества (1–2 часа)

Действия

  1. Определить ground truth

    • Для каждого запроса вручную или автоматически (по схожести) задать релевантные doc_id.
    • Для common: top-1 по dense считать релевантным.
    • Для rare: взять top-3 по BM25 (так как dense работает плохо).
    • Сохранить в ground_truth.json: {query: [rel_doc_ids]}.
  2. Вычислить метрики

    • Recall@K (K=1,3,5) отдельно для common, rare и всех запросов.
    • Fallback rate = fallback_count / total_queries.
    • Average latency (использовать time.perf_counter).
    • MRR для top-5.

    Код:

    def recall_at_k(predicted, relevant, k):
        return len(set(predicted[:k]) & set(relevant)) / len(relevant)
    
  3. Сравнить три режима

    • Dense only (без fallback)
    • BM25 only
    • Fallback (порог 0.3)

    Результаты свести в таблицу.

РежимRecall@3 (common)Recall@3 (rare)Avg latency
Dense0.950.1015ms
BM250.800.8550ms
Fallback0.950.8520ms
  1. Подобрать оптимальный порог
    • Перебрать threshold от 0.1 до 0.8 с шагом 0.05.
    • Для каждого посчитать recall@3 на rare и общий recall. Выбрать порог, где rare recall >= 0.85, а common recall не падает более чем на 2%.

Ожидаемый результат этапа

  • Таблица сравнения, оптимальный threshold, график зависимости recall от threshold (опционально).

Этап 4: Production-готовность (1–2 часа)

Действия

  1. Добавить мониторинг и алерты

    • Счётчики Prometheus: fallback_requests_total, fallback_duration_seconds.
    • Или простой JSON-лог с записью в файл.
  2. Оптимизировать latency

    • Кэшировать результаты BM25 для повторяющихся запросов (LRU cache 1000 записей).
    • Если dense сходство чуть ниже порога, можно не сразу падать, а взвешивать: score = w * dense_sim + (1-w) * bm25_score.
  3. Написать unit-тесты

    • test_fallback_when_low_sim – dense отдаёт низкое сходство → BM25.
    • test_no_fallback_when_high_sim.
    • test_edge_case_threshold_equal.
    • test_latency_under_200ms.
  4. Добавить конфигурацию через YAML/ENV

    fallback:
      threshold: 0.35
      top_k: 5
      cache_size: 1000
    

Ожидаемый результат этапа

  • Код с мониторингом, кэшем, тестами и конфигом.
  • README с инструкцией запуска.

Этап 5: Итоговый отчет и демо (30 минут)

Действия

  1. Собрать итоговые метрики на финальном threshold.
  2. Написать краткий отчет (1 страница): цель, архитектура, результаты, выводы.
  3. Подготовить демо: скрипт, который на 2–3 запросах показывает fallback.

Ожидаемый результат этапа

  • Файл report.md.
  • Демонстрация работы: python demo.py.

5. Критерии приемки (Definition of Done)

  • Класс FallbackRetriever реализован и работает на тестовом датасете.
  • Порог threshold выбран обоснованно (с графиком или таблицей).
  • Recall@3 на rare queries не ниже 0.85 (сравнение с BM25 baseline).
  • Fallback rate не превышает 30% на common запросах (не должен часто срабатывать на нормальных).
  • Средняя latency <200ms (p99) на всех запросах.
  • Написаны unit-тесты (минимум 3) и они проходят.
  • Добавлен мониторинг (логирование fallback-ов и времени).
  • Код оформлен как модуль с README, requirements.txt, конфигом.
  • Есть сравнение трёх режимов (dense-only, BM25-only, fallback).
  • Отчёт содержит выводы и рекомендации по порогу.

6. Ожидаемый результат

  • Основной артефакт Python модуль retriever.py с FallbackRetriever и вспомогательными функциями.
  • Данные корпус документов, файл запросов, ground truth, результаты экспериментов.
  • Отчёт report.md с метриками, графиками (опционально), сравнением режимов.
  • Тесты test_retriever.py (pytest).
  • Демо demo.py с 3 примерами запросов (common → dense, редкий → fallback, borderline).

7. Возможные сложности и их решение

СложностьРешение
Как объективно определить rare queriesИспользовать метрику rarity: cosine similarity < 0.2 к любому документу корпуса.
Порог может быть разным для разных бизнес-сценариевСделать порог параметром, провести кросс-валидацию на отложенной выборке.
BM25 может быть медленнее denseИспользовать sparse index (Elasticsearch) или LRU кэш.
На граничных запросах частые переключенияДобавить гистерезис: fallback включается только если за последние N запросов среднее сходство ниже порога.
Ground truth не идеаленДля rare можно использовать BM25 top-1+ LLM проверку релевантности (LLM-as-judge).

8. Бюджет времени (оценка)

ЭтапВремя
1. Подготовка окружения и данных1–2 ч
2. Разработка детектора2–3 ч
3. Оценка качества1–2 ч
4. Production-готовность1–2 ч
5. Итоговый отчёт и демо0.5 ч
Итого6–9 ч

Примечание: Для первого раза с полным погружением может потребоваться до 2 дней (16 ч) из-за отладки порога и тестов.


9. Связанные вопросы из базы знаний

ВопросТема
49Dense retrieval failure detection
12Основы vector similarity (cosine, dot)
17BM25 и TF-IDF реализация
23Оценка качества поиска (recall, MRR)
31Гистерезис и пороговые значения
44Hybrid search (dense + sparse)
67Мониторинг ML-систем (Prometheus)
83Кэширование запросов
91Unit-тестирование retrieval
145Data drift в запросах

10. Чек-лист самопроверки

  • Я построил dense и BM25 индексы и проверил, что каждый возвращает документы.
  • Я выбрал порог не «на глаз», а на основе распределения similarity common запросов.
  • Я посчитал recall отдельно для common и rare запросов, а не только средний.
  • Я проверил, что latency не превышает 200ms на p99 с включенным fallback.
  • Я написал тесты на случай низкого сходства и на случай, когда fallback не нужен.
  • Я задокументировал результаты в отчёте с таблицей сравнения.
  • Я убедился, что rare запросы действительно редкие (sim < 0.2).