Как стать автором
Поиск
Написать публикацию
Обновить

Hybrid RAG: методы реализации. Часть 1 — Поиск

Время на прочтение13 мин
Количество просмотров2.1K

С ростом популярности Retrieval-Augmented Generation (RAG), как архитектуры для построения систем генерации контента на основе извлечённых данных, стало очевидно, что односложный подход к выбору источников знаний ограничивает качество результатов. В этой связи особый интерес представляют Hybrid RAG подходы, сочетающие различные методы поиска и представления данных, в целях улучшения полноты, точности и релевантность ответа.

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

Если возникнут вопросы по теме статьи их можно задать здесь

Что такое Hybrid RAG

Hybrid RAG (гибридная генерация дополненная извлечёнными из базы источников данными) — это расширение базового RAG-подхода, в концепции которого для поиска знаний используются несколько различных источников и стратегий извлечения. Основная идея — комбинировать способы поиска и извлечения информации для дополнения запроса

Тогда как RAG системы основаны исключительно на дополняемых данных извлеченных из документов, Hybrid RAG предлагает подход, комбинирования и сложного ранжирования результатов поиска дополняемых данных. Это могут быть не только источники в виде документов, разбитых на фрагменты, но также и мультимодальные данные.. Методы поиска и извлечения также намного шире, например данные могут быть получены получены путем запроса через локальное или внешнее API сервиса, SQL запроса к БД, таким образом запрос Hybrid RAG можно дополнить сведениями из сервиса геокодирования или результатами SQL запроса из CRM системы

Архитектура HybridRAG

В основе архитектуры лежит базовая архитектура RAG дополненная альтернативными поисковыми методами, такими как: Sparse retrieval, Dense Retrieval, Поисковые движки специализированные API, базы данных с табличными, временными или мультимодальными данными, графовые базы данных

Схема HybridRAG
Схема HybridRAG

Этап извлечения данных завершается ранжированием, которое обеспечивает уравновешивание весов данных в общем массиве переданном LLM

Основные методы поиска

Sparse retrieval — Ключевой поиск

Этот метод извлечения информации основан на разреженном представлении текста. Он предполагает использование индекса, в котором документы представлены в виде набора отдельных слов (токенов) или терминов, и поиск выполняется путём сопоставления токенов запроса и документов.

Это наиболее традиционный метод поиска, лежащий в основе классических поисковых систем (например, Google до внедрения BERT) и реализуемый с помощью моделей вроде BM25, TF-IDF, Boolean Retrieval.

Принцип основан на следующих стадиях обработки данных

  1. Токенизация текста. Каждый документ и каждый поисковый запрос разбиваются на слова или токены (обычно с применением нормализации: лемматизация, удаление стоп-слов и т.д.). Документ: "Подключение роуминга за границей" Токены: ["подключить", "роуминг", "за", "граница"]

  2. Построение инвертированного индекса (Inverted Index) Создаётся индекс, в котором для каждого уникального слова хранится список документов, в которых оно встречается. { "обучение": [doc1, doc4, doc5], "медицина": [doc2, doc5], ... } Это позволяет быстро находить документы, содержащие определённые слова.

  3. Взвешивание токенов: TF, IDF, TF-IDF TF (Term Frequency) — насколько часто термин встречается в документе.

Пример реализации с использованием библиотеки rank_bm25 :

from rank_bm25 import BM25Okapi

# Корпус документов
docs = [
    "Чтобы сменить тариф, воспользуйтесь приложением или отправьте команду *111#.",
    "При утере телефона SIM-карту можно заблокировать через горячую линию.",
    "Для подключения роуминга отправьте SMS на номер 1234 или активируйте в приложении."
]

# Токенизация (очень простая)
tokenized_docs = [doc.lower().split() for doc in docs]

# Создание индекса BM25
bm25 = BM25Okapi(tokenized_docs)

# Запрос пользователя
query = "как подключить роуминг за границей"
tokenized_query = query.lower().split()

# Получение релевантности
scores = bm25.get_scores(tokenized_query)

# Вывод самых релевантных документов
import numpy as np
best_doc_idx = np.argmax(scores)
print(f"Наиболее релевантный документ:\n{docs[best_doc_idx]}")

Вывод:

Наиболее релевантный документ:
Для подключения роуминга отправьте SMS на номер 1234 или активируйте в приложении.

Пошаговая реализация алгоритма:

Токенизация текста

Для каждого документа нужно выделить токены (слова), привести к нижнему регистру, удалить стоп-слова и знаки препинания.

import re
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

stop_words = set(stopwords.words("russian"))

