中文翻译暂不可用,显示俄语原文。

RAG с гибридным поиском (Qdrant + BM25 + RRF)

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: RAG с гибридным поиском (Qdrant + BM25 + RRF)

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

Разработать RAG-систему, которая использует гибридный поиск: векторный (Qdrant) + лексический (BM25) + комбинирование через Reciprocal Rank Fusion (RRF). Текущий baseline — только векторный поиск. Цель — увеличить полноту поиска (Recall@10) минимум на 15% за счёт объединения двух типов релевантности. Вы научитесь интегрировать BM25 в конвейер RAG, настраивать Qdrant для sparse/dense поиска и реализовывать RRF.

Ключевой результат Рабочий pipeline гибридного поиска с измеренным улучшением Recall@10 ≥15% на тестовом наборе запросов.

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

Что нужноОткуда взять
Датасет документов (корпус)Собственный или публичный (например, SQuAD, Wikipedia abstracts, или корпус из предыдущих pet-проектов)
Тестовые запросы с релевантными документамиРазметить вручную 30–50 запросов, для каждого указать 1–5 релевантных doc_id
Векторная БД QdrantУстановить локально (Docker) или использовать SaaS (qdrant.cloud free tier)
Базовая RAG-система (опционально)Из pet-проекта №221 (RAG с Qdrant) или собрать заново
Модель для эмбеддинговintfloat/multilingual-e5-small (или любая другая)
BM25 индексаторrank_bm25 (Python) или Elasticsearch BM25

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

  1. Взять 500–1000 статей из Wikipedia (можно через wikipedia-api случайные абзацы)
  2. Каждую статью нарезать на чанки по 256 токенов с overlap 32 токена (через langchain.text_splitter.RecursiveCharacterTextSplitter)
  3. Для 30–50 запросов вручную отметить 2–5 релевантных чанков на основе семантического совпадения (или взять статьи, где запрос встречается в названии/тексте)

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

КомпонентИнструментыНазначение
Язык программированияPython 3.10+Разработка пайплайна
Векторный поискQdrant (Docker) + grpc-клиентХранение dense эмбеддингов, поиск по cosine
Лексический поискrank_bm25 (BM25Okapi) / ElasticsearchИндексация чанков как sparse-векторов
КомбинированиеRRF (Reciprocal Rank Fusion)Ранжирование по пересечению результатов
Эмбеддинг модельsentence-transformers (intfloat/multilingual-e5-small)Преобразование текста в dense-вектор
Токенизация для BM25nltk / jieba (для русского)Токенизация по словам
Метрикиsklearn.metrics, matplotlibRecall@k, MRR, визуализация
ОркестрацияDocker Compose (Qdrant)Запуск Qdrant
Управление экспериментамиmlflow или простые CSVЛогирование метрик baseline vs hybrid

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

Этап 1: Подготовка данных и baseline (2 часа)

Действия

  1. Загрузка и очистка корпуса

    • Собрать 500–1000 текстовых документов (например, через wikipedia-api).
    • Разбить каждый документ на чанки по 256 токенов с overlap 32.
    • Сохранить чанки в JSON-файл: [{"id": str, "text": str, "doc_id": str}]
  2. Создание dense-индекса в Qdrant

    • Запустить Qdrant через Docker: docker run -p 6333:6333 qdrant/qdrant
    • Создать коллекцию с размерностью 384 (для e5-small).
    • Загрузить чанки: для каждого чанка получить эмбеддинг через SentenceTransformer, выполнить upsert.
  3. Измерение baseline: Recall@10 (только dense)

    • Взять 30–50 тестовых запросов с разметкой.
    • Для каждого запроса выполнить поиск в Qdrant с limit=10.
    • Вычислить Recall@10: доля запросов, у которых хотя бы один релевантный документ попал в top-10.
    • Записать значение (например, 0.62).

