Spec-Zone.ru › Elasticsearch 8
›Elasticsearch Guide [8.17] ›Отображение ›Типы данных полей

Тип поля Join

Тип данных join — специальное поле, которое создаёт родительско-дочерние отношения между документами одного индекса. В разделе relations определён набор возможных отношений между документами, каждое отношение — это имя родителя и имя ребёнка.

Не рекомендуется использовать несколько уровней отношений для моделирования реляционных баз данных. Каждый уровень отношений добавляет издержки при выполнении запросов, в плане памяти и вычислений. Для лучшей производительности поиска, выполните денормализацию данных.

Родительско-дочернее отношение можно определить следующим образом:

resp = client.indices.create(
    index="my-index-000001",
    mappings={
        "properties": {
            "my_id": {
                "type": "keyword"
            },
            "my_join_field": {
                "type": "join",
                "relations": {
                    "question": "answer"
                }
            }
        }
    },
)
print(resp)
response = client.indices.create(
  index: 'my-index-000001',
  body: {
    mappings: {
      properties: {
        my_id: {
          type: 'keyword'
        },
        my_join_field: {
          type: 'join',
          relations: {
            question: 'answer'
          }
        }
      }
    }
  }
)
puts response
const response = await client.indices.create({
  index: "my-index-000001",
  mappings: {
    properties: {
      my_id: {
        type: "keyword",
      },
      my_join_field: {
        type: "join",
        relations: {
          question: "answer",
        },
      },
    },
  },
});
console.log(response);
PUT my-index-000001
{
  "mappings": {
    "properties": {
      "my_id": {
        "type": "keyword"
      },
      "my_join_field": { 
        "type": "join",
        "relations": {
          "question": "answer" 
        }
      }
    }
  }
}

Имя поля

Определяет единственное отношение, где question является родителем answer.

Для индексации документа с Join, необходимо указать имя отношения и, необязательно, родителя документа в source. Например, следующий пример создаёт два документа parent в контексте question:

resp = client.index(
    index="my-index-000001",
    id="1",
    refresh=True,
    document={
        "my_id": "1",
        "text": "This is a question",
        "my_join_field": {
            "name": "question"
        }
    },
)
print(resp)

resp1 = client.index(
    index="my-index-000001",
    id="2",
    refresh=True,
    document={
        "my_id": "2",
        "text": "This is another question",
        "my_join_field": {
            "name": "question"
        }
    },
)
print(resp1)
response = client.index(
  index: 'my-index-000001',
  id: 1,
  refresh: true,
  body: {
    my_id: '1',
    text: 'This is a question',
    my_join_field: {
      name: 'question'
    }
  }
)
puts response

response = client.index(
  index: 'my-index-000001',
  id: 2,
  refresh: true,
  body: {
    my_id: '2',
    text: 'This is another question',
    my_join_field: {
      name: 'question'
    }
  }
)
puts response
const response = await client.index({
  index: "my-index-000001",
  id: 1,
  refresh: "true",
  document: {
    my_id: "1",
    text: "This is a question",
    my_join_field: {
      name: "question",
    },
  },
});
console.log(response);

const response1 = await client.index({
  index: "my-index-000001",
  id: 2,
  refresh: "true",
  document: {
    my_id: "2",
    text: "This is another question",
    my_join_field: {
      name: "question",
    },
  },
});
console.log(response1);
PUT my-index-000001/_doc/1?refresh
{
  "my_id": "1",
  "text": "This is a question",
  "my_join_field": {
    "name": "question" 
  }
}

PUT my-index-000001/_doc/2?refresh
{
  "my_id": "2",
  "text": "This is another question",
  "my_join_field": {
    "name": "question"
  }
}

Этот документ является документом question.

При индексации родительских документов, можно указать только имя отношения, как сокращение, вместо использования обычной нотации объекта:

resp = client.index(
    index="my-index-000001",
    id="1",
    refresh=True,
    document={
        "my_id": "1",
        "text": "This is a question",
        "my_join_field": "question"
    },
)
print(resp)

resp1 = client.index(
    index="my-index-000001",
    id="2",
    refresh=True,
    document={
        "my_id": "2",
        "text": "This is another question",
        "my_join_field": "question"
    },
)
print(resp1)
const response = await client.index({
  index: "my-index-000001",
  id: 1,
  refresh: "true",
  document: {
    my_id: "1",
    text: "This is a question",
    my_join_field: "question",
  },
});
console.log(response);

const response1 = await client.index({
  index: "my-index-000001",
  id: 2,
  refresh: "true",
  document: {
    my_id: "2",
    text: "This is another question",
    my_join_field: "question",
  },
});
console.log(response1);
PUT my-index-000001/_doc/1?refresh
{
  "my_id": "1",
  "text": "This is a question",
  "my_join_field": "question" 
}

PUT my-index-000001/_doc/2?refresh
{
  "my_id": "2",
  "text": "This is another question",
  "my_join_field": "question"
}

Более простая нотация для родительского документа использует только имя отношения.

При индексации дочернего документа, необходимо указать имя отношения и идентификатор родителя в _source.

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

Например, следующий пример показывает, как индексировать два документа child:

resp = client.index(
    index="my-index-000001",
    id="3",
    routing="1",
    refresh=True,
    document={
        "my_id": "3",
        "text": "This is an answer",
        "my_join_field": {
            "name": "answer",
            "parent": "1"
        }
    },
)
print(resp)

resp1 = client.index(
    index="my-index-000001",
    id="4",
    routing="1",
    refresh=True,
    document={
        "my_id": "4",
        "text": "This is another answer",
        "my_join_field": {
            "name": "answer",
            "parent": "1"
        }
    },
)
print(resp1)
response = client.index(
  index: 'my-index-000001',
  id: 3,
  routing: 1,
  refresh: true,
  body: {
    my_id: '3',
    text: 'This is an answer',
    my_join_field: {
      name: 'answer',
      parent: '1'
    }
  }
)
puts response

response = client.index(
  index: 'my-index-000001',
  id: 4,
  routing: 1,
  refresh: true,
  body: {
    my_id: '4',
    text: 'This is another answer',
    my_join_field: {
      name: 'answer',
      parent: '1'
    }
  }
)
puts response
const response = await client.index({
  index: "my-index-000001",
  id: 3,
  routing: 1,
  refresh: "true",
  document: {
    my_id: "3",
    text: "This is an answer",
    my_join_field: {
      name: "answer",
      parent: "1",
    },
  },
});
console.log(response);

const response1 = await client.index({
  index: "my-index-000001",
  id: 4,
  routing: 1,
  refresh: "true",
  document: {
    my_id: "4",
    text: "This is another answer",
    my_join_field: {
      name: "answer",
      parent: "1",
    },
  },
});
console.log(response1);
PUT my-index-000001/_doc/3?routing=1&refresh 
{
  "my_id": "3",
  "text": "This is an answer",
  "my_join_field": {
    "name": "answer", 
    "parent": "1" 
  }
}

PUT my-index-000001/_doc/4?routing=1&refresh
{
  "my_id": "4",
  "text": "This is another answer",
  "my_join_field": {
    "name": "answer",
    "parent": "1"
  }
}

Значение маршрутизации является обязательным, так как родительские и дочерние документы должны быть индексированы в одном фрагменте.

answer — имя join для этого документа

Идентификатор родителя этого дочернего документа

Родительский Join и производительность

Поле Join не должно использоваться как Join в реляционной базе данных. В Elasticsearch ключом к хорошей производительности является денормализация данных в документы. Каждое поле join, has_child или запрос has_parent добавляет значительную нагрузку на производительность запроса. Это также может привести к созданию глобальных порядков eager_global_ordinals.

Единственный случай, когда поле Join имеет смысл, — это если ваши данные содержат отношение «один ко многим», где один из объектов значительно больше другого. Примером такого случая является использование с продуктами и предложениями для этих продуктов. Если предложений значительно больше, чем продуктов, тогда имеет смысл смоделировать продукт как родительский документ, а предложение — как дочерний.

Ограничения родительского Join

  • Допускается только одна карта join поля на индекс.
  • Родительские и дочерние документы должны быть индексированы в одном фрагменте. Это означает, что необходимо предоставить одно и то же значение routing при получении, удалении или обновлении дочернего документа.
  • Элемент может иметь несколько дочерних элементов, но только одного родителя.
  • Возможна добавление нового отношения к существующему полю join.
  • Также возможно добавить дочерний элемент к существующему элементу, но только если элемент уже является родителем.

Поиск с родительским Join

Родительский Join создаёт одно поле для индексации имени отношения в документе (my_parent, my_child, …​).

Он также создаёт одно поле на каждое родительско-дочернее отношение. Имя этого поля — имя поля join, за которым следует # и имя родителя в отношении. Например, для отношения my_parent → [my_child, another_child], поле join создаёт дополнительное поле, названное my_join_field#my_parent.

Это поле содержит родительский _id, к которому документ ссылается, если документ является дочерним (my_child или another_child), и _id документа, если он является родительским (my_parent).

При поиске в индексе, который содержит поле join, эти два поля всегда возвращаются в ответе поиска:

resp = client.search(
    index="my-index-000001",
    query={
        "match_all": {}
    },
    sort=[
        "my_id"
    ],
)
print(resp)
const response = await client.search({
  index: "my-index-000001",
  query: {
    match_all: {},
  },
  sort: ["my_id"],
});
console.log(response);
GET my-index-000001/_search
{
  "query": {
    "match_all": {}
  },
  "sort": ["my_id"]
}

Возвращает:

{
  ...,
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": null,
    "hits": [
      {
        "_index": "my-index-000001",
        "_id": "1",
        "_score": null,
        "_source": {
          "my_id": "1",
          "text": "This is a question",
          "my_join_field": "question"         
        },
        "sort": [
          "1"
        ]
      },
      {
        "_index": "my-index-000001",
        "_id": "2",
        "_score": null,
        "_source": {
          "my_id": "2",
          "text": "This is another question",
          "my_join_field": "question"          
        },
        "sort": [
          "2"
        ]
      },
      {
        "_index": "my-index-000001",
        "_id": "3",
        "_score": null,
        "_routing": "1",
        "_source": {
          "my_id": "3",
          "text": "This is an answer",
          "my_join_field": {
            "name": "answer",                 
            "parent": "1"                     
          }
        },
        "sort": [
          "3"
        ]
      },
      {
        "_index": "my-index-000001",
        "_id": "4",
        "_score": null,
        "_routing": "1",
        "_source": {
          "my_id": "4",
          "text": "This is another answer",
          "my_join_field": {
            "name": "answer",
            "parent": "1"
          }
        },
        "sort": [
          "4"
        ]
      }
    ]
  }
}

Этот документ принадлежит к join question

Этот документ принадлежит к join question

Этот документ принадлежит к join answer

Связанный идентификатор родителя для дочернего документа

Запросы и агрегации с родительским Join

См. запросы has_child и has_parent, агрегацию children и вложенные результаты для получения дополнительной информации.

Значение поля join доступно в агрегациях и скриптах, и может быть запрошено с помощью запроса parent_id:

resp = client.search(
    index="my-index-000001",
    query={
        "parent_id": {
            "type": "answer",
            "id": "1"
        }
    },
    aggs={
        "parents": {
            "terms": {
                "field": "my_join_field#question",
                "size": 10
            }
        }
    },
    runtime_mappings={
        "parent": {
            "type": "long",
            "script": "\n        emit(Integer.parseInt(doc['my_join_field#question'].value)) \n      "
        }
    },
    fields=[
        {
            "field": "parent"
        }
    ],
)
print(resp)
const response = await client.search({
  index: "my-index-000001",
  query: {
    parent_id: {
      type: "answer",
      id: "1",
    },
  },
  aggs: {
    parents: {
      terms: {
        field: "my_join_field#question",
        size: 10,
      },
    },
  },
  runtime_mappings: {
    parent: {
      type: "long",
      script:
        "\n        emit(Integer.parseInt(doc['my_join_field#question'].value)) \n      ",
    },
  },
  fields: [
    {
      field: "parent",
    },
  ],
});
console.log(response);
GET my-index-000001/_search
{
  "query": {
    "parent_id": { 
      "type": "answer",
      "id": "1"
    }
  },
  "aggs": {
    "parents": {
      "terms": {
        "field": "my_join_field#question", 
        "size": 10
      }
    }
  },
  "runtime_mappings": {
    "parent": {
      "type": "long",
      "script": """
        emit(Integer.parseInt(doc['my_join_field#question'].value)) 
      """
    }
  },
  "fields": [
    { "field": "parent" }
  ]
}

