Многоиндексный конвейер RAG

Чему научитесь

Видео 6:45 и текст урока

К концу урока вы сможете:

Видео и текст урока.

Смотреть урок на Skilljar A Multi-Index RAG pipeline · 6:45

Мы сделали отдельные реализации семантического поиска (на векторных эмбеддингах) и лексического (на BM25). Пора объединить их в единый конвейер поиска, который использует сильные стороны обоих подходов.

Многоиндексная архитектура

У классов VectorIndex и BM25Index почти одинаковый API: у обоих есть методы add_document() и search(). Благодаря этому их легко обернуть в новый класс Retriever.

Вопрос уходит в семантический и лексический поиск, затем результаты объединяются.
Вопрос уходит в оба поиска, результаты объединяются.

Retriever — координатор: он передаёт запрос пользователя в оба индекса, собирает их результаты и объединяет методом reciprocal rank fusion (RRF).

Что такое reciprocal rank fusion

Объединить результаты разных методов поиска — не то же самое, что склеить списки. У каждого метода своя система оценок, поэтому нужен способ привести их рейтинги к общему знаменателю и честно объединить.

Таблицы рангов 1–3 от VectorIndex с оценками 0.234, 0.913, 1.327 и от BM25Index с 0.184, 0.414, 0.781.
Ранги 1–3 от VectorIndex (оценки 0.234, 0.913, 1.327) и от BM25Index (0.184, 0.414, 0.781) — шкалы разные.

Разберём RRF на примере. Допустим, ищем информацию об «INC-2023-Q4-011» и получаем:

Ранжированные списки VectorIndex и BM25Index сливаются в одну таблицу с рангом каждого фрагмента в обоих индексах.
Два ранжированных списка сводятся в одну таблицу: ранг каждого фрагмента в обоих индексах.

Сводим их в одну таблицу с рангами каждого фрагмента в обоих индексах и применяем формулу RRF:

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

Здесь k — константа (часто 60, но мы возьмём 1, чтобы результаты были нагляднее), а rank_i(d) — ранг документа d в i-м рейтинге.

Таблица: ранги каждого фрагмента в векторном и BM25 поиске и оценка RRF; чем выше оценка, тем релевантнее фрагмент.
Ранги в обоих поисках и оценка RRF: чем выше оценка, тем релевантнее фрагмент.

Для нашего примера:

Таблица трёх фрагментов: ранги, оценка RRF и итоговый ранг.
Итоговая таблица: ранги, оценка RRF и итоговое место каждого фрагмента.

Итоговый рейтинг: Section 2 (0.833), Section 6 (0.75), Section 7 (0.583). Это интуитивно понятно: Section 2 хорошо показал себя в обоих индексах, поэтому поднимается наверх.

Детали реализации

Класс Retriever оборачивает несколько поисковых индексов и даёт единый интерфейс:

class Retriever:
    def __init__(self, *indexes: SearchIndex):
        if len(indexes) == 0:
            raise ValueError("At least one index must be provided")
        self._indexes = list(indexes)

    def add_document(self, document: Dict[str, Any]):
        for index in self._indexes:
            index.add_document(document)

    def search(self, query_text: str, k: int = 1, k_rrf: int = 60):
        # Get results from all indexes
        all_results = []
        for idx, results in enumerate(all_results):
            for rank, (doc, _) in enumerate(results):
                # Track document ranks across indexes
                # Apply RRF scoring formula
        # Return merged and sorted results

Ключевая мысль: если разные реализации поиска имеют одинаковый API, их легко объединять без жёсткой связанности.

Проверяем гибридный подход

Помните прошлую проблему: поиск «what happened with INC-2023-Q4-011?» только по векторам давал неожиданные результаты. Инцидент по кибербезопасности (Section 10) шёл первым, но вторым оказывался финансовый анализ (Section 3) вместо более подходящего раздела о разработке ПО.

С гибридным Retriever результаты гораздо лучше:

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

Расширяемость

Схема Retriever: вопрос уходит в VectorIndex и BM25Index, результаты объединяются; есть заглушка ???Index.
Retriever: вопрос уходит в VectorIndex и BM25Index, плюс место для нового индекса «???Index».

Красота этой архитектуры — в расширяемости. Поскольку все индексы реализуют один протокол SearchIndex с методами add_document() и search(), можно легко добавлять новые методы поиска:

Retriever оборачивает VectorIndex и BM25Index: вопрос про INC-2023-Q4-011 уходит в оба, результаты объединяются.
Retriever оборачивает оба индекса: вопрос про INC-2023-Q4-011 уходит в каждый, результаты сливаются.

Нужен индекс по ключевым словам? Поиск по графу? Специализированный индекс для предметной области? Достаточно реализовать тот же интерфейс, и Retriever автоматически включит его в слияние.

Модульный подход сохраняет каждую реализацию поиска сфокусированной и тестируемой и даёт чистый способ объединить их сильные стороны в итоговой системе.

Дальше

Раздел о RAG пройден — проверьте себя пробным экзаменом. Официального квиза Skilljar у этого раздела нет. Следующий раздел курса — возможности Claude: thinking, изображения, PDF, цитаты и prompt caching.

Оригинал урока: A Multi-Index RAG pipeline.