Ожидаемый результат этапа JSON-файл с чанками, запущенный Qdrant с dense-индексом, baseline Recall@10.

Этап 2: Индексация BM25 (2 часа)

Действия

  1. Установка rank_bm25
    pip install rank_bm25 nltk

  2. Токенизация корпуса

    • Для каждого чанка применить токенизацию: разбить на слова (lowercase, удалить стоп-слова опционально).
    • Для русского языка использовать nltk.tokenize.word_tokenize с punkt.
  3. Построение BM25-индекса

    • Создать BM25Okapi(corpus_tokens, k1=1.5, b=0.75).
  4. Сохранение индекса (для повторного использования)

    • Сериализовать через pickle в файл bm25_index.pkl.

Ожидаемый результат этапа BM25-индекс, готовый к поиску (файл .pkl).

Этап 3: Реализация RRF (2–3 часа)

Действия

  1. Функция RRF

    def rrf(dense_ranks: dict, sparse_ranks: dict, k: int = 60) -> list:
        all_ids = set(dense_ranks.keys()) | set(sparse_ranks.keys())
        scores = {}
        for doc_id in all_ids:
            score = 0.0
            if doc_id in dense_ranks:
                score += 1 / (k + dense_ranks[doc_id])
            if doc_id in sparse_ranks:
                score += 1 / (k + sparse_ranks[doc_id])
            scores[doc_id] = score
        # сортируем по убыванию score
        return sorted(scores.keys(), key=lambda x: scores[x], reverse=True)
    
  2. Объединение поисков

    • Для каждого запроса:
      1. Выполнить dense поиск в Qdrant с limit=20 (получить ранги).
      2. Выполнить BM25 поиск с top_n=20.
      3. Преобразовать списки в словарь {doc_id: rank} (ранг с нуля).
      4. Применить rrf с k=60, взять top-10.
  3. Обработка граничных случаев

    • Если dense или sparse вернул пустой список — использовать только непустой.
    • Документы, не попавшие ни в один список, получают нулевой ранг (не влияют).

Ожидаемый результат этапа Функция hybrid_search(query, n=10), возвращающая список doc_id.

Этап 4: Сравнение метрик и оптимизация (2 часа)

Действия

  1. Измерение Recall@10 для гибрида

    • Прогнать все тестовые запросы через hybrid_search.
    • Вычислить Recall@10 (аналогично baseline).
    • Сравнить: должен быть ≥ baseline + 15% (например, 0.62 → 0.71).
  2. Дополнительные метрики

    • MRR@10.
    • Precision@5.
    • Время ответа (среднее, p95).
  3. Настройка параметров RRF (опционально)

    • Перебрать k в RRF (10, 30, 60, 100).
    • Веса для dense/sparse: можно добавить взвешенный RRF.
    • Выбрать конфигурацию с максимальным Recall@10.
  4. Визуализация

    • Построить график: Recall@k для k=1..10 (dense vs hybrid).
    • Таблица сравнения метрик.

Ожидаемый результат этапа Численное подтверждение улучшения Recall@10 ≥15%, график сравнения.

Этап 5: Документирование и финализация (1 час)

Действия

  1. Написать README

    • Описание архитектуры (Dense + Sparse + RRF).
    • Инструкция по запуску: requirements.txt, Docker Compose для Qdrant.
    • Пример использования hybrid_search.
  2. Сохранить артефакты

    • Код: hybrid_rag.py, bm25_index.pkl, embedding_model.
    • Метрики: results.csv с запросами, baseline метриками, hybrid метриками.
  3. Проверить чеклист DoD (см. раздел 5).