def preprocess(text):
    tokens = word_tokenize(text.lower())
    return [t for t in tokens if t.isalpha() and t not in stop_words]

docs = {
    1: "Как подключить безлимитный интернет на тарифе Супер Макс",
    2: "Инструкция по смене тарифа через личный кабинет",
    3: "Что делать если не работает мобильный интернет",
    4: "Подключение роуминга в Европе шаг за шагом"
}

tokenized_docs = {doc_id: preprocess(text) for doc_id, text in docs.items()}

Построение инвертированного индекса (Inverted Index)

from collections import defaultdict

inverted_index = defaultdict(list)

for doc_id, tokens in tokenized_docs.items():
    for token in set(tokens):  # уникальные токены
        inverted_index[token].append(doc_id)

Результат:

{
 'интернет': [1, 3],
 'тарифе': [1],
 'подключить': [1],
 'мобильный': [3],
 'роуминга': [4],
 ...
}

Взвешивание токенов: TF, IDF, TF-IDF

Определяем TF (Term Frequency) — сколько раз токен встречается в документе следующей функцией:

def compute_tf(tokens):
    tf = defaultdict(int)
    for token in tokens:
        tf[token] += 1
    for token in tf:
        tf[token] /= len(tokens)
    return tf

определяем IDF (Inverse Document Frequency) — насколько редкое слово в корпусе:

import math

N = len(tokenized_docs)
df = {token: len(docs) for token, docs in inverted_index.items()}
idf = {token: math.log(N / (1 + df[token])) for token in df}

Определяем TF-IDF — итоговая важность токена в документе:

doc_vectors = {}

for doc_id, tokens in tokenized_docs.items():
    tf = compute_tf(tokens)
    tf_idf = {token: tf[token] * idf[token] for token in tf}
    doc_vectors[doc_id] = tf_idf

Поиск по запросу

query = "не работает интернет"

Преобразуем запрос как документ:

query_tokens = preprocess(query)
query_tf = compute_tf(query_tokens)
query_vector = {token: query_tf[token] * idf.get(token, 0) for token in query_tokens}

Сравниваем с документами по косинусному сходству:

from numpy import dot
from numpy.linalg import norm

def cosine_similarity(v1, v2):
    common_tokens = set(v1.keys()) & set(v2.keys())
    numerator = sum(v1[t] * v2[t] for t in common_tokens)
    norm1 = math.sqrt(sum(x*x for x in v1.values()))
    norm2 = math.sqrt(sum(x*x for x in v2.values()))
    return numerator / (norm1 * norm2 + 1e-10)

results = {
    doc_id: cosine_similarity(query_vector, vec)
    for doc_id, vec in doc_vectors.items()
}

top_result = sorted(results.items(), key=lambda x: -x[1])

Результат:

# top_result может вернуть:
[(3, 0.65), (1, 0.40), ...]

То есть документ 3 («Что делать, если не работает мобильный интернет») — наиболее релевантен запросу.

Поиск и ранжирование обеспечивают поиск документов содержащих хотя бы один из терминов запроса. Затем по формуле (TF-IDF или BM25) вычисляется оценка релевантности каждого документа. Документы сортируются по убыванию этой оценки, и пользователь получает top-k результатов.

К плюсам Sparse Retrieval можно отнести:

  • Быстрый поиск — благодаря инвертированному индексу.

  • Интерпретируемость — понятно, почему документ релевантен (термины совпали).

  • Эффективность на больших корпусах.

  • Простой деплой и настройка.

  • Легко интегрируется с фильтрами, булевыми операциями и фасетами (в отличие от dense retrieval).

Минусы Sparse Retrieval

  • Плохо работает при синонимах и переформулировках (не найдёт «врач», если запрос «доктор»).

  • Не учитывает контекст слов (bag‑of‑words).

  • Зависит от точного совпадения слов, не умеет «понимать» текст.

  • Ограничен в мультиязычности и переносе на новые домены без переиндексации.

Dense Retrieval — Векторный поиск

Это метод поиска, основанный не на ключевых словах, а на векторных представлениях текста. Вся информация (документы и запросы) представляется как многомерные векторы в общем векторном пространстве. Поиск осуществляется через сравнение (обычно по косинусной или евклидовой близости) между вектором запроса и векторами документов. Метод заключается в поиске информации, при котором как запрос, так и документы представляются в виде векторных эмбеддингов, обычно полученных с помощью нейросетевых моделей. Вместо совпадений по ключевым словам (как в sparse retrieval), dense retrieval оценивает семантическую близость между запросом и документами в многомерном пространстве.

