Реализация Hybrid Search (векторный + полнотекстовый поиск) для RAG

Когда в базе тысячи документов, поиск по смыслу часто не находит точный номер или редкий термин, а полнотекстовый запрос теряет синонимы и перефразировки. Мы реализуем гибридный поиск, объединяя векторный и полнотекстовый подходы, чтобы ваш RAG находил и точные совпадения, и семантически близкие документы. Наша команда внедряет решение под ключ с последующей поддержкой, обеспечивая надёжный retrieval на ваших данных.
Часто задаваемые вопросы

При реализации RAG-систем часто возникает дилемма: как одновременно найти документ по смыслу и по точному номеру? Hybrid Search — комбинация векторного (dense) и полнотекстового (sparse/BM25) поиска с последующим слиянием результатов — решает эту задачу. На практике hybrid search стабильно превосходит любой из методов в отдельности на большинстве корпоративных датасетов. Например, на одном из проектов hybrid search (RRF) улучшил MRR@5 на 12% относительно pure dense search при сохранении высокого recall по точным терминам. Мы реализуем такие решения под ключ, с гарантией качества retrieval на ваших данных. Закажите консультацию по внедрению hybrid search и получите оценку вашего проекта.

Почему нельзя обойтись только dense search

Dense embedding усредняет семантику — это и сила, и слабость. Запрос «договор №ДА-2023-451» будет иметь высокое косинусное сходство с договорами вообще, но не с конкретным документом по номеру. BM25 найдёт точное совпадение строки «ДА-2023-451» мгновенно.

  • Dense search плохо работает для: точных номеров (договор, артикул, серийный номер), аббревиатур и специфических акронимов, редких технических терминов, запросов на поиск точной цитаты.
  • BM25 плохо работает для: перефразированных запросов (синонимы), семантически похожих концепций с разными словами, межъязыковых запросов, неточных описаний («что-то про оплату после поставки»).

Почему hybrid search лучше, чем dense или BM25 по отдельности?

Сочетание двух подходов даёт синергию: dense покрывает семантику, BM25 — точные совпадения. Ниже на реальном кейсе видно, что hybrid RRF (без реранкера) превосходит dense+reranker по MRR@5 (0.83 vs 0.80) и NDCG@5 (0.81 vs 0.77). При этом hybrid+reranker даёт уже 0.89/0.87. Другими словами, реализация гибридного поиска (hybrid search implementation) позволяет достичь баланса между семантической близостью и точным совпадением ключевых слов. По данным нашего A/B-тестирования на 400 запросах, hybrid RRF превосходит dense+reranker в 1.04 раза по MRR@5. Для многих задач это отменяет необходимость в дорогом реранкере.

Алгоритмы слияния результатов

Reciprocal Rank Fusion (RRF) — наиболее устойчивый метод. RRF — метод слияния, предложенный Кормаком и др. (2009) — подробнее на Wikipedia.

Код RRF
from collections import defaultdict

def reciprocal_rank_fusion(
    dense_results: list[tuple],  # [(doc_id, score), ...]
    sparse_results: list[tuple],
    k: int = 60  # RRF константа (обычно 60)
) -> list[tuple]:
    """
    RRF score = sum(1 / (k + rank_i)) по всем спискам
    k=60 стандартное значение (Cormack et al.)
    """
    scores = defaultdict(float)
    for rank, (doc_id, _) in enumerate(dense_results, 1):
        scores[doc_id] += 1 / (k + rank)
    for rank, (doc_id, _) in enumerate(sparse_results, 1):
        scores[doc_id] += 1 / (k + rank)
    return sorted(scores.items(), key=lambda x: -x[1])

Relative Score Fusion (RSF) — нормализованное объединение:

Код RSF
def relative_score_fusion(
    dense_results: list[tuple],
    sparse_results: list[tuple],
    alpha: float = 0.5  # Вес dense
) -> list[tuple]:
    """Нормализует оценки в [0,1] и взвешивает"""
    scores = defaultdict(float)

    # Нормализация dense
    if dense_results:
        max_d = max(s for _, s in dense_results)
        min_d = min(s for _, s in dense_results)
        for doc_id, score in dense_results:
            norm = (score - min_d) / (max_d - min_d + 1e-8)
            scores[doc_id] += alpha * norm

    # Нормализация sparse
    if sparse_results:
        max_s = max(s for _, s in sparse_results)
        min_s = min(s for _, s in sparse_results)
        for doc_id, score in sparse_results:
            norm = (score - min_s) / (max_s - min_s + 1e-8)
            scores[doc_id] += (1 - alpha) * norm

    return sorted(scores.items(), key=lambda x: -x[1])

Сравнение алгоритмов слияния

Параметр RRF RSF
Принцип Сумма обратных рангов Взвешенная сумма нормализованных оценок
Чувствительность к шкалам Низкая (использует только ранг) Высокая (требует нормализации)
Настройка Один параметр k Параметр alpha
Устойчивость Высокая Средняя (зависит от alpha)
Рекомендуемый k/alpha k=60 (эмпирически) alpha=0.5 (по умолчанию)

