При определении маппингов Elasticsearch конфигурирует поля, содержащие в себе массив объектов, как тип "объект". Во многих случаях это нормально, но иногда маппинги необходимо корректировать. Ниже мы рассмотрим различные сценарии и то, как выбрать правильный мапинг для каждого случая.
Поля объектов
Одним из преимуществ использования структур на основе документов является то, что их свойства могут быть сгруппированы в иерархической форме. Такие свойства мы называем объектами.
1 2 3 4 | { "name":"I'm an object", "category": "single-object" } |
Объекты можно встраивать внутрь объектов и проникать в них настолько глубоко, насколько это необходимо.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | { "name": "Duveteuse", "category": "dog", "human_partner": { "full_name": "Ami Chien", "address": { "street": "Jolie Rue #1234", "city": "Paris", "country": { "name": "France", "code": "FR" } } } } |
При этом не имеет значения, насколько глубока связь "объект в объекте", поскольку Elasticsearch внутренне сгладит ее (см. пояснения ниже).
В качестве значений свойств можно создавать массивы объектов.
1 2 3 4 5 6 7 8 9 10 | { "name": "Father object", "age": 50, "category": "self-explaining", "children": [ { "name": "Child object1", "age": 1, "category": "learning-objects" }, { "name": "Child object2", "age": 2, "category": "learning-objects" }, { "name": "Child object3", "age": 3, "category": "learning-objects" } ] } |
В этой ситуации тип поля имеет значение, и иногда приходится переходить от стандартного типа объекта к вложенному типу.
Тип вложенного поля
Вложенный - это особый тип объекта, который индексируется как отдельный документ, и ссылка на каждый из этих внутренних документов хранится вместе с содержащим документом, так что мы можем запросить данные соответствующим образом.
Проблема использования объектных полей
Чтобы продемонстрировать использование объектных полей в сравнении с вложенными типами полей, сначала проиндексируем несколько документов. Примеры можно выполнить в консоли Kibana.
1 | PUT books_test |
1 2 3 4 5 6 7 8 9 | PUT books_test/_doc/1 { "name": "An Awesome Book", "tags": [{ "name": "best-seller" }, { "name": "summer-sale" }], "authors": [ { "name": "Gustavo Llermaly", "age": "32", "country": "Chile" }, { "name": "John Doe", "age": "20", "country": "USA" } ] } |
1 2 3 4 5 6 7 8 9 | PUT books_test/_doc/2 { "name": "A Regular Book", "tags": [{ "name": "free-shipping" }, { "name": "summer-sale" }], "authors": [ { "name": "Regular author", "age": "40", "country": "USA" }, { "name": "John Doe", "age": "20", "country": "USA" } ] } |
Elasticsearch будет динамически генерировать эти маппинги:
1 | GET books_test/_mapping |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | { "books_test": { "mappings": { "properties": { "authors": { "properties": { "age": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "country": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "name": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } }, "name": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "tags": { "properties": { "name": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } } } } } |
Остановимся на полях "авторы" и "теги". Оба поля заданы как поля типа "объект". Это означает, что Elasticsearch сгладит их свойства. Документ 1 будет выглядеть следующим образом:
1 2 3 4 5 6 7 | { "name": "An Awesome Book", "tags.name": ["best-seller", "summer-sale"], "authors.name": ["Gustavo Llermaly", "John Doe"], "authors.age": [32, 20], "authors.country": ["Chile, USA"] } |
Как видно, поле "tags" выглядит как обычный строковый массив, а вот поле "authors" выглядит иначе - оно было разбито на множество полей-массивов.
Проблема заключается в том, что Elasticsearch не хранит свойства каждого объекта "authors" отдельно от свойств всех остальных объектов "authors".
Чтобы проиллюстрировать проблему с таким маппингом, рассмотрим два следующих запроса.
Запрос 1: Поиск книг с авторами из Чили или авторами в возрасте 30 лет и моложе.
Спойлер: Обе книги удовлетворяют этим условиям.
Чтобы найти книги, удовлетворяющие этим критериям, мы должны выполнить следующий запрос:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | GET books_test/_search { "query": { "bool": { "should": [ { "term": { "authors.country.keyword": "Chile" } }, { "range": { "authors.age": { "lte": 30 } } } ] } } } |
Возвращаются обе книги, что правильно, поскольку Густаво Ллермали родом из Чили, а Джону Доу меньше 30 лет.
Запрос 2: Книги, написанные авторами, которым 30 лет или меньше и которые являются выходцами из Чили.
Спойлер: Ни одна книга не соответствует критериям.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | GET books_test/_search { "query": { "bool": { "filter": [ { "term": { "authors.country.keyword": "Chile" } }, { "range": { "authors.age": { "lte": 30 } } } ] } } } |
Этот запрос также вернет оба документа, что неверно. Мы знаем, что единственному автору из Чили 32 года, и поэтому он не соответствует всем необходимым критериям, но Elasticsearch не сохранил эту связь между авторами и возрастом.
Как решить эту проблему
Для точного выполнения второго запроса нам необходимо использовать другой тип поля, называемый вложенным.
Вложенный - это особый тип объекта, который индексируется как отдельный документ, и ссылка на каждый из этих внутренних документов хранится вместе с содержащим документом, так что мы можем запросить данные соответствующим образом.
Нам придется изменить тип маппинга. Для изменения существующих маппингов необходимо переиндексировать данные.
Сначала создадим пустой индекс, чтобы функция динамических маппингов Elasticsearch не генерировала маппинг для нашего поля authors:
1 2 3 4 5 6 7 8 9 10 | PUT books_test_nested { "mappings": { "properties": { "authors": { "type": "nested" } } } } |
*Все остальные маппинги будут сгенерированы Elasticsearch на основе проиндексированных документов.
Теперь воспользуйтесь API reindex для перемещения документов из старого индекса в новый:
1 2 3 4 5 6 7 8 9 | POST _reindex { "source": { "index": "books_test" }, "dest": { "index": "books_test_nested" } } |
Выполните эту процедуру, чтобы убедиться в правильности передачи документов:
1 | GET books_test_nested/_search |
Если теперь выполнить запросы, которые мы использовали для ответа на два вышеприведенных вопроса о книгах, то оба запроса вернут 0 результатов. Это связано с тем, что тип вложенного поля использует другой тип запроса, называемый вложенным запросом.
Если мы попробуем снова ответить на эти вопросы с помощью вложенных запросов, то это будет выглядеть следующим образом:
Запрос 1: Ищем книги с авторами из Чили или авторами в возрасте 30 лет и моложе.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | GET books_test_nested/_search { "query": { "nested": { "path": "authors", "query": { "bool": { "should": [ { "term": { "authors.country.keyword": "Chile" } }, { "range": { "authors.age": { "lte": 30 } } } ] } } } } } |
Обе книги продолжают появляться в результатах, что очень хорошо.
Запрос 2: Книги, написанные авторами в возрасте до 30 лет и родом из Чили.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | GET books_test_nested/_search { "query": { "nested": { "path": "authors", "query": { "bool": { "filter": [ { "term": { "authors.country.keyword": "Chile" } }, { "range": { "authors.age": { "lte": 30 } } } ] } } } } } |
Ни одна книга не возвращается, что является ожидаемым результатом.
Почему это важно
Использование типа вложенного поля для каждого поля массива объекта "на всякий случай" звучит заманчиво, но его следует использовать исключительно по мере необходимости. Под капотом Lucene создает новый документ для каждого объекта в массиве, и это может привести к снижению производительности или даже к взрыву маппинга.
Чтобы избежать низкой производительности, количество вложенных полей в одном индексе ограничено 50, а количество вложенных объектов в одном документе - 10000.
Оба параметра могут быть изменены, но делать это не рекомендуется:
index.mapping.nested_fields.limit
index.mapping.nested_objects.limit
Если необходимо проиндексировать большое и непредсказуемое количество полей ключевых слов во внутренних объектах, то можно использовать тип поля flattened, при котором весь маппинг содержимого объекта сводится к одному полю и позволяет выполнять основные операции запроса.
Заключение
- Поля, основанные на объектах или массивах объектов, по умолчанию создаются с типом object.
- Объектный тип поля не поддерживает запрос связанных свойств внутри отдельных объектов.
- Не используйте вложенный тип, если на один внешний объект будет приходиться только один внутренний объект.
- В противном случае используйте поля вложенного типа, если необходимо запросить два или более полей в пределах одного внутреннего объекта, в противном случае используйте объектный тип.
- Слишком большое количество вложенных объектов может привести к снижению производительности или взрыву маппинга.
- Для маппинга всех ключевых полей внутреннего объекта в одно поле используйте тип поля flattened.