В Elasticsearch существует свой собственный язык запросов — Query DSL (в дальнейшем EQL).

Обладая широкими возможностями и гибкостью, он в тоже время крайне многословен и плохо читаем - основан на json .

В Kibana используется свой язык — Kibana Query Language (KQL), основанный на Lucene Query Syntax .

По сути KQL является надстройкой над EQL и призван облегчить взаимодействие пользователя с поисковой строкой.

В данном посте предлагаю пробежаться по основным возможностям KQL, а так же подсмотреть в каком виде запросы прилетают в Elasticsearch (в дальнейшем ES).

Для тестов я буду использовать набор nginx логов, предварительно загруженных в ES.

Загрузить файл с логами в ES можно напрямую через curl:

curl -H "Content-Type: application/x-ndjson" -XPOST http://localhost:9200/_bulk --data-binary "@<FILENAME_WITH_LOGS>" -u <YOUR_ES_USER>

Типичный экземпляр лога может содержать поля (часть опущена для краткости):

"message": "...GET /posts/golang-prometheus HTTP/2.0 ...Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0)...Firefox/54.0",

"client.geo.country_name": "Russia",

"url.path": "/posts/golang-prometheus",

"user_agent": {
  "name": "Firefox",
  "version": "54.0"
}

Индексы, отображения и простые запросы

И так, начнем.

Допустим мы хотим отобразить все логи, где пользователи запрашивали страницу https://mixanemca.ru/posts/golang-prometheus .

Искать будем по полю message. Не долго думая вбиваем:

message:posts/golang-prometheus

И… результат нас не устраивает.

Да, мы видим те документы, которые хотели, но и кучу лишних со значениями:

posts/posts/gtd/
posts/
tags/golang/
...

Давайте разбираться что и почему.

Для просмотра полного запроса в json идем сюда:

kibana-queries-1-set-lucene

...
"query": {
  "bool": {
    "must": [],
    "filter": [
      {
        "bool": {
          "should": [
            {
              "match": {
                "message": "posts/golang-prometheus"
              }
            }
          ],
          "minimum_should_match": 1
        }
      },
      {
        "range": {
          "@timestamp": {
            "gte": "2019-12-31T21:00:00.000Z",
            "lte": "2020-03-31T20:30:00.000Z",
            "format": "strict_date_optional_time"
          }
        }
      }
    ],
    "should": [],
    "must_not": []
  }
}
...

Именно в таком виде в ES прилетает запрос.

Здесь и далее будем фокусироваться на его сути, опуская второстепенные детали:

"match": {
  "message": "posts/golang-prometheus"
}

Запрос имеет тип match .

Это стандартный запрос для полнотекстового поиска. Он возвращает коль сколько нибудь подходящие под него документы — удовлетворяющий как весь целиком, так и любую из его частей.

Про индексы

Полезно вспомнить как данные хранятся в индексах ES, точнее в индексах Lucene. Их еще называют обратными .

Представим, у нас есть несколько документов содержащие строки:

"posts/golang-prometheus"       # Первый документ
"posts/gtd"                     # Второй документ
"posts/golang-errgroup/"        # Третий документ

Обратный индекс примет вид:

"posts":       {0, 1, 2}
"golang":      {0, 2}
"prometheus":  {0}
"gtd":         {1}
"errgroup":    {2}

Т.е. каждый документ разбивается на отдельные слова(токены), причем все символы токенов приводятся к их строчных аналогам. Такое поведение будет характерно для типа данных text .

Справа отображаются номера документов, откуда токены были извлечены. Занимается этим standard analyzer .

Поэтому ES возвратил документы как с искомыми полями, так и кучу “родственников”.

Исправляем ситуацию

Дабы отсеять лишнее, обернем запрос в двойные кавычки:

message:"posts/golang-prometheus"

и заглянем в json:

"match_phrase": {
    "message": "get"
 }

match_phrase – фразовый запрос, он “склеивает” токены вместе и выдает документы только со 100% совпадением.

