Разработка RAG с векторной базой данных Elasticsearch (kNN)
Elasticsearch с версии 8.x поддерживает нативный k-Nearest Neighbors поиск по dense векторам (dense_vector field). Для команд, уже использующих Elasticsearch как поисковый движок, это наиболее естественный путь к RAG — без добавления новой инфраструктуры. Нативная интеграция полнотекстового BM25 и векторного поиска делает ES сильным выбором для hybrid retrieval.
Создание индекса с 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",
# HNSW параметры
"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]
# Батч embedding
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 в одном запросе:
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}})
# Hybrid: kNN + BM25 через RRF
body = {
"query": {
"bool": {
"must": {
"match": {
"content": {
"query": query,
"analyzer": "russian"
}
}
},
"filter": filter_clause,
}
},
"knn": {
"field": "embedding",
"query_vector": query_embedding,
"k": top_k * 3, # Расширенный набор для fusion
"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: ["договор", "аренд"] — стеммированные формы
Практический кейс: миграция существующего Elasticsearch на RAG
Контекст: компания использует ES 8.x как поисковик по 500K документов. Задача: добавить 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
Переход от pure BM25 к hybrid kNN+BM25 дал +27% к NDCG без смены инфраструктуры.
Сроки
- Добавление vector field + reindexing: 2–5 дней
- Разработка hybrid search запросов: 3–5 дней
- RAG-пайплайн и оценка: 1–2 недели
- Итого: 2–4 недели







