Skip to content

Latest commit

 

History

History
1393 lines (1066 loc) · 70.8 KB

Elasticsearch.md

File metadata and controls

1393 lines (1066 loc) · 70.8 KB

Базовые понятия Elasticsearch

Elasticsearch (эластичный поиск) — распределённый (distributed), RESTful поисковой движок по всему тексту (full-text search).

Elasticsearch использует JSON-документы без схемы. Эти документы передаются при помощи REST API для сохранения их в хранилище и поиска.

Elasticsearch построен поверх поискового движка Apache Lucene, написанном на Java.

Индекс, тип, документ

Индекс (Index) — эквивалент базы данных в SQL или NoSQL.

Тип (Type) — эквивалент таблицы в SQL или коллекции в NoSQL.

Имеется тип по умолчанию, который создаётся автоматически (_doc) при создании индекса.

Документ (Document) — эквивалент строки в SQL или документа в NoSQL.

Документы хранятся в JSON-формате

 {
    "_index": "notes",
    "_type": "_doc",
    "_id": "sYKRT3EBYbOH8y9AHDYY",
    "_source": {
      "name": "Elasticsearch",
      "author": "Max-Starling",
      "date": "2020-04-07T23:15:50Z"
    }
}

Полнотекстовый поиск и индексирование

Полнотекстовый поиск (Full text searching) — поиск документов не по их идентификаторам, а по их содержимому.

Индексирование (Indexing) в поисковых системах — процесс добавления сведений (о сайте) роботом поисковой машины в базу данных, которая используется для полнотекстового поиска информации на проиндексированных сайтах.

В рамках Elasticsearch индексированием называют запись данных в индекс.

Реплики и шарды

Индексы обычно разредяются на несколько подиндексов (sub-indices), называемые осколками, шардами (shards). Каждый шард хранит какую-то часть документов индекса.

Шардинг (Sharding) — процесс разбиения на шарды и одна из стратегий масштабирования баз данных.

Шарды распределяются между несколькими узлами, экземплярами приложения (Elasticsearch nodes).

Каждый шард является экземпляром дижка Lucene. Таким образом все данные хранятся в Lucene, а Elasticsearch распределённо управляет этими данными.

Количество шардов указано в настройках индекса в свойстве number_of_shards.

Резервная копия всех шардов называется репликой (replica). Если один экземпляр приложения падает вместе с данными, которые на нём хранились, реплика позволяет не терять эти данные.

Репликация (Sharding) — процесс, при котором данные постоянно реплицируются (копируются) на один или несколько других серверов. Репликация тоже является стратегией масштабирования баз данных.

Количество реплик указано в настройках индекса в свойстве number_of_replicas.

Перевёрнутые индексы

Elasticsearch хранит данные как перевёрнутые индексы (inverted indexes) на диске, что позволяет очень быстро искать данные.

Поисковой движок Apache Lucene реализует перевёрнутый индекс, используя структуру данных список с пропусками (Skip List). Эта структура данных основана на связных списках, но по трудоёмкости сравнима с двоичным деревом поиска (B-tree).

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

Индекс в Lucene очень похож на индекс в конце книги: указываются определённый список ключевых слов (важных определений) и рядом страницы, на которых они упоминаются. Index example

Аналогично работает и Google. Рядом с проиндексированными словами сохраняются ссылки на страницы. Таким образом их можно найти при поиске. Это же касается и Elasticsearch, только вместо ссылок на страницы используются ссылки на документы.

Пример с разбиением на слова

Посмотрим, как обратные индексы работают на примере.

Пусть у нас есть два текста.

  • I don't like to work alone.
  • I often work on weekends.

Разобьём их на слова и выберем только уникальные слова из обоих текстов.

Множество уникальных слов будет следующим: I, don't, like, to, work, alone, often, on, weekends.

Сохраним каждое уникальное слово в память, а рядом с ним — ссылки на те документы, которые используют это слово.

Слово Документ #1 Документ #2
I + +
don't +
like +
to +
work + +
alone +
often +
on +
weekends +

Теперь произведём поиск по словам. Документ либо содержит слово, либо не содержит.

Например, поиск по слову work выдаст оба документа, по слову like — только первый, по слову often — только второй.

Если ввести искать по двум словам одновремено often и alone, то выдадутся оба документа.

Слово Документ #1 Документ #2
alone +
often +

Маппинги и типы данных

Маппинг (mapping) — процесс, определяющий, как документ и поля в нём хранятся и индексируются.

Маппинги задаются при создании индекса в поле mappings. Они содержат свойства документов и типы их значений.

Поскольку маппинги могут быть разными для разных типов type индекса, они привязываются не к самому индексу, а к самим типам (например, к типу по умолчанию _doc). Поэтому при создании маппинга нужно всегда указывать тип.

Если mappings не задаётся при создании индекса, то Elasticsearch создаёт его автоматически в режиме реального времени на основании данных индексируемых документов.

Создание индекса

Для создания индекса используется PUT-запрос с его названием.

PUT <ELASTICSEARCH_URL>/index_name
/* response body */
{
  "acknowledged": true,
  "shards_acknowledged": true,
  "index": "index_name"
}

