Як впровадити 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+ подібних проектів. Зв'яжіться з нами, щоб обговорити деталі та отримати індивідуальну оцінку. Гарантія якості та сертифікована підтримка.







