Elasticsearch — поисковый движок и аналитическая база
Elasticsearch — распределённая поисковая и аналитическая система, построенная на Lucene. Она умеет полнотекстовый поиск с релевантностью, агрегации в реальном времени и лежит в основе стека ELK вместе с Kibana и Logstash.
1. Что такое Elasticsearch
Elasticsearch — это NoSQL-хранилище документов и полнотекстовый поисковый движок в одном флаконе. Данные хранятся как JSON-документы, а поиск по ним выполняется за десятки миллисекунд даже на миллиардах записей.
- Распределённость — данные автоматически раскладываются по узлам кластера.
- Полнотекстовый поиск — учитывает морфологию, синонимы, опечатки (fuzzy).
- Релевантность — результаты ранжируются по релевантности, а не по времени вставки.
- REST API — весь сервер управляется простыми HTTP-запросами.
Области применения
| Задача | Пример |
|---|---|
| Поиск по сайту | каталог товаров, статьи, подсказки |
| Логи и мониторинг | ELK-стек, сбор логов приложений |
| Аналитика | агрегации по событиям в реальном времени |
| Поиск в приложениях | фильтры, фасеты, геопоиск |
2. Документ и индекс
Базовые единицы Elasticsearch напоминают реляционную модель, но со свободной схемой:
- Документ — JSON-объект, аналог строки таблицы.
- Индекс — коллекция документов, аналог таблицы.
- Поле — ключ в документе, аналог колонки.
- Shard / Replica — физические осколки индекса и их копии.
{
"name": "Ноутбук UltraBook 15",
"brand": "TechCorp",
"price": 79990,
"in_stock": true,
"tags": ["notebook", "office"]
}POST /index/_doc. Тип _doc остался как техническая отметка, но делить индекс на несколько типов больше нельзя.3. Инвертированный индекс
Секрет скорости Elasticsearch — инвертированный индекс: словарь терминов, где каждому слову соответствует список документов, где оно встречается.
Для трёх документов «синий чайник», «красный чайник», «синий чай» этот словарь выглядит так:
| Термин | Документы |
|---|---|
| синий | док 1, док 3 |
| чайник | док 1, док 2 |
| красный | док 2 |
| чай | док 3 |
При поиске «синий чайник» Elasticsearch берёт из словаря списки для синий и чайник и пересекает их. Сложность зависит от числа слов, а не от размера хранилища.
Чайник -> чайник), стемминг («чайники» -> «чайник») и стоп-слова. Именно поэтому поиск справляется с формами слова, а сравнение строк в SQL — нет.4. Mapping и типы полей
Mapping — описание того, как поля документа анализируются и хранятся. Elasticsearch умеет выводить его автоматически (dynamic mapping), но для контроля лучше задавать руками:
PUT /products
{
"mappings": {
"properties": {
"name": { "type": "text" },
"brand": { "type": "keyword" },
"price": { "type": "double" },
"in_stock": { "type": "boolean" },
"tags": { "type": "keyword" },
"created": { "type": "date" }
}
}
}| Тип | Назначение |
|---|---|
| text | полнотекстовый поиск, анализируется |
| keyword | точные значения: фильтры, сортировка, фасеты |
| double / long | числа и агрегации |
| boolean | true / false |
| date | даты в формате ISO или timestamp |
| nested | вложенные массивы объектов |
long там, где нужна double.5. Запуск кластера и REST API
Проще всего поднять Elasticsearch в Docker. Обрати внимание: в версии 8 включена безопасность по умолчанию, поэтому передай пароль:
docker run -d --name es -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e "ELASTIC_PASSWORD=yourpassword" \
docker.elastic.co/elasticsearch/elasticsearch:8.13.0Проверь, что сервер отвечает (порт 9200):
curl -u elastic:yourpassword http://localhost:9200{
"name": "...",
"cluster_name": "docker-cluster",
"version": { "number": "8.13.0" }
}9200 — HTTP API для клиентов, 9300 — транспортный протокол для общения узлов между собой.6. Добавление и изменение документов
Добавить документ можно через POST (ид автоматически) или PUT (ид задаёшь сам):
POST /products/_doc
{
"name": "Ноутбук UltraBook 15",
"brand": "TechCorp",
"price": 79990,
"in_stock": true
}PUT /products/_doc/product-1
{
"name": "Мышь беспроводная",
"brand": "TechCorp",
"price": 1990,
"in_stock": false
}Частичное обновление полей — через _update:
POST /products/_update/product-1
{
"doc": { "price": 1500 }
}Чтение документа по id и удаление:
GET /products/_doc/product-1
DELETE /products/_doc/product-1_update применяет только перечисленные поля и не трогает остальные, а PUT с id полностью перезаписывает документ. Для правки одного поля всегда используй _update.7. Поисковые запросы: match и term
Полнотекстовый поиск выполняется через match — он анализирует запрос теми же правилами, что и документ:
GET /products/_search
{
"query": {
"match": { "name": "беспроводная мышь" }
}
}Точное совпадение по ключевому полю — через term. Он не анализирует запрос и подходит для фильтров по keyword, числам, булевым и датам:
GET /products/_search
{
"query": {
"term": { "brand": "TechCorp" }
}
}term по text-полю не найдёт ничего, ведь текст уже разбит на анализированные термы. Точные значения ищут по keyword-полям, а текстовый поиск делают через match.Для результата важна оценка релевантности — поле _score в ответе:
GET /products/_search
{
"query": { "match": { "name": "ноутбук" } },
"sort": [ { "_score": "desc" } ]
}8. Фильтры и булев поиск
Комбинацию условий собирают в запросе bool: секция must (обязательно, влияет на релевантность), filter (обязательно, без влияния на релевантность и кэшируется), should (желательно) и must_not (исключить):
GET /products/_search
{
"query": {
"bool": {
"must": [ { "match": { "name": "ноутбук" } } ],
"filter": [
{ "term": { "brand": "TechCorp" } },
{ "range": { "price": { "gte": 50000, "lte": 200000 } } }
],
"must_not": [ { "term": { "in_stock": false } } ]
}
}
}Однострочные фильтры удобно выносить в секцию filter внутри bool — они кэшируются и не замедляют поиск:
GET /products/_search
{
"query": { "bool": { "filter": { "term": { "brand": "TechCorp" } } } }
}filter — это быстрые условия да/нет: диапазоны цен, категории, даты. Они попадают в фильтр-кэш и исполняются быстрее, чем must, потому что не вычисляют релевантность.9. Агрегации
Агрегации считают статистику по документам — это полноценная аналитика без отдельного хранилища. Простейшая — группировка по полю:
GET /products/_search
{
"size": 0,
"aggs": {
"by_brand": {
"terms": { "field": "brand" }
}
}
}Минимумы, максимумы и средние:
GET /products/_search
{
"size": 0,
"aggs": {
"price_stats": {
"stats": { "field": "price" }
}
}
}Агрегации можно комбинировать с поиском: сначала выполняется query, затем агрегации считаются уже по найденным документам.
"size": 0 отключает возврат самих документов — нужны только агрегированные значения. Это стандартный приём для дашбордов и отчётов, чтобы не тратить трафик на лишние данные.10. Кластер, узлы и шарды. Путь к стеку ELK
Установка одного сервера — уже кластер, просто из одного узла. Продуктовый кластер состоит из нескольких узлов, а каждый индекс делится на первичные шарды (shards) и их реплики по умолчанию:
| Роль узла | Задача |
|---|---|
| Master-eligible | управляет метаданными кластера и назначает шарды |
| Data node | хранит шарды и выполняет поиск и агрегации |
| Ingest node | предобрабатывает документы перед индексацией |
| Ingest pipeline | конвейеры обработки, например обогащение логов |
Вместе с Эластиком обычно используют Kibana — веб-интерфейс для визуализации и дашбордов, и Logstash (или Beats) — для сбора и подачи данных. Такой набор образует стек ELK.
GET /_cluster/health{
"status": "green",
"number_of_nodes": 1,
"active_primary_shards": 10,
"active_shards": 20,
"unassigned_shards": 0
}green — все шарды и реплики на месте, yellow — нет реплик (например, один узел), red — потеряны первичные шарды, часть данных недоступна. Одноузловой кластер с репликами всегда жёлтый.11. Анализаторы и русская морфология
Анализатор превращает текст в токены: «котов» ищется по «кот», «котик» — по «котик» и т. д. Для русского языка подключается стеммер:
PUT /products
{
"settings": {
"analysis": {
"analyzer": {
"ru_analyzer": {
"type": "standard",
"stopwords": "_russian_",
"filter": ["lowercase", "russian_stemmer"]
}
},
"filter": {
"russian_stemmer": {
"type": "stemmer",
"language": "russian"
}
}
}
},
"mappings": {
"properties": {
"title": { "type": "text", "analyzer": "ru_analyzer" }
}
}
}POST /products/_analyze
{ "analyzer": "ru_analyzer", "text": "котиков и собак" }12. Reindex и алиасы
Mapping менять нельзя, но можно пересоздать индекс и перелить данные — reindex:
POST /_reindex
{
"source": { "index": "products" },
"dest": { "index": "products_v2" }
}Чтобы приложение не знало о «версиях», используют алиас:
POST /_aliases
{
"actions": [
{ "add": { "index": "products_v2", "alias": "products" } },
{ "remove": { "index": "products_v1", "alias": "products" } }
]
}- применение индексов через алиас — бесшовное;
- reindex можно фильтровать по query;
- для больших индексов — реиндекс по slices параллельно.
13. Глубокая выдача: search_after и PIT
По умолчанию Elasticsearch отдаёт максимум 10000 результатов. Для глубоких страниц («показать ещё») нужен search_after:
POST /products/_search
{
"size": 20,
"sort": [{ "price": "asc" }, { "_id": "asc" }],
"search_after": [1250, "product:id"] // значениe последней записи
}С Point in Time можно стабильно листать по меняющемуся индексу:
POST /products/_pit?keep_alive=1m
// возвращает id PITPOST /_search
{
"pit": { "id": "pit-id", "keep_alive": "1m" },
"size": 20,
"sort": [{ "price": "asc" }, { "_shard_doc": "asc" }],
"search_after": [1250, 701]
}from + size для глубоких страниц не стоит — тяжело и в итоге хрупко при параллельных записях.14. Безопасность кластера
Без настройки доступа любой, кто достанет до порта 9200, получит данные:
- закрой порты фаерволом (9200, 9300) и используй только внутренние сети;
- включи TLS для транспорта и веб-API;
- заведи пользователей с минимальными ролями (read_only и т. п.);
- шифруй диски с данными.
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true15. Bulk-индексация и производительность записи
Поштучная индексация документов через _index медленна. Пакетная запись _bulk в 10–100 раз быстрее:
POST /products/_bulk
{"index": {"_id": "1"}}
{"title": "Ноутбук", "price": 1000}
{"index": {"_id": "2"}}
{"title": "Монитор", "price": 300}- размер пакета 5–15 МБ — оптимум; следи за откликами и ретраями;
- на время первичной загрузки повысь
refresh_intervalдо 30s или отключи refresh; - число реплик при загрузке можно временно снизить, затем поднять;
- транслог растёт на скорости: следи за
translogи настройками durability.
PUT /products/_settings
{ "index": { "refresh_interval": "30s" } }16. Вложенные объекты: nested-запросы
В обычном объекте поле «массив объектов» теряет связь внутри каждого элемента: запрос «покупатель A + купил товар B» найдёт ответ, где A купил одно, а B купил другой. Исправляет это nested:
{
"mappings": {
"properties": {
"orders": { "type": "nested" }
}
}
}POST /customers/_search
{ "query": { "nested": {
"path": "orders",
"query": { "bool": { "must": [
{ "match": { "orders.customer": "A" } },
{ "match": { "orders.product": "B" } }
]}}
}}}- nested считается отдельно от родительских документов — поиск по ним дороже, но корректен;
- при агрегации и сортировке по вложенным тоже используй nested-контекст;
- глубокую вложенность избегай: по два-три уровня для 99% задач хватает.
17. Автодополнение: completion suggester
Поиск «по мере ввода» строится над completion-полем:
{
"mappings": {
"properties": {
"title": { "type": "text" },
"suggest": { "type": "completion" }
}
}
}POST /products/_search?pretty
{ "suggest": {
"title-suggest": {
"prefix": "ноут",
"completion": { "field": "suggest" }
}}}- completion требует данных в поле: индексируй «предложения» вместе с документом;
- вариант поля «search_as_you_type» — обычный text-автодополнение при вводе;
- объедини с фильтрами: category в контексте (
contexts) даёт ранжируемые подсказки.
18. Ingest pipelines: обогащение на входе
Пайплайны преобразуют документ прямо при индексации — разбор, замены, добавление полей из справочника:
PUT _ingest/pipeline/logs-enrich
{
"processors": [
{ "grok": { "field": "message",
"patterns": ["%{IP:client_ip} %{WORD:method}"] } },
{ "set": { "field": "env", "value": "prod" } },
{ "enrich": { "policy_name": "geo-policy",
"field": "client_ip", "target_field": "geo" } }
]
}PUT /logs/_doc/1?pipeline=logs-enrich
{ "message": "192.168.0.1 GET /api" }- enrich policy с источником-индексом подтягивает справочные данные (гео, долги, роли);
grok/dissectразбирают текст;rename/removeчистят;- ошибка в процессоре роняет документ — настраивай
on_failureиpipeline_failures.
19. Поиск с опечатками: fuzziness
Пользователи печатают быстро. Подхвати опечатки через нечёткое соответствие:
GET /products/_search
{ "query": { "match": {
"title": { "query": "кот", "fuzziness": "AUTO" } } } }fuzziness: AUTO— допустимое редактирование по длине слова;match_phrase_prefix— подсказки по мере ввода;- синонимы в анализаторе: «телефон» = «мобильный».
GET /products/_search
{ "query": { "match_phrase_prefix": { "title": "смарт" } } }20. Сколько шардов и hot/warm/cold
Неверный выбор числа шардов — самая частая причина «ES стал медленнее». Опорные числа:
| Параметр | Рекомендация |
|---|---|
| размер шарда | 10–50 ГБ |
| общее число шардов | ≤ 1000 на кластер |
| свободная память JVM | ≥ 25% heap |
# роли узлов: сортировка по ценности данных
node.roles: [data_hot] # свежие и быстрые SSD
node.roles: [data_warm] # тёплые, дешёвые диски
node.roles: [data_cold] # почти offline- hot-warm-cold снижает стоимость хранения при том же поиске по свежему;
- переобъём через reindex, а не путём лишних шардов на старте;
- каждый шард — это накладные расходы на каждое обращение.
21. Продвинутые агрегации: composite и pipeline
Многоключевые разрезы и математика по результатам прошлых агрегатов:
GET /logs/_search
{ "size": 0, "aggs": {
"by_stack": { "composite": {
"sources": [
{ "svc": {"terms": {"field": "service"}} },
{ "code": {"terms": {"field": "status"}} }
] } } } }compositeдаёт пагинацию по всем разрезам через after_key;- pipeline-агрегаты:
bucket_script— «ошибки / запросы» как метрика; date_histogramс calendar_interval учитывает календарь (дни, недели).
22. Кейс: поиск в интернет-магазине
Релевантность — это запрос + сортировка + фасеты. Базовый рецепт:
GET /products/_search
{ "query": { "function_score": {
"query": { "bool": {
"must": [{ "match": { "title": "диван" } }],
"filter": [{ "term": { "in_stock": true } }] } },
"script_score": { "script": "_score * (doc['rating'].value + 0.1)" } } } }- фильтры (категория, цена) — в
filter, они не влияют на score; - фасеты — терм-агрегации по category/brand;
- сортировка: релевантность по умолчанию, цена/новизна — по клику пользователя.
Что дальше
Ты разобрался с Elasticsearch: документами, индексами, инвертированным индексом, mapping, поиском, фильтрами, агрегациями и устройством кластера. Следующим шагом изучи полноценный стек ELK с Kibana и Logstash, а для сравнения с классическими хранилищами — ClickHouse, который тоже силён в аналитике, но по-другому. Основы реляционного поиска — в курсе SQL.