Простые типы данных

  • Строковые (string): text, keyword.
  • Числовые (Numberic): long, integer, short, byte, double, float, half_float, scaled_float.
  • Логический (Boolean): boolean.
  • Дата (Date): date.
  • Бинарный (Binary): binary.
  • Диапазон (Range): integer_range, float_range, long_range, double_range, date_range.

Тип данных text

Строковый тип text используется для индексирования полнотекстовых значений (full-text values). Примерами полнотекстовых значений являются поля: название, сообщение, описание.

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

Каждое полнотекстовое поле проходит перед индексированием через анализатор (analyzer), который конвертирует строку в список отдельных термов (list of individual terms), затем этот список индексируется, а поле называют проанализированным (analyzed).

Анализирование (analysis) позволяет Elasticsearch искать отдельные слова в каждом полнотекстовом поле.

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

Задание типа text.

PUT <ELASTICSEARCH_URL>/index_name
Content-Type: application/json

{
  "mappings": {
    "_doc": {
      "properties": {
        "description": {
          "type":  "text"
        }
      }
    }
  }
}

Тип данных keyword

Строковый тип keyword (ключевое слово) используется для индексирования таких значений, как: ID, email, hostname, status code, tag и прочих. Эти значения используются для фильтрации, сортировки и агрегации.

Поля типа keyword не анализируются. Они ищутся только по точному значению, совпадению (exact value). Например, нельзя найти [email protected] по слову tom или gmail.

Задание типа keyword.

PUT <ELASTICSEARCH_URL>/index_name
Content-Type: application/json

{
  "mappings": {
    "_doc": {
      "properties": {
        "email": {
          "type":  "keyword"
        }
      }
    }
  }
}

Составные типы данных

  • Объект (Object): object. Для JSON-объекта.
  • Вложенный (Nested): nested. Для массива JSON-объектов.

Объект

Объект (Object) предназначен для хранения JSON-объектов.

JSON-объекты могут содержать в себе другие JSON-объекты, то есть они имеют иерархичную структуру.

Например, для индексируемого объекта ниже

{
  "name": "Manfredi",
  "age": 27,
  "settings": {
    "theme": "dark"
  }
}

может быть задан следующий маппинг.

{
  "mappings": {
    "_doc": {
      "properties": {
        "email": {
          "name":  { "type": "text"  },
          "age": { "type": "integer"  },
          "settings": {
            "properties": {
              "theme": { "type": "keyword"  }
            }
          }
        }
      }
    }
  }
}

Elasticsearch распознаёт, что поле settings является объектом, благодаря свойству properties. Вложенные свойства properties отображают иерархию JSON-объектов.

Массив

Массив (Array) в Elasticsearch не существует как отдельная сущность, поскольку любое поле может иметь одно или несколько значений по умолчанию. Тем не менее, все эти значения должны быть одного типа.

  • Массив строк: ["1", "two"].
  • Массив чисел: [1, 7].
  • Массив объектов [{ "name": "John", "experience": 5 }, { "name": "Sam", "experience": 3 }].

В массиве объектов нельзя выделить конкретный объект. При такой необходимости нужно использовать тип nested.

Массивы смешанных типов не поддерживаются: [1, "two"].

Пустой массив интерпретируется как отсутствующее значение (поле без значений).

Вставка объекта с массивом tags.

PUT <ELASTICSEARCH_URL>/index_name/_doc/1
Content-Type: application/json

{
  "message": "The problem with useEffect",
  "tags":  [ "js", "react", "react-hooks" ]
}

Особенность массива объектов

В массиве объектов Elasticsearch не рассматривает объекты как независимые сущности, поэтому Elasticsearch просто разбивает их на список полей и значений.

Например, вставка следующего массива объектов

PUT <ELASTICSEARCH_URL>/index_name/_doc/1
Content-Type: application/json

{
  "user" : [ 
    {
      "firstName" : "Max",
      "lastName" :  "Starling"
    },
    {
      "firstName" : "Richard",
      "lastName" :  "Stone"
    }
  ]
}

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

{
  "user.firstName" : [ "Max", "Richard" ],
  "user.lastName" :  [ "Starling", "Stone" ]
}

Связь между полями firstName и lastName теряется и они уже больше не являются одним объектом.

Поэтому при поиске следующий запрос не выдаст совпадений.

[
  { "match": { "user.firstName": "Max" }},
  { "match": { "user.lastName": "Starling" }}
]

Сравнение массива объектов и вложенного типа

Вложенный тип данных (Nested datatype) — специальный подтип объекта (object), который позволяет индексировать массив JSON-объектов таким образом, чтобы объекты можно было получать отдельно друг от друга.

Специализированные типы данных

  • IP: ip. Для IPv4 и IPv6 адресов.
  • Completion (Completion datatype): completion. Для автозаполнения предложений.
  • Join: join. Для создания отношений между документами одного индекса.
  • Search-as-you-type: search_as_you_type. Для поиска по мере ввода.

Мультиполя

Elasticserach предоставляет возможность хранить одно и то же полt несколькими способами для разных целей. Такое поле называется мультиполем (multi-field).

Например, текстовое поле может быть одновременно представлено типом text для полтотекстового поиска и типом keyword для сортировки и агрегаций.

Для определения мультиролей используется свойство fields, значением которого выступает объект. Ключом этого объекта является название мультиполя, а в значении описывается тип.

