При определении сопоставлений OpenSearch будет конфигурировать поля, содержащие массив объектов, как тип " object". Во многих случаях это нормально, но иногда сопоставления необходимо корректировать. Ниже мы рассмотрим различные сценарии и то, как выбрать правильное отображение для каждого случая.
Поля объектов
Одно из преимуществ использования структур на основе документов заключается в том, что их свойства можно группировать в иерархической форме. Это то, что мы называем объектами.
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" } } } } |
Неважно, насколько глубока связь "объект в объекте", потому что OpenSearch внутренне сгладит ее (см. пояснения ниже).
В качестве значений свойств можно создавать массивы объектов.
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" } ] } |
В этой ситуации тип поля имеет значение, и иногда нам придется перейти от стандартного типа объекта к вложенному типу.
Тип вложенного поля
Что такое тип вложенного поля в OpenSearch?
Вложенный - это особый тип объекта, который индексируется как отдельный документ, и ссылка на каждый из этих внутренних документов хранится вместе с содержащим документом, так что мы можем запрашивать данные соответствующим образом.
Проблема с использованием объектных полей
Чтобы продемонстрировать использование объектных полей по сравнению с вложенными типами полей, мы сначала проиндексируем несколько документов. Примеры можно выполнить в OpenSearch Dashboards.
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" } ] } |
OpenSearch будет динамически генерировать эти сопоставления:
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 } } } } } } } } } |
Давайте сосредоточимся на полях "authors" и "tags". Оба поля заданы как поля типа "объект". Это означает, что OpenSearch сгладит свойства. Документ 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" выглядит иначе - оно было разбито на множество полей массива.
Проблема в том, что OpenSearch не хранит свойства каждого объекта "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 года, и поэтому он не соответствует всем необходимым критериям, но OpenSearch не сохранил эту связь между авторами и возрастом.
Как решить эту проблему
Чтобы точно выполнить второй запрос, нам нужно использовать другой тип поля, называемый вложенным.
Вложенный - это особый тип объекта, который индексируется как отдельный документ, а ссылка на каждый из этих внутренних документов хранится вместе с содержащим документом, поэтому мы можем запросить данные соответствующим образом.
Нам придется изменить тип отображения. Чтобы изменить существующие отображения, нам нужно переиндексировать наши данные.
Сначала создайте пустой индекс, чтобы функция динамических сопоставлений OpenSearch не генерировала автоматически сопоставления для нашего поля authors:
1 2 3 4 5 6 7 8 9 10 | PUT books_test_nested { "mappings": { "properties": { "authors": { "type": "nested" } } } } |
OpenSearch создаст все остальные сопоставления на основе проиндексированных документов.
Теперь воспользуйтесь 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.
- Объектный тип поля не поддерживает запрос связанных свойств внутри отдельных объектов.
- Не используйте вложенный тип, если на один внешний объект будет приходиться только один внутренний объект.
- В противном случае используйте поля вложенного типа (nested), если вам нужно запросить два или более полей в одном внутреннем объекте, в противном случае используйте объектный тип.
- Слишком большое количество вложенных объектов может привести к снижению производительности или взрыву отображения.
- Используйте сплющенный тип поля, чтобы сопоставить все поля ключевых слов внутреннего объекта одному полю.