English translation is not available yet. Showing Russian content.
Как вы делаете 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 search | Qdrant, Pinecone, Weaviate, Milvus, FAISS | ANN (Approximate Nearest Neighbor) по эмбеддингам (cosine, dot product) |
| Keyword search | Elasticsearch, 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.
- 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 vector | Pure keyword | Hybrid |
|---|---|---|---|
| Recall на редких терминах | Низкий | Высокий | Высокий |
| Понимание синонимов | Высокое | Низкое | Высокое |
| Latency | Низкая | Низкая | Средняя (два запроса + слияние) |
| Сложность инфраструктуры | Низкая | Средняя | Высокая |
| Стоимость (вычислительные ресурсы) | Средняя | Низкая | Высокая (два индекса) |
Когда hybrid не нужен:
- Корпус < 100K документов.
- Запросы всегда точные (ID, коды).
- Допустимо жертвовать recall ради latency.
11. Best practices
- Нормализация скоров: перед объединением приведите скоры к одному диапазону (например, z-score или min-max).
- Подбор весов: используйте grid search по α (для weighted sum) или k (для RRF) на валидационном наборе.
- A/B тестирование: запустите hybrid для 10% трафика, сравните click-through rate и user satisfaction.
- Фильтры: применяйте фильтры (категория, дата) до объединения, чтобы уменьшить кандидатов.
- Мониторинг дрейфа: эмбеддинги могут устаревать, BM25 — нет. Периодически пересчитывайте метрики.
- 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.
Шаги:
- Загрузить 1M статей, разбить на чанки по 512 токенов.
- Для каждого чанка вычислить эмбеддинг и сохранить в Qdrant (с метаданными).
- Индексировать те же чанки в Elasticsearch (поле text с анализатором
standard). - Написать сервис, который принимает запрос, параллельно опрашивает Qdrant (top-100) и Elasticsearch (top-100), объединяет через RRF (k=60).
- Разметить 200 запросов (релевантные документы) для оценки.
- Сравнить 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 | Выбор эмбеддингов |
Навигация
- Предыдущий: 232
- Следующий: 234
- Индекс: 00. Индекс разборов