В основе dense retrieval лежит нейросетевая модель, преобразующая текст в вектор фиксированной размерности (обычно 128–1024).

Принцип основан на следующих стадиях обработки данных:

  1. Фрагментация текста. Каждый документ разбивается на небольшие фрагменты (chunks) для дальнейшей обработки

  2. Векторизация текста Каждый фрагмент текста преобразуется в вектор при помощи эмбеддинг модели

  3. Индексирование Каждый векторизованный фрагмент вносится в векторную базу данных

  4. Векторизация запроса Запрос преобразуется в вектор при помощи той же эмбеддинг модели, которая использовалась для векторизации исходных фрагментов

  5. Поиск ближайших векторов Сравнение между вектором запроса и векторами документов позволяет выявить ближайшие вектора

Пример реализации метода с использованием FAISS

from sentence_transformers import SentenceTransformer
import faiss
import numpy as np

# Пример исходных данных базы знаний
documents = [
    "Для активации тарифа 'ГигаМакс' наберите *123*5#. Абонентская плата — 600 рублей в месяц.",
    "Пополнить счёт можно через мобильное приложение, терминалы или команду *100#.",
    "Если вы находитесь в роуминге, стоимость звонков определяется тарифом 'Мир Онлайн'.",
    "Вы можете отключить платные подписки через личный кабинет или набрав *456#.",
    "Проверка остатка интернет-трафика осуществляется командой *111*3#."
]



# Загружаем ембеддинг модель
model = SentenceTransformer("all-MiniLM-L6-v2")

# Векторизируем
doc_embeddings = model.encode(documents, normalize_embeddings=True)
doc_embeddings = np.array(doc_embeddings)

# Индексируем
dimension = doc_embeddings.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(doc_embeddings)

# Обрабатываем запрос
query = "Как подключить тариф ГигаМакс?"
query_embedding = model.encode([query], normalize_embeddings=True)
query_embedding = np.array(query_embedding)

# ищем ближайшие фрагменты
k = 3
distances, indices = index.search(query_embedding, k)

# Выводим результат
print(f"\nЗапрос: {query}\n")
print("Найденные документы:")
for i, idx in enumerate(indices[0]):
    print(f"{i+1}. {documents[idx]} (расстояние: {distances[0][i]:.4f})")

Пример вывода

Запрос: Как подключить тариф ГигаМакс?

Найденные документы:
1. Для активации тарифа 'ГигаМакс' наберите *123*5#. Абонентская плата — 600 рублей в месяц. (расстояние: 0.1234)
2. Пополнить счёт можно через мобильное приложение, терминалы или команду *100#. (расстояние: 0.6821)
3. Проверка остатка интернет-трафика осуществляется командой *111*3#. (расстояние: 0.7430)

Пошаговая реализация алгоритма:

Фрагментация текста

Большие документы разбиваются на семантические фрагменты (chunks), чтобы повысить точность поиска и уменьшить «размывание» смысла текста

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=300,  # символов
    chunk_overlap=50
)

chunks = text_splitter.split_text(doc_text)

Векторизация

Каждый фрагмент текста преобразуется в вектор с помощью Dense Embedding Model. Примеры моделей:

  • sentence-transformers/all-MiniLM-L6-v2 (open-source)

  • text-embedding-ada-002 (OpenAI)

  • cohere.embed()

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("all-MiniLM-L6-v2")
document_vectors = model.encode(chunks, normalize_embeddings=True)

Теперь каждый чанк имеет числовое представление, например:

[0.12, -0.05, 0.34, ..., 0.07]  # 384 или 768-мерный вектор

Индексирование

Все эмбеддинги документов индексируются в векторной базе данных (FAISS, Qdrant, Pinecone, Weaviate и др.).

import faiss
import numpy as np

dimension = document_vectors.shape[1]
index = faiss.IndexFlatL2(dimension)
index.add(np.array(document_vectors))  # добавляем документы

Обработка запроса и поиск ближайших векторов

Пользователь задаёт вопрос:

"Как подключить тариф ГигаМакс?"

Этот запрос также кодируется в вектор:

query = "Как подключить тариф ГигаМакс?"
query_vector = model.encode([query], normalize_embeddings=True)

FAISS находит топ-k наиболее близких по вектору фрагментов:

k = 3
D, I = index.search(np.array(query_vector), k)
  • D: расстояния до ближайших векторов

  • I: индексы найденных фрагментов

Результат:

closest_docs = [chunks[i] for i in I[0]]

Например:

[
  "Для активации тарифа 'ГигаМакс' наберите *123*5#. Абонентская плата — 600 рублей в месяц.",
  ...
]

