RAG на Elasticsearch kNN: гибридный поиск BM25 и векторы

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

Разработка RAG с Elasticsearch kNN: гибридный поиск BM25 и векторы

Представьте: ваш Elasticsearch обрабатывает сотни тысяч документов, но пользователи жалуются, что поиск не находит релевантные ответы. BM25 отлично работает с точными совпадениями, но пасует перед синонимами, тавтологией и сложными запросами на русском языке. Результаты: клиенты уходят, операторы тратят время на поиск. Добавлять отдельную векторную базу? Это рост стоимости инфраструктуры, сетевые задержки и ещё одна система для поддержки. Оптимальное решение — использовать встроенный kNN в Elasticsearch 8.x, объединив полнотекстовый и векторный поиск в одном индексе. Мы помогли нескольким командам внедрить такой гибрид без смены инфраструктуры, и сейчас расскажем, как это сделать. Свяжитесь с нами для аудита вашего текущего поиска — мы предложим оптимальный маршрут.

Почему Elasticsearch kNN — оптимальное решение для гибридного поиска?

Типичные боли при внедрении RAG без перестройки: разрозненный поиск (BM25 пропускает семантику), инфраструктурный хаос (поднимать Pinecone или Weaviate параллельно с ES — увеличивать стоимость и сложность), латентность (внешние векторные БД добавляют сетевые задержки p99 до 50+ мс). Elasticsearch kNN решает все три: hybrid search (kNN + BM25) через RRF fusion в одном запросе, минимальный оверхед, не нужны новые серверы.

Elasticsearch — зрелая технология с 15+ лет на рынке, используется в тысячах продакшенов. Встроенная поддержка русского анализатора Snowball даёт качественный стемминг: запрос «договором» найдёт «договор», «договоры», «договорам». Это критично для BM25-части гибрида. Кроме того, ELK-стек (Logstash, Kibana) позволяет мониторить индексы и визуализировать метрики поиска без дополнительных инструментов.

Какие настройки HNSW дают наилучший баланс скорости и качества?

Для продакшена мы рекомендуем HNSW с параметрами m=16, ef_construction=100. Это оптимальный баланс между скоростью индексации и точностью поиска. Слишком маленькое num_candidates (менее 100) снижает recall, слишком большое — увеличивает latency. В наших проектах используем cosine similarity как метрику расстояния для эмбеддингов.

Как мы это делаем: стек, конфиги, кейс

Стек: Elasticsearch 8.11+, OpenAI text-embedding-3-small (1536-dim), Python 3.11, официальный клиент elasticsearch-py.

Кейс из нашей практики: миграция существующего Elasticsearch на RAG

Контекст: наш клиент — компания с 500K юридических документов в Elasticsearch 8.x. Задача: добавить RAG-поверхность без смены инфраструктуры.

Шаги:

  1. Добавление поля embedding (dense_vector, dims=1536) к существующему маппингу.
  2. Батчевая векторизация существующих документов (2 дня, 500K × $0.02/1M = $10).
  3. Reindexing с новым полем (6 часов).
  4. Добавление RRF fusion в поисковые запросы.
  5. RAG-слой поверх ES retrieval.

Результаты (vs чистый BM25):

  • NDCG@5: 0.64 → 0.81
  • Recall@10: 0.71 → 0.88
  • Latency P95: 85мс → 140мс (hybrid)
  • Faithfulness (RAGAS): 0.76 → 0.91

Экономия на инфраструктуре: не нужно поднимать отдельный сервер за $200/мес. Переход от pure BM25 к hybrid kNN+BM25 дал +27% к NDCG без смены инфраструктуры. Клиент получил работающий RAG за 2 недели.

Создание индекса и индексация документов

Добавляем поле dense_vector в существующий индекс и выполняем батчевую векторизацию.

from elasticsearch import Elasticsearch
es = Elasticsearch("http://localhost:9200")
# Создание индекса с маппингом
index_config = {
    "mappings": {
        "properties": {
            "content": {
                "type": "text",
                "analyzer": "russian",  # Нативная поддержка русской морфологии
            },
            "source": {"type": "keyword"},
            "doc_type": {"type": "keyword"},
            "page": {"type": "integer"},
            "date": {"type": "date"},
            "embedding": {
                "type": "dense_vector",
                "dims": 1536,
                "index": True,
                "similarity": "cosine",
                "index_options": {
                    "type": "hnsw",
                    "m": 16,
                    "ef_construction": 100,
                }
            }
        }
    },
    "settings": {
        "number_of_shards": 1,
        "number_of_replicas": 1,
    }
}
es.indices.create(index="knowledge_base", body=index_config)
from openai import OpenAI
from elasticsearch.helpers import bulk

