Як впровадити 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 в одному запиті, мінімальний оверхед, не потрібні нові сервери. Ми гарантуємо сумісність з існуючою інфраструктурою та забезпечуємо підтримку сертифікованими фахівцями з 5+ років досвіду.
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-поверхню без зміни інфраструктури.
Кроки:
- Додавання поля embedding (dense_vector, dims=1536) до існуючого мапінгу.
- Батчева векторизація існуючих документів (2 дні, 500K × $0.02/1M = $10).
- Reindexing з новим полем (6 годин).
- Додавання RRF fusion у пошукові запити.
- 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/міс — у 3 рази дешевше, ніж рішення з Pinecone + окремий ES. Перехід від 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-моделі
- Написання скриптів батчевої векторизації та реіндексації
- Реалізація об'єднаного пошуку з RRF fusion
- Інтеграція RAG-пайплайну (з LangChain або прямими викликами OpenAI)
- Тестування: NDCG, Recall, latency, faithfulness
- Документація та навчання команди (2 години воркшопу)
- Ми маємо 5+ років досвіду в пошукових системах і реалізували понад 10 проектів RAG.
Терміни орієнтовно
| Етап | Тривалість |
|---|---|
| Аналіз та проектування | 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 (аналогічно) |
| Вартість інфраструктури | $0 + існуючий ES | $200/міс за сервер |
Elasticsearch виграє за комплексною вартістю та простотою, якщо ES вже в продакшені. Для стартапів без легасі — Pinecone може бути швидшим у запуску. Наш досвід показує: Elasticsearch kNN у 3 рази дешевший за інфраструктуру, ніж Pinecone + окремий ES.
Типові помилки при впровадженні ES kNN
- Використовувати занадто маленьке num_candidates (менше 100) — падає recall.
- Не налаштовувати analyzer для російських текстів — BM25 марний.
- Намагатися подати embedding розмірності 768 в поле з dims=1536 — ES поверне помилку.
- Забути про rrf — без fusion об'єднаний пошук не працює як очікується.
Отримайте консультацію по вашому проекту — ми вже реалізували 10+ подібних проектів. Зв'яжіться з нами, щоб обговорити деталі та отримати індивідуальну оцінку. Гарантія якості та сертифікована підтримка.







