- Базовые понятия Elasticsearch
- Маппинги и типы данных
- Работа с данными
- Поиск документов
- Анализаторы
- Токенизаторы
- Настройки индекса (settings)
- Установка 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 очень похож на индекс в конце книги: указываются определённый список ключевых слов (важных определений) и рядом страницы, на которых они упоминаются.
Аналогично работает и 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
используется для индексирования полнотекстовых значений (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
(ключевое слово) используется для индексирования таких значений, как: 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"
}
Обновление документа типа _doc
в индексе users
по id.
PUT <ELASTICSEARCH_URL>/users/_doc/H3tVi3ABpFL-9AlTbAgj
Content-Type: application/json
{
"name": "Richard Stone",
"job": "Full-stack Enginer"
}
Удаление документа типа _doc
по id.
DELETE <ELASTICSEARCH_URL>/users/_doc/H3tVi3ABpFL-9AlTbAgj
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
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
.
Является самым простым запросом, поскольку возвращает все документы и не принимает какие-либо параметры.
{
"match_all": {}
}
Сам по себе избыточен, но может использоваться в комбинации с более сложными запросами.
В запрос с 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
так же передаётся текстовое, числовое, логическое значение или дата.
В отличии от запроса с match
, результатом запроса с term
являются документы, которые содержат точный терм (exact term) в указанном поле.
{
"term": {
"fieldName": "a"
}
}
Текстовое значение, переданное в запрос с term
не анализируется. Но данные документов уже проанализированы перед индексированием, поэтому поиск может выдать неверные результаты и лучше не использовать запрос с term
для полей типа text
, а использовать с типом keyword
.
Запрос с terms аналогичен запросу с term, но позволяет передать несколько допустимых значений вместо одного.
{
"terms": {
"fieldName": ["a", "b"]
}
}
Важно отметить, что при работе с массивом значений в документе свойства term
и terms
ищут не точное совпадение, а включение (contains). Оба примера выше с term
и с terms
включат в выборку документ со значением ["a", "b", "c"]
.
Запрос с 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
) позволяет находить документы, у которых значение конкретного поля присутствует (отсутствует).
{
"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
работает как логическое ИЛИ, позволяя задавать несколько условий, не все из которых должны выполняться одновременно, по умолчанию при наличии 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
, но по дате он новее.
Один поисковой запрос в 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) поисковых операций выполняется, тем больше объектов поискового контекста существуют одновременно. Когда поисковая операция завершается, контекст поиска удаляется.
К обычному поисковому запросу следует добавить 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
можно рекурсивно примерно следующим образом.
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) разбивают текст или слова на маленькие фрагменты для проверки на частичное совпадение слов.
Токенизаторы структурированного текста (Structured text tokenizer) обычно используются не для полнотекстового поиска, а для идентификаторов (id
, email
, phone
и так далее).
У каждого индекса при создании задаётся набор настроек.
Текущие настройки индекса можно получить по 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
.
- Скачать архив.
- Разархивировать и запустить
bin\elasticsearch
илиbin\elasticsearch.bat
(в зависимости от операционной системы). - Поскольку общение с Elasticserach идёт при помощи HTTP-запросов, можно использовать Postman (или что-то похожее) и делать запросы на
http://localhost:9200
.