Elasticsearch — поисковый движок и аналитическая база

Elasticsearch — распределённая поисковая и аналитическая система, построенная на Lucene. Она умеет полнотекстовый поиск с релевантностью, агрегации в реальном времени и лежит в основе стека ELK вместе с Kibana и Logstash.

Уровень: средний 18 разделов ⏱ 3–4 часа

1. Что такое Elasticsearch

Elasticsearch — это NoSQL-хранилище документов и полнотекстовый поисковый движок в одном флаконе. Данные хранятся как JSON-документы, а поиск по ним выполняется за десятки миллисекунд даже на миллиардах записей.

Области применения

ЗадачаПример
Поиск по сайтукаталог товаров, статьи, подсказки
Логи и мониторингELK-стек, сбор логов приложений
Аналитикаагрегации по событиям в реальном времени
Поиск в приложенияхфильтры, фасеты, геопоиск

2. Документ и индекс

Базовые единицы Elasticsearch напоминают реляционную модель, но со свободной схемой:

{
  "name": "Ноутбук UltraBook 15",
  "brand": "TechCorp",
  "price": 79990,
  "in_stock": true,
  "tags": ["notebook", "office"]
}
В Elasticsearch 7+ индексу не нужен жёсткий тип документа: документы кладут в индекс просто через 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числа и агрегации
booleantrue / false
dateдаты в формате ISO или timestamp
nestedвложенные массивы объектов
Сменить тип уже проиндексированного поля нельзя — для этого индекс пересоздают (reindex). Продумывай mapping до загрузки данных. Автоматический вывод для чисел часто даёт 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" }
}
Кластер Elasticsearch в базовой настройке слушает два порта: 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 — потеряны первичные шарды, часть данных недоступна. Одноузловой кластер с репликами всегда жёлтый.
Для построения полноценной аналитической платформы изучи отдельный урок ELK — там разбирается связка Elasticsearch + Logstash + Kibana. А Kafka поможет доставлять логи в Elasticsearch без потерь при больших объёмах.

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": "котиков и собак" }
Правильное стеммирование делает поиск «как человек»: поймут и «котами», и «котам». Проверяй результат через _analyze до того, как моросить над индексируемыми полями.

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" } }
  ]
}
При большом объёме задавай лучшие настройки заранее и делай реиндекс во время затишья.

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 PIT
POST /_search
{
  "pit": { "id": "pit-id", "keep_alive": "1m" },
  "size": 20,
  "sort": [{ "price": "asc" }, { "_shard_doc": "asc" }],
  "search_after": [1250, 701]
}
Использовать from + size для глубоких страниц не стоит — тяжело и в итоге хрупко при параллельных записях.

14. Безопасность кластера

Без настройки доступа любой, кто достанет до порта 9200, получит данные:

xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
Версии Elastic Stack идут с бесплатным слоем безопасности (TLS, auth). Включай его при первом запуске, не после инцидента.

15. Bulk-индексация и производительность записи

Поштучная индексация документов через _index медленна. Пакетная запись _bulk в 10–100 раз быстрее:

POST /products/_bulk
{"index": {"_id": "1"}}
{"title": "Ноутбук", "price": 1000}
{"index": {"_id": "2"}}
{"title": "Монитор", "price": 300}
PUT /products/_settings
{ "index": { "refresh_interval": "30s" } }
Перед массовой загрузкой сфотографируй состояние кластера (health, disk watermark), чтобы откатить настройки без потерь.

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" } }
    ]}}
}}}
Число вложенных объектов влияет на размер индекса: оценивай growth, прежде чем делать всё вложенным.

17. Автодополнение: completion suggester

Поиск «по мере ввода» строится над completion-полем:

{
  "mappings": {
    "properties": {
      "title": { "type": "text" },
      "suggest": { "type": "completion" }
    }
  }
}
POST /products/_search?pretty
{ "suggest": {
    "title-suggest": {
      "prefix": "ноут",
      "completion": { "field": "suggest" }
}}}
Для мобильного интерфейса сделай автодополнение по подстроке (fuzzy), а для панелей — по префиксу: первое ошибочнее, но дружелюбнее.

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 — используй Logstash/Filebeat до ES, оставив ingest лёгким.

19. Поиск с опечатками: fuzziness

Пользователи печатают быстро. Подхвати опечатки через нечёткое соответствие:

GET /products/_search
{ "query": { "match": {
    "title": { "query": "кот", "fuzziness": "AUTO" } } } }
GET /products/_search
{ "query": { "match_phrase_prefix": { "title": "смарт" } } }
Не сочетай fuzz со строгими фильтрами: нечёткость уместна в поиске, но не в сортировках и facet'ах.

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
Лишний шард не ускоряет, а режет QPS: проектируй число шардов под объём, а не под «больше = быстрее».

21. Продвинутые агрегации: composite и pipeline

Многоключевые разрезы и математика по результатам прошлых агрегатов:

GET /logs/_search
{ "size": 0, "aggs": {
  "by_stack": { "composite": {
    "sources": [
      { "svc":  {"terms": {"field": "service"}} },
      { "code": {"terms": {"field": "status"}} }
    ] } } } }
Pipeline-агрегаты нельзя фильтровать на том же уровне: считай на верхнем и фильтруй внутри bucket.

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)" } } } }
Перед продом прогони типовые запросы по тестовому индексу: пустые результаты = проверь индексацию и анализатор.

Что дальше

Ты разобрался с Elasticsearch: документами, индексами, инвертированным индексом, mapping, поиском, фильтрами, агрегациями и устройством кластера. Следующим шагом изучи полноценный стек ELK с Kibana и Logstash, а для сравнения с классическими хранилищами — ClickHouse, который тоже силён в аналитике, но по-другому. Основы реляционного поиска — в курсе SQL.

← Вернуться к курсам