Ожидаемый результат этапа Репозиторий с кодом, документацией и файлом результатов.

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

  • Реализован dense-поиск на Qdrant (загрузка чанков, поиск по cosine).
  • Реализован BM25-поиск (индекс, поиск с токенизацией).
  • Реализована функция RRF для комбинирования двух списков рангов.
  • Собран тестовый набор запросов (30–50) с размеченной релевантностью.
  • Измерен baseline Recall@10 (только dense) — зафиксировано значение.
  • Измерен Recall@10 для гибрида — улучшение ≥15% относительно baseline.
  • Код опубликован (GitHub/gist) с README и requirements.txt.
  • Запуск системы возможен одной командой (docker-compose up + python run.py).
  • Артефакты (BM25 индекс, тестовый набор) приложены или воспроизводимы.
  • Добавлены минимум 3 тестовых запроса с выводом top-5 документов и объяснением разницы с dense-only.

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

Основной файл hybrid_rag.py — скрипт, который:

  • Загружает корпус и Qdrant коллекции (или восстанавливает из данных).
  • Выполняет гибридный поиск для одного запроса.
  • Выводит результат в виде [doc_id, score, text[:200]].

Результаты в файле metrics_report.md:

  • Таблица с Recall@10 (dense) и Recall@10 (hybrid).
  • Процент улучшения.
  • График Recall@k.

Опциональные дополнительные результаты

  • Jupyter Notebook с визуализацией и экспериментами.
  • Docker Compose для полного окружения.
  • Бенчмарк времени выполнения.

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

СложностьРешение
Медленная токенизация большого корпусаИспользовать nltk с кэшированием или jieba для русского; предварительно сохранить токены в файл
Различие в индексации dense и sparse (разные doc_id)Убедиться, что обе системы используют одинаковые doc_id (строковые UUID или целые числа)
Qdrant не возвращает ранги (только документы)Использовать scored_point и отсортировать; ранг = индекс в отсортированном списке
BM25 и dense дают сильно разные спискиRRF хорошо с этим справляется; можно увеличить k для сглаживания
Тестовые запросы нерепрезентативныСобрать 50+ запросов, покрывающих разные темы; проверить, что каждый запрос имеет хотя бы один релевантный документ
Улучшение Recall@10 < 15%Попробовать изменить k RRF, добавить весовой коэффициент, увеличить список dense/sparse до 50, проанализировать ошибки

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

ЭтапВремя (часы)
Этап 1: Подготовка данных и baseline2
Этап 2: Индексация BM252
Этап 3: Реализация RRF2.5
Этап 4: Сравнение метрик и оптимизация2
Этап 5: Документирование и финализация1
Итого9.5

Примечание для первого раза Добавьте 2–3 часа на установку Qdrant, sentence-transformers и отладку несовместимости версий.

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

ВопросТема
12Как настроить Qdrant для production retrieval?
45BM25: формулы и оптимальные параметры k1, b
78RRF: теория, сравнение с weighted sum
112Оценка качества retrieval: Recall@k, MRR, NDCG
203Аугментация запросов для RAG (query expansion)
301Chunking стратегии для RAG (размер, overlap)
415Сравнение sparse и dense retrieval
520Как измерять latency в RAG-пайплайне?
621Оптимизация индекса BM25 для русского языка
788Docker Compose для ML-приложений (Qdrant + API)

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

  • Я установил и запустил Qdrant в Docker, создал коллекцию с правильной размерностью.
  • Я загрузил все чанки в Qdrant и проверил, что поиск возвращает осмысленные результаты.
  • Я построил BM25-индекс и убедился, что он ищет по тем же doc_id.
  • Я реализовал функцию RRF и протестировал на 3–5 ручных запросах — результат кажется логичным.
  • Я зафиксировал baseline (dense) и гибридный Recall@10 — улучшение не менее 15%.
  • Я написал README с инструкцией по воспроизведению и примером вызова.
  • Я добавил файл requirements.txt и docker-compose.yml.
  • Я проверил, что код не содержит секретов/ключей и проходит flake8.
  • Я могу объяснить, почему гибридный поиск даёт лучший recall (например, BM25 ловит точные совпадения терминов, а dense — семантически близкие).