ElasticSearch. Начало работы. Часть 1 из 3
Elasticsearch — это мощный инструмент с открытым исходным кодом, который представляет собой поисковую и аналитическую систему. Основное назначение — обеспечение быстрого и эффективного поиска по большим объемам данных. Elasticsearch построен на движке Apache Lucene, который считается одним из самых лучших решений в области индексации и поиска данных.
Установка Elasticsearch
Обновим списки пакетов и установим apt-transport-http для доступа к репозиториям через HTTPS, а также gnupg для работы с ключами шифрования.
$ sudo apt update $ sudo apt install apt-transport-https gnupg -y
Чтобы система доверяла пакетам из репозитория Elastic, нужно импортировать их публичный GPG-ключ.
$ wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /usr/share/keyrings/elastic-key.gpg
Теперь добавим сам репозиторий Elastic в список источников пакетов для установки и обновления.
$ echo "deb [signed-by=/usr/share/keyrings/elastic-key.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" | \ > sudo tee /etc/apt/sources.list.d/elastic-8.x.list
Снова обновим списки пакетов, чтобы система «увидела» новый репозиторий, и установим Elasticsearch.
$ sudo apt update $ sudo apt install elasticsearch -y
Файлы конфигурации
Файл конфигурации elasticsearch.service
Мы настроим Elasticsearch для локальной разработки, у нас будет только один узел, доступ возможен только с localhost.
$ sudo nano /etc/elasticsearch/elasticsearch.yml
cluster.name: catalog-search node.name: node-1 path.data: /var/lib/elasticsearch path.logs: /var/log/elasticsearch discovery.type: single-node #discovery.seed_hosts: ["192.168.100.4", "192.168.100.5"] #cluster.initial_master_nodes: ["node-1", "node-2"] network.host: localhost http.host: localhost http.port: 9200 xpack.security.enabled: false xpack.security.enrollment.enabled: false
Перезагрузим конфигурацию системных служб (на всякий случай), запустим elasticsearch.service и добавим в автозагрузку.
$ sudo systemctl daemon-reload # на всякий случай $ sudo systemctl enable elasticsearch.service $ sudo systemctl start elasticsearch.service
Elasticsearch требуется некоторое время для запуска (иногда до минуты), проверить статус службы можно командой
$ systemctl status elasticsearch.service
Директива cluster.name задает имя кластера. Все узлы, которые должны работать вместе, обязаны иметь одинаковое имя кластера. Эта настройка не позволяет узлам из разных окружений (например, dev и prod) случайно объединиться.
Директива node.name задает уникальное имя для каждого конкретного узла в кластере. Очень полезно для идентификации узла в логах и при мониторинге.
Директива path.data задает путь к директории, где Elasticsearch хранит все данные индексов. Это самая важная директория, ее необходимо регулярно бэкапить. Перед бэкапом нужно обязательно остановить службу Elasticsearch.
Директива path.logs задает путь к лог-файлам. Первое место, куда нужно смотреть при возникновении любых проблем.
Директива discovery.type указывает, как этот узел ищет другие узлы кластера. Значение single-node — режим для разработки или одиночного сервера, узел не будет искать другие. В этом случае отпадает необходимость в использовании директив discovery.seed_hosts и cluster.initial_master_nodes (cм. ниже). Если директва не установлена — используется стандартный механизм поиска других узлов.
Директива discovery.seed_hosts задает список ip-адресов (и портов) других узлов, к которым нужно попытаться подключиться для формирования кластера.
discovery.seed_hosts: ["10.0.0.1", "10.0.0.2:9300"]
Директива cluster.initial_master_nodes задает список имен (node.name) узлов, которые могут быть выбраны «мастером» при самом первом запуске кластера. Критически важная настройка для стабильности многоузлового кластера.
Директива network.host задает сетевой интерфейс, который Elasticsearch будет прослушивать. Значение localhost (или если закомментировано) — доступ только с этой же машины, значение 0.0.0.0 — доступ со всех сетевых интерфейсов (требует настройки файрвола).
Директива http.host задает сетевой интерфейс для HTTP-запросов к API. Директива network.host задает сетевой интерфейс для всех видов сетевого взаимодействия — для общения с другими узлами (порт 9300) + для HTTP-запросов к API кластера (порт 9200). Значение http.host переопределяет значение network.host только для HTTP API.
# узел может общаться с другими узлами в сети 192.168.100.0/24 network.host: 192.168.100.10 # HTTP-запросы к API кластера возможны только через localhost http.host: localhost
Директива http.port задает порт для HTTP-запросов к API, по умолчанию 9200.
Директива xpack.security.enabled включает встроенную систему безопасности, которая требует аутентификации (логин/пароль или токены) и позволяет настраивать права доступа (авторизацию) для всех запросов к кластеру.
Директива xpack.security.http.ssl.enabled включает шифрование (HTTPS) для HTTP-трафика, идущего к API Elasticsearch. Чтобы пароли, данные запросов и ответов не передавались по сети в открытом виде. Критически важно, если Elasticsearch доступен не только с localhost.
Директива bootstrap.memory_lock запрещает операционной системе «выгружать» память, выделенную для Elasticsearch (Java Heap), в файл подкачки (swap) на диске. Если ОС начинает «свопить» память Elasticsearch, производительность падает катастрофически, и узел становится практически неработоспособным. Включение директивы требует дополнительной конфигурации на уровне операционной системы (через systemd или ulimit).
$ sudo systemctl edit elasticsearch.service
[Service] LimitMEMLOCK=infinity
$ sudo systemctl daemon-reload $ sudo systemctl restart elasticsearch.service
$ curl -X GET "localhost:9200/_nodes?pretty&filter_path=**.process.mlockall"
{ "nodes": { "1vVIuiMnQhqHlUw8WPEqZg": { "process": { "mlockall": true } } } }
Директива http.cors.enabled разрешает Cross-Origin Resource Sharing (CORS). Это механизм браузера, который по умолчанию блокирует AJAX-запросы с сайта на другой домен. Эта директива потребуется, если нужно обращаться к Elasticsearch напрямую из JavaScript-кода на сайте.
Директива http.cors.allow-origin указывает, с каких именно доменов разрешено принимать запросы. Использование * (разрешить с любого домена) — крайне небезопасно.
Директива indices.breaker.total.limit устанавливает общий лимит памяти, который могут занять все операции (агрегации, запросы и т.д.). Если этот лимит превышается, самый «тяжелый» запрос будет прерван с ошибкой. Это защитный механизм, который предотвращает падение всего узла из-за одного слишком «жадного» запроса, который мог бы вызвать ошибку OutOfMemoryError. Эту директиву обычно не нужно изменять, значение по умолчанию 70% вполне разумное.
Файл конфигурации Java-машины (JVM)
Управляет настройками Java-машины (JVM), на которой работает Elasticsearch. Эти настройки влияют на производительность и стабильность.
$ sudo nano /etc/elasticsearch/jvm.options
# Минимальный и максимальный размер кучи (Java Heap) 1 Гбайт
-Xms1g
-Xmx1g
Настройка -Xms устанавливает начальный размер оперативной памяти, -Xmx устанавливает максимальный размер. Рекомендуется всегда устанавливать одинаковое значение для этих настроек. Это предотвращает паузы в работе, связанные с изменением размера кучи во время работы.
Слишком маленький размер кучи приведет к частой «сборке мусора» и ошибкам нехватки памяти. Слишком большой — отнимет память у операционной системы, которая нужна ей для кэширования файлов индекса, что парадоксально замедлит поиск.
Не рекомендуется выделять больше 50% от общей оперативной памяти, вторая половина нужна ОС для файлового кэша. При значениях выше ~30-32 ГБ — Java теряет преимущество от сжатия указателей, и эффективность использования памяти падает.
Есть еще несколько директив, на которые нужно обратить внимание
# Куда JVM должна сбрасывать дампы кучи (heap dumps) в случае исчерпания памяти (OutOfMemoryError) # или принудительного создания дампа. дам кучи позволяет диагностировать причины OutOfMemoryError # и обнаруживать утечки памяти (Memory Leaks). -XX:HeapDumpPath=/var/lib/elasticsearch # Путь и шаблон имени файла для фатальных логов JVM (HotSpot Error Logs). Это специальный лог-файл, # который JVM создает, когда происходит критическая, невосстановимая ошибка. Эти ошибки обычно # вызваны багами в JVM, проблемами с железом или конфликтами с нативными библиотеками. -XX:ErrorFile=/var/log/elasticsearch/hs_err_pid%p.log # Настройки логирования — файл /var/log/elasticsearch/gc.log, хранить всего 32 файла логов (включая # текущий), выполнять ротацию, когда текщий файл достиг размера 64 Мбайт. -Xlog:gc*,gc+age=trace,safepoint:file=/var/log/elasticsearch/gc.log:utctime,level,pid,tags:filecount=32,filesize=64m
Файл конфигурации ротации логов
ElasticSearch использует мощную библиотеку логирования для Java под названием Log4j2. В отличие от простых демонов, которые пишут в один файл до его ротации logrotate.service, Log4j2 имеет собственные, очень гибкие правила ротации (rollover) по времени, размеру и другим условиям.
Elasticsearch разделяет логи по их назначению, чтобы упростить анализ и отладку.
- Файл
xxxxx_server.json— содержит информацию о запуске, остановке, состоянии кластера, ошибках в формате JSON (который легко парсить машинным способом). Первая часть имени файлаxxxxx— это имя кластера, которое задается в файле конфигурацииelasticsearch.yml. - Файл
xxxxx.log— содержит информацию о запуске, остановке, состоянии кластера, ошибках в традиционном формате plain text. Удобен для быстрого просмотра человеком, но его сложно автоматически анализировать (парсить) машинным способом. - Файлы
xxxxx-YYYY-MM-DD-N.json.gz— старые, ротированные и сжатые архиватором gzip лог-файлы в формате JSON. Elasticsearch сам их создает, когда текущий лог достигает определенного размера или наступает новый день (в зависимости от настроек). - Файлы
xxxxx-YYYY-MM-DD-N.log.gz— старые, ротированные и сжатые архиватором gzip лог-файлы в традиционном формате plain text. - Файл
xxxxx_deprecation.json— лог устаревших функций. Elasticsearch записывает сюда предупреждения, если используются запросы или настройки, которые будут удалены в будущих версиях. - Файлы
xxxxx_index_search_slowlog.jsonиxxxxx_index_indexing_slowlog.json— логи медленных запросов. Сюда записываются запросы на поиск или индексацию, которые выполнялись дольше заданного порога. Это ключевой инструмент для оптимизации производительности. - Файл
xxxxx_audit.json— если включены функции безопасности, сюда записывается информация о попытках доступа, успешных и неуспешных логинах, выполненных действиях и т.д. - Файл
xxxxx_esql_querylog.json— лог запросов, выполненных с использованием ESQL (Elasticsearch Query Language). Это относительно новый и мощный язык запросов в Elasticsearch, который очень похож на SQL. Он позволяет писать запросы в более привычном и читаемом виде. - Файл
gc.log— лог сборщика мусора (Garbage Collector) Java Virtual Machine (JVM). Он не относится напрямую к логике Elasticsearch, а показывает, как JVM управляет памятью. Это отдельная подсистема логирования, которая настраивается в файлеjvm.options.
Основные директивы конфигурации в файле log4j2.properties
# Политика ротации — когда создавать новый файл лога # 1. Ротация по времени — например, раз в день appender.rolling.policy.type = TimeBasedTriggeringPolicy appender.rolling.policy.interval = 1 # 2. Ротация по размеру — например, 128 Мбайт appender.rolling.policy.type = SizeBasedTriggeringPolicy appender.rolling.policy.size = 128MB
# Стратегия ротации — что делать со старыми файлами # переименование, сжатие и удаление старых appender.rolling.strategy.type = DefaultRolloverStrategy # сколько старых файлов нужно хранить appender.rolling.strategy.max = 7
Открываем на редактирование файл конфигурации Log4j2
$ sudo nano /etc/elasticsearch/log4j2.properties
######## Server JSON ############################ appender.rolling.type = RollingFile appender.rolling.name = rolling appender.rolling.fileName = ${sys:es.logs.base_path}/${sys:es.logs.cluster_name}_server.json appender.rolling.layout.type = ECSJsonLayout appender.rolling.layout.dataset = elasticsearch.server appender.rolling.filePattern = ${sys:es.logs.base_path}/${sys:es.logs.cluster_name}-%d{yyyy-MM-dd}-%i.json.gz # Ротация происходит, если выполняется одно из условий — наступил # новый день (время 00:00:00) или лог достиг размера 128 Мбайт appender.rolling.policies.type = Policies appender.rolling.policies.time.type = TimeBasedTriggeringPolicy appender.rolling.policies.time.interval = 1 appender.rolling.policies.time.modulate = true appender.rolling.policies.size.type = SizeBasedTriggeringPolicy appender.rolling.policies.size.size = 128MB # Стратегия — переименование, сжатие и удаление старых appender.rolling.strategy.type = DefaultRolloverStrategy # Хранить не более 7 старых файлов логов appender.rolling.strategy.max = 7 ######## Server - old style pattern ########### appender.rolling_old.type = RollingFile appender.rolling_old.name = rolling_old appender.rolling_old.fileName = ${sys:es.logs.base_path}/${sys:es.logs.cluster_name}.log appender.rolling_old.layout.type = PatternLayout appender.rolling_old.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] [%node_name]%marker %m%n appender.rolling_old.filePattern = ${sys:es.logs.base_path}/${sys:es.logs.cluster_name}-%d{yyyy-MM-dd}-%i.log.gz # Ротация происходит, если выполняется одно из условий — наступил # новый день (время 00:00:00) или лог достиг размера 128 Мбайт appender.rolling_old.policies.type = Policies appender.rolling_old.policies.time.type = TimeBasedTriggeringPolicy appender.rolling_old.policies.time.interval = 1 appender.rolling_old.policies.time.modulate = true appender.rolling_old.policies.size.type = SizeBasedTriggeringPolicy appender.rolling_old.policies.size.size = 128MB # Стратегия — переименование, сжатие и удаление старых appender.rolling_old.strategy.type = DefaultRolloverStrategy # Хранить не более 7 старых файлов логов appender.rolling_old.strategy.max = 7
######## Server JSON ############################ appender.rolling.type = RollingFile appender.rolling.name = rolling appender.rolling.fileName = ${sys:es.logs.base_path}/${sys:es.logs.cluster_name}_server.json appender.rolling.layout.type = ECSJsonLayout appender.rolling.layout.dataset = elasticsearch.server appender.rolling.filePattern = ${sys:es.logs.base_path}/${sys:es.logs.cluster_name}-%d{yyyy-MM-dd}-%i.json.gz # Ротация происходит, если выполняется одно из условий — наступил # новый день (время 00:00:00) или лог достиг размера 128 Мбайт appender.rolling.policies.type = Policies appender.rolling.policies.time.type = TimeBasedTriggeringPolicy appender.rolling.policies.time.interval = 1 appender.rolling.policies.time.modulate = true appender.rolling.policies.size.type = SizeBasedTriggeringPolicy appender.rolling.policies.size.size = 128MB # Стратегия — переименование, сжатие и удаление старых appender.rolling.strategy.type = DefaultRolloverStrategy # Действие после ротации лога — удаление appender.rolling.strategy.action.type = Delete # Базовый путь, где искать файлы для удаления appender.rolling.strategy.action.basepath = ${sys:es.logs.base_path} # Условие 1: файл должен соответствовать шаблону appender.rolling.strategy.action.condition.type = IfFileName appender.rolling.strategy.action.condition.glob = ${sys:es.logs.cluster_name}-*.json.gz # Условие 2: файл должен быть старше 7 дней appender.rolling.strategy.action.condition.nested_condition.type = IfLastModified appender.rolling.strategy.action.condition.nested_condition.age = 7d ######## Server - old style pattern ########### appender.rolling_old.type = RollingFile appender.rolling_old.name = rolling_old appender.rolling_old.fileName = ${sys:es.logs.base_path}/${sys:es.logs.cluster_name}.log appender.rolling_old.layout.type = PatternLayout appender.rolling_old.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] [%node_name]%marker %m%n appender.rolling_old.filePattern = ${sys:es.logs.base_path}/${sys:es.logs.cluster_name}-%d{yyyy-MM-dd}-%i.log.gz # Ротация происходит, если выполняется одно из условий — наступил # новый день (время 00:00:00) или лог достиг размера 128 Мбайт appender.rolling_old.policies.type = Policies appender.rolling_old.policies.time.type = TimeBasedTriggeringPolicy appender.rolling_old.policies.time.interval = 1 appender.rolling_old.policies.time.modulate = true appender.rolling_old.policies.size.type = SizeBasedTriggeringPolicy appender.rolling_old.policies.size.size = 128MB # Стратегия — переименование, сжатие и удаление старых appender.rolling_old.strategy.type = DefaultRolloverStrategy # Действие после ротации лога — удаление appender.rolling_old.strategy.action.type = Delete # Базовый путь, где искать файлы для удаления appender.rolling_old.strategy.action.basepath = ${sys:es.logs.base_path} # Условие 1: файл должен соответствовать шаблону appender.rolling_old.strategy.action.condition.type = IfFileName appender.rolling_old.strategy.action.condition.glob = ${sys:es.logs.cluster_name}-*.log.gz # Условие 2: файл должен быть старше 7 дней appender.rolling_old.strategy.action.condition.nested_condition.type = IfLastModified appender.rolling_old.strategy.action.condition.nested_condition.age = 7d
Здесь два варианта ротации и удаления старых логов — простой (хранить 7 старых файлов) и сложный (хранить старые логи 7 дней).
Первый вариант предусматривает использование appender.rolling.strategy.max — мы даем Log4j2 команду «Используй встроенную логику для очистки по количеству». Внутри DefaultRolloverStrategy уже «зашита» логика, которая знает, что делать, когда используется max.
Второй вариант предусматривает использование appender.rolling.strategy.action — что делать со старым файлом (Delete), как найти этот файл (путь для поиска + шаблон имени), каким еще условиям должен соответствовать файл, чтобы применить к нему действие.
Внутреннее устройство ElasticSearch
Кластер
Кластер — это набор узлов, которые взаимодействуют между собой, создавая целостную и распределенную среду для хранения и обработки данных. Каждый кластер идентифицируется уникальным именем, что позволяет узлам соединяться и взаимодействовать. Узлы внутри кластера работают вместе, обеспечивая единое и согласованное представление данных. Они взаимодействуют друг с другом для равномерного распределения и репликации данных на нескольких узлах. Это позволяет повысить доступность данных, отказоустойчивость и общую производительность системы.
Индекс
Индекс представляет собой логическое пространство имен, или контейнер, в котором хранится коллекция документов. Индекс можно рассматривать как традиционную базу данных, где данные организованы и хранятся в структурированном виде. Задача индекса — сгруппировать документы, которые имеют схожие характеристики или принадлежат к одной категории. Например, в приложении онлайн-магазина можно задать отдельные индексы для товаров, клиентов и заказов.
Elasticsearch использует структуру инвертированного индекса для быстрого полнотекстового поиска. Инвертированный индекс состоит из списка уникальных терминов, встречающихся во всех документах в индексе, а также идентификаторов документов, указывающих на вхождения каждого термина. Подобная техника индексирования значительно ускоряет операции поиска за счет предварительного вычисления связей между терминами и документами.
Для обработки больших объемов данных и распределения рабочей нагрузки индекс делится на один или несколько шардов. Каждый шард — это независимое подмножество данных индекса, которое может храниться на отдельном узле в кластере. Разбивая индекс на шарды, Elasticsearch может распараллелить операции поиска и индексирования, повышая производительность и масштабируемость.
Шарды
Индекс состоит из одного или нескольких шардов, и каждый шард является самостоятельной единицей индекса. Разбивая индекс на более мелкие шарды, Elasticsearch распределяет данные и операции между несколькими узлами, что повышает производительность и масштабируемость системы. Шарды позволяют Elasticsearch распараллеливать операции поиска и индексирования. Выдаваемый запрос на поиск параллельно передается во все шарды, а результаты поиска объединяются и возвращаются пользователю.
Документы
Документ — это основная единица информации, которую можно индексировать и искать. Он представляет собой один объект данных, обычно в формате JSON, и содержит фактические данные, которые хранятся и извлекаются. Документы в Elasticsearch организованы в индексе и однозначно идентифицируются по идентификатору. Каждый документ состоит из полей, которые представляют собой пары «ключ-значение», отражающие атрибуты или свойства данных. Типы данных полей могут быть различными, включая строки, числа, булевы выражения, даты и др.
Роли узлов ElasticSearch
Главный узел (Master Node) — «мозг» кластера. Не хранит данные, не обрабатывает документы. Его задача — управлять состоянием кластера: создавать/удалять индексы, отслеживать, какие узлы «живы», и решать, на какие узлы размещать части индекса (шарды). Требует мало CPU и диска, но критически важна стабильная работа и быстрая сеть. Если мастер-узел перегружен другой работой, весь кластер становится нестабильным.
node.roles: [ master ]
Узел данных (Data Node) — «рабочая лошадка» кластера. Хранит данные и выполняет основную работу по поиску и агрегации. Требует много CPU, много оперативной памяти и очень быстрые диски (SSD). Это самая ресурсоемкая роль.
node.roles: [ data ]
Узел поглощения данных (Ingest Node) — специализированный тип узла, который позволяет выполнять предварительную обработку документов перед добавлением в индекс.
node.roles: [ ingest ]
Узел только для координации (Coordinating Only Node) — это «умный балансировщик». Не хранит данные, не управляет кластером и не обрабатывает документы. Его единственная задача — принимать входящие запросы, распределять их по узлам данных, собирать результаты и возвращать клиенту. В больших кластерах, где выполняются «тяжелые» поисковые запросы с агрегациями, фаза сбора и объединения результатов может сильно нагружать память и CPU. Выделение этой роли на отдельные узлы защищает узлы данных от этой нагрузки.
node.roles: []
Мастер-совместимый узел (Master-Eligible Node) — любой узел, у которого в конфигурации есть роль master. Это означает, что он имеет право участвовать в голосовании и может стать действующим мастером. Однако, может выполнять и другие роли. Аналогия — руководитель отдела в небольшой компании, который и управляет командой, и сам выполняет часть работы. Такой узел используется в небольших кластерах или тестовой среде.
node.roles: [ master, data, ingest ]
Выделенный мастер-совместимый узел (Dedicated Master-Eligible Node) — узел, который выполняет только роль мастера и никаких других ролей. Аналогия — генеральный директор в крупной корпорации, который занимается исключительно стратегией и управлением, не отвлекаясь на операционную деятельность. Такой узел используется в средних и больших продакшн-кластерах для обеспечения стабильности.
node.roles: [ master ]
Если директива node.roles полностью отсутствует в файле конфигурации, узел получает все возможные роли. Это поведение по умолчанию, сделанное для простоты запуска в небольших окружениях.
Однако для любого кластера, кроме тестового, это крайне не рекомендуется. Каждый узел будет «мастером на все руки», что приведет к нестабильности — ресурсоемкие операции с данными (роль data) будут мешать узлу выполнять критически важные задачи по управлению кластером (роль master).
Кворум и голосование
Голосование (Voting) — это процесс выборов действующего мастер-узла.
Чтобы кластер работал стабильно и не произошло ситуации «раздвоения личности» (split-brain), когда две части кластера думают, что они — главные, необходимо наличие кворума. Кворум — это большинство (более 50%) от общего числа мастер-совместимых (Master-Eligible Node) узлов.
Классическое правило гласит — для отказоустойчивости всегда должно быть нечетное количество мастер-совместимых узлов. Это гарантирует, что при любом разделении сети только одна группа узлов сможет составить большинство и выбрать мастера.
Узел с ролью voting_only — это особый, «облегченный» мастер-совместимый узел, который участвует в голосовании, но не может стать мастером. Такой узел не хранит состояние кластера (cluster state), что делает его чрезвычайно легковесным (очень мало CPU и RAM).
Допустим, у нас есть два полноценных выделенных мастер-узла. Этого недостаточно для кворума. Нужен третий голос, но мы не хотим разворачивать еще один полноценный, ресурсоемкий мастер-узел. Но зато мы можем добавить узел с ролью voting_only — для которого нужно минимум ресурсов.
# первый узел, может быть мастером node.roles: [ master ]
# второй узел, может быть мастером node.roles: [ master ]
# третий узел, только голосует, не может быть мастером node.roles: [ master, voting_only ]
Если первый узел по каким-то причинам становится недоступным, второй и третий узлы смогут сформировать кворум и выбрать второй узел новым мастером. Кластер останется работоспособным и сможет обслуживать клиентов. При этом мы сэкономили ресурсы на третьем узле.
Основные API ElasticSearch
В ElasticSearch доступны несколько API, которые работают на разных уровнях — для управления индексами и кластером, для работы с документами, для поиска по документам в индексе и т.д.
Cat API— для получения простой, человекочитаемой информации о состоянии кластера, индексов, узловIndices API— для управления индексами: создание, удаление, обновление маппингов и управление псевдонимамиAnalyze API— для диагностики и отладки, как именно анализатор преобразует текст в токеныDocument API— для CRUD-операций с отдельными документами в индексе (index,get,update,delete)Search API— главный API для выполнения поиска с использованием Query DSL, сортировок и агрегацийBulk API— для пакетной (массовой) индексации, обновления или удаления документов за один запросScroll API— для эффективного извлечения большого количества документов из одного поискового запросаReindex API— для копирования и преобразования данных из одного индекса в другой
Cat API — показать список всех индексов в кластере в удобном для чтения виде
$ curl -X GET "localhost:9200/_cat/indices?v"
Indices API — создать новый индекс products с полями name и price
$ curl -X PUT "localhost:9200/products" -H 'Content-Type: application/json' -d 'json-данные'
{ "mappings": { "dynamic": "strict", "properties": { "name": { "type": "text" }, "price": { "type": "double" } } } }
Analyze API — проверить, как анализатор english обработает строку «Running Foxes»
$ curl -X POST "localhost:9200/_analyze" -H 'Content-Type: application/json' -d 'json-данные'
{ "analyzer": "english", "text": "Running Foxes" }
Document API — добавить (проиндексировать) новый документ с ID=1 в индекс products
$ curl -X PUT "localhost:9200/products/_doc/1?pretty" -H 'Content-Type: application/json' -d 'json-данные'
{ "name": "Super Drill", "price": 999.99 }
Search API — найти все документы в индексе products, у которых в поле name встречается слово «drill»
$ curl -X GET "localhost:9200/products/_search?pretty" -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "match": { "name": "drill" } } }
Bulk API — добавить два документа в индекс products одним запросом
$ curl -X POST "localhost:9200/products/_bulk?pretty" -H "Content-Type: application/x-ndjson" -d 'json-данные'
{ "index": {"_id": "2"} } { "name":"Power Screwdriver", "price": 1500.00 } { "index": {"_id": "3"} } { "name":"Heavy Duty Drill", "price":1700.00 }
Scroll API — выгрузить все документы из индекса products порциями по одному документу за раз
Шаг 1. Инициализация скролла
$ curl -X GET "localhost:9200/products/_search?scroll=1m&pretty" -H 'Content-Type: application/json' -d 'json-данные'
{ "size": 1, "query": { "match_all": {} } }
Шаг 2. Получение следующей порции
$ curl -X GET "localhost:9200/_search/scroll?pretty" -H 'Content-Type: application/json' -d 'json-данные'
{ "scroll": "1m", "scroll_id": "scroll_id_из_предыдущего_ответа" }
Reindex API — скопировать все документы из индекса products в новый индекс products_v2
$ curl -X POST "localhost:9200/_reindex?pretty" -H 'Content-Type: application/json' -d 'json-данные'
{ "source": { "index": "products" }, "dest": { "index": "products_v2" } }
Настройки при создании индекса
Количество шардов и реплик
Количество первичных шардов number_of_shards — самая важная настройка, которую нельзя изменить после создания индекса. Она определяет, на сколько частей будет «нарезан» наш индекс. Если у нас 5 узлов данных, то 5 шардов позволят каждому узлу работать над своей частью индекса одновременно, что значительно ускоряет поиск. Но, каждый шард — это небольшая нагрузка на кластер (метаданные, ресурсы). Слишком много шардов для небольшого индекса может замедлить, а не ускорить работу.
Количество реплик number_of_replicas — вторая по важности настройка, которая обеспечивает отказоустойчивость и распределение нагрузки. Если узел с первичным шардом выходит из строя, его реплика на другом узле автоматически становится первичным, данные не теряются. Поисковые запросы могут обслуживаться как с первичного шарда, так и с его реплик, что распределяет нагрузку между узлами. Для production минимально необходимое значение — 1. Это гарантирует, что кластер переживет отказ одного узла данных без потери информации. Значение 2 обеспечит еще большую надежность.
Индексы в одном кластере могут иметь разное количество шардов и реплик, в зависимости от их размера и важности. Допустим, у нас есть 10 узлов, из них три узла — типа master, два узла — координирующие, пять узлов — типа data. Шарды и реплики будут распределены между 5 узлами типа data в зависимости от значений number_of_shards и number_of_replicas.
- индекс
products(важный, большой) — 5 шардов, 2 реплики - индекс
logs(менее важный, огромный) — 10 шардов, 1 реплика - индекс
users(важный, маленький) — 1 шард, 2 реплики
$ curl -X PUT 'localhost:9200/products' -H 'Content-Type: application/json' -d 'json-данные'
{ "settings": { "number_of_shards": 5, "number_of_replicas": 2 } }
Структура документа (mappings)
При создании индекса нужно обязательно указывать структуру документа (mappings). Хотя Elasticsearch может сам «угадать» типы полей (Dynamic Mapping), это часто приводит к ошибкам и неоптимальной работе.
Это позволяет установить для поля name — тип text для полнотекстового поиска с анализаторами и синонимами, для поля code — тип keyword для точного поиска, для поля price — тип double для для сортировки и агрегаций.
$ curl -X PUT 'localhost:9200/products' -H 'Content-Type: application/json' -d 'json-данные'
{ "settings": { "number_of_shards": 5, "number_of_replicas": 2 }, "mappings": { "dynamic": "strict", // запрет на dynamic mapping "properties": { "code": { "type": "keyword" }, "name": { "type": "text", "analyzer": "russian" }, "description": { "type": "text", "analyzer": "russian" }, "price": { "type": "double" }, "created_at": { "type": "date" } } } }
Отличие между keyword и text заключается в процессе анализа (analysis), который Elasticsearch применяет к данным перед их индексацией.
При сохранении строки типа text, она разбивается на отдельные слова (токены), приводит их к нижнему регистру, удаляет общие слова (стоп-слова) и может применять стемминг (приведение слова к его основе). Поля типа text предназначены для полнотекстового поиска.
При сохранении строки типа keyword — она сохраняется «как есть». То есть, вся строка целиком рассматривается как один единый токен. Поля типа keyword используются для точного поиска (например, артикул), сортировки или агрегации данных.
Анализатор, токенизатор, фильтр
Подготовка данных, или анализ (analysis) — это фундаментальный процесс в Elasticsearch, который превращает сырой текст в структурированный, оптимизированный для поиска формат. Он состоит из трех последовательных этапов, которые выполняются внутри анализатора.
Сырой текст => Анализатор => Готовые токены для индекса
Анализатор (Analyzer) — обертка, которая объединяет в себе три компонента. Можно использовать встроенные анализаторы (например, standard, simple, whitespace, stop, keyword, russian) или создать свой собственный, комбинируя нужные компоненты.
Сырой текст => [Символьные фильтры => Токенизатор => Фильтры токенов] => Готовые токены для индекса
Встроенные анализаторы
standard— разбивает текст на токены (используя токенизаторstandard) и приводит токены к нижнему региструsimple— разбивает текст на токены по всему, что не является буквой, и приводит токены к нижнему региструwhitespace— разбивает текст на токены только по пробелам, не приводит токены к нижнему региструstop— разбивает текст на токены какsimple, но дополнительно удаляет стоп-словаkeyword— не делает ничего, вся строка рассматривается как один единый токен
Токенизатор (Tokenizer) — сердце анализатора, его единственная задача — получить на вход поток символов и разбить его на отдельные части — токены (слова). Токенизатор standard разбивает текст по пробелам, знакам препинания и дефисам. Токенизатор whitespace просто разбивает текст по пробелам. Токенизатор pattern разбивает текст по заданному регулярному выражению.
Строка "Быстрый поиск товаров" => ["Быстрый", "поиск", "товаров"]
Символьные фильтры (Character Filters) работают до токенизатора. Их задача — «причесать» сырую строку перед тем, как она будет разбита на слова. Фильтр html_strip удаляет HTML-теги, фильтр mapping заменяет одни символы на другие.
Фильтры токенов (Token Filters) работают после токенизатора, могут изменять, удалять или даже добавлять токены.
lowercase— приводит все токены к нижнему региструstop— удаляет «стоп-слова» (артикли, предлоги, союзы)stemmer(стеммер) — приводит слова к их основе (стем)synonym— добавляет синонимы
Пример создания кастомного анализатора — символьный фильтр html_strip + токенизатор standard + фильтры токенов lowercase, stop, stemmer с учетом русского языка.
$ curl -X PUT 'localhost:9200/products_custom' -H 'Content-Type: application/json' -d 'json-данные'
{ "settings": { "analysis": { "filter": { "my_russian_stop_filter": { // создание кастомного фильтра "type": "stop", "stopwords": "_russian_" }, "my_russian_stemmer_filter": { // создание кастомного фильтра "type": "stemmer", "language": "russian" } }, "analyzer": { "my_russian_analyzer": { // созданние кастомного анализатора "char_filter": [ // символьные фильтры "html_strip" ], "tokenizer": "standard", // токенизатор "filter": [ // фильтры токенов "lowercase", "my_russian_stop_filter", "my_russian_stemmer_filter" ] } } } }, "mappings": { "dynamic": "strict", "properties": { "code": { "type": "keyword" } "name": { "type": "text", "analyzer": "my_russian_analyzer" }, "description": { "type": "text", "analyzer": "my_russian_analyzer" }, "price": { "type": "double" }, "created_at": { "type": "date" } } } }
Фактически, мы создали аналог встроенного анализатора russian. Анализатор russian — это «коробочное решение» от Elasticsearch. Он уже содержит в себе готовую комбинацию компонентов, оптимизированных для русского языка. Наш анализатор делает немного больше — использует символьный фильтр html_strip, который удаляет HTML-теги до того, как текст будет разбит на слова.
$ curl -X PUT 'localhost:9200/products_russian' -H 'Content-Type: application/json' -d 'json-данные'
{ "mappings": { "dynamic": "strict", "properties": { "code": { "type": "keyword" }, "name": { "type": "text", "analyzer": "russian" }, "description": { "type": "text", "analyzer": "russian" }, "price": { "type": "double" }, "created_at": { "type": "date" } } } }
Full-text и Term-level запросы
Все поисковые запросы можно условно разделить на две большие группы — полнотекстовые (full-text) и терминовые (term-level). Полнотекстовые запросы — анализируют поисковый запрос, предназначены для поиска по полям типа text. Терминовые запросы — не анализируют поисковый запрос, обычно используются для поиска по полям типа keyword.
match— для полнотекстового поиска (full-text query)match_phrase— для поиска фразы (full-text query)match_phrase_prefix— для фразы с префиксом (full-text query)match_bool_prefix— полнотекстовой с префиксом (full-text query)multi_match— поиск по нескольким полям (full-text query)
term— для поиска точного значения (term-level query)terms— для поиска точных значений (term-level query)range— для поиска в диапазоне (term-level query)prefix— для поиска по префиксу (term-level query)
Анализатор для full-text запроса
Важно понимать, что при full-text запросе строка для поиска подвергается обработке тем же самым анализатором, который был использован для индексации поля. Индекс состоит из токенов и связей между токенами и документами, где эти токены встречаются. Elasticsearch должен преобразовать строку запроса в токены, чтобы потом найти связанные с этими токенами документы.
ElasticSearch определяет анализатор, следуя следующей иерархии
- Анализатор, заданный в самом запросе — для поля, по которому нужно искать
- Специальный анализатор для поиска, заданный в маппинге для этого поля
- Основной анализатор, указанный в маппинге для этого поля (если не указан специальный)
- Анализатор по умолчанию для индекса или стандартный (
standard)
{ "settings": { "analysis": { "analyzer": { "default": { // анализатор по умолчанию для индекса (4) "type": "simple" } } } }, "mappings": { "properties": { "field_with_base_analyzer": { "type": "text", "analyzer": "english" // основной анализатор для поля (3) }, "field_with_search_analyzer": { "type": "text", "analyzer": "standard", // анализатор при индексации "search_analyzer": "whitespace" // анализатор для поиска (2) }, "field_default_analyzer": { // поле без своего анализатора (1) "type": "text" } } } }
{ "query": { "match": { "field_with_base_analyzer": { "query": "Wi-Fi", "analyzer": "standard" // указан явно, имеет высший приоритет } } } }
{ "query": { "match": { // не указан явно, будет использован основной анализатор (english) "field_with_base_analyzer": "Wi-Fi" } } }
{ "query": { "match": { // будет использован анализатор search_analyzer (whitespace) "field_with_search_analyzer": "Wi-Fi" } } }
{ "query": { "match": { // будет использован анализатор по умолчанию (simple) "field_default_analyzer": "Wi-Fi" } } }
Стандартные анализаторы и фильтры
В поставку ElasticSearch входят анализатор для английского языка english и анализатор для русского языка russian.
Анализатор english включает в себя
- Токенизатор
standard(разбивает по пробелам и знакам препинания) - Четыре стандартных фильтра
english_possessive_stemmer(удаляет «'s» в конце слов)lowercase(приводит к нижнему регистру)english_stop(удаляет стоп-слова из списка_english_)english_stemmer(применяет стемминг для английского языка)
Анализатор russian включает в себя
- Токенизатор
standard(разбивает по пробелам и знакам препинания) - Три стандартных фильтра
lowercase(приводит к нижнему регистру)russian_stop(удаляет стоп-слова из списка_russian_)russian_stemmer(применяет стемминг для русского языка)
Мульти-поля (multi-fields)
Мульти-поля (Multi-fields) — это возможность проиндексировать одно и то же поле в Elasticsearch несколько раз с разными настройками (например, разными анализаторами или типами данных). При этом, при обновлении или добавлении документа в индекс — значение поля отправляется только один раз.
// неправильный подход { "title_for_search": "Ноутбук Lenovo", "title_for_group": "Ноутбук Lenovo" }
// правильный подход { "title": "Ноутбук Lenovo" }
Это позволяет использовать одно и то же поле для разных задач
- Для полнотекстового поиска. Например, название товара «Apple iPhone 15 Pro» должно быть разбито на токены («apple», «iphone», «15», «pro») и приведено к нижнему регистру, чтобы его можно было найти по запросу «iphone pro». Для этого используется тип
text. - Для сортировки, агрегации или фильтрации. Для этих операций нужно полное, неизменное значение «Apple iPhone 15 Pro». При сортировке по полю
text— Elasticsearch будет сортировать по отдельным токенам («apple», «iphone», «15», «pro»), что приведет к некорректному результату. Для этого нужен типkeyword.
Настройка маппинга для решения двух задач — поиска и сортировки
{ "mappings": { "dynamic": "strict", "properties": { "name": { "type": "text", // для полнотекстового поиска "analyzer": "russian_analyzer", "fields": { "keyword": { "type": "keyword" // для сортировки, агрегации или фильтрации } } } } } }
Если поле может содержать текст на нескольких языках, стандартные языковые анализаторы (russian или english) будут неоптимальны, так как они будут плохо обрабатывать «чужой» язык. Но можно создать вспомогательные поля — russian и english, которые будут обрабатываться анализаторами russian и english.
{ "mappings": { "dynamic": "strict", "properties": { "code": { "type": "keyword" }, "name": { // поле на английском и русском "type": "text", "analyzer": "standard", "fields": { "russian": { // вспомогательное поле на русском "type": "text", "analyzer": "russian" }, "english": { // вспомогательное поле на английском "type": "text", "analyzer": "english" } } } "description": { "type": "text", "analyzer": "russian" }, "price": { "type": "double" }, "created_at": { "type": "date" } } } }
При поиске можно использовать multi_match, чтобы отправить запрос сразу во все три поля, давая больший вес языковым версиям.
{ "query": { "multi_match": { "query": "дрели bosch", "fields": [ "name", // совпадение в name.russian в два раза важнее, чем совпадение в name "name.russian^2", // совпадение в name.english в два раза важнее, чем совпадение в name "name.english^2" ], "type": "most_fields" } } }
По умолчанию multi_match использует режим best_fields. В нашем случае это означает, что Elasticsearch выполнит два отдельных поиска и возьмет только одну, самую высокую оценку релевантности. Чтобы просуммировать значения релевантности — мы используем режим most_fields вместо best_fields.
Основные операции с индексом
Рассмотрим основные операции, которые можно выполнять с индексом Elasticsearch, с примерами запросов к API через утилиту curl.
Создание индекса (create index)
Создание нового индекса с заданными настройками и структурой (маппингом)
$ curl -X PUT 'localhost:9200/products' -H 'Content-Type: application/json' -d 'json-данные'
{ "settings": { "number_of_shards": 1, "number_of_replicas": 0 }, "mappings": { "dynamic": "strict", "properties": { "code": { "type": "keyword" }, "name": { "type": "text" }, "price": { "type": "double" } } } }
Создание / обновление документа (index document)
Добавляет новый или полностью заменяет существующий документ с идентификатором 12345
$ curl -X PUT 'localhost:9200/products/_doc/12345' -H 'Content-Type: application/json' -d 'json-данные'
{ "code": "A-54321", "name": "Аккумуляторная дрель Bosch", "price": 7990.50 }
Создание нового документа (create document)
Добавляет новый документ, если он еще не существует. ElasticSearch возвращает ошибку, если документ уже существует.
$ curl -X PUT 'localhost:9200/products/_create/12345' -H 'Content-Type: application/json' -d 'json-данные'
{ "code": "A-54321", "name": "Аккумуляторная дрель Bosch", "price": 7990.50 }
Получение документа по id (get document)
Возвращает документ по его уникальному идентификатору 12345
$ curl -X GET 'localhost:9200/products/_doc/12345'
{ "code": "A-54321", "name": "Аккумуляторная дрель Bosch", "price": 7990.50 }
Частичное обновление документа (update document)
Изменяет только указанные поля в существующем документе, не трогая остальные
$ curl -X POST 'localhost:9200/products/_update/12345' -H 'Content-Type: application/json' -d 'json-данные'
{ "doc": { "price": 7500.00 } }
Удаление документа по id (delete document)
Удаляет документ по его уникальному идентификатору 12345
$ curl -X DELETE 'localhost:9200/products/_doc/12345'
Поиск документов (search documents)
Основная операция для поиска документов по различным критериям
$ curl -X GET 'localhost:9200/products/_search' -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "match": { "name": "дрель" } } }
Пакетная обработка (bulk)
Выполняет несколько операций (добавление, обновление, удаление) за один запрос для максимальной производительности
$ curl -X POST 'localhost:9200/products/_bulk' -H 'Content-Type: application/x-ndjson' -d 'json-данные'
{ "index": { "_index": "products", "_id": "1" } } { "code": "A-54321", "name": "Дрель Bosch (новая версия)", "price": 8100 } { "create": { "_index": "products", "_id": "2" } } { "code": "M-11223", "name": "Шуруповерт Makita", "price": 9500 } { "update": { "_index": "products", "_id": "3" } } { "doc": { "price": 15000 } } { "delete": { "_index": "products", "_id": "4" } }
Структура данных для операции bulk называется Newline Delimited JSON (обратите внимание на заголовок content-type). Каждая строка в теле запроса — это отдельный, самостоятельный JSON-объект. Такая структура позволяет Elasticsearch обрабатывать запрос потоково, не загружая весь многомегабайтный файл в память для парсинга.
Удаление индекса (delete index)
Полностью и безвозвратно удаляет индекс и все его данные
$ curl -X DELETE 'localhost:9200/products'
Особенности операций index и update
На первый взгляд, операция update кажется более производительной, чем операция index, так как отправляетя меньше данных. Однако в большинстве случаев полная замена (index) оказывается быстрее частичного обновления (update). Любое изменение (update) — это на самом деле «пометить старый документ как удаленный и добавить новый». Получается три операции — прочитать старый документ в память, применить к нему изменения, выполнить операцию index с новым документом (пометить старый как удаленный, индексировать новый).
Query DSL (Domain Specific Language)
В основе всех поисковых запросов к Elasticsearch лежит Query DSL (Domain Specific Language). Любой запрос — это JSON-объект, построенный по правилам этого языка.
Листовые запросы (Leaf Query Clauses) — простые запросы, которые ищут конкретное значение в конкретном поле
match— для полнотекстового поиска (full-text query)match_phrase— для поиска фразы (full-text query)match_phrase_prefix— для фразы с префиксом (full-text query)match_bool_prefix— полнотекстовой с префиксом (full-text query)multi_match— поиск по нескольким полям (full-text query)
term— для поиска точного значения (term-level query)terms— для поиска точных значений (term-level query)range— для поиска в диапазоне (term-level query)prefix— для поиска по префиксу (term-level query)
Поиск match анализирует поисковый запрос — разбивает на слова, приводит к нижнему регистру, выполняет стемминг. Получившиеся в итоге токены используются для поиска документов в инвертированном индексе. Если говорить упрощенно, релевантными поисковому запросу будут документы, в которых найдено больше всего токенов, полученных из поискового запроса.
Поиск term ищет один, точный, неанализируемый токен в инвертированном индексе. Ключевое слово здесь — неанализируемый. Поисковая строка, которая передается в term, не разбивается на слова, не приводится к нижнему регистру и не проходит стемминг. Основное применение — для точной фильтрации по полям типа keyword (статусы, категории, теги, артикулы).
Поиск match_phrase ищет последовательность токенов в указанном порядке. Поисковая строка обрабатывается тем же анализатором, что и поле при индексации. После получения токенов из фразы — выполняется поиск документов, где эти токены идут именно в таком порядке друг за другом.
Поиск match_phrase_prefix — сочетает в себе поведение match_phrase (поиск по точной фразе) и prefix (поиск по префиксу). Ищет документы, содержащие определенную последовательность слов, где последнее слово может быть неполным (являться префиксом).
Поиск match_bool_prefix — сочетает в себе поведение match и prefix. Ищет документы, содержащие перечисленные в запросе слова, где последнее слово может быть неполным. Очень похоже на match_phrase_prefix — но порядок слов не важен. По умолчанию все слова обязательны, но это можно изменить с помощью operotor.
{ "query": { "match": { // полнотекстовой поиск "name": "дрель bosch", // искать оба слова "operator": "and" // по умолчанию or (или) } } }
{ "query": { "term": { // поиск точного значения "code": "A-54321" } } }
{ "query": { "terms": { // точный поиск любого значения "code": [ "A-54321", "B-98765" ] } } }
{ "query": { "range": { // поиск в диапазоне "price": { "gt": 5000 } } } }
{ "query": { "prefix": { // поиск по префиксу "name": { "value": "смарт" } } } }
{ "query": { "match_phrase_prefix": { // поиск фразы + поиск префикса "name": "дрель электическая bo" } } }
{ "query": { "match_bool_prefix": { // поиск всех слов + поиск префикса "name": "дрель электическая bo" } } }
{ "query": { "multi_match": { // поиск в нескольких полях "query": "bosch", // совпадение в name в три раза важнее, чем совпадение в description "fields": [ "name^3", "brand^2", "description" ] } } }
Составные запросы (Compound Query Clauses) — контейнер, который содержит внутри себя другие запросы (как листовые, так и другие составные) и комбинируют их с помощью логики.
Логический конструктор bool (Boolean Query) — главный контейнер, который позволяет комбинировать запросы с помощью логики, содержит внутри себя следующие блоки.
must— условия, которые обязательно должны выполняться (влияет на релевантность)should— условия, которые желательно должны выполниться (влияет на релевантность)filter— условия, которые обязательно должны выполниться (не влияет на релевантность)must_not— условия, которые не должны выполняться (этоfilterнаоборот)
Блок filter работает в режиме «да/нет». Его задача — быстро отсеять документы, которые точно не соответствуют условию, не тратя время на вычисление релевантности. Блок filter оптимизирован для работы со структурированными, неанализируемыми данными, чтобы обеспечивать максимальную скорость. Поэтому его идеально использовать с такими типами данных, как keyword, integer, double, date и boolean.
Блок should увеличивает значение релевантности, полученное must. Если есть must или filter, то они определяют, попадет ли документ в результат поиска, should на это не влияет. Если нет ни must, ни filter, то документ должен соответствовать хотя бы одному условию из should.
{ "query": { "bool": { "must": [ { "match": { // вычисляем релевантность "name": "дрель" } } ], "should": [ // повышаем релевантность { "match": { "name": "аккумуляторная" } } ] "filter": [ { "term": { // отсеиваем по бренду "brand": "Bosch" } } ] } } }
Чтобы документ был возвращен в результатах поиска, он должен удовлетворять следующим условиям одновременно
- Правило 1 (обязательные условия). Документ должен соответствовать всем условиям из блоков
mustиfilter - Правило 2 (исключающие условия). Документ не должен соответствовать ни одному из условий в блоке
must_not - Правило 3 (желательные условия). Роль блока
shouldзависит от других директив- Если есть
mustилиfilter, то блокshouldне влияет на то, попадет ли документ в результаты поиска - Если нет ни
must, ниfilter, то документ должен соответствовать хотя бы одному условию изshould
- Если есть
Поиск может быть выполнен в одном из двух режимов — с расчетом релевантности и без расчета релевантности. Поиск с расчетом релевантности активируется при использовании must и should (Query Context, медленный). Поиск без расчета релевантности активируется при использовании filter и must_not (Filter Context, быстрый).
Если must или should расположены внутри filter — то будет работать режим без расчета релеватности. Директивы must или should внутри filter просто задают логику И (AND) или ИЛИ (OR). При этом filter и must_not всегда работают в режиме без расчета релевантности, даже если расположены внутри must или should.
Следует использовать filter для всего, что является строгим, бинарным критерием (да/нет). И использовать must только для той части запроса, где действительно важно ранжирование по релевантности. Это позволяет Elasticsearch сначала молниеносно отсеять 99% ненужных документов, а затем выполнить расчет релевантности на очень маленьком наборе данных.
Практический пример — найти аккумуляторные дрели производителя Bosch или Samsung ценой от 10000 до 20000 рублей
{ "query": { "bool": { "must": [ // режим с расчетом релевантности, медленный { "match": { "name": { "query": "аккумуляторная дрель", "operator": "and" } } } ], "filter": [ // режим без расчета релевантности, быстрый { "bool": { // should внутри filter задает логику ИЛИ (OR) "should": [ // фильтр по брендам { "term": { "brand": "Bosch" } }, { "term": { "brand": "Samsung" } } ] } }, { "range": { // фильтр по цене "price": { "gte": 10000, "lte": 20000, } } } ] } } }
Управление релевантностью
Представим, что мы ищем смартфон в интернет-магазине. Обязательное требование — найти «iPhone 14», это самое важное. Желательное условие — товар есть «в наличии», это хорошо и должно поднять его в выдаче. Нежелательное, но допустимо — если товар «восстановленный», это плохо и должно опустить его в выдаче.
{ "query": { "bool": { "must": [ // Документ ДОЛЖЕН соответствовать этому условию { "match": { "title": "iPhone 14" } } ], "should": [ // Документ МОЖЕТ соответствовать этому для повышения релевантности { "term": { "status": { "value": "in_stock", "boost": 2.0 // повышаем релевантность, если товар в наличии } } }, { "term": { "condition": { "value": "refurbished", "boost": 0.1 // понижаем релевантность, если товар восстановленный } } } ] } } }
Представим, что мы ищем смартфон в интернет-магазине. У нас есть несколько пожеланий, мы хотим, чтобы результаты поиска это учли.
- В названии смартфона желательно наличие слова «galaxy»
- Крайне желательно, чтобы производителем был «samsung»
- Также неплохо, чтобы в описании будет «быстрая зарядка»
Ни один из этих критериев не является абсолютно обязательным, но чем большему числу из них соответствует смартфон — тем лучше.
{ "query": { "bool": { "should": [ // все условия необязательные { "match": { "name": "galaxy" } }, { "match": { "brand": { "query": "samsung", "boost": 3.0 // этот критерий в три раза важнее других } } }, { "match": { "description": { "query": "быстрая зарядка", "operator": "and" } } } ] } } }
Условие поиска в описании требует, чтобы были найдены слова «быстрая» и «зарядка», но не гарантирует, что они будут рядом. Документ с описанием «Очень быстрая работа процессора. Устройство поддерживает беспроводную зарядку» тоже будет найден, хотя фразы «быстрая зарядка» в нем нет. Для поиска точной фразы или слов, расположенных рядом — нужно использовать match_phrase.
{ "query": { "bool": { "should": [ // все условия необязательные { "match": { "name": "galaxy" } }, { "match": { "brand": { "query": "samsung", "boost": 3.0 // этот критерий в три раза важнее других } } }, { "match_phrase": { "description": { "query": "быстрая зарядка", // сколько слов допускается между «быстрая» и «зарядка», по умолчанию ноль "slop": 1 } } } ] } } }
Теперь поиск найдет документы, которые содержат «быстрая зарядка» или «быстрая беспроводная зарядка» (между ними одно лишнее слово) — но не найдет документы, которые содержат «быстрая и эффективная зарядка» (между ними два лишних слова).
Нечеткий поиск (Fuzziness)
Fuzziness (нечеткость) — это механизм, который позволяет находить документы, даже если поисковый запрос содержит опечатки. В основе этого механизма лежит расстояние Левенштейна. Проще говоря, это минимальное количество односимвольных изменений (вставка, удаление или замена символа), которые нужно сделать, чтобы превратить одно слово в другое.
Нечеткий поиск по умолчанию полностью отключен. Чтобы включить — нужно установить для fuzziness значение AUTO, 1 или 2.
// исходный запрос { "query": { "match": { "title": { "query": "быстры поиск", // опечатка в первом слове "operator": "and", "fuzziness": "AUTO" } } } }
// фактический запрос { "query": { "bool": { // должны быть выполнены оба условия, поскольку operator: and "must": [ { "fuzzy": { "title": { "value": "быстры", "fuzziness": "AUTO" } } }, { "fuzzy": { "title": { "value": "поиск", "fuzziness": "AUTO" } } } ] } } }
Значение AUTO — это «умная» настройка, которую рекомендует сам Elasticsearch. Вместо того чтобы жестко задавать максимальное расстояние Левенштейна (например, 1 или 2), AUTO генерирует его автоматически, в зависимости от длины поискового слова.
Правила AUTO следующие
- для слов длиной 1-2 символа — нечеткость отключена (расстояние 0)
- для слов длиной 3-5 символов — допускается одна опечатка (расстояние 1)
- для слов длиной более 5 символов — допускается две опечатки (расстояние 2)
Для более тонкой настройки и оптимизации производительности — рекомендуется использовать prefix_length. Это количество символов в начале слова, которые должны совпадать точно. Нечеткость будет применяться только к оставшейся части слова. Это значительно ускоряет нечеткий поиск, так как сильно сужает круг возможных вариантов. Обычно пользователи правильно пишут начало слова.
{ "query": { "multi_match": { "query": "дрел", // совпадение в name в три раза важнее, чем совпадение в description "fields": [ "name^3", "brand^2", "description" ], "fuzziness": "AUTO", "prefix_length": 2 } } }
Поиск по нескольким полям
По умолчанию multi_match использует режим best_fields, который находит поле с наилучшим совпадением и берет только его оценку релевантности. Подходит для поиска по полям, которые являются конкурентами друг другу. Например, поиск по title и body статьи. Если слово найдено в заголовке, это гораздо важнее, чем если оно найдено в теле статьи.
// исходный запрос { "query": { "multi_match": { "query": "быстрый поиск elasticsearch", "fields": ["title", "announce", "content"], "type": "best_fields" } } }
// фактический запрос { "query": { "dis_max": { // возвращает максимальный _score из внутренних запросов "queries": [ { "match": { "title": "быстрый поиск elasticsearch" } }, { "match": { "announce": "быстрый поиск elasticsearch" } }, { "match": { "content": "быстрый поиск elasticsearch" } } ] } } }
Режим most_fields ищет каждое слово в каждом поле отдельно и суммирует оценки за каждое совпадение. Документ «Иван Сидоров» при поиске «Иван Петров» может получить высокую оценку, если было пять совпадений слова «Иван». Документ «Иван Петров», в котором были найдены слова «Иван» и «Петров» может получить оценку существенно ниже, потому что было только два совпадения.
// исходный запрос { "query": { "multi_match": { "query": "быстрый поиск elasticsearch", "fields": ["title", "announce", "content"], "type": "most_fields" } } }
// фактический запрос { "query": { "bool": { // итоговый _score равен сумме _score из внутренних запросов "should": [ { "match": { "title": "быстрый поиск elasticsearch" } }, { "match": { "announce": "быстрый поиск elasticsearch" } }, { "match": { "content": "быстрый поиск elasticsearch" } } ] } } }
Режим cross_fields подходит для поиска по полям, которые дополняют друг друга и описывают один и тот же объект (например first_name и last_name для имени и фамилии). Фактически, cross_fields склеивает все поля в одно большое виртуальное поле и ищет совпадения в нём. Если поисковый запрос «Иван Петров» находит «Иван» в поле first_name и «Петров» в поле last_name — режим cross_fields это правильно оценит, в то время как best_fields потерпит неудачу.
// исходный запрос { "query": { "multi_match": { "query": "Иван Петров", "fields": ["first_name", "last_name"], "type": "cross_fields", "operator": "and" } } }
// фактический запрос { "query": { "bool": { "must": [ // должны быть выполнены оба условия (найдены оба слова) { "bool": { "should": [ // должно быть хотя бы одно совпадение { "match": { "first_name": "Иван" } }, { "match": { "last_name": "Иван" } } ] } }, { "bool": { "should": [ // должно быть хотя бы одно совпадение { "match": { "first_name": "Петров" } }, { "match": { "last_name": "Петров" } } ] } } ] } } }
Режимы phrase и phrase_prefix — это best_fields, но вместо match запроса они выполняют match_phrase или match_phrase_prefix для каждого поля. Они ищут точную фразу или точную фразу с префиксом в конце.
// исходный запрос { "multi_match": { "query": "быстрый поиск elas", "fields": ["title", "content"], "type": "phrase_prefix" } }
// фактический запрос { "query": { "dis_max": { // или bool/should "queries": [ { "match_phrase_prefix": { "title": "быстрый поиск elas" } }, { "match_phrase_prefix": { "body": "быстрый поиск elas" } } ] } } }
Режим bool_prefix — умный гибрид, который идеально подходит для «поиска по мере ввода». Все слова, кроме последнего, считаются завершенными. Их нужно искать как точные, полные слова. Самое последнее слово считается незавершенным, «в процессе набора». Его нужно искать как префикс (по началу слова).
// исходный запрос { "query": { "multi_match": { "query": "быстрый поиск elas", // совпадение в title в три раза важнее, чем совпадение в content "fields": [ "title^3", "content" ], "fuzziness": "AUTO", "prefix_length": 2, "type": "bool_prefix" } } }
// фактический запрос { "query": { "dis_max": { // multi_match по умолчанию ищет в режиме best_fields "queries": [ // Запрос для поля title с усилением { "bool": { "must": [ { "fuzzy": { // ищем «быстрый» нечетко "title": { "value": "быстрый", "fuzziness": "AUTO", "prefix_length": 2 } } }, { "fuzzy": { // ищем «поиск» нечетко "title": { "value": "поиск", "fuzziness": "AUTO", "prefix_length": 2 } } }, { "prefix": { // ищем «elas» как префикс "title": "elas" } } ], "boost": 3.0, // усиление score в 3 раза } }, // Запрос для поля content без усиления { "bool": { "must": [ { "fuzzy": { // ищем «быстрый» нечетко "content": { "value": "быстрый", "fuzziness": "AUTO", "prefix_length": 2 } } }, { "fuzzy": { // ищем «поиск» нечетко "content": { "value": "поиск", "fuzziness": "AUTO", "prefix_length": 2 } } }, { "prefix": { // ищем «elas» как префикс "content": "elas" } } ] } } ] } } }
Поиск multi_match по умолчанию использует оператор or (или) — документ считается совпавшим, если в полях нашлось хотя бы одно слово из запроса.
Режим best_fields, оператор or
// исходный запрос { "multi_match": { "query": "быстрый поиск elasticsearch", "fields": ["title", "content"], "type": "best_fields", "operator": "or" } }
// фактический запрос { "dis_max": { // возвращает максимальный _score из внутренних запросов "queries": [ { "match": { "title": { "query": "быстрый поиск elasticsearch", "operator": "or" }}}, { "match": { "content": { "query": "быстрый поиск elasticsearch", "operator": "or" }}} ] } }
Режим best_fields, оператор and
// исходный запрос { "multi_match": { "query": "быстрый поиск elasticsearch", "fields": ["title", "content"], "type": "best_fields", "operator": "and" } }
// фактический запрос { "dis_max": { // возвращает максимальный _score из внутренних запросов "queries": [ { "match": { "title": { "query": "быстрый поиск elasticsearch", "operator": "and" }}}, { "match": { "content": { "query": "быстрый поиск elasticsearch", "operator": "and" }}} ] } }
Режим most_fields, оператор or
// исходный запрос { "multi_match": { "query": "быстрый поиск elasticsearch", "fields": ["title", "content"], "type": "most_fields", "operator": "or" } }
// фактический запрос { "bool": { // итоговый _score равен сумме _score из внутренних запросов "should": [ { "match": { "title": { "query": "быстрый поиск elasticsearch", "operator": "or" }}}, { "match": { "content": { "query": "быстрый поиск elasticsearch", "operator": "or" }}} ] } }
Режим most_fields, оператор and
// исходный запрос { "multi_match": { "query": "быстрый поиск elasticsearch", "fields": ["title", "content"], "type": "most_fields", "operator": "and" } }
// фактический запрос { "bool": { // итоговый _score равен сумме _score из внутренних запросов "should": [ { "match": { "title": { "query": "быстрый поиск elasticsearch", "operator": "and" }}}, { "match": { "body": { "query": "быстрый поиск elasticsearch", "operator": "and" }}} ] } }
Режим cross_fields, оператор and
// исходный запрос { "multi_match": { "query": "Иван Петров", "fields": ["first_name", "last_name"], "type": "cross_fields", "operator": "and" } }
// фактический запрос { "query": { "bool": { "must": [ // должны быть выполнены оба условия (найдены оба слова) { "bool": { "should": [ // должно быть хотя бы одно совпадение { "match": { "first_name": "Иван" } }, { "match": { "last_name": "Иван" } } ] } }, { "bool": { "should": [ // должно быть хотя бы одно совпадение { "match": { "first_name": "Петров" } }, { "match": { "last_name": "Петров" } } ] } } ] } } }
Режим cross_fields с использованием оператора or выдает бессмысленный результат и здесь не рассматривается.
Поиск по синонимам
Чтобы поиск по слову «дрель» находил также «перфоратор», нужно настроить синонимы на уровне анализатора индекса в Elasticsearch.
$ sudo nano /etc/elasticsearch/synonyms.txt
дрель, перфоратор смартфон, мобильник, телефон
Нельзя просто добавить синонимы в существующий индекс, нужно удалить старый индекс и создать новый с настройками, которые будут использовать файл синонимов.
$ curl -X DELETE 'localhost:9200/products'
$ curl -X PUT 'localhost:9200/products' -H 'Content-Type: application/json' -d 'json-данные'
{ "settings": { "analysis": { "filter": { "my_synonyms_filter": { "type": "synonym", "synonyms_path": "synonyms.txt" } }, "analyzer": { "my_custom_analyzer": { "tokenizer": "standard", "filter": [ "lowercase", "my_synonyms_filter" ] } } } }, "mappings": { "dynamic": "strict", "properties": { "code": { "type": "keyword" }, "name": { "type": "text", "analyzer": "my_custom_analyzer" }, "price": { "type": "double" }, "description": { "type": "text", "analyzer": "my_custom_analyzer" }, } } }
Чтобы поиск по слову «дрель» находил «перфоратор», но поиск по слову «перфоратор» не находил «дрель» — используется направленный синтаксис в файле синонимов.
$ sudo nano /etc/elasticsearch/synonyms.txt
дрель => дрель, перфоратор смартфон, мобильник, телефон
При поиске «дрель» — ElasticSearch будет искать как «дрель», так и «перфоратор». При этом обратное правило не создается.
Чтобы Elasticsearch перечитал измененный файл синонимов — достаточно перезапустить службу
$ sudo systemctl restart elasticsearch.service
Сортировка и агрегация
По умолчанию Elasticsearch сортирует результаты по релевантности, однако можно изменить это поведение, чтобы сортировать по другому полю. Сортировка возможна только по неанализируемым полям типа keyword, integer, double, date, boolean.
Сортировка товаров по цене (по возрастанию)
{ "query": { "match_all": {} }, "sort": [ { "price": "asc" } ] }
Сортировка сначала по бренду, а затем по цене
{ "query": { "match_all": {} }, "sort": [ { "brand": "asc" }, { "price": "desc" } ] }
match_all — это самый простой из всех возможных запросов в Elasticsearch. Единственная задача такого запроса — найти все документы в указанном индексе. Присваивает каждому найденному документу нейтральный балл релевантности _score равный 1.0.
Агрегации — инструмент для анализа данных, который работает по принципу, схожему с GROUP BY в языке SQL. Позволяет сгруппировать результаты поиска и вычислить для них различные метрики. Агрегация возможна только по неанализируемым полям типа keyword, integer, double, date, boolean.
Найти все товары, содержащие слово «дрель», отсортировать их по убыванию цены, и при этом получить фасеты по производителю и по диапазонам цен (чтобы добавить фильтры на сайте).
$ curl -X GET 'localhost:9200/products/_search?pretty' -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "match": { "name": "дрель" } }, "sort": [ { "price": "desc" } ], "aggs": { "all_brands": { "terms": { // агрегация, аналог GROUP BY в языке SQL "field": "brand", "size": 10 // только первые 10 производителей }, "aggs": { // средняя цена у производителя, просто для примера вложенной агрегации "avg_price": { "avg": { "field": "price" } } } }, "price_ranges": { "range": { "field": "price", "ranges": [ { "to": 5000.0 }, { "from": 5000.0, "to": 10000.0 }, { "from": 10000.0 } ] } } }, "size": 10 // только первые 10 товаров }
Ответ ElasticSearch на этот поисковый запрос имеет вид
{ "took": 61, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 165, // всего найдено 165 товаров по запросу «дрель» "relation": "eq" }, "max_score": null, "hits": [ // массив с первыми 10 товарами по запросу { "_index": "products", "_id": "123456", "_score": null, "_source": { // исходные данные, которые были добавлены в индекс "code": "987654", "name": "Дрель A-54321", "price": "11000.00000", "brand": "Samsung" }, "sort": [ 11000.00 // сортировка по цене, цена этого товара ] }, ..... ] }, "aggregations": { "all_brands": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 45, // товаров других производителей по запросу «дрель» "buckets": [ // массив с первыми 10 производителями { "key": "Samsung", "doc_count": 32, // товаров Samsung по запросу «дрель» "avg_price": { "value": 10005.6574074074 // средняя цена } }, ..... ] }, "price_ranges": { "buckets": [ { "key": "*-5000.0", "to": 5000.0, "doc_count": 11 }, { "key": "5000.0-10000.0", "from": 5000.0, "to": 10000.0, "doc_count": 110 }, { "key": "10000.0-*", "from": 10000.0, "doc_count": 44 } ] } } }
Этот JSON содержит три основные части — метаданные о запросе, найденные и отсортированные товары, результаты агрегаций.
1. Метаданные (общая информация)
took— запрос занял 61 миллисекунду (полезно для мониторинга)timed_out— запрос не был прерван по таймауту, все хорошо_shards— техническая информация, сколько шардов было опрошено
2. Результаты поиска (hits) — отвечает за отображение списка товаров на странице
total— запрос вернул 165 товаров, можно использовать для пагинации «Найдено 165 товаров, показано 1-10»max_score— максимальный балл релевантности равенnull(по причине использования сортировки)hits— массив с первыми 10 товарами (так как в запросеsizeравно 10), отсортированными по цене_source— исходные данные товара при добавлении в индекс (отсюда можно взятьname,price,brandи т.п.)sort— значение поляpriceдля данного конкретного товара (оно здесь для отладки)
3. Агрегации (aggregations) — для построения боковой панели с фильтрами по производителю и диапазонам цен (фасетного поиска)
all_brands— результат агрегации по производителямprice_ranges— результат агрегации по диапазонам цен
Технически можно выполнить агрегацию по полю типа text, но для этого нужно явно включить опцию fielddata. Это заставит Elasticsearch загрузить все отдельные токены из всех документов в оперативную память, что является чрезвычайно ресурсоемкой операцией. По умолчанию эта возможность отключена, чтобы защитить кластер от случайного исчерпания памяти.
$ curl -X PUT 'localhost:9200/products' -H 'Content-Type: application/json' -d 'json-данные'
{ // обновляем маппинг, разрешая fielddata "properties": { "description": { "type": "text", "fielddata": true } } }
При выполнении terms агрегации — Elasticsearch не только группирует, но и сортирует результаты. По умолчанию сортровка выполнется по кол-ву результатов группировки, то есть, фактически выполняется такой запрос.
"aggs": { "all_brands": { "terms": { "field": "brand.name.keyword", "order": { "_count": "desc" } // по умолчанию } } }
Сортировка по значению ключа (алфавитный порядок)
"aggs": { "all_brands": { "terms": { "field": "brand.name.keyword", "order": { "_key": "asc" } // по алфавиту } } }
Сортировка по результату вложенной агрегации
"aggs": { // отсортировать бренды по максимальной цене товара внутри каждого бренда "brands_by_max_price": { "terms": { "field": "brand.name.keyword", "order": { "max_price_in_brand": "desc" } // cортируем по результату вложенной агрегации }, "aggs": { "max_price_in_brand": { // вложенная агрегация, по которой будет сортировка "max": { "field": "price" // поле с ценой } } } } }
Агрегация composite
Если terms агрегация — это поиск «самых популярных» элементов, то composite агрегацию лучше представлять как чтение отсортированной телефонной книги. Ее цель — не найти топ, а дать возможность последовательно прочитать весь список уникальных значений в предсказуемом, детерминированном порядке. Поля, по которым будет идти сортировка результатов, задаются с помощью sources.
"aggs": { "all_brands": { "composite": { "sources": [ { "brand_name": { "terms": { "field": "brand.name.keyword", "order": "desc" // сортировка по убыванию вместо сортировки asc по умолчанию } } } ] } } }
Агрегация composite создана для того, чтобы эффективно и надежно получать полный список всех уникальных значений, даже если их миллионы, с помощью механизма after_key.
"aggs": { "all_brands": { "composite": { "sources": [ { "brand_name": { "terms": { "field": "brand.name.keyword" } } } ], "size": 5 // возвращать по 5 брендов на страницу } } }
ElasticSearch возвращает первую «страницу» результатов агрегации, при этом результаты отсортированы не по популярности, а по значению ключа (алфавиту). after_key — это закладка, оставленная на последнем прочитанном элементе. Для получения следующей страницы — нужно выполнить запрос, используя after_key из предыдущего ответа.
"buckets": [ { "key": {"brand_name": "Apple"}, "doc_count": 105 }, { "key": {"brand_name": "Samsung"}, "doc_count": 150 } ], "after_key": { "brand_name": "Samsung" }
Кроме того, composite агрегация позволяет создать единый «составной» ключ из нескольких полей, что идеально подходит для получения в ответе связанных данных, таких как «идентификатор + наименование».
"aggs": { "all_brands": { "composite": { "sources": [ { "brand_id": { "terms": { "field": "brand.id" } } }, { "brand_name": { "terms": { "field": "brand.name.keyword" } } } ], "size": 5 } } }
Сложные типы данных в индексе
О простых типах данных (text, keyword, float, integer, date, boolean) особо говорить нечего, с ними все понятно. Давайте рассмотрим массивы и сложные типы данных (object, nested, join).
Массив значений для поля
Любое поле в Elasticsearch по умолчанию может быть массивом. Допустим, у каждого товара в магазине есть набор тегов (Новинка, Распродажа, Подарок), также товар может храниться на нескольких складах.
$ curl -X PUT 'localhost:9200/products' -H 'Content-Type: application/json' -d 'json-данные'
{ "mappings": { "dynamic": "strict", "properties": { "name": { "type": "text" }, "price": { "type": "double" }, "tags": { "type": "keyword" }, "warehouse_ids": { "type": "integer" } } } }
Запрос на добавление товара в индекс
$ curl -X PUT 'localhost:9200/products/_doc/1' -H 'Content-Type: application/json' -d 'json-данные'
{ "name": "Умные часы «Вектор»", "price": 12345.67, "tags": [ "новинка", "электроника", "подарок" ], "warehouse_ids": [ 101, 205, 310 ] }
Тип object (объект)
Назначение — группировка связанных полей. Например, для хранения габаритов (длина, ширина, высота)
// Маппинг { "mappings": { "dynamic": "strict", "properties": { "name": { "type": "text" }, "dimensions": { "type": "object", "properties": { "height": { "type": "integer" }, "width": { "type": "integer" }, "depth": { "type": "integer" } } } } } }
// Документ { "name": "Книжный шкаф", "dimensions": { "height": 220, "width": 150, "depth": 40 } }
Пример поискового запроса — найти все шкафы высотой более 200 см и шириной более 140 см
$ curl -X GET 'localhost:9200/products/_search' -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "bool": { "filter": [ { "range": { "dimensions.height": { "gt": 200 } } }, { "range": { "dimensions.width": { "gt": 140 } } } ] } } }
По умолчанию Elasticsearch «сплющивает» (flattens) внутренние объекты. Связь между полями внутри одного объекта теряется. Рассмотрим это на примере авторов книг.
// Маппинг { "mappings": { "dynamic": "strict", "properties": { "authors": { "type": "object", "properties": { "first": { "type": "text" }, "last": { "type": "text" } } } } } }
// Документ { "title": "Статья об Elasticsearch", "authors": [ { "first": "Иван", "last": "Иванов" }, { "first": "Петр", "last": "Петров" } ] }
Пример ошибочного запроса — найти статьи, где есть автор «Иван Петров». Запрос вернет результат, хотя автора «Иван Петров» не существует.
$ curl -X GET 'localhost:9200/articles/_search' -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "bool": { "must": [ { "match": { "authors.first": "Иван" } }, { "match": { "authors.last": "Петров" } } ] } } }
Из-за «сплющивания» Elasticsearch видит это так (имена — отдельно, фамилии — отдельно)
authors.first: ["Иван", "Петр"] authors.last: ["Иванов", "Петров"]
«Сплющивание» существует для обеспечения высокой производительности. Elasticsearch внутри работает на движке Lucene, который не имеет нативного понятия о вложенных объектах. Сплющивая структуру, Elasticsearch преобразует ее в простой плоский формат, который Lucene может эффективно индексировать.
Чтобы решить проблему потери связей в массивах, Elasticsearch предоставляет специальный, отдельный тип данных — nested.
Тип nested (вложенный)
Предназначен для сохранения связи между полями внутри объектов в массиве. Каждый объект в массиве индексируется как отдельный, скрытый мини-документ, что сохраняет внутреннюю структуру.
Допустим, есть каталог товаров. У товаров есть свойства (цвет, размер, вес) — по которым их можно фильтровать.
// Маппинг { "mappings": { "dynamic": "strict", "properties": { "name": { "type": "text" }, "price": { "type": "double" }, "filters": { "type": "nested", "properties": { "prop": { "type": "keyword" }, "value": { "type": "keyword" } } } } } }
// Документ { "name": "Ухокрут", "price": 12345.67, "filters": [ { "prop": "Размер", "value": "большой" }, { "prop": "Цвет", "value": "красный" } ] }
Найти товары, у которых свойство «Размер» имеет значение «большой» и свойство «Цвет» имеет значение «красный»
$ curl -X GET 'localhost:9200/products/_search' -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "bool": { "filter": [ // оба условия должны быть выполнены (логическое И) { "nested": { // внутри nested отдельная область видимости "path": "filters", "query": { "bool": { "filter": [ // используем Filter Context для быстрого поиска { "term": { "filters.prop": "Размер" } }, { "term": { "filters.value": "большой" } } ] } } } }, { "nested": { // внутри nested отдельная область видимости "path": "filters", "query": { "bool": { "filter": [ // используем Filter Context для быстрого поиска { "term": { "filters.prop": "Цвет" } }, { "term": { "filters.value": "красный" } } ] } } } } ] } } }
Найти товары, у которых свойство «Цвет» имеет значение «красный» или «синий»
$ curl -X GET 'localhost:9200/products/_search' -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "bool": { "filter": [ // весь запрос помещаем в Filter Context { "nested": { // внутри nested отдельная область видимости "path": "filters", "query": { "bool": { "filter": [ // используем Filter Context для быстрого поиска { "term": { "filters.prop": "Цвет" } }, { "terms": { "filters.value": ["красный", "синий"] } } ] } } } } ] } } }
Найти товары, у которых свойство «Цвет» имеет значение «красный» или «синий» и свойство «Размер» имеет значение «большой»
$ curl -X GET 'localhost:9200/products/_search' -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "bool": { "filter": [ // оба условия должны быть выполнены (логическое И) { "nested": { // внутри nested отдельная область видимости "path": "filters", "query": { "bool": { "filter": [ // используем Filter Context для быстрого поиска { "term": { "filters.prop": "Цвет" } }, { "terms": { "filters.value": ["красный", "синий"] } } ] } } } }, { "nested": { // внутри nested отдельная область видимости "path": "filters", "query": { "bool": { "filter": [ // используем Filter Context для быстрого поиска { "term": { "filters.prop": "Размер" } }, { "term": { "filters.value": "большой" } } ] } } } } ] } } }
Найти товары, у которых свойство «Размер» имеет значение «большой» или свойство «Цвет» имеет значение «красный»
$ curl -X GET 'localhost:9200/products/_search' -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "bool": { "filter": [ // весь запрос помещаем в Filter Context { "bool": { // внутри filter используем bool, чтобы задать логику "should": [ // should внутри filter работает как логическое ИЛИ { "nested": { // внутри nested отдельная область видимости "path": "filters", "query": { "bool": { "filter": [ // внутренние условия тоже помещаем в Filter Context { "term": { "filters.prop": "Размер" } }, { "term": { "filters.value": "большой" } } ] } } } }, { "nested": { "path": "filters", "query": { "bool": { "filter": [ // внутренние условия тоже помещаеи в Filter Context { "term": { "filters.prop": "Цвет" } }, { "term": { "filters.value": "красный" } } ] } } } } ], "minimum_should_match": 1 // гарантирует, что хотя бы одно условие выполнится } } ] } } }
Здесь нужен отдельный комментарий, как правильно построить запрос прои использовании nested. Верхний filter создает Filter Context (быстрый поиск, без расчета релевантности). Вроде бы нет необходимости еще раз использовать filter на нижнем уровне — контекст уже задан. Но это не так, nested, has_child и has_parent — особенные запросы. Они создают собственный, изолированный скоуп (область видимости) запроса.
То есть, nested — это отдельный мини-поиск, который выполняется для каждого документа
- Elasticsearch находит родительский документ
- Заходит в его вложенные документы, указанные в
path - Выполняет
query, который описан внутриnested
Поскольку query внутри nested — самостоятельный запрос, он следует тем же правилам
- При использовании
mustилиshould— будет работать в Query Context (считать релевантность для вложенных документов) - При использовании
filter— будет работать в Filter Context (просто выдавать ответ «да/нет» для вложенных документов)
Тип join (родитель-потомок)
Назначение — моделирование отношений «один-ко-многим» (например, компания и ее сотрудники, вопрос и ответы на него) для документов, которые должны оставаться независимыми. Это значит, что родительские и дочерние документы можно добавлять и изменять независимо. Поиск выполняется с помощью специальных запросов has_child и has_parent.
Допустим, у нас есть сущности «Вопрос» и «Ответ» на форуме. «Ответ» не может существовать без «Вопроса» (это отношение «один-ко-многим»). При этом «Ответ» — это полноценный, самостоятельный документ. У него есть свой автор, дата, текст. Можно обновить один ответ, не трогая основной вопрос, или искать только по ответам конкретного пользователя.
Чтобы поиск по связям (has_child и has_parent) работал быстро, Elasticsearch вводит одно жесткое правило — родитель и все его потомки обязаны физически находиться на одном и том же шарде (узле). Для этого при индексации дочернего документа нужно явно указать идентификатор его родителя через параметр routing.
Создание индекса со связью, структура «Вопроса» и «Ответа» должна быть одинаковой — хотя это разные сущности, но mappings у них один.
$ curl -X PUT 'localhost:9200/qa_index' -H 'Content-Type: application/json' -d 'json-данные'
{ "mappings": { "dynamic": "strict", "properties": { "text": { "type": "text" }, "author": { "type": "keyword" }, "created_at": { "type": "date" } "join_field": { "type": "join", "relations": { "question": "answer" } } } } }
Индексация родителя (вопроса)
# curl -X PUT 'localhost:9200/qa_index/_doc/1' -H 'Content-Type: application/json' -d 'json-данные'
{ "text": "Как работает Elasticsearch?", "author": "user1", "created_at": "2025-01-02" "join_field": "question" }
Индексация потомка (ответа) с указанием родителя
$ curl -X PUT 'localhost:9200/qa_index/_doc/2?routing=1' -H 'Content-Type: application/json' -d 'json-данные'
{ "text": "Он очень быстрый!", "author": "user2", "created_at": "2025-01-02" "join_field": { "name": "answer", "parent": "1" } }
Пример поискового запроса — найти все вопросы, у которых есть ответы со словом «быстрый»
$ curl -X GET 'localhost:9200/qa_index/_search' -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "has_child": { "type": "answer", "query": { "match": { "text": "быстрый" } } } } }
При удалении родительского документа (вопроса), его дочерние документы (ответы) не удаляются автоматически. Они становятся «осиротевшими» документами (orphaned documents), продолжают занимать место и могут быть найдены в общих поисковых запросах (например, при поиске по всем ответам).
Сначала удалить всех потомков
$ curl -X POST 'localhost:9200/qa_index/_delete_by_query' -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "parent_id": { "type": "answer", "id": "1" } } }
Затем удалить самого родителя
$ curl -X DELETE 'localhost:9200/qa_index/_doc/1'
Как работает токенизатор N-Gram
Токенизатор N-Gram — это мощный инструмент для реализации поиска «по части слова» и повышения устойчивости к опечаткам. Вместо того чтобы разбивать текст на целые слова, N-Gram токенизатор разбивает его на небольшие, перекрывающиеся последовательности символов фиксированной длины N. Это похоже на «скользящее окно», которое движется по слову.
Возьмем для примера слово «поиск». Стандартный токенизатор выдаст один токен «поиск». N-Gram токенизатор с N=2 (биграммы) выдаст набор токенов «по», «ои», «ис», «ск». N-Gram токенизатор с N=3 (триграммы) выдаст набор токенов «пои», «оис», «иск». Допустим теперь, что пользователь допустил опечатку и ввел «посик». Стандартный поиск ничего не найдет. Но если разложить «посик» на биграммы, то будет найдена общая биграмма «по». Этого уже может быть достаточно, чтобы найти совпадение.
Таким образом, N-Gram токенизатор отлично подходит для
- Поиска с опечатками, когда
fuzzinessне справляется или работает медленно - Поиска по части слова (например, по части артикула или серийного номера)
Давайте создадим кастомный анализатор с использованием N-Gram токенизатора
$ curl -X PUT 'localhost:9200/products' -H 'Content-Type: application/json' -d 'json-данные'
{ "settings": { "analysis": { "analyzer": { "my_ngram_analyzer": { "tokenizer": "my_ngram_tokenizer", "filter": [ "lowercase" ] } }, "tokenizer": { "my_ngram_tokenizer": { "type": "ngram", "min_gram": 2, // кусочки длиной от двух до трех символов "max_gram": 3, "token_chars": [ "letter", "digit" ] } } } }, "mappings": { "dynamic": "strict", "properties": { "code": { "type": "keyword" }, "name_ngram": { // для поиска по кусочкам слов в два-три символа "type": "text", "analyzer": "my_ngram_analyzer" }, "price": { "type": "double" } } } }
Примеры поисковых запросов — слово введено с ошибкой (1) или есть только часть слова (2)
$ curl -X GET 'localhost:9200/products/_search' -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "match": { "name": "дриль" // пользователь ищет «дрель», но допустил ошибку } } }
$ curl -X GET 'localhost:9200/products/_search' -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "match": { "name": "дре" // поисковый запрос содержит только часть слова } } }
Можно одноврменно искать по словам (обычный полнотекстовой поиск) и по кусочкам слов с помощью multi-fields и multi_match.
$ curl -X PUT 'localhost:9200/products' -H 'Content-Type: application/json' -d 'json-данные'
{ "settings": { "analysis": { "analyzer": { "my_russian_analyzer": { "tokenizer": "standard", "filter": [ "lowercase", "russian_stemmer" ] }, "my_ngram_analyzer": { "tokenizer": "my_ngram_tokenizer", "filter": [ "lowercase" ] } }, "tokenizer": { "my_ngram_tokenizer": { "type": "ngram", "min_gram": 2, "max_gram": 10 } } } }, "mappings": { "dynamic": "strict", "properties": { "code": { "type": "keyword" }, "name": { "type": "text", "analyzer": "my_russian_analyzer", "fields": { "ngram": { "type": "text", "analyzer": "my_ngram_analyzer" } } }, "price": { "type": "double" } } } }
Поиск по полям name и name.ngram с бустингом
$ curl -X GET 'localhost:9200/products/_search' -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "multi_match": { "query": "дре", "fields": [ "name^3", "name.ngram" ] } } }
Поиск с автодополнением (autocomplete)
Допустим, мы хотим при вводе «mac» — показывать подсказки, то есть товары, которые начинаются на «mac», например «MacBook Pro». Для этого потребуется использовать токенизатор Edge N-Gram и настройку поиска search_analyzer.
Токенизатор Edge N-Gram — это частный случай токенизатора N-Gram. Тоже создает кусочки слова, но всегда начиная с края слова (с начала). Например, с настройками min_gram=2, max_gram=4, для слова «поиск» будут созданы токены «по», «пои», «поис» — каждый токен привязан к началу слова.
По умолчанию поисковый запрос обрабатывается тем же самым анализатором, что и значение поля при индексации. Опция search_analyzer позволяет переопределить это поведение и использовать для обработки поискового запроса другой анализатор.
При индексации (с помощью analyzer) нам нужно разбить «MacBook Pro» на множество маленьких кусочков, чтобы поймать любое начало слова. При поиске (с помощью search_analyzer) — пользователь вводит «mac». Если мы обработаем этот поисковый запрос тем же edge_ngram анализатором, он тоже разобьется на «m», «ma», «mac» — и поиск будет некорректным. Нам нужно искать целиком то, что ввел пользователь — токен «mac». Для этого search_analyzer устанавливается в обычный standard или keyword анализатор, который не будет дробить поисковый запрос.
$ curl -X PUT 'localhost:9200/products' -H 'Content-Type: application/json' -d 'json-данные'
{ "settings": { "analysis": { "analyzer": { "autocomplete_analyzer": { "tokenizer": "autocomplete_tokenizer" } }, "tokenizer": { "autocomplete_tokenizer": { "type": "edge_ngram", "min_gram": 1, "max_gram": 10 } } } }, "mappings": { "dynamic": "strict", "properties": { "code": { "type": "keyword" }, "name": { "type": "text", "analyzer": "autocomplete_analyzer", // используется при индексации "search_analyzer": "standard" // используется при поиске }, "price": { "type": "double" } } } }
$ curl -X GET 'localhost:9200/products/_search' -H 'Content-Type: application/json' -d 'json-данные'
{ "query": { "match": { "name": "mac" // поисковый запрос содержит начало слова } } }
Пагинация результатов поиска
Пагинация с использованием from и size
Традиционный способ пагинации с использованием from и size работает аналогично LIMIT и OFFSET в SQL-запросе.
size— указывает, сколько документов нужно вернуть на одной странице, по умолчанию —10from— указывает, сколько документов нужно пропустить от начала списка, по умолчанию —0
Пример запроса — показать третью страницу результатов, если на каждой странице по 20 товаров
{ "query": { "match_all": {} }, "from": 40, "size": 20 }
Но этот метод очень неэффективен для перехода на далекие страницы. Чтобы показать 1001-ю страницу (from: 10000), Elasticsearch должен найти все 10010 документов, отсортировать их на координирующей ноде и только потом отбросить первые 10000. Это создает огромную нагрузку на память и процессор.
Пагинация с использованием search_after
Это современный и высокопроизводительный способ для реализации пагинации типа «Загрузить еще» или «Следующая страница». Позволяет избежать проблемы глубокой пагинации, которая возникает при использовании from и size.
Вместо того чтобы указывать, сколько документов пропустить (from), мы указываем Elasticsearch точку отсчета — значение (или значения) сортировки последнего документа, который был на предыдущей странице. При этом в поисковом запросе обязательно должно быть sort и не должно быть from.
Запрос первой страницы разультатов поиска
{ "query": { "match_all": {} }, "sort": [ { "price": "asc" }, { "_id": "asc" } ], "size": 10, }
В ответе смотрим на последний (десятый) документ
"hits": [ ....., { "_source": { ..... }, "sort": [5499.99, "product-id-12345"] // значения сортировки } ]
Запрос следующей страницы — значения sort используем в search_after
{ "query": { "match_all": {} }, "sort": [ { "price": "asc" }, { "_id": "asc" } ], "size": 10, "search_after": [5499.99, "product-id-12345"] // откуда продолжить }
Elasticsearch мгновенно «перепрыгнет» к этой точке и вернет следующие 10 документов. Это очень быстро, так как не требует пересортировки всего набора данных.
Как использовать Scroll API
Scroll API — это механизм для эффективного извлечения большого количества документов из одного поискового запроса. Его можно представить как «курсор» базы данных. Он не предназначен для обычной пользовательской пагинации (переключения страниц на сайте). Его главная задача — обработка больших объемов данных, например, для экспорта, переиндексации или фоновой обработки.
Стандартный поиск с параметрами from и size имеет ограничение — по умолчанию нельзя запросить документы глубже 10000-й позиции. Чтобы показать 1001-ю страницу по 10 товаров, Elasticsearch должен найти все 10010 товаров, отсортировать их и отбросить первые 10000. Это очень неэффективно.
Scroll решает эту проблему, создавая временный «снимок» результатов поиска. Не нужно пересчитываеть результаты каждый раз, достаточно прокручиваеть этот снимок порциями.
Первый запрос (инициация) — обычный поисковый запрос, но с добавлением параметра scroll, который устанавливает время жизни «снимка».
$ curl -X GET 'localhost:9200/products/_search?scroll=1m' -H 'Content-Type: application/json' -d 'json-данные'
{ "size": 100, "query": { "match_all": {} } }
Ответ будет содержать первую порцию из 100 документов и специальный идентификатор _scroll_id
{ ..... "_scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WY...", "hits": { ..... } }
Для получения следующей порции документов нужно отправить _scroll_id на специальный endpoint /_search/scroll. Каждый такой запрос сбрасывает таймер времени жизни.
$ curl -X POST 'localhost:9200/_search/scroll' -H 'Content-Type: application/json' -d 'json-данные'
{ "scroll": "1m", "scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WY..." }
Этот шаг нужно повторять до тех пор, пока в ответе массив hits не станет пустым. Это означает, что были получены все документы «снимка».
Очень важно явно удалить scroll-контекст, когда он больше не нужен, чтобы освободить ресурсы на сервере.
$ curl -X DELETE 'localhost:9200/_search/scroll' -H 'Content-Type: application/json' -d 'json-данные'
{ "scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WY..." }
Как использовать Analyze API
Analyze API — инструмент для диагностики и отладки, позволяет увидеть, как именно анализатор (или его отдельные части — токенизатор, фильтры) преобразует текст в токены.
Давайте посмотрим, как анализатор standard обработает строку
$ curl -X POST 'localhost:9200/_analyze?pretty' -H 'Content-Type: application/json' -d 'json-данные'
{ "analyzer": "standard", "text": "The 2 QUICK Brown-Foxes!" }
Результат обработки строки анализатором standard
{ "tokens": [ { "token": "the", "start_offset": 0, "end_offset": 3, "type": "<ALPHANUM>", "position": 0 }, { "token": "2", "start_offset": 4, "end_offset": 5, "type": "<NUM>", "position": 1 }, { "token": "quick", "start_offset": 6, "end_offset": 11, "type": "<ALPHANUM>", "position": 2 }, { "token": "brown", "start_offset": 12, "end_offset": 17, "type": "<ALPHANUM>", "position": 3 }, { "token": "foxes", "start_offset": 18, "end_offset": 23, "type": "<ALPHANUM>", "position": 4 } ] }
Анализатор standard разбил текст по пробелам и дефису, удалил восклицательный знак и привел все токены к нижнему регистру.
Нет необходимости создавать индекс, чтобы протестировать кастомный анализатор — можно создать анализатор «на лету» прямо в запросе.
$ curl -X POST 'localhost:9200/_analyze?pretty' -H 'Content-Type: application/json' -d 'json-данные'
{ "tokenizer": "standard", "filter": [ "lowercase", { "type": "stop", "stopwords": "_russian_" }, { "type": "stemmer", "language": "russian" } ], "text": "Красивые и новые дрели" }
{ "tokens": [ { "token": "красив", "start_offset": 0, "end_offset": 8, "type": "<ALPHANUM>", "position": 0 }, { "token": "нов", "start_offset": 11, "end_offset": 16, "type": "<ALPHANUM>", "position": 2 }, { "token": "дрел", "start_offset": 17, "end_offset": 22, "type": "<ALPHANUM>", "position": 3 } ] }
Если уже есть индекс с настроенными полями — можно проверить, как будет проанализирован текст для конкретного поля.
$ curl -X POST 'localhost:9200/products/_analyze?pretty' -H 'Content-Type: application/json' -d 'json-данные'
{ "field": "name", "text": "Электрические шуруповерты" }
{ "tokens": [ { "token": "электрическ", "start_offset": 0, "end_offset": 13, "type": "<ALPHANUM>", "position": 0 }, { "token": "шуруповерт", "start_offset": 14, "end_offset": 25, "type": "<ALPHANUM>", "position": 1 } ] }
Зачем нужны алиасы
Алиас (alias) — это псевдоним или указатель, который может ссылаться на один или несколько индексов. Они нужны для гибкости и упрощения управления.
- Бесшовное обновление (Zero-Downtime Reindexing). Позволяют переключить приложение с одного индекса на другой (например, с
products_v1наproducts_v2) мгновенно и без остановки сервиса. - Группировка индексов. Можно объединить несколько индексов (например,
logs-2024-11,logs-2023-12) под одним алиасомall_logsи искать по ним всем сразу. - Упрощение кода. Приложение всегда обращается к одному имени алиаса, а не к реальному имени индекса, которое может меняться.
Резервное копирование (snapshot)
Резервное копирование в Elasticsearch — это встроенная и одна из важнейших функций. Для этого используется механизм Snapshot and Restore (Снапшоты и Восстановление). Для этого нужно настроить репозиторий (Repository) — место, где будут храниться резервные копии. Elasticsearch поддерживает несколько типов репозиториев, чаще всего это общая файловая система (NFS) или облачное хранилище S3.
Резервная копия в локальную директорию
Редактируем файл конфигурации и перезапускаем службу
$ sudo nano /etc/elasticsearch/elasticsearch.yml
path.repo: ["/mnt/backups"]
$ sudo systemctl restart elasticsearch.service
Регистрация файлового репозитория
$ curl -X PUT 'localhost:9200/_snapshot/local_backup_repo' -H 'Content-Type: application/json' -d 'json-данные'
{ "type": "fs", "settings": { "location": "/mnt/backups" } }
Создание резервной копии всех индексов и состояния кластера
$ curl -X PUT 'localhost:9200/_snapshot/local_backup_repo/all_indices?wait_for_completion=true'
Создание резервной копии всех индексов, которые начинаются на nginx-
$ curl -X PUT "localhost:9200/_snapshot/local_backup_repo/all_nginx?wait_for_completion=true" \ > -H 'Content-Type: application/json' -d 'json-данные'
{ "indices": "nginx-*", "ignore_unavailable": true }
Восстановление из резервной копии индекса nginx-logs-today
$ curl -X POST 'localhost:9200/_snapshot/local_backup_repo/all_nginx/_restore' \ > -H 'Content-Type: application/json' -d 'json-данные'
{ "indices": "nginx-logs-today" }
Резервная копия в облачное хранилище
Не рекомендуется прописывать ключи доступа (access key и secret key) в файле elasticsearch.yml, для этого существует специальное хранилище — Elasticsearch Keystore. На одном из узлов кластера нужно выполнить команды для добавления ключей в хранилище — Elasticsearch сам синхронизирует их по кластеру.
$ sudo /usr/share/elasticsearch/bin/elasticsearch-keystore add s3.client.timeweb_cloud.access_key Enter value for s3.client.timeweb_cloud.access_key: 5731GB5SS4NFXGEL7RT1 $ sudo /usr/share/elasticsearch/bin/elasticsearch-keystore add s3.client.timeweb_cloud.secret_key Enter value for s3.client.timeweb_cloud.secret_key: ZdHvvsl9rxXByUr8FLlKR3D4SZffdO3EDPuXZMTt
$ sudo systemctl restart elasticsearch.service
Посмотреть список ключай, сохраненных в Elasticsearch Keystore, можно с помощью команды
$ sudo /usr/share/elasticsearch/bin/elasticsearch-keystore list autoconfiguration.password_hash keystore.seed s3.client.timeweb_cloud.access_key s3.client.timeweb_cloud.secret_key xpack.security.http.ssl.keystore.secure_password xpack.security.transport.ssl.keystore.secure_password xpack.security.transport.ssl.truststore.secure_password
Теперь нужно зарегистрировать S3-хранилище как репозиторий в Elasticsearch
$ curl -X PUT "localhost:9200/_snapshot/cloud_timeweb_repo" -H 'Content-Type: application/json' -d 'json-данные'
{ "type": "s3", "settings": { "bucket": "24a5b925-55831976-7054-42a3-8280-df3982169ed0", "client": "timeweb", "endpoint": "s3.twcstorage.ru", "protocol": "https" } }
Создание резервной копии всех индексов и состояния кластера
$ curl -X PUT 'localhost:9200/_snapshot/cloud_timeweb_repo/all_indices?wait_for_completion=true&&pretty'
{ "snapshot": { "snapshot": "all_indices", "uuid": "q9pescUjT5av4_oSdzO3nA", "repository": "cloud_timeweb_repo", "version_id": 8536000, "version": "8.19.0-8.19.5", "indices": [ "catalog_search_3", "catalog_search_2", "catalog_search_1", "products", "catalog_filter" .......... ], "data_streams": [ ".logs-deprecation.elasticsearch-default", ".kibana-event-log-ds", "ilm-history-7" ], "include_global_state": true, // в резервную копию было включено состояние кластера "state": "SUCCESS", // резервное копирование прошло успешно "start_time": "2025-10-20T14:23:14.910Z", "start_time_in_millis": 1760970194910, "end_time": "2025-10-20T14:23:26.748Z", "end_time_in_millis": 1760970206748, "duration_in_millis": 11838, "failures": [ ], "shards": { "total": 61, "failed": 0, "successful": 61 }, "feature_states": [ { "feature_name": "async_search", "indices": [ ".async-search" ] }, { "feature_name": "kibana", "indices": [ .......... ] }, { "feature_name": "tasks", "indices": [ ".tasks" ] }, { "feature_name": "inference_plugin", "indices": [ ".secrets-inference", ".inference" ] } ] } }
Опция wait_for_completion=true заставляет curl ждать, пока процесс не завершится — для больших объемов данных это может занять время.
Чтобы убедиться, что снэпшот создался, можно запросить список всех снапшотов в репозитории
$ curl -X GET 'localhost:9200/_snapshot/cloud_timeweb_repo/_all?pretty'
{ "snapshots": [ { "snapshot": "all_indices", "uuid": "q9pescUjT5av4_oSdzO3nA", "repository": "cloud_timeweb_repo", "version_id": 8536000, "version": "8.19.0-8.19.5", "indices": [ "catalog_search_3", "catalog_search_2", "catalog_search_1", "products", "catalog_filter", // .......... ], "data_streams": [ ".logs-deprecation.elasticsearch-default", ".kibana-event-log-ds", "ilm-history-7" ], "include_global_state": true, "state": "SUCCESS", "start_time": "2025-10-20T14:23:14.910Z", "start_time_in_millis": 1760970194910, "end_time": "2025-10-20T14:23:26.748Z", "end_time_in_millis": 1760970206748, "duration_in_millis": 11838, "failures": [ ], "shards": { "total": 61, "failed": 0, "successful": 61 }, "feature_states": [ { "feature_name": "async_search", "indices": [ ".async-search" ] }, { "feature_name": "kibana", "indices": [ // .......... ] }, { "feature_name": "tasks", "indices": [ ".tasks" ] }, { "feature_name": "inference_plugin", "indices": [ ".secrets-inference", ".inference" ] } ] } ], "total": 1, "remaining": 0 }
В ответе будут все снэпшоты и все индексы в каждом снэпшоте — разбирать все это неудобно, но можно отфильтровать с помощью утилиты jq
$ curl -s -X GET 'localhost:9200/_snapshot/cloud_timeweb_repo/_all?pretty' | \ > jq '[.snapshots[] | {name: .snapshot, status: .state, end_time: .end_time, indices: (.indices | map(select(startswith(".") | not)))}]'
[ { "name": "all_indices", "status": "SUCCESS", "end_time": "2025-10-20T14:23:26.748Z", "indices": [ "catalog_search_3", "catalog_search_2", "catalog_filter", "products", "catalog_search_1", ] } ]
Чтобы проверить восстановление индекса — сначала удалим его, а потом восстановим из облачного хранилища
$ curl -X DELETE "localhost:9200/catalog_search_3"
$ curl -I "localhost:9200/catalog_search_3" HTTP/1.1 404 Not Found X-elastic-product: Elasticsearch content-type: application/json content-length: 425
$ curl -X POST 'localhost:9200/_snapshot/cloud_timeweb_repo/all_indices/_restore?wait_for_completion=true&pretty' \ > -H 'Content-Type: application/json' -d 'json-данные'
{ "indices": "catalog_search_3" }
{ "snapshot": { "snapshot": "all_indices", "indices": [ "catalog_search_3" ], "shards": { "total": 1, "failed": 0, "successful": 1 } } }
$ curl -I "localhost:9200/catalog_search_3" HTTP/1.1 200 OK X-elastic-product: Elasticsearch content-type: application/json Transfer-Encoding: chunked
Удалим и восстановим все индексы, которые начинаются на catalog_. Здесь есть важный момент — ElasticSearch не позволит удалить сразу несколько индексов, для этого нужно временно отключить защиту.
$ curl -X DELETE "localhost:9200/catalog_*"
$ curl -X POST "localhost:9200/_snapshot/cloud_timeweb_repo/all_indices/_restore?wait_for_completion=true&pretty" \ > -H 'Content-Type: application/json' -d 'json-данные'
{ "indices": "catalog_*" }
{ "snapshot": { "snapshot": "all_indices", "indices": [ "catalog_search_2", "catalog_filter", "catalog_search_1", "catalog_search_3" ], "shards": { "total": 4, "failed": 0, "successful": 4 } } }
- ElasticSearch. Начало работы. Часть 2 из 3
- SSH как SOCKS сервер
- RESTfull API приложение на фреймворке Express.js
- Let's Encrypt. Получение и обновление сертификатов
- Настройка кластера MariaDB Galera на сервере Ubuntu 18.04
- Утилита командной строки Wget
- Postfix и Dovecot. Установка и настройка. Часть 2 из 2
Поиск: API • HTTP • HTTPS • Linux • База данных • Поиск • Сервер • Индекс • Elasticsearch • Kibana • Logstash