Преимущества Dense Retrieval:

  • Понимает семантику — находит релевантные документы даже при синонимах и переформулировках.

  • Не зависит от совпадений токенов.

  • Поддержка мультиязычности — можно искать на одном языке, а документы — на другом (если модель мультилингвальна).

  • Гибкость и адаптивность — модель можно дообучать под домен.

  • Лучше масштабируется с ростом словаря, т.к. нет токенов, как в sparse.

Недостатки Dense Retrieval

  • Требует значительных ресурсов для обучения/инференса.

  • Индексация и обновление коллекции — медленнее, чем в sparse.

  • Переиндексация при изменении модели — нужно пересоздавать векторное хранилище.

  • Меньшая интерпретируемость — сложно выявить прямые закономерности релевантной выборки.

Сравнение основных поисковых методов

Как видно из таблицы Sparse и Dense Retrieval подходы взаимно дополняют друг друга и их совместное использование позволяет извлекать релевантные данные в рамках поискового запроса практически в большинстве случаев

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

Альтернативные методы поиска

Кроме основных методов поиска приведённых выше, распространены также и альтернативные подходы поиска и извлечения данных:

Поиск по табличным, временным и мультимодальным данным

Современные системы работы с данными охватывают не только классические текстовые документы, но и сложные типы данных:

  • Табличные данные — данные, структурированные в виде таблиц (например, SQL-базы, Excel-таблицы).

  • Временные данные — данные, упорядоченные по времени, например, временные ряды, логи, датчики IoT.

  • Мультимодальные данные — данные, включающие несколько типов информации одновременно: текст, изображения, звук, видео, сенсорные данные.

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

Поиск по табличным данным

В рамках метода реализуются следующие задачи:

  • Поиск информации в SQL/CSV/Parquet таблицах

  • Объединение табличных и текстовых данных

  • Генерация на основе таблиц (вопросы, анализ, отчёты)

данные задачи решаются следующими подходами:

Semantic Table Embedding (Dense)

  • Каждая строка или таблица кодируется в вектор:

    • Модели: TAPAS, TaBERT, TUTA, TabFormer

    • Используются Transformer-модели с инжекцией структуры таблицы

  • Для поиска: запрос преобразуется в вектор и сравнивается с векторами строк/таблиц

Hybrid Retrieval: SQL + Dense

  • Используется SQL-запрос для фильтрации, затем векторный поиск по описанию или метаданным:

    • Пример: найти строки по product_id → затем искать релевантные записи по description

LLM + SQL Agent

  • LLM генерирует SQL-запрос на основе пользовательского ввода:

SELECT * FROM tariffs WHERE price < 500 ORDER BY internet_data DESC;
  • Применяется в LangChain, LlamaIndex, OpenAI function calling

  • Часто используется RAG-подход: text-to-SQL → поиск → SQL-to-text ответ

Временные ряды

В рамках метода реализуются следующие задачи:

  • Найти похожие паттерны

  • Сравнить интервалы, аномалии, тренды

  • Семантический поиск по поведению во времени

данные задачи решаются следующими подходами:

Time Series Embeddings

  • Преобразование временных рядов в вектор:

    • Модели: TS2Vec, TSiT, MiniRocket, InceptionTime

    • Поддерживают обработку и сравнение временных последовательностей

Vector Index + Metadata Filtering

  • Пример: IoT-сенсоры → эмбеддинги по 24ч-окну + фильтр по device_id или location

  • Индексация через FAISS, Qdrant, Vespa

LLM-Assisted Analytics

  • LLM получает метаописание временного ряда и сгенерированные признаки:

"Временной ряд показал резкий рост нагрузки на сеть с 18:00 до 21:00."

Мультимодальные данные

В рамках метода реализуются следующие задачи:

  • Поиск по изображениям с текстом

  • Комбинированные запросы: «Фото SIM‑карты с повреждением»

  • Аудиозапросы: голосовой поиск, запись звонка

данные задачи решаются следующими подходами:

Multimodal Embedding Models

  • Модели, преобразующие разные типы данных в общее векторное пространство

    • CLIP (OpenAI) — текст + изображение

    • BLIP / GIT / Flamingo — мультимодальная генерация

    • Whisper + CLIP — для аудио + текст

Cross-modal Retrieval

  • Запрос в одном модальности (текст), поиск в другой (изображения, звук):

Запрос: "Неисправная SIM-карта"
→ Поиск по изображениям клиентов, загруженных в CRM

Multimodal RAG

  • Используется LLM с внешним поиском по изображениям, PDF, аудио

    • Подключается OCR, ASR, Vision-модель

    • Пример: клиент загрузил скриншот экрана с ошибкой → CLIP ищет похожие случаи