Запрос к полю parent id (см. также запрос has_parent и запрос has_child)

Агрегация по полю parent id (см. также агрегацию children)

Доступ к полю parent id в скриптах.

Глобальные порядковые номера

Поле join использует глобальные порядковые номера для ускорения объединений. Глобальные порядковые номера необходимо перестраивать после любого изменения фрагмента. Чем больше значений идентификаторов родителей хранится во фрагменте, тем дольше занимает перестройка глобальных порядковых номеров для поля join.

Глобальные порядковые номера по умолчанию строятся немедленно: если индекс был изменён, глобальные порядковые номера для поля join будут перестроены как часть обновления. Это может значительно увеличить время обновления. Однако в большинстве случаев это правильный компромисс, иначе глобальные порядковые номера перестраиваются при первом использовании запроса с объединением по родителям или агрегации. Это может привести к существенному увеличению задержки для ваших пользователей, и обычно это ухудшается, так как несколько глобальных порядковых номеров для поля join могут быть перестроены в течение одного интервала обновления, когда происходит много записей.

Если поле join используется редко, а записи происходят часто, может быть целесообразно отключить немедленную загрузку:

resp = client.indices.create(
    index="my-index-000001",
    mappings={
        "properties": {
            "my_join_field": {
                "type": "join",
                "relations": {
                    "question": "answer"
                },
                "eager_global_ordinals": False
            }
        }
    },
)
print(resp)
response = client.indices.create(
  index: 'my-index-000001',
  body: {
    mappings: {
      properties: {
        my_join_field: {
          type: 'join',
          relations: {
            question: 'answer'
          },
          eager_global_ordinals: false
        }
      }
    }
  }
)
puts response
const response = await client.indices.create({
  index: "my-index-000001",
  mappings: {
    properties: {
      my_join_field: {
        type: "join",
        relations: {
          question: "answer",
        },
        eager_global_ordinals: false,
      },
    },
  },
});
console.log(response);
PUT my-index-000001
{
  "mappings": {
    "properties": {
      "my_join_field": {
        "type": "join",
        "relations": {
           "question": "answer"
        },
        "eager_global_ordinals": false
      }
    }
  }
}

Объём кучи, используемый глобальными порядковыми номерами, можно проверить для каждого родительского отношения следующим образом:

resp = client.indices.stats(
    metric="fielddata",
    human=True,
    fields="my_join_field",
)
print(resp)

resp1 = client.nodes.stats(
    metric="indices",
    index_metric="fielddata",
    human=True,
    fields="my_join_field",
)
print(resp1)
response = client.indices.stats(
  metric: 'fielddata',
  human: true,
  fields: 'my_join_field'
)
puts response

response = client.nodes.stats(
  metric: 'indices',
  index_metric: 'fielddata',
  human: true,
  fields: 'my_join_field'
)
puts response
const response = await client.indices.stats({
  metric: "fielddata",
  human: "true",
  fields: "my_join_field",
});
console.log(response);

const response1 = await client.nodes.stats({
  metric: "indices",
  index_metric: "fielddata",
  human: "true",
  fields: "my_join_field",
});
console.log(response1);
# Per-index
GET _stats/fielddata?human&fields=my_join_field#question

# Per-node per-index
GET _nodes/stats/indices/fielddata?human&fields=my_join_field#question

Несколько дочерних элементов на один родительский

Также можно определить несколько дочерних элементов для одного родителя:

