现实中,关系模型是到处存在的,例如书本与作者的关系。但是在ES中想要处理这个事情并不简单
在ES中保存关系型模型数据的方式主要有以下几种:
- nested:在这种方式中,会通过一对多的放系保存在同一个文档中
- join(parent/child):通过维护文档的父子节点,将两个对象分离
上面这2种方式都可以描述一对多的关系,今天我们就来了解一下
nested(嵌套类型)
nested类型是一种特别的object类型,其允许数组中的对象可以被单独索引,使他们可以被独立地索引。下面的示例是使用普通的object数组来保存书本与作者的一对多关系,我们看看会产生什么问题
bash
# 创建 Mapping
PUT books_index
{
"mappings": {
"properties": {
"book_id": { "type": "keyword" },
"author": {
"properties": {
"first_name": { "type": "keyword" },
"last_name": { "type": "keyword" }
}
}
}
}
}
# 写入书本数据
PUT books_index/_doc/1
{
"book_id": "1234",
"author": [
{ "first_name": "zhang", "last_name": "san" },
{ "first_name": "wang", "last_name": "wu" }
]
}
如上面的示例,我们创建了books_index索引,其中author字段是一个对象,包括了first_name与last_name两个属性,并且在我们写入数据的时候,书本的作者有两个(描述了一对多的关系):zhangsan和wangwu。
我们对上面的数据进行一个查询:
按道理来说我们的数据中是没有zhangwu这个作者的,但是在实际查询中能命中文档1,为什么那?
因为object被扁平化指挥,其丢失了first_name和last_name之间的关系,变成了下面这样的关系
对于这个扁平化的数组,原先first_name和last_name见的关系当然以及不复存在了。所以我们的查询语句在author.first_name中匹配了"zhang",在author.last_name匹配了"wu",自然就命中了文档1,那么有什么办法解决这个问题吗?
那就是使用nested数据类型,他可以是对象数组的对象被独立索引,这样fist_name和last_name之间的关系就不会丢失了。下面我们修改一下mapping,把author的类型定义为nested:
如上所示,使用nested关键字指定一个nested对象的查询,使用path指定nested对象的名字。
从上面的示例来看,nested通过冗余的方式将对象和文档存储在一起,所以查询时的性能是很高的,但是需要更新对象的时候,需要更新所有包含此对象的文档,例如某个作者的信息更改了,那么所有这个作者的书本文档都需要更新。所以nested适合查询频繁但更新频率低的场景
parent/child(父子类型)
join数据类型允许在一个索引中的文档创建父子关系,通过维护父子文档的关系独立出来两个对象。父文档和自文档是相互独立的,通过类似引用的关系进行绑定,所以当父文档更新时,不需要更新自文档,而自文档可以被任意的添加、修改、删除而不会影响到父文档和其他自文档
需要注意的是,为了维护父子文档的关系需要占用额外的内存资源,并且读取性能相对较差。但是由于父子文档是互相独立的,所以适合自文档更新频率高的场景
在Mapping中定义join数据类型
bash
PUT join_books_index
{
"mappings": {
"properties": {
"book_id": { "type": "keyword" },
"name": { "type": "text" },
"book_comments_relation": { # 定义字段名字
"type": "join", # 此字段为 join 类型
"relations": { # 声明 Parent / Child 的关系
"book": "comment" # book 是 Parent 的名称,comment 是 Child 的名称
}
}
}
},
"settings": {
"number_of_shards": 3, # 定义 3 个主分片
"number_of_replicas": 1
}
}
如上示例,book_comments_relation是字段的名字,使用join关键字定义此字段的类型为join类型,relations处声明了Parent/Child的关系,其中book是Parent的名称, comment是Child的名称
索引父文档(创建)
在定义了Mapping之后,我们写入父文档的数据
索引子文档(创建)
如上所示,book_comments_relation中声明了文档的类型为comment(即mapping中的自文档),并且使用parent字段指向父文档的id
为了确保查询时候的性能,父文档和子文档必须在同一个分片,所以需要强制使用routing参数,并且其值为父文档的Id(如果写入父文档的时候也用routing参数,那么需要保证他们的值是一样的)
数据检索
返回结果:
如图所示,我们在获取父文档的数据时候是不返回子文档的信息的,因为父子文档是相互独立的
获取子文档:
如上图所示,在获取子文档时,如果不加routing参数时,是无法找到对应的子文档的。routing参数的值为父文档的Id
parent id查询
如上所示,parent_id字段里面,我们查询了父文档Id为11并且comment类型的文档
返回结果:
has child查询
如果我们想查询用户"fork"评论了那些书本,可以使用 Has Child 查询 。Has Child 查询将在子文档中进行条件匹配,然后返回匹配文档对应的父文档的信息
返回结果:
has parent查询
那如果我们想查询java相关书籍的评论时,可以使用Has Parent 查询 。 Has Parent 查询会在父文档中进行匹配,然后返回匹配文档对应的子文档的信息。
返回结果: