中文翻译暂不可用,显示俄语原文。

Как вы делаете hybrid search (vector + keyword) в production на 10M документов?

Краткий тезис

Hybrid search (поиск|гибридный поиск) объединяет семантический (векторный) и лексический (ключевой) подходы для повышения recall и precision на больших корпусах. В production на 10M документов используют готовые решения (Qdrant, Vespa) с built-in hybrid или собирают связку Elasticsearch (BM25) + Qdrant (vector) с объединением через RRF (Fusion) или learning-to-rank. Ключевые вызовы — нормализация скоров, latency, шардирование и A/B-тестирование весов.


1. Термин: Hybrid Search (гибридный поиск)

Hybrid search — это техника, при которой поисковая система одновременно выполняет vector search (поиск по эмбеддингам) и keyword search (поиск по точным совпадениям, обычно BM25), а затем объединяет результаты. Цель — компенсировать недостатки каждого подхода:

  • Vector search отлично понимает семантику, синонимы, контекст, но плохо справляется с редкими терминами, аббревиатурами и точными совпадениями (например, «Qdrant v1.2.0»).
  • Keyword search (BM25) точен для редких слов и фраз, но не улавливает смысл запроса («автомобиль» vs «машина»).

Гибридный поиск даёт лучший recall (находит больше релевантных документов) и precision (релевантные документы выше в выдаче), что критично для production RAG-систем на 10M+ документов.


2. Компоненты гибридного поиска

КомпонентИнструментыПринцип
Vector searchQdrant, Pinecone, Weaviate, Milvus, FAISSANN (Approximate Nearest Neighbor) по эмбеддингам (cosine, dot product)
Keyword searchElasticsearch, OpenSearch, Lucene, TantivyИнвертированный индекс + BM25 (TF-IDF с длиной поля)
ОбъединениеRRF, weighted sum, learning-to-rankСлияние двух ранжированных списков в один

Для 10M документов оба индекса должны быть шардированы и реплицированы для отказоустойчивости и низкой латентности.


3. Почему pure vector search недостаточен

  • Проблема редких терминов: эмбеддинг модели (например, text-embedding-ada-002) может «размазать» редкое слово по всему вектору, и ANN его не найдёт.
  • Точные совпадения: запрос «Qdrant v1.2.0» — векторный поиск может найти документы про «Qdrant» и «версии», но не обязательно именно про v1.2.0.
  • Аббревиатуры и коды: «API key: sk-...» — BM25 найдёт точное совпадение, векторный — нет.
  • Доменная лексика: в медицинских или юридических текстах точные термины критичны.

4. Почему pure keyword search недостаточен

  • Синонимы: запрос «автомобиль» не найдёт документы со словом «машина».
  • Морфология: BM25 чувствителен к словоформам (без стемминга/лемматизации).
  • Семантический разрыв: «как лечить головную боль» vs «терапия мигрени» — разные по лексике, но одинаковые по смыслу.
  • Омонимия: «лук» (оружие vs овощ) — BM25 выдаст оба, векторный может уточнить по контексту.

5. Архитектура production hybrid search на 10M документов

Типовая архитектура:

Запрос пользователя
       |
       v
   Router (определяет, какие индексы опрашивать)
       |
       +----> Vector DB (Qdrant) — ANN search, top-k1
       |
       +----> Keyword DB (Elasticsearch) — BM25 search, top-k2
       |
       v
   Fusion Module (RRF / weighted sum / LTR)
       |
       v
   Top-N результатов -> LLM (RAG) или пользователю

Варианты реализации:

  • Built-in hybrid: Qdrant (с версии 1.8) поддерживает hybrid search на уровне API — отправляете запрос и вектор, получаете объединённый результат. Vespa также имеет встроенный гибрид с настраиваемыми весами.
  • Ручная связка: Elasticsearch для BM25 + Qdrant для vector, объединение на стороне приложения (Python-сервис).

Почему выбирают ручную связку:

  • Гибкость в настройке весов и нормализации.
  • Возможность использовать разные модели эмбеддингов и конфигурации BM25.
  • Независимое масштабирование каждого индекса.

6. Стратегии объединения результатов

6.1 RRF (Reciprocal Rank Fusion)

Простая и эффективная формула без обучения:

score(doc) = Σ_{i=1}^{N} 1 / (k + rank_i(doc))

где rank_i(doc) — позиция документа в i-м списке (1-based), k — константа (обычно 60).

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

  • Не требует нормализации скоров.
  • Робастна к разным шкалам.
  • Не нужны обучающие данные.

Недостатки:

  • Не учитывает уверенность каждого метода (все ранги равны).
  • Константа k подбирается эмпирически.

6.2 Weighted sum (линейная комбинация)

score(doc) = α * score_vector(doc) + (1-α) * score_keyword(doc)

Требует нормализации скоров в единый диапазон (например, min-max или z-score). α подбирается на валидации.

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

  • Можно учесть качество каждого метода.
  • Интерпретируемо.

Недостатки:

  • Чувствительно к выбросам в скорах.
  • Нормализация может исказить распределение.

6.3 Learning-to-rank (LTR)

Обучается модель (XGBoost, LambdaRank) на парах запрос-документ с фичами: BM25 score, cosine similarity, длина документа, частота терминов и т.д.

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

  • Максимальная точность.
  • Адаптация под специфику данных.

Недостатки:

  • Требует размеченных данных (релевантность).
  • Дороже в поддержке и инференсе.

Когда использовать: для высоконагруженных систем, где каждый процент recall важен (например, поиск по юридическим документам).


7. Инструменты для production

ИнструментПоддержка hybridОсобенности
QdrantВстроенный (с 1.8)Простая настройка, REST API, поддержка фильтров
VespaВстроенныйМощный язык запросов, LTR, real-time обновления
ElasticsearchЧерез плагины (kNN) + BM25Зрелый, но hybrid требует ручного объединения
WeaviateВстроенный (hybrid search)Простой API, но меньше настроек
MilvusТолько vector, keyword через внешний сервисВысокая производительность ANN

Для 10M документов рекомендую Qdrant (built-in hybrid) или Vespa (если нужен LTR). Если уже есть Elasticsearch — добавляют Qdrant как sidecar.


8. Масштабирование на 10M документов

Шардирование: разбиваем индекс на N шардов по ID документа. Каждый шард хранит часть данных и обрабатывает запросы параллельно.

Репликация: каждый шард имеет 2-3 реплики для отказоустойчивости и read scalability.

Latency:

  • Vector search: ANN (HNSW, IVF) даёт latency <10ms на шард.
  • Keyword search: BM25 на инвертированном индексе — <5ms.
  • Суммарно с объединением: 20-50ms.

Кэширование: кэшируем результаты популярных запросов (Redis) для снижения нагрузки.

Мониторинг: метрики latency, recall@k, throughput. Используем Prometheus + Grafana.


9. Метрики оценки hybrid search

МетрикаОписание
Recall@kДоля релевантных документов среди top-k
MRRСредний обратный ранг первого релевантного
NDCG@kУчитывает позицию и graded relevance
Precision@kДоля релевантных среди top-k
Latency p99Время ответа для 99% запросов

Оценку проводим на золотом стандарте (ручная разметка релевантности для ~1000 запросов). Сравниваем pure vector, pure keyword и hybrid.


10. Trade-offs

АспектPure vectorPure keywordHybrid
Recall на редких терминахНизкийВысокийВысокий
Понимание синонимовВысокоеНизкоеВысокое
LatencyНизкаяНизкаяСредняя (два запроса + слияние)
Сложность инфраструктурыНизкаяСредняяВысокая
Стоимость (вычислительные ресурсы)СредняяНизкаяВысокая (два индекса)

Когда hybrid не нужен:

  • Корпус < 100K документов.
  • Запросы всегда точные (ID, коды).
  • Допустимо жертвовать recall ради latency.

11. Best practices

  1. Нормализация скоров: перед объединением приведите скоры к одному диапазону (например, z-score или min-max).
  2. Подбор весов: используйте grid search по α (для weighted sum) или k (для RRF) на валидационном наборе.
  3. A/B тестирование: запустите hybrid для 10% трафика, сравните click-through rate и user satisfaction.
  4. Фильтры: применяйте фильтры (категория, дата) до объединения, чтобы уменьшить кандидатов.
  5. Мониторинг дрейфа: эмбеддинги могут устаревать, BM25 — нет. Периодически пересчитывайте метрики.
  6. Graceful degradation: если один из индексов недоступен, используйте только второй.

12. Пример кода: RRF на Python

import numpy as np

def reciprocal_rank_fusion(results_list, k=60):
    """
    results_list: list of lists of (doc_id, rank) for each search method
    returns: list of (doc_id, score) sorted by score descending
    """
    scores = {}
    for results in results_list:
        for rank, (doc_id, _) in enumerate(results, start=1):
            scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank)
    return sorted(scores.items(), key=lambda x: x[1], reverse=True)

# Пример использования
vector_results = [("doc1", 0.9), ("doc2", 0.8), ("doc3", 0.7)]
keyword_results = [("doc2", 12.5), ("doc4", 10.0), ("doc1", 8.0)]

# Преобразуем в ранги (1-based)
vector_ranks = [(doc_id, i+1) for i, (doc_id, _) in enumerate(vector_results)]
keyword_ranks = [(doc_id, i+1) for i, (doc_id, _) in enumerate(keyword_results)]

final = reciprocal_rank_fusion([vector_ranks, keyword_ranks], k=60)
print(final)  # [('doc2', 0.0328), ('doc1', 0.0323), ('doc4', 0.0164), ('doc3', 0.0164)]

Пет-проект для закрепления

Задача: Реализовать hybrid search для корпуса из 1M документов (например, дамп Wikipedia на 1M статей) и сравнить с pure vector и pure keyword.

Инструменты:

  • Qdrant (vector index) — запустить в Docker.
  • Elasticsearch (keyword index) — тоже в Docker.
  • Python (FastAPI для сервиса объединения).
  • Эмбеддинги: sentence-transformers/all-MiniLM-L6-v2 (384d).
  • Датасет: wikipedia-1m из Hugging Face.

Шаги:

  1. Загрузить 1M статей, разбить на чанки по 512 токенов.
  2. Для каждого чанка вычислить эмбеддинг и сохранить в Qdrant (с метаданными).
  3. Индексировать те же чанки в Elasticsearch (поле text с анализатором standard).
  4. Написать сервис, который принимает запрос, параллельно опрашивает Qdrant (top-100) и Elasticsearch (top-100), объединяет через RRF (k=60).
  5. Разметить 200 запросов (релевантные документы) для оценки.
  6. Сравнить Recall@10, MRR для pure vector, pure keyword и hybrid.

Ожидаемый результат:

  • Hybrid покажет Recall@10 на 10-20% выше, чем каждый метод по отдельности.
  • Latency < 100ms на запрос.
  • Понимание trade-offs: hybrid требует больше ресурсов, но даёт лучший recall.

Связь с другими вопросами

ВопросТема
1Проектирование RAG
5Метрики retrieval
7Оптимизация latency
9Обновление индексов
10Продвинутые RAG
12Выбор эмбеддингов

Навигация