resp = client.indices.create(
    index="my-index-000001",
    mappings={
        "properties": {
            "my_join_field": {
                "type": "join",
                "relations": {
                    "question": [
                        "answer",
                        "comment"
                    ]
                }
            }
        }
    },
)
print(resp)
response = client.indices.create(
  index: 'my-index-000001',
  body: {
    mappings: {
      properties: {
        my_join_field: {
          type: 'join',
          relations: {
            question: [
              'answer',
              'comment'
            ]
          }
        }
      }
    }
  }
)
puts response
const response = await client.indices.create({
  index: "my-index-000001",
  mappings: {
    properties: {
      my_join_field: {
        type: "join",
        relations: {
          question: ["answer", "comment"],
        },
      },
    },
  },
});
console.log(response);
PUT my-index-000001
{
  "mappings": {
    "properties": {
      "my_join_field": {
        "type": "join",
        "relations": {
          "question": ["answer", "comment"]  
        }
      }
    }
  }
}

question является родителем answer и comment.

Несколько уровней объединения по родителям

Мы не рекомендуем использовать несколько уровней отношений для репликации реляционной модели. Каждый уровень отношений добавляет накладные расходы во время запроса с точки зрения памяти и вычислений. Для лучшей производительности поиска денормализуйте свои данные.

Несколько уровней родитель/дочерний:

resp = client.indices.create(
    index="my-index-000001",
    mappings={
        "properties": {
            "my_join_field": {
                "type": "join",
                "relations": {
                    "question": [
                        "answer",
                        "comment"
                    ],
                    "answer": "vote"
                }
            }
        }
    },
)
print(resp)
response = client.indices.create(
  index: 'my-index-000001',
  body: {
    mappings: {
      properties: {
        my_join_field: {
          type: 'join',
          relations: {
            question: [
              'answer',
              'comment'
            ],
            answer: 'vote'
          }
        }
      }
    }
  }
)
puts response
const response = await client.indices.create({
  index: "my-index-000001",
  mappings: {
    properties: {
      my_join_field: {
        type: "join",
        relations: {
          question: ["answer", "comment"],
          answer: "vote",
        },
      },
    },
  },
});
console.log(response);
PUT my-index-000001
{
  "mappings": {
    "properties": {
      "my_join_field": {
        "type": "join",
        "relations": {
          "question": ["answer", "comment"],  
          "answer": "vote" 
        }
      }
    }
  }
}

question является родителем answer и comment

answer является родителем vote

Приведённое выше отображение представляет собой следующее дерево:

   question
    /    \
   /      \
comment  answer
           |
           |
          vote

Индексация документа внука требует значения routing, равного значению прародителя (более высокого родителя в родословной):

resp = client.index(
    index="my-index-000001",
    id="3",
    routing="1",
    refresh=True,
    document={
        "text": "This is a vote",
        "my_join_field": {
            "name": "vote",
            "parent": "2"
        }
    },
)
print(resp)
response = client.index(
  index: 'my-index-000001',
  id: 3,
  routing: 1,
  refresh: true,
  body: {
    text: 'This is a vote',
    my_join_field: {
      name: 'vote',
      parent: '2'
    }
  }
)
puts response
const response = await client.index({
  index: "my-index-000001",
  id: 3,
  routing: 1,
  refresh: "true",
  document: {
    text: "This is a vote",
    my_join_field: {
      name: "vote",
      parent: "2",
    },
  },
});
console.log(response);
PUT my-index-000001/_doc/3?routing=1&refresh 
{
  "text": "This is a vote",
  "my_join_field": {
    "name": "vote",
    "parent": "2" 
  }
}

Этот дочерний документ должен находиться в том же фрагменте, что и его прародитель и родитель

Идентификатор родителя этого документа (должен указывать на документ answer)

© 2023-2025 Elasticsearch
As of September 2024, Elasticsearch is available under a choice of three licenses: the Server Side Public License (SSPL), the Elastic License, or the AGPLv3 (OSI approved).
Elasticsearch and the Elasticsearch logo are trademarks of Elasticsearch B.V., registered in the U.S. and in other countries.
https://www.elastic.co/guide/en/elasticsearch/reference/8.17/parent-join.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API