关系型数据库中有表的关联关系,在 es 中,我们也有类似的需求,例如订单表和商品表,在 es 中,这样的一对多一般来说有两种方式:
嵌套文档(nested)
父子文档
18.1 嵌套文档
假设:有一个电影文档,每个电影都有演员信息:
PUT movies{ "mappings": { "properties": { "actors":{ "type": "nested" } } }}PUT movies/_doc/1{ "name":"霸王别姬", "actors":[ { "name":"张国荣", "gender":"男" }, { "name":"巩俐", "gender":"女" } ]}
注意 actors 类型要是 nested,具体原因参考 10.2.3 小节。
缺点
查看文档数量:
GET _cat/indices?v
查看结果如下:
image-20201119162958456
这是因为 nested 文档在 es 内部其实也是独立的 lucene 文档,只是在我们查询的时候,es 内部帮我们做了 join 处理,所以最终看起来就像一个独立文档一样。因此这种方案性能并不是特别好。
18.2 嵌套查询
这个用来查询嵌套文档:
GET movies/_search{ "query": { "nested": { "path": "actors", "query": { "bool": { "must": [ { "match": { "actors.name": "张国荣" } }, { "match": { "actors.gender": "男" } } ] } } } }}
18.3 父子文档
相比于嵌套文档,父子文档主要有如下优势:
更新父文档时,不会重新索引子文档
创建、修改或者删除子文档时,不会影响父文档或者其他的子文档。
子文档可以作为搜索结果独立返回。
例如学生和班级的关系:
PUT stu_class{ "mappings": { "properties": { "name":{ "type": "keyword" }, "s_c":{ "type": "join", "relations":{ "class":"student" } } } }}
s_c
表示父子文档关系的名字,可以自定义。join 表示这是一个父子文档。relations 里边,class 这个位置是 parent,student 这个位置是 child。
接下来,插入两个父文档:
PUT stu_class/_doc/1{ "name":"一班", "s_c":{ "name":"class" }}PUT stu_class/_doc/2{ "name":"二班", "s_c":{ "name":"class" }}
再来添加三个子文档:
PUT stu_class/_doc/3?routing=1{ "name":"zhangsan", "s_c":{ "name":"student", "parent":1 }}PUT stu_class/_doc/4?routing=1{ "name":"lisi", "s_c":{ "name":"student", "parent":1 }}PUT stu_class/_doc/5?routing=2{ "name":"wangwu", "s_c":{ "name":"student", "parent":2 }}
首先大家可以看到,子文档都是独立的文档。特别需要注意的地方是,子文档需要和父文档在同一个分片上,所以 routing 关键字的值为父文档的 id。另外,name 属性表明这是一个子文档。
父子文档需要注意的地方:
每个索引只能定义一个 join filed
父子文档需要在同一个分片上(查询,修改需要routing)
可以向一个已经存在的 join filed 上新增关系
18.4 has_child query
通过子文档查询父文档使用 has_child
query。
GET stu_class/_search{ "query": { "has_child": { "type": "student", "query": { "match": { "name": "wangwu" } } } }}
查询 wangwu 所属的班级。
18.5 has_parent query
通过父文档查询子文档:
GET stu_class/_search{ "query": { "has_parent": { "parent_type": "class", "query": { "match": { "name": "二班" } } } }}
查询二班的学生。但是大家注意,这种查询没有评分。
可以使用 parent id 查询子文档:
GET stu_class/_search{ "query": { "parent_id":{ "type":"student", "id":1 } }}
通过 parent id 查询,默认情况下使用相关性计算分数。
18.6 小结
整体上来说:
普通子对象实现一对多,会损失子文档的边界,子对象之间的属性关系丢失。
nested 可以解决第 1 点的问题,但是 nested 有两个缺点:更新主文档的时候要全部更新,不支持子文档属于多个主文档。
父子文档解决 1、2 点的问题,但是它主要适用于写多读少的场景。