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 |
Если нет реального датасета — симулируем:
- Взять 500–1000 статей из Wikipedia (можно через wikipedia-api случайные абзацы)
- Каждую статью нарезать на чанки по 256 токенов с overlap 32 токена (через langchain.text_splitter.RecursiveCharacterTextSplitter)
- Для 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-вектор |
| Токенизация для BM25 | nltk / jieba (для русского) | Токенизация по словам |
| Метрики | sklearn.metrics, matplotlib | Recall@k, MRR, визуализация |
| Оркестрация | Docker Compose (Qdrant) | Запуск Qdrant |
| Управление экспериментами | mlflow или простые CSV | Логирование метрик baseline vs hybrid |
4. Этапы выполнения
Этап 1: Подготовка данных и baseline (2 часа)
Действия
-
Загрузка и очистка корпуса
-
Создание dense-индекса в Qdrant
Ожидаемый результат этапа JSON-файл с чанками, запущенный Qdrant с dense-индексом, baseline Recall@10.
Этап 2: Индексация BM25 (2 часа)
Действия
-
Токенизация корпуса
- Для каждого чанка применить токенизацию: разбить на слова (lowercase, удалить стоп-слова опционально).
- Для русского языка использовать nltk.tokenize.word_tokenize с
punkt.
-
Построение BM25-индекса
- Создать BM25Okapi(corpus_tokens, k1=1.5, b=0.75).
-
Сохранение индекса (для повторного использования)
- Сериализовать через pickle в файл bm25_index.pkl.
Ожидаемый результат этапа BM25-индекс, готовый к поиску (файл .pkl).
Этап 3: Реализация RRF (2–3 часа)
Действия
-
Функция 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) -
Объединение поисков
-
Обработка граничных случаев
- Если dense или sparse вернул пустой список — использовать только непустой.
- Документы, не попавшие ни в один список, получают нулевой ранг (не влияют).
Ожидаемый результат этапа Функция hybrid_search(query, n=10), возвращающая список doc_id.
Этап 4: Сравнение метрик и оптимизация (2 часа)
Действия
-
Измерение Recall@10 для гибрида
-
Дополнительные метрики
- MRR@10.
- Precision@5.
- Время ответа (среднее, p95).
-
Настройка параметров RRF (опционально)
- Перебрать
kв RRF (10, 30, 60, 100). - Веса для dense/sparse: можно добавить взвешенный RRF.
- Выбрать конфигурацию с максимальным Recall@10.
- Перебрать
-
Визуализация
- Построить график: Recall@k для k=1..10 (dense vs hybrid).
- Таблица сравнения метрик.
Ожидаемый результат этапа Численное подтверждение улучшения Recall@10 ≥15%, график сравнения.
Этап 5: Документирование и финализация (1 час)
Действия
-
Написать README
-
Сохранить артефакты
- Код:
hybrid_rag.py,bm25_index.pkl,embedding_model. - Метрики:
results.csvс запросами, baseline метриками, hybrid метриками.
- Код:
-
Проверить чеклист 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:
Опциональные дополнительные результаты
- 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: Подготовка данных и baseline | 2 |
| Этап 2: Индексация BM25 | 2 |
| Этап 3: Реализация RRF | 2.5 |
| Этап 4: Сравнение метрик и оптимизация | 2 |
| Этап 5: Документирование и финализация | 1 |
| Итого | 9.5 |
Примечание для первого раза Добавьте 2–3 часа на установку Qdrant, sentence-transformers и отладку несовместимости версий.
9. Связанные вопросы из базы знаний
| Вопрос | Тема |
|---|---|
| 12 | Как настроить Qdrant для production retrieval? |
| 45 | BM25: формулы и оптимальные параметры k1, b |
| 78 | RRF: теория, сравнение с weighted sum |
| 112 | Оценка качества retrieval: Recall@k, MRR, NDCG |
| 203 | Аугментация запросов для RAG (query expansion) |
| 301 | Chunking стратегии для RAG (размер, overlap) |
| 415 | Сравнение sparse и dense retrieval |
| 520 | Как измерять latency в RAG-пайплайне? |
| 621 | Оптимизация индекса BM25 для русского языка |
| 788 | Docker 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 — семантически близкие).