Например, создадим текстовое поле position, которое может быть использовано как ключевое слово.

PUT <ELASTICSEARCH_URL>/index_name
Content-Type: application/json

{
  "mappings": {
    "properties": {
      "position": {
        "type": "text",
        "fields": {
          "keyword": { 
            "type":  "keyword"
          }
        }
      }
    }
  }
}

Теперь, при необходимости использования position как ключевого слова, необходимо писать position.keyword, иначе оно будет работать как текстовое значение.

Получение текущих маппингов

Текущие маппинги индекса можно получить по GET-запросу _mappings.

GET <ELASTICSEARCH_URL>/index_name/_mappings

Работа с данными

Создание индекса

Создание индекса с названием index_name.

PUT <ELASTICSEARCH_URL>/index_name

Создание типа

PUT <ELASTICSEARCH_URL>/index_name/type_name

В последних версиях Elasticsearch рекомендуется не создавать тип, а использовать тип по умолчанию _doc.

Удаление индекса

DELETE <ELASTICSEARCH_URL>/index_name

Создание документа

Создание документа в типе type_name индекса index_name.

POST <ELASTICSEARCH_URL>/index_name/type_name
Content-Type: application/json

{
  "name": "Alen Stone",
  "job": "Full-stack Enginer"
}

Обновление документа по ID

Обновление документа типа _doc в индексе users по id.

PUT <ELASTICSEARCH_URL>/users/_doc/H3tVi3ABpFL-9AlTbAgj
Content-Type: application/json

{
  "name": "Richard Stone",
  "job": "Full-stack Enginer"
}

Удаление документа по ID

Удаление документа типа _doc по id.

DELETE <ELASTICSEARCH_URL>/users/_doc/H3tVi3ABpFL-9AlTbAgj

Группировка нескольких запросов в один (bulk)

Elasticasearch предоставляет возможность группировки нескольких изменяющих данные запросов в один при помощи специального POST-запроса /_bulk.

При помощи этого запроса можно манипулировать данными в нескольких индексах и их типах одновременно.

Запрос и его тело выглядят примерно следующим образом.

POST <ELASTICSEARCH_URL>/_bulk

{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "delete" : { "_index" : "test", "_id" : "3" } }
{ "update" : { "_index" : "test", "_id" : "1" } }
{ "doc" : { "field2" : "value2" } }

Возможные операции

  • create - создание документа. Выдаёт ошибку, если документ с указанным _id уже существует.
  • index - создание документа (аналогично create, но без ошибки, а с замещением существующего).
  • update - обновление части документа, указанной в переданном свойстве doc.
  • delete - удаление документа.

Указание _index является обязательным, указание _id - нет (это поле может быть сгенерировано автоматически).

Можно также указать type, но поскольку он не указан, все документы индексируются в _doc.

В конце тела запроса обязателен переход на новую строку.

Пример вставки двух пользователей

POST <ELASTICSEARCH_URL>/_bulk
Content-Type: application/json

{ "index": { "_index": "users", "_id" : "1" } }
{ "name": "Harry Smith", "job": "Dev Ops" }
{ "index": { "_index": "users", "_id" : "2" } }
{ "name":" Sam Brave", "job": "QA" }
   

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

Для получения документов индекса используется запрос /_search.

Вид запроса, параметры запроса и ответ

Все документы

Получить информацию о всех документах можно отправив GET-запрос, ничего не указывая.

GET <ELASTICSEARCH_URL>/users/_search

Ответ выглядит следующим образом.

/* response body */
{
  "took": 80,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 1.0,
    "hits": [
        {
          "_index": "users",
          "_type": "user",
          "_id": "Jntsi3ABpFL-9AlTdggH",
          "_score": 1.0,
          "_source": {
            "name": "Harry Smith",
            "job": "Dev Ops"
          }
        },
        {
          "_index": "users",
          "_type": "user",
          "_id": "J3tsi3ABpFL-9AlTdggH",
          "_score": 1.0,
          "_source": {
              "name": "Sam Brave",
              "job": "QA"
          }
        }
    ]
  }
}

Наиболее полезная информация лежит в свойстве hits: hits.total.value - количество всех докуметров, удовлетворяющих поиску, hits.hits - массив самих документов (хранятся в _source) и дополнительной информации о них. По умолчанию возвращается только 10 документов (см. Пагинация).

По конкретному слову (все поля документа)

Поиск по конкретному слову (слову ops) во всех полях (и name, и job).

GET <ELASTICSEARCH_URL>/users/_search?q=ops

Query DSL

Elasticsearch предоставляет Query DSL (Domain Specific Language) — предметно-ориентированный язык, позволяющий описывать запрос query в формате JSON и отправлять его в теле запроса (request body). Тело можно отравлять даже с GET-запросами.

Создатели Elasticsearch предлагают рассматривать Query DSL как абстрактное синтаксическое дерево (AST, Abstract Syntax Tree) запросов, которое имеет два типа предложений (clauses):

  • Листовые, конечные (leaf query clauses). Предназначены для поиска определённого значения в определённом поле. Пример: match, term, range.
  • Составные (compound query clauses). Предназначены для логического объединения листовых и других составных предложений. Пример: bool.

Основные запросы Elasticsearch