Годится!

Это будет работать и в google search

Поиск по всему документу

Опустив имя конкретного поля, поищем совпадения по всему документу.

На выбор два варианта — использовать кавычки или нет:

"posts/golang-prometheus"
posts/golang-prometheus

соответственно за них отвечают и разные подтипы запросов:

### для поиска цельной фразы (вариант с кавычками)
"multi_match": {
    "type": "phrase",
    "query": "posts/golang-prometheus"
  }

### для "свободного" поиска по всем вхождениям в запрос (без кавычек)
"multi_match": {
    "type": "best_fields",
    "query": "posts/golang-prometheus"
 }
  • multi_match строит свой результат из склейки match запросов по каждому полю документа;
  • Способ выполнения запроса будет зависеть от его типа:
    • тип phrase запускает match_phrase для каждого из полей;
    • тип best_fields возвращает документы при совпадением токенов.

Разобравшись как выглядят индексы и простейшие запросы под капотом, рассмотрим еще несколько вариантов.

Регулярные выражения (regex)

Из Wiki: Регуля́рные выраже́ния (англ. regular expressions) — формальный язык поиска и осуществления манипуляций с подстроками в тексте, основанный на использовании метасимволов (символов-джокеров, англ. wildcard characters).

Если простым языком, то regex позволяют задать шаблон выражения/строки, по которому происходит поиск в документе.

Стоит заметить, регулярные выражения работают только в Lucene Query Syntax . Изменим режим поиска на lucene:

kibana-queries-1-inspect-json

Для использования rexep требуется обернуть паттерн в две косые черты (/.../).

Для примера отобразим логи тех запросов, которые были отправлены из браузера Firefox с версий отличающихся от 54.0:

user_agent.name:Firefox AND user_agent.version:/[^5].\.0/

Давайте разбираться:

  • В user_agent.name поле я указал Firefox с большой буквы, т.к. тип у данного поля keyword .

Как так вышло, мы поговорим в следующий раз. Но если коротко, при импорте лог-данных в ES, я предварительно загрузил index templates для создания схемы данных. В ней поле user_agent.name числится за типом keyword , а значения данного поля не приводятся к нижнему регистру при складировании в index.

  • user_agent.version:/[^5].\.0/
    • в поле user_agent.version мы ищем значения, первый символ которого не должен быть 5;
    • далее идет управляющий символ ., олицетворяющий собой один любой символ;
    • так как символ . является зарезервированным, то для использования обычной точки (.) его требуется экранировать через \ .

regex синтаксис очень гибок, он позволяет описывать почти любые последовательности. Больше подробностей читайте по ссылке .

Boolean запросы

Выше мы уже затронули оператор AND (логическое И). Существует оператор OR (логическое ИЛИ). Они относятся к Boolean запросам .

Важно держать в голове, что AND имеет более высокий приоритет над OR и запрос:

user_agent.name:Firefox AND url.path:"posts/golang-prometheus" OR  url.path:"posts/gtd"

вернет документы только с user_agent.name:Firefox и url.path:"posts/golang-prometheus", отбрасывая url.path:"posts/gtd".

Группировка

Для группировки запросов нужно поместить их в круглые скобки:

user_agent.name:Firefox AND (url.path:"posts/golang-prometheus" OR  url.path:"posts/gtd")

На выходе мы получим логи посетителей статей golang-prometheus или gtd из браузера Firefox.

Операторы NOT и Plus

Оператор исключения - (NOT):

-client.geo.country_name:China

отобразит запросы из всех стран, за исключением Китая.

NOT и ! являются синонимами

Оператор + (Plus) имеет обратную логику.

Wildcard queries

И напоследок операторы wildcard :

  • * (asterisk)
  • ? (questionmark)

Первый способен подменять собой 0 или более символов, второй ноль или один символ:

client.geo.country_name:A?gent*

Отобразит все запросы из Аргентины. Или Aдгента, если таковой имеется;)