Join 字段类型
join 数据类型是一个特殊字段,用于在同一索引的文档之间创建父/子关系。relations 部分定义了文档内可能的一组关系,每个关系由一个父名称和一个子名称组成。
我们不建议使用多级关系来复制关系模型。每一级关系都会在查询时增加内存和计算方面的开销。为了获得更好的搜索性能,请改用非规范化(denormalize)你的数据。
可以按如下方式定义父/子关系
PUT my-index-000001
{
"mappings": {
"properties": {
"my_id": {
"type": "keyword"
},
"my_join_field": {
"type": "join",
"relations": {
"question": "answer"
}
}
}
}
}
- 字段名称
- 定义了一个单一关系,其中
question是answer的父级。
要索引带有 join 的文档,必须在 source 中提供关系的名称以及文档的可选父级。例如,以下示例在 question 上下文中创建了两个 parent 文档
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文档。
索引父文档时,你可以选择仅指定关系的名称作为捷径,而不必将其封装在常规的对象表示法中
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 中添加关系名称以及文档的父 ID。
必须将父级的血缘关系(lineage)索引在同一个分片中,因此你必须始终使用其最高层父 ID 来路由子文档。
例如,以下示例展示了如何索引两个 child 文档
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 名称- 此子文档的父 ID
不应像关系数据库中的 join 那样使用 join 字段。在 Elasticsearch 中,获得良好性能的关键是将数据非规范化(de-normalize)到文档中。每一个 join 字段、has_child 或 has_parent 查询都会对你的查询性能造成巨大的消耗。它还可能触发构建 全局序数(global ordinals)。
join 字段有意义的唯一情况是,你的数据包含一对多关系,其中一个实体的数量远多于另一个实体。这种情况的一个例子是产品以及这些产品的报价(offer)。如果报价的数量远多于产品的数量,那么将产品建模为父文档、报价建模为子文档就是合理的。
- 每个索引只允许有一个
join字段映射。 - 父文档和子文档必须索引在同一个分片上。这意味着在获取、删除或更新子文档时,需要提供相同的
routing值。 - 一个元素可以有多个子级,但只能有一个父级。
- 可以向现有的
join字段添加新关系。 - 也可以向现有元素添加子级,但前提是该元素已经是一个父级。
父子关联创建一个字段来索引文档中关系的名称(my_parent、my_child 等)。
它还为每个父/子关系创建一个字段。此字段的名称是 join 字段的名称后跟 # 以及关系中父级的名称。例如,对于 my_parent → [my_child, another_child] 关系,join 字段会创建一个名为 my_join_field#my_parent 的附加字段。
如果文档是子级(my_child 或 another_child),此字段包含文档链接到的父级 _id;如果文档是父级(my_parent),则包含该文档自身的 _id。
在搜索包含 join 字段的索引时,这两个字段总是会在搜索响应中返回
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"
]
}
]
}
}
- 此文档属于
questionjoin - 此文档属于
questionjoin - 此文档属于
answerjoin - 子文档链接的父 ID
有关更多信息,请参阅 has_child 和 has_parent 查询、children 聚合以及 内部命中(inner hits)。
join 字段的值在聚合和脚本中是可访问的,并且可以使用 parent_id 查询进行查询
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。分片有任何更改后,都需要重新构建全局序数。分片中存储的父 ID 值越多,重新构建 join 字段的全局序数所需的时间就越长。
默认情况下,全局序数是急切构建的(eagerly built):如果索引发生了更改,join 字段的全局序数将作为刷新(refresh)的一部分进行重建。这会显著增加刷新的时间。然而,大多数时候这是一个正确的权衡,否则全局序数会在使用第一个父子关联查询或聚合时重建。这可能会为你的用户带来显著的延迟激增,并且通常情况会更糟,因为当发生大量写入时,可能会在单个刷新间隔内尝试多次重建 join 字段的全局序数。
当 join 字段使用不频繁且写入频繁时,禁用急切加载(eager loading)可能是明智的
PUT my-index-000001
{
"mappings": {
"properties": {
"my_join_field": {
"type": "join",
"relations": {
"question": "answer"
},
"eager_global_ordinals": false
}
}
}
}
可以按如下方式检查每个父关系中全局序数使用的堆内存量
# 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
也可以为单个父级定义多个子级
PUT my-index-000001
{
"mappings": {
"properties": {
"my_join_field": {
"type": "join",
"relations": {
"question": ["answer", "comment"]
}
}
}
}
}
question是answer和comment的父级。
我们不建议使用多级关系来复制关系模型。每一级关系都会在查询时增加内存和计算方面的开销。为了获得更好的搜索性能,请改用非规范化(denormalize)你的数据。
多级父/子关系
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 值
PUT my-index-000001/_doc/3?routing=1&refresh
{
"text": "This is a vote",
"my_join_field": {
"name": "vote",
"parent": "2"
}
}
- 此子文档必须与其祖父级和父级在同一个分片上
- 此文档的父 ID(必须指向一个
answer文档)