SPLADE: продвинутый sparse encoder

SPLADE (Sparse Lexical and Expansion Model) генерирует sparse векторы с лексическим расширением — модель учится «расширять» запрос синонимами и связанными терминами. По данным бенчмарка BEIR, SPLADE превосходит BM25 в 1.2–1.5 раза по NDCG@10.

from fastembed import SparseTextEmbedding

sparse_model = SparseTextEmbedding(
    model_name="prithivida/Splade_PP_en_v1"
)


def encode_sparse(text: str) -> dict:
    """Возвращает sparse вектор {token_id: weight}"""
    output = list(sparse_model.embed([text]))[0]
    return {
        "indices": output.indices.tolist(),
        "values": output.values.tolist(),
    }

SPLADE превосходит BM25 на большинстве BEIR бенчмарков. Для русского языка рекомендуем модель naver/efficient-splade-VI-BT-large-query или multilingual варианты.

Реализация с Qdrant (практический пример)

from qdrant_client import QdrantClient
from qdrant_client.models import (
    SparseVector,
    Prefetch,
    FusionQuery,
    Fusion,
    NamedVector,
    NamedSparseVector,
)
from fastembed import TextEmbedding, SparseTextEmbedding

dense_model = TextEmbedding("BAAI/bge-m3")  # Multilingual dense
sparse_model = SparseTextEmbedding("prithivida/Splade_PP_en_v1")

client = QdrantClient(url="http://localhost:6333")


def hybrid_search(query: str, top_k: int = 5) -> list[dict]:
    # Dense embedding
    dense_vec = list(dense_model.embed([query]))[0].tolist()

    # Sparse embedding
    sparse_output = list(sparse_model.embed([query]))[0]
    sparse_vec = SparseVector(
        indices=sparse_output.indices.tolist(),
        values=sparse_output.values.tolist(),
    )

    results = client.query_points(
        collection_name="hybrid_docs",
        prefetch=[
            Prefetch(query=dense_vec, using="dense", limit=50),
            Prefetch(query=sparse_vec, using="sparse", limit=50),
        ],
        query=FusionQuery(fusion=Fusion.RRF),
        limit=top_k,
        with_payload=True,
    )

    return [
        {"text": r.payload["text"], "source": r.payload["source"], "score": r.score}
        for r in results.points
    ]

Практический кейс: влияние alpha на качество retrieval

Из нашей практики: на проекте с 12 000 документов корпоративной базы знаний (договоры, регламенты, FAQ) мы протестировали 400 запросов разных типов. Результаты:

Конфигурация MRR@5 NDCG@5 Точные термины recall
Dense only (BGE-M3) 0.74 0.71 0.58
BM25 only 0.67 0.63 0.91
Hybrid RRF (k=60) 0.83 0.81 0.84
Hybrid RSF (α=0.6) 0.81 0.79 0.81
Dense + Reranker 0.80 0.77 0.61
Hybrid + Reranker 0.89 0.87 0.86

Hybrid RRF без reranker уже бьёт dense+reranker. Комбинация hybrid+reranker — наилучший результат. Для сравнения, SPLADE в качестве sparse encoder даёт прирост MRR@5 примерно на 0.03–0.05 относительно BM25 при том же методе слияния.

Как настроить RRF-слияние на вашем датасете?

Оптимальное k для RRF: k=60 — эмпирически устойчивое значение. Слишком малое k (10–20) даёт большой вес топ-позициям. Слишком большое (100+) нивелирует разницу между позициями. На реальных данных проверьте k∈{20, 40, 60, 80} на валидационном наборе. Для RSF подбирайте alpha от 0.3 до 0.7 с шагом 0.1.

Пошаговый процесс внедрения hybrid search

  1. Аудит текущей схемы retrieval: анализ используемых эмбеддингов, стека векторной БД и метрик качества.
  2. Выбор и настройка sparse encoder: установка SPLADE или другого sparse encoder под ваш язык и домен.
  3. Интеграция двойного поиска: настройка индексации dense и sparse векторов в Qdrant/Pinecone/Weaviate.
  4. Реализация слияния: внедрение RRF или RSF с начальными параметрами (k=60, alpha=0.5).
  5. Тестирование и оптимизация: прогон ваших запросов, подбор параметров по метрикам MRR/NDCG.
  6. Документация и передача: описание процесса, обучение команды, передача кода и конфигов.

Что вы получите в результате

  • Интеграционный код hybrid search в вашу RAG-систему.
  • Конфигурационные файлы для Qdrant/Pinecone.
  • Документация по настройке и эксплуатации.
  • Обучение команды (2-часовой вебинар).
  • Гарантия качества retrieval (фиксация метрик до/после).
  • Пост-проектная поддержка 1 месяц.

Свяжитесь с нами для оценки вашего проекта. Получите консультацию по внедрению hybrid search и повысьте качество retrieval вашей RAG-системы.

Направления AI-разработки

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1394
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1369
  • Разработка логотипа компании B2B Advance
    Разработка логотипа компании B2B Advance
    816
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1142