Внешние API запросы

Поиск через специализированные API — это подход, при котором вместо либо в дополнение построения собственной инфраструктуры (индексация, хранение, ранжирование) используются готовые API от внешних сервисов, предоставляющие доступ к сырым либо уже проиндексированным и обработанным данным

Сервисы могут предоставлять доступ к «живым» данным например посредством API сервисов Google легко извлекаются такие данные как погода, поисковая выдача, почта, контакты, диск, и т.д.

Сервисы через API также могут открывать доступ к данным хранящимся в базах данных например API сервиса геокодирования nominatim позволит извлечь данные о координатах местоположения по указанному адресу

API специализированных баз знаний позволяют извлекать справочно- информационные данные например:

  • Wikipedia API / MediaWiki API — Позволяет получать статьи, категории, ссылки и т. д.

  • PubMed API / Entrez E‑utilities — Медицинские публикации, аннотации, авторы

  • ArXiv API — Научные публикации, метаданные и PDF‑ссылки

  • StackExchange API — Вопросы и ответы с StackOverflow

(прямые SQL-запросы, Semantic Table Search (семантический поиск по таблицам), Поиск по значениям и структуре таблиц, Vector Search для табличных данных), поиск по временным данным (Time Series Search) (поиск по шаблонам (Pattern Matching, поиск аномомалий и событий, индексация временных рядов, семантический поиск и векторные эмбеддинги (мультимодальные эмбеддинги, Cross-modal Retrieval (кросс-модальный поиск), Интеграция векторных и классических индексов, Мультимодальные базы данных и фреймворки)

Задачи ранжирования

После получения нескольких наборов результатов (ранжированных списков документов) из разных поисковых методов необходимо:

  • Объединить (merge) результаты в единый список.

  • Ранжировать документы так, чтобы наиболее релевантные оказались сверху.

  • Учесть особенности каждого источника и веса их значимости.

Основные подходы к ранжированию в гибридном поиске

  1. Линейное комбинирование скоров (Score Fusion) Каждый документ получает баллы от каждого поискового метода (например, BM25 score и cosine similarity). Итоговый скор вычисляется как взвешенная сумма по формуле

    Плюсы: Простота реализации и интерпретируемость.

    Минусы: Требует нормализации скора каждого метода, иногда сложно подобрать веса.

  2. Ранжирование через Learning-to-Rank (LTR) Использование ML-моделей, обученных на размеченных данных с релевантностью. В качестве признаков используются скоры из разных методов, метаданные, дополнительные тригеры. Модели: Gradient Boosted Trees, LambdaMART, нейронные сети.

    Плюсы: Высокое качество ранжирования, возможность учитывать множество факторов.

    Минусы: Требует обучающих данных и ресурсов.

  3. Ранжирование с помощью переобучения (Re-ranking) Сначала выбирается расширенный список из разных источников (например, топ-100 документов). Затем мощная модель (например, LLM или cross-encoder) переоценивает каждый документ по запросу. Итоговый список сортируется по оценкам переоценки.

    Плюсы: Улучшает качество выдачи, учитывая глубокий контекст.

    Минусы: Вычислительно дорого, обычно применяется к ограниченному числу кандидатов.

  4. Каскадное ранжирование (Cascading ranking) На первом этапе используется быстрый метод (BM25 или dense retriever) для отбора. На втором — более точный, но медленный метод (реранжирование, LTR). Позволяет балансировать скорость и качество.

Пример простого гибридного ранжирования:

alpha = 0.6  # вес sparse поиска
 results_sparse = get_sparse_results(query)  # [(doc_id, score), ...]
 results_dense = get_dense_results(query)    # [(doc_id, score), ...]

Нормализация скоров

norm_sparse = normalize_scores(results_sparse)
norm_dense = normalize_scores(results_dense)

Объединение результатов

combined_scores = {}
 for doc_id, score in norm_sparse:
 combined_scores[doc_id] = combined_scores.get(doc_id, 0) + alpha  score
 for doc_id, score in norm_dense:
 combined_scores[doc_id] = combined_scores.get(doc_id, 0) + (1 - alpha)  score

Сортировка по итоговому скору

final_ranking = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)

Метод ранжирования в гибридном поиске это основа объединения сильных сторон разных поисковых методов. Правильно выбранная стратегия позволяет улучшить полноту и точность поиска, адаптироваться под разные задачи и обеспечить более релевантные и разнообразные результаты.

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

Теги:
Хабы:
+8
Комментарии0

Публикации

Ближайшие события