Parent and Child used in Elasticsearch

Elasticsearch 父子关系

Elasticsearch的Parent和Child非常有利于我们进行关联查询,父子关系的查询在一定的场合下使用能加快我们的索引速度。由于ElasticSearch不是关系数据库,它只与搜索效率有关,与存储效率无关。存储的数据已被非规范化。这意味着联接不能跨索引,子文档和父文档必须位于相同的索引和相同的分片中。

父子关系图

对于Elasticsearch的 Parent and Child:

  1. 家庭关系:

  2. 学校关系:

  3. 等等关系我们都可以用 Parent and Child 来表示,非常有利于我们进行父子关系的查询。

Parent and Child 有如下特点:
  • 父子关系

  • 每个父母有多个孩子

  • 多个层次的亲子关系

创建 “Family_Tree” 索引

这里我们使用汽车关系来进行相关展示:

创建相关索引

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
PUT family_tree
{
"settings": {
"index":{
"number_of_shards":1,
"number_of_replicas":0
}
},

"mappings": {
"properties": {
"name":{
"type": "text"
},
"price":{
"type": "text"
},
"isSale":{
"type": "boolean"
},
"relation_type":{
"type": "join",
"eager_global_ordinals": true,
"relations":{
"parent":"child"
}
}
}
}
}

注意:Parent-child uses the "eager_global_ordinals" to speed up joins.

父子关系需要在统一分片中:我们在插入数据的时候,一般通过固定值来路由(routing)到同一个分片中。

分片规则:shard = hash(routing_value) % number_of_primary_shards

开始相关操作

父节点插入数据

1
2
3
4
5
6
7
8
9
PUT family_tree/_doc/1?routing=Car
{
"name":"Car",
"price":"2000000",
"isSale":true,
"relation_type":{
"name":"parent"
}
}

子节点插入数据

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
PUT family_tree/_doc/2?routing=Car
{
"name":"Van",
"price":"10000",
"isSale":true,
"relation_type":{
"name":"child",
"parent":1
}
}

PUT family_tree/_doc/3?routing=Car
{
"name":"Sedan",
"price":"10000",
"isSale":true,
"relation_type":{
"name":"child",
"parent":1
}
}

PUT family_tree/_doc/4?routing=Car
{
"name":"SUV",
"price":"8000",
"isSale":true,
"relation_type":{
"name":"child",
"parent":1
}
}

注意:子文档和父文档必须位于同一分片上的限制。

查询数据

搜索和过滤指定的父节点

获取Car的所有子级:parent_id查询可用于查找属于特定父级的子级文档。

1
2
3
4
5
6
7
8
9
GET /family_tree/_search?pretty=true
{
"query": {
"parent_id":{
"type":"child",
"id":"1"
}
}
}

结果:以查找出属于parent_id为 1 的所有子级文档。

在这之前我们先为 Car 添加一个不再销售的汽车类型:

1
2
3
4
5
6
7
8
9
10
PUT family_tree/_doc/5?routing=Car
{
"name":"Sports car",
"price":"30000000",
"isSale":false,
"relation_type":{
"name":"child",
"parent":1
}
}
  1. 通过bool与must结合获取所有未售的 Car 的孩子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET /family_tree/_search
{
"query": {
"bool": {
"filter": {
"term": {
"isSale": "false"
}
},
"must": [
{
"parent_id":{
"type":"child",
"id":"1"
}
}
]
}
}
}

结果:从查询到的结果中可以看到:只有”Sports car”符合我们查询的条件。

  1. 我们也可以通过has_child查询拥有子节点未销售状态的父节点信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /family_tree/_search?pretty
{
"query": {
"has_child": {
"type": "child",
"query": {
"bool": {
"must": [
{"match": {"isSale": "false"}}
]
}
}
}
}
}

提示:has_parent关键字可帮助我们获取所有有父母且符合过滤条件的孩子信息。

通过has_parent来查询父节点状态为在售的所有子节点信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /family_tree/_search?pretty
{
"query": {
"has_parent": {
"parent_type": "parent",
"query": {
"match": {
"isSale": "true"
}
}
}
}
}

小建议:

每个关系级别都会在查询时增加内存和计算方面的开销,不是很建议使用多个级别的关系模型。

小收获:

  • 父子文档必须索引到同一个分片中。

  • 每个索引仅允许一个连接字段映射。

  • 一个元素可以有多个子级,但只能有一个父级。

  • 可以向已存在的联接字段添加新关系。

  • 也可以将子元素添加到现有元素中,但前提是该元素已经是父元素。

本次总结

当索引时间性能比搜索时间性能更重要时,父子联接可能是管理关系的有用技术,但代价是很高的。必须意识到这种权衡,例如父子文档的物理存储约束和增加的复杂性。另一个预防措施是避免多层父子关系,因为这将消耗更多的内存和计算量。这些都是我们在使用父子关系的时候必须要考虑到的相关内容,避免造成不必要的损失。

---- The end of this article ----