С ростом популярности 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, базы данных с табличными, временными или мультимодальными данными, графовые базы данных

Этап извлечения данных завершается ранжированием, которое обеспечивает уравновешивание весов данных в общем массиве переданном LLM
Основные методы поиска
Sparse retrieval — Ключевой поиск
Этот метод извлечения информации основан на разреженном представлении текста. Он предполагает использование индекса, в котором документы представлены в виде набора отдельных слов (токенов) или терминов, и поиск выполняется путём сопоставления токенов запроса и документов.
Это наиболее традиционный метод поиска, лежащий в основе классических поисковых систем (например, Google до внедрения BERT) и реализуемый с помощью моделей вроде BM25, TF-IDF, Boolean Retrieval.
Принцип основан на следующих стадиях обработки данных
Токенизация текста. Каждый документ и каждый поисковый запрос разбиваются на слова или токены (обычно с применением нормализации: лемматизация, удаление стоп-слов и т.д.). Документ: "Подключение роуминга за границей" Токены: ["подключить", "роуминг", "за", "граница"]
Построение инвертированного индекса (Inverted Index) Создаётся индекс, в котором для каждого уникального слова хранится список документов, в которых оно встречается. { "обучение": [doc1, doc4, doc5], "медицина": [doc2, doc5], ... } Это позволяет быстро находить документы, содержащие определённые слова.
Взвешивание токенов: 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).
Принцип основан на следующих стадиях обработки данных:
Фрагментация текста. Каждый документ разбивается на небольшие фрагменты (chunks) для дальнейшей обработки
Векторизация текста Каждый фрагмент текста преобразуется в вектор при помощи эмбеддинг модели
Индексирование Каждый векторизованный фрагмент вносится в векторную базу данных
Векторизация запроса Запрос преобразуется в вектор при помощи той же эмбеддинг модели, которая использовалась для векторизации исходных фрагментов
Поиск ближайших векторов Сравнение между вектором запроса и векторами документов позволяет выявить ближайшие вектора
Пример реализации метода с использованием 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) результаты в единый список.
Ранжировать документы так, чтобы наиболее релевантные оказались сверху.
Учесть особенности каждого источника и веса их значимости.
Основные подходы к ранжированию в гибридном поиске
Линейное комбинирование скоров (Score Fusion) Каждый документ получает баллы от каждого поискового метода (например, BM25 score и cosine similarity). Итоговый скор вычисляется как взвешенная сумма по формуле
Плюсы: Простота реализации и интерпретируемость.
Минусы: Требует нормализации скора каждого метода, иногда сложно подобрать веса.
Ранжирование через Learning-to-Rank (LTR) Использование ML-моделей, обученных на размеченных данных с релевантностью. В качестве признаков используются скоры из разных методов, метаданные, дополнительные тригеры. Модели: Gradient Boosted Trees, LambdaMART, нейронные сети.
Плюсы: Высокое качество ранжирования, возможность учитывать множество факторов.
Минусы: Требует обучающих данных и ресурсов.
Ранжирование с помощью переобучения (Re-ranking) Сначала выбирается расширенный список из разных источников (например, топ-100 документов). Затем мощная модель (например, LLM или cross-encoder) переоценивает каждый документ по запросу. Итоговый список сортируется по оценкам переоценки.
Плюсы: Улучшает качество выдачи, учитывая глубокий контекст.
Минусы: Вычислительно дорого, обычно применяется к ограниченному числу кандидатов.
Каскадное ранжирование (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 систем приведу во второй части статьи.