Запрос с match_all

Является самым простым запросом, поскольку возвращает все документы и не принимает какие-либо параметры.

{
  "match_all": {}
}

Сам по себе избыточен, но может использоваться в комбинации с более сложными запросами.

Запрос с match

В запрос с match передаётся текстовое, числовое, логическое значение или дата. Результатом поиска становятся документы, которые соответствуют переданному значению.

{
  "match": {
    "fieldName": "search text"
  }
}

Если значением является текст, то он анализируется перед поиском. Поэтому запрос с match является стандартным для полнотекстового поиска.

Пример поиска пользователя с именем Sam.

```http
GET <ELASTICSEARCH_URL>/users/_search
Content-Type: application/json

{
  "query": {
    "match": {
      "name": "Sam"
    }
  }
}

Краткая версия запроса.

GET <ELASTICSEARCH_URL>/users/_search?q=name:Sam

Запрос с term

В запрос с term так же передаётся текстовое, числовое, логическое значение или дата.

В отличии от запроса с match, результатом запроса с term являются документы, которые содержат точный терм (exact term) в указанном поле.

{
  "term": {
    "fieldName": "a"
  }
}

Текстовое значение, переданное в запрос с term не анализируется. Но данные документов уже проанализированы перед индексированием, поэтому поиск может выдать неверные результаты и лучше не использовать запрос с term для полей типа text, а использовать с типом keyword.

Запрос с terms

Запрос с terms аналогичен запросу с term, но позволяет передать несколько допустимых значений вместо одного.

{
  "terms": {
    "fieldName": ["a", "b"]
  }
}

Важно отметить, что при работе с массивом значений в документе свойства term и terms ищут не точное совпадение, а включение (contains). Оба примера выше с term и с terms включат в выборку документ со значением ["a", "b", "c"].

Запрос с range

Запрос с range позволяет задать промежуток значений для конкретного поля. Результатом станут документы, удовлетворяющее промежутку.

Предоставляемые свойства для задания промежутка:

  • gt - больше (greater than).
  • lt - меньше (less than).
  • gte - больше или равно (greater than or equal to).
  • lte - меньше или равно (less than or equal to).

Пример запроса для поиска пользователей от 18 до 25 лет.

GET <ELASTICSEARCH_URL>/users/_search
Content-Type: application/json

{
  "query": {
    "range": {
      "age": {
        "gte": 18,
        "lte": 25
      }
    }
  }
}

Запрос с exists и missing

Запрос с existsmissing) позволяет находить документы, у которых значение конкретного поля присутствует (отсутствует).

{
  "exists": {
    "field": "username"
  },
  "missing": {
    "field": "bio"
  }
}

Поиск по нескольким полям

Для поиска по нескольким полям используется запрос с multi_match.

GET <ELASTICSEARCH_URL>/users/_search
Content-Type: application/json

{
 "query": {
   "multi_match": {
     "query": "sam",
     "fields": ["firstName", "lastName"]
   }
}

Можно задавать приоритеты полей при помощи символа ^. В примере ниже firstName в два раза важнее lastName.

"fields": ["firstName^2", "lastName"]

Если свойство fields не указано, то по умолчанию Elasticsearch берёт из маппинга все поля индекса, которые удовлетворяют типу искомого значения, и ищет по этим полям.

Пагинация

За пагинацию отвечают параметры size и from запроса _search.

Параметр size определяет количество возвращаемых документов. Значение по умолчанию: 10.

Свойство from отвечает за смещение документов (количество документов, которые должны быть пропущены).

Пример пагинации

Создадим индекс films с объектами, имеющими поля name (keyword), date, rating.

PUT <ELASTICSEARCH_URL>/films
Content-Type: application/json

{
  "mappings": {
    "properties": {
      "name": { "type": "keyword" },
      "date": { "type": "date" },
      "rating": { "type": "float" }
    }
  }
}

Проиндексируем 3 фильма.

PUT <ELASTICSEARCH_URL>/films/_doc/_bulk
Content-Type: application/json

{ "index":{} }
{ "name": "film 1", "date": "2020-05-01T12:10:30Z", "rating": 4.5 }
{ "index":{} }
{ "name": "film 2", "date": "2020-06-30T16:00:45Z", "rating": 4.5 }
{ "index":{} }
{ "name": "film 3", "date": "2020-04-07T23:15:50Z", "rating": 4.7 }

Сделаем поисковый запрос к индексу и добавим параметр size.

GET <ELASTICSEARCH_URL>/films/_doc/_search
Content-Type: application/json

{
  "size": 2
}

Результат

[{
  "...": "...",
  "name": "film 1"
},
{
  "...": "...",
  "name": "film 2"
}]

Добавим также параметр from.

GET <ELASTICSEARCH_URL>/films/_doc/_search
Content-Type: application/json

{
  "size": 2,
  "from": 1
}

Результат

[{
  "...": "...",
  "name": "film 2"
},
{
  "...": "...",
  "name": "film 3"
}]

Релевантность, контекст запроса и контекст фильтра

По умолчанию Elasticsearch ищет по релевантности, которая показывает, насколько запрос документ удовлетворяет поисковому запросу.

Релевантность определяется оценкой релевантности (relevance score). Эта оценка зависит от самого поискового запроса, а также от контекста.

В контексте запроса (query context) предложения (query clauses) отвечают на вопрос "Насколько хорошо документ удовлетворяет запросу?". Помимо выяснения, соответствует ли документ запросу или нет, вычисляется оценка релевантности и записывается в мета-свойство _score в ответе.

Контекст запроса относится к параметру query.

В контексте фильтра (filter context) предложения отвечает на запрос "Удовлетворяет ли документ запросу?". Ответом является либо да, либо нет, и документ либо включается в выборку, либо нет в соответствии с ответом.

Примером фильтра являются свойства filter и must_not для запроса с bool, о которых будет рассказано далее.

Фильтрация

Фильтр (в программировании) — функция или программа, которая принимает структуру данных (обычно список) и возвращает новую структуру данных, содержащую только те элементы из исходной структуры, которые удовлетворяют заданному условию.

Elasticsearch предоставляет составное предложение bool, которое позволяет задавать условия запроса и комбинировать их.

Предложение bool принимает объект, свойства которого обозначают логические операции.

  • must — аналог логического И (AND, объединение условий).
  • should — аналог логического ИЛИ (OR, пересечение условий).
  • must_not — аналог логического НЕ (NOT, исключение). Выполняется в контексте фильтра, поэтому не высчитывает оценку релевантности.

Также bool предоставляет свойство filter. Оно работает аналогично must, но в контексте фильтра, поэтому вычисление оценки релевантности игнорируется.

Каждое из указанных выше свойств может принимать объект, содержащий условие, или массив таких объектов.

Условия задаются при помощи свойств match, term, terms, range.

Пример поиска специалиста с фильтрами

В следующем примере производится поиск специалиста, который:

  • Имеет позицию Software Enginer И знает технологии React, Vue (must).
  • Имеет опыт работы более одного года ИЛИ его желаемый уровень заработной платы не привышает 1000$ (should).
  • Его возраст НЕ меньше 25 лет. (must_not).
{
  "query" : {
    "bool" : {
      "must": [{
        "term": {
          "position": "Software Enginer"
        }
      }, {
        "terms": {
          "technology": ["React", "Vue"]
        }
      }],
      "should": [{
        "range": {
          "experience": {
            "gte": "1 year"
          }
        }
      }, {
        "range": {
          "desiredSalary": {
            "lte": "1000 USD"
          }
        }
      }],
      "must_not": {
        "range": {
          "age": {
            "lte": 25
          }
        }
      }
    }
  }
}

Важное замечание про should

Несмотря на то, что should работает как логическое ИЛИ, позволяя задавать несколько условий, не все из которых должны выполняться одновременно, по умолчанию при наличии must ни одно из условий should не должно обязательно выполняться.

В этом случае should просто увеличивает значимость тех документов (увеличивая значение _score, которое по умолчанию = 1), которые удовлетворяют заданным условиям, но не исключает из выборки те документы, которые не условиям удовлетворяют.

Чтобы это исправить, нужно использовать свойство minimum_should_match. Оно позволяет задать минимальное количество условий should, которые должны выполниться, чтобы документ попал в выборку.

По умолчанию свойство minimum_should_match = 1, если отсутствуют must или filter, 0 — иначе.

В примере ниже искомый специалист обязательно должен или иметь опыт больше года, или иметь желаемую зарплату меньше 1000$.

{
  "query": {
    "bool": {
      "must": [],
      "should": [{
        "range": {
          "experience": {
            "gte": "1 year"
          }
        }
      }, {
        "range": {
          "desiredSalary": {
            "lte": "1000 USD"
          }
        }
     }],
     "minimum_should_match": 1
    }
  }
}

Если количество условий в should совпадает с minimum_should_match, то should вернёт те же документы, что и must при тех же условиях.

Если количество условий в should меньше, чем minimum_should_match, то вернётся пустая выборка.

Сортировка

Elasticsearch позволяет при поиске сортировать документы по одному или нескольким полям.

За сортировку (sort) отвечает параметр sort.

Сортировка может производиться по возрастанию (asc, ascending) и по убыванию (desc, descending).

{
  "sort" : [
    { "likes" : { "order" : "desc" } },
    { "date" : { "order" : "asc" } }
  ]
}

При сортировке по нескольким полям более приоритетным является то поле, которое указано первым в массиве.

Сортировка полей-массивов

Elasticsearch также поддерживает сортировку полей, значениями которых являются массивы. В этом случае доступны следующие режимы (mode)

  • min — сортировка по минимальным значениям массивов.
  • max — сортировка по максимальным значениям массивов.
  • avg — сортировка по средним значениям массивов.
  • sum — сортировка по сумме значений массива.
{
  "sort" : [
    { "values" : { "order" : "desc", "mode": "avg" } },
  ]
}

Пример сортировки фильмов по рейтингу и дате

Воспользуемся примером из раздела с пагинацией.

Сортировка индекса films по убыванию рейтинга и даты.

GET <ELASTICSEARCH_URL>/films/_doc/_search
Content-Type: application/json

{
  "sort" : [
    { "rating" : { "order" : "desc" } },
    { "date" : { "order" : "desc" } }
  ]
}

Результат

[{
  "...": "...",
  "name": "film 3"
},
{
  "...": "...",
  "name": "film 2"
},
{
  "...": "...",
  "name": "film 1"
}]

film 3 является самым старым, но имеет выше рейтинг, а рейтинг приоритетнее даты, поскольку указан раньше в параметре sort. film 2 имеет такой же рейтинг, как и film 1, но по дате он новее.

Обработка больших объёмов данных (scroll)

Один поисковой запрос в Elasticsearch не может обработать более 10000 элементов.

Свойство size не может превышать 10000 элементов. Параметр from, отвечающий за пагинацию, не может захватить элементы с индексом, большим 10000. Свойство total также не может возвращать более 10000 элементов.

Можно повысить лимит, увеличив значение параметра index.max_result_window value в настройках индекса settings. Но делать это не желательно без крайней необходимости, поскольку поисковые запросы занимают память кучи (heap memory) и время пропорционально формуле max(max_result_window, from + size). Если убрать лимит, то лимит памяти будет отсутствовать и кластер может упасть от перенагрузки. Лучше получать данные меньшими порциями.

Когда необходимо обработать более 10000 документов одного индекса, следует использовать Scroll API.

Scroll позволяет возвращать большое количество результатов (или все результаты) аналогично курсору (cursor) в традиционных базах данных.

Scroll не используется для пользовательских запросов в режиме реального времени, но используется для получения больших объёмов данных, чтобы как-то обработать их (например, проиндексировать все документы индекса заново с обновлённой конфигурацией индекса или скопировать их для записи куда-либо).

Контекст поиска (Search context) — состояние, которые поддерживается в течение всей операции поиска в шарде. Чем больше параллельных (concurrent) поисковых операций выполняется, тем больше объектов поискового контекста существуют одновременно. Когда поисковая операция завершается, контекст поиска удаляется.

Как работать со Scroll API

К обычному поисковому запросу следует добавить query-параметр scroll и в качестве значения передать туда время, которое будет жить поисковой контекст до следующего вызова.

Для оптимальной работы поисковой контекст должен жить как можно меньше, но этого времени должно хватать, чтобы обработать результат предыдущего результат запроса. Поставим 1 минуту в качестве времени жизни поискового контекста.

GET <ELASTICSEARCH_URL>/index_name/_search?scroll=1m

Основные единицы времени

  • h — час.
  • m — минута.
  • s — секунда.
  • ms — миллисекунда.

В ответ на такой GET-запрос приходит ответ, который помимо привычных свойств содержит

  • Достоверный hits.total (количество всех документов в индексе, не ограниченное лимитом в 10000).
  • _scroll_id — идентификатор контекста поиска, который используется для получения следующей части результатов.

Каждый запрос со scroll возвращает в свойстве _scroll_id ссылку на текущий контекст поиска, по которой можно сослаться на следующую порцию результатов. Эта ссылка может меняться, а может оставаться прежней (то есть при двух идентичных последовательных запросах можно получить разные данные) — важно использовать её последнюю версию.

Для запроса со scroll контекст поиска создаётся при первоначальом (initial) запросе и живёт для выполения последующих запросов.

Для получения следующей порции результатов используется запрос следующего вида (в запросе отсутствует название индекса). Каждый такой запрос устанавливает своё время жизни следующего запроса. Время начинает считаться с момента, когда предыдущий запрос вернул данные.

GET <ELASTICSEARCH_URL>/_search/scroll 

{
    "scroll" : "1m", 
    "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==" 
}

Когда время жизни контекста поиска истекает, получить дальнейшие документы при помощи scroll не получится. Выдаётся ошибка о том, что контекст поиска не существует.

 {
   "type": "search_context_missing_exception",
   "reason": "No search context found for id [xxxx]"
}

Важно отметить, что использование from запрещено при использовании scroll.

После завершения работы со scroll можно удалить его вручную, не дожидаясь, пока истечёт время его жизни. Это освободит занимаемую память.

DELETE <ELASTICSEARCH_URL>/_search/scroll

{
    "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}

Псевдокод для работы со scroll

Получать данные при помощи scroll можно рекурсивно примерно следующим образом.

const getScrollDataRec = (documents, scroll_id) => {
  const { scroll_id, hits } = POST(`${ELASTICSEARCH_URL}/_search/scroll`, {
    scroll_id,
    scroll: '1m',
  });

  if (hits.hits.length) {
    documents.push(hits.hits);
    return getScrollDataRec(documents, scroll_id);
  }
  return documents;
};
  
const getAllDocuments = (indexName) => {
  const { scroll_id, hits } = GET(`${ELASTICSEARCH_URL}/${indexName}/_search?scroll=1m`);
  const documents = getScrollDataRec(hits.hits, scroll_id);
  return documents;
};

Анализаторы

Анализаторы (Analyzers) определяют способ, которым данные будут анализироваться перед индексацией.

Анализ текста (Text analysis) — процесс преобразования обычного текста в структурированный формат, оптимизированный для поиска. Используется, когда установлен тип данных text.

После анализа текст поля разделяется на термы (terms). Таким образом, после анализа поле представлено в виде списка термов (list of terms), в котором оно и индексируется.

Встроенные анализаторы

Elasticsearch предоставляет набор встроенных анализаторов (build-in analyzers).

Для их будем анализировать фразу "- How old are you? - I'm 17.".

Чтобы проверить, как работает конкретный анализатор, можно отправить следующий запрос.

GET <ELASTICSEARCH_URL>/_analyze
Content-Type: application/json

{ 
  "analyzer": "analyzer_name",
  "text":"- How old are you? - I'm 17."
}
  • Стандартный: standard. Используется по умолчанию. Разбивает текст на слова, переводит их в нижний регистр (lowercase), удаляет знаке препинания, при необходимости удаляет стоп-слова.
/* terms */
["how", "old", "are", "you", "i'm", "17"]
  • Простой: simple. Разделяет слова каждый раз, когда встречает не букву. Все термы переводятся в нижний регистр.
/* terms */
["how", "old", "are", "you", "i", "m"]
  • Стоп-анализатор: stop. Как simple, но с возможностью удалять стоп-слова. По умолчанию используются стоп-слова английского языка (вспомогательные глаголы, предлоги и так далее).
/* terms */
["how", "old", "you", "i", "m"]
  • Пробельный: whitespace. Разделяет текст, когда находит пробельные символы.
/* terms */
["-", "How", "old", "are", "you?", "-", "I'm", "17."]
  • Анализатор ключевых слов: keyword. Принимает текст и его возвращает как есть.
/* terms */
["- How old are you? - I'm 17."]
  • Языковой: english, french. Анализирует текст соответственно специфике языка. Удаляет стоп-слова, характерные языку. Переводит в нижний регистр.
/* terms */
["how", "old", "you", "i'm", "17"]
  • Шаблонный: pattern. Для разделения текста на термы использует регулярные выражения. По умолчанию используется регулярное выражение \W+ (всё, что не может быть словом). Переводит в нижний регистр.
/* terms */
["how", "old", "are", "you", "i", "m", "17"]

Пользовательские анализаторы

Чтобы расширить функциональность встроенного анализатора (например, заменить стоп-слова или заменить регулярное выражение), необходимо создать пользовательский анализатор (custom analyzer) в настройках индекса (settings), что обычно делается при создании индекса.

Пользовательские анализаторы существуют в пределах индекса.

Например, создадим пользовательский анализатор, который игнорирует слово old. Для этого добавим его в поле stopwords.

PUT <ELASTICSEARCH_URL>/index_name
Content-Type: application/json

{
  "settings": {
    "analysis": {
      "analyzer": {
        "custom_stop": {
          "type": "stop",
          "stopwords": ["old"]
        }
      }
    }
  }
}

Проверим, как анализируется текст "- How old are you? - I'm 17.".

GET <ELASTICSEARCH_URL>/index_name/_analyze
Content-Type: application/json

{ 
  "analyzer": "analyzer_name",
  "text":"- How old are you? - I'm 17."
}
/* terms */
["how", "are", "you", "i", "m"]

Для более гибкой настройки пользовательских анализаторов необходимо ознакомиться с блоками, из которых состоит каждый анализатор.

Составляющие анализатора

Анализатор является пакетом, который состоит из нескольких строительных блоков: фильтры символов, токенизатор, фильтры токенов.

При преобразовании текста эти блоки вызываются в указанном выше порядке.

Фильтр символов

Фильтр символов (Character filter) принимает оригинальный текст в качестве потока символов и трансформирует этот поток, добавляя, удаляя и изменяя символы.

Например, римские цифры (I, II, III) могут переводиться в арабские (1, 2, 3).

У анализатора может быть несколько фильтров символов или не быть вообще. Они применяются в указанном порядке.

Токенизатор

Токенизатор (Tokenizer) принимает поток символов (stream of characters), разбивает его на отдельные токены (individual tokens) и возвращает поток токенов. Чаще всего токенами являются отдельные слова.

Процесс разбиения потока символов на токены называется токенизацией (tokenization).

Именно благодаря токенизации доступен полтотекстовый поиск, ведь каждый токен индексируется отдельно.

Ранее было показано, как токенизаторы встроенных анализаторов разделяют текст на токены (термы).

Токенизатор также отвечает за порядок термов (порядок может меняться).

Один анализатор имеет ровно один токенизатор.

Фильтр токенов

Фильтр токенов (Token filter) принимает поток токенов (stream of tokens) и транмформирует его, удаляя, добавляя и изменяя токены.

Например, фильтр токенов lowercase переводит все токены в нижний регистр, фильтр stop удаляет стоп-слова, фильтр synonym добавляет синонимы в поток токенов.

У анализатора может быть несколько фильтров токенов или не быть вообще. Они применяются в указанном порядке.

Граф токенов

Когда токенизатор превращает поток символов в поток токенов, он запоминает позицию (position) каждого токена в потоке и число позиций (positionLength), которые охватывает токен.

Это позволяет построить ориентированный (имеющий направление движения) ациклический (без циклов) граф, который называется графом токенов (token graph).

Каждая позиция представляет вершину графа (node).

Каждый токен представляет дугу графа (edge), указывающую на следующую позицию.

/* граф токенов для текста "Hello our users!" после токенизации */

   hello      our       users    
0 ------> 1 -------> 2 -------> 3

Фильтры токенов могут добавлять новые токены (например, синонимы) в поток токенов, а значит и в граф токенов.

Синонимы записываются на ту же позицию, что и существующие токены.

/* граф токенов для текста "Hello our users!" после токенизации
и фильтрации фильтром токенов с синонимами */

   hello      our       users    
0 ------> 1 -------> 2 -------> 3
    hi                 clients

Многопозиционные токены

По умолчанию токен занимает только одну позицию, то есть его positionLength равняется 1.

Но некоторые синонимы могут занимать несколько позиций. Например, расшифровки аббревиатур (CSS, Cascading Style Sheets).

  cascading      style       sheets         is          ...
0 ----------> 1 --------> 2 ---------> 3 ---------> 4 --------> 5
|                                      |
----------------------------------------
                  css

Фильтры, которые могут добавлять многопозиционные токен, называются фильтрами токенов графа ( graph token filters). Такими являются фильтры synonym_graph и word_delimiter_graph.

Нормализация

В то время, как благодаря токенизации доступен полтотекстовый поиск, каждый отдельный токен при поиске сравнивается посимвольно.

  • При поиске How, токен how не пройдёт проверку на совпадение.
  • При поиске user, токен users не пройдёт проверку на совпадение.
  • При поиске hello, токен hi не пройдёт проверку на совпадение.

Чтобы этого избежать, можно нормализовать (normalize) данные, то есть привести их к стандартному формату. Таким образом токены не будут точно совпадать (not exact match), но будут достаточно похожи, чтобы попасть в результат поиска.

К примеру, токен Hello может быть переведено в нижний регистру (be lowercased), users может быть приведён к его корневому слову user (stemmed), hello и hi являются синонимами и могут индексироваться как единственное слово hello.

Анализатор индекса и анализатор поиска

Анализ текста осуществляется дважды

  • при индексации документа (Index time)
  • во время поиска (Search time, query time).

Анализатор индекса (Index analyzer) анализирует текстовые данные перед индексацией.

Анализатор поиска (Search analyzer) анализирует текс поискового запроса (query).

В большитсве случаев эти анализаторы должны иметь одинаковый набор правил токенизации и нормализации.

Например, при текст "Hello our USERS!" может быть преобразован анализатором индекса в [hello, our, user], а текст поискового запроса "Hi user" — анализатором поиска в [hello, user].

Слово Поиск Индекс
hello + +
our +
user + +

Тогда документ со значением "Hello our USERS!" в текстовом поле попадёт в результат поиска по запросу "Hi user".

Разные анализаторы поиска и инекса

Иногда может появиться необходимость использовать разные анализаторы индекса и поиска.

Например, когда мы хотим, чтобы убрать из поиска некоторые нежелательные результаты.

В этом случае можно указать отдельный анализатор для поиска.

GET <ELASTICSEARCH_URL>/index_name/_search
Content-Type: application/json

{
  "query": {
    "match": {
      "message": {
        "query": "Hi user",
        "analyzer": "stop"
      }
    }
  }
}

Можно также задать разные анализаторы для конкретного поля при создании индекса в mappings.

PUT <ELASTICSEARCH_URL>/index_name
Content-Type: application/json

{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "whitespace",
        "search_analyzer": "simple"
      }
    }
  }
}

Токенизаторы

В главе Составляющие анализатора было рассказано, что такое токенизатор.

Помимо перечисленных ранее функций, токенизаторы также задают тип токенов (token type).

Простые токенизаторы разбиват текст на слова и задают тип word. Другие токенизаторы могут задавать типы <ALPHANUM>, <HANGUL>, <NUM>.

Виды токенизаторов

  • Ориентированные на слова.
  • Токенизаторы частичных слов.
  • Токенизаторы структурированного текста.

Ориентированные на слова

Ориентированные на слова токенизаторы (Word oriented tokenizer) разбивают текст на отдельные токены, которые явяются словами.

Задаваемый тип токенов: word.

Токенизаторы частичных слов

Токенизаторы частичных слов (Partial word tokenizer) разбивают текст или слова на маленькие фрагменты для проверки на частичное совпадение слов.

N-gram

Edge n-gram

Токенизаторы структурированного текста

Токенизаторы структурированного текста (Structured text tokenizer) обычно используются не для полнотекстового поиска, а для идентификаторов (id, email, phone и так далее).

Настройки индекса (settings)

У каждого индекса при создании задаётся набор настроек.

Получение текущих настроек индекса

Текущие настройки индекса можно получить по GET-запросу _settings.

GET <ELASTICSEARCH_URL>/users/_settings
/* response body */
{
  "users": {
    "settings": {
      "index": {
        "number_of_shards": "1",
        "provided_name": "users",
        "creation_date": "1582885295047",
        "number_of_replicas": "1",
        "uuid": "cNAP5avkRueTAkUGNxsHew",
        "version": {
          "created": "7030299"
        }
      }
    }
  }
}

Количество шардов указано в настройках индекса в свойстве number_of_shards.

Количество реплик указано в настройках индекса в свойстве number_of_replicas.

Установка Elasticsearch на ПК

  • Скачать архив.
  • Разархивировать и запустить bin\elasticsearch или bin\elasticsearch.bat (в зависимости от операционной системы).
  • Поскольку общение с Elasticserach идёт при помощи HTTP-запросов, можно использовать Postman (или что-то похожее) и делать запросы на http://localhost:9200.