openai_client = OpenAI()


def generate_actions(chunks: list):
    texts = [c["text"] for c in chunks]
    response = openai_client.embeddings.create(
        model="text-embedding-3-small",
        input=texts
    )
    embeddings = [e.embedding for e in response.data]
    for chunk, embedding in zip(chunks, embeddings):
        yield {
            "_index": "knowledge_base",
            "_source": {
                "content": chunk["text"],
                "source": chunk["source"],
                "doc_type": chunk["doc_type"],
                "page": chunk.get("page", 0),
                "embedding": embedding,
            }
        }

bulk(es, generate_actions(document_chunks))

Hybrid Search: BM25 + kNN на практике

Elasticsearch поддерживает гибридный поиск через knn + query в одном запросе с RRF fusion.

def hybrid_search_es( query: str, doc_type_filter: str = None, top_k: int = 5 ) -> list:
    query_embedding = openai_client.embeddings.create(
        model="text-embedding-3-small",
        input=query
    ).data[0].embedding

    filter_clause = []
    if doc_type_filter:
        filter_clause.append({"term": {"doc_type": doc_type_filter}})

    body = {
        "query": {
            "bool": {
                "must": {
                    "match": {
                        "content": {
                            "query": query,
                            "analyzer": "russian"
                        }
                    }
                },
                "filter": filter_clause,
            }
        },
        "knn": {
            "field": "embedding",
            "query_vector": query_embedding,
            "k": top_k * 3,
            "num_candidates": 100,
            "filter": filter_clause,
        },
        "rank": {
            "rrf": {
                "window_size": 50,
                "rank_constant": 20,
            }
        },
        "size": top_k,
        "_source": ["content", "source", "doc_type"],
    }

    response = es.search(index="knowledge_base", body=body)

    return [
        {
            "text": hit["_source"]["content"],
            "source": hit["_source"]["source"],
            "score": hit["_score"],
        }
        for hit in response["hits"]["hits"]
    ]

Преимущество русской морфологии из коробки

Elasticsearch с analyzer russian поддерживает стемминг русских слов через Snowball. Это критично для BM25 части гибридного поиска — запрос «договором» найдёт документы с «договор», «договоры», «договорам».

es.indices.analyze(
    index="knowledge_base",
    body={"analyzer": "russian", "text": "договором аренды"}
)  # tokens: ["договор", "аренд"] — стеммированные формы

Что входит в работу

  • Аудит текущего индекса ES (маппинг, шарды, производительность)
  • Проектирование схемы dense_vector и выбор embedding-модели
  • Написание скриптов батчевой векторизации и реиндексации
  • Реализация hybrid search с RRF fusion
  • Интеграция RAG-пайплайна (с LangChain или прямыми вызовами OpenAI)
  • Тестирование: NDCG, Recall, latency, faithfulness
  • Документация и обучение команды (2 часа воркшопа)

Сроки ориентировочно

Этап Длительность
Анализ и проектирование 2–3 дня
Векторизация и реиндексирование 2–5 дней
Разработка гибридных запросов 3–5 дней
RAG-пайплайн и оценка 1–2 недели
Итого 2–4 недели

Сравнение Elasticsearch kNN с альтернативными векторными базами

Характеристика Elasticsearch kNN Pinecone / Qdrant
Инфраструктура Уже есть? Не нужно новой Отдельный сервис
Гибридный поиск Встроенный BM25 + kNN Через отдельный BM25 + конкатенация
Russian stemmer Да (Snowball) Нет (нужен внешний)
Latency p99 140 мс (гибрид) 50-100 мс (только вектор)
NDCG@5 (наш опыт) 0.81 vs 0.64 (pure BM25) ~0.75-0.80 (аналогично)

Elasticsearch выигрывает по комплексной стоимости и простоте, если ES уже в продакшене. Для стартапов без легаси — Pinecone может быть быстрее в запуске.

Типичные ошибки при внедрении ES kNN
  • Использовать слишком маленькое num_candidates (меньше 100) — падает recall.
  • Не настраивать analyzer для русских текстов — BM25 бесполезен.
  • Пытаться скормить embedding размерности 768 в поле с dims=1536 — ES вернёт ошибку.
  • Забыть про rrf — без fusion гибрид не работает как ожидается.

Получите консультацию по вашему проекту — мы уже реализовали 10+ подобных проектов. Свяжитесь с нами, чтобы обсудить детали и получить индивидуальную оценку.

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

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

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