一、前言
这篇文章还是延续着第三篇文章的精神,概述相关概念与本人的学习心得, 不涉及 API 与具体的操作,有兴趣的同学可以另行查阅学习。前几篇文章的传送门:
【Elasticsearch学习笔记-基础篇1】Elasticsearch介绍及设计概念【Elasticsearch学习笔记-基础篇2】Elasticsearch倒排索引、分析及打分【Elasticsearch学习笔记-基础篇3】Elasticsearch 聚集(aggregation)与过滤器(filter)
首先奉上这次的学习地图:
二、文档的关系
有经验的工程师一定都清楚,只要出现了数据,一定会有组织形式,有了组织形式,不同数据单元之间就一定会产生关系。es 的文档作为数据单元也不例外。在 es 中文档有以下关系:
1. 对象关系
一个文档中,对象作为值。
理论上可以实现一对多关系,但实际被用为一对一实现的情况较多。
{
"code": "200",
"msg": "Authentication Success!",
"data": {
"testFirstTimeLogin": false,
}
}
上例中,字段 data 的值就是一个对象。这样的优点是显而易见的,不管是对于搜索还是增删改,亦或是数据意义本身与组织形式的契合度。
但是在实现一对多关系上(一般为数组),在索搜时,可能会遇到对象无边界(跨对象匹配)的问题。
比如下面这个例子:
{
"code": "200",
"msg": "Authentication Success!",
"data": [
{
"name" : "polo",
"nickname" : "small nick"
},
{
"name" : "amber",
"nickname" : "big name"
}
]
}
实际 Lucene 索引数据时,是存储的是这样的结构:
{
"code": "200",
"msg": "Authentication Success!",
"data.name": [ "polo" , "amber" ],
"data.nickname": [ "small nick" , "big name" ]
}
那么假设,现在我要搜索 “name是polo && nickname是big name” 的数据,也会匹配到这篇文档,但我们的实际需求并不是获得这篇文档。 这就是我们说的对象无边际,也就是说 lucene 并不会将数组元素组成一个数据单元,而是把同一字段的值存在一起。 所以即使是命中了不同元素,但由于元素都属于这篇文档,所以也会返回这篇文档。
优点
对于对象关系存储,有着显而易见的优点:
- 易用 在存储对象时,无需进行任何的配置,数据可直接存储以及被索引。
- 搜索时,更低的性能开销 数据都存储在同一文档中,所以不存在 join 查询,所以查询的开销更小,搜索速度更快。
缺点
上面分析了,缺点一共有两个:
- 对象无边界 对象无边界实际上是最难察觉,也是最大的缺点。包含数组时,一定要多加注意。
- 修改对象内容会重新索引整个文档 由于整个文档是一体的,所以自然而然要重新索引整个文档。
2. 嵌套关系
嵌套关系也是一对多关系的一种实现。在对象关系中,如果包含了数组,会出现对象无边界的问题,嵌套关系就是用来解决这个问题的。如果不设置索引,es 默认数组还是对象关系。
如果启用嵌套关系,就需要在索引数据前,设置索引字段的类型。
比如如果 user 的值是数组,那我们就需要设置:
"mappings":{
"my_type":{
"properties":{
"group":{ "type":"string"},
"user":{
"type":"nested",
"properties":{
"first":{ "type":"string"},
"second":{ "type":"string"}
}
}
}
}
}
设置后,虽然表面上,他们还是在同一个文档中。但在索引时 es 就会保证每个数组包含的对象分别索引到多个文档。但是又因为,系统会把这些对象他们放到同一个 block (这里的 block 并不是磁盘的物理 block ,而是 es 规定的 block。)中,所以他们的在被查询时,获取错做次数是最少的。形如下图:
在查询时,也需要使用嵌套过滤器或嵌套查询来搜索。 例如:
GET my_index/_search
{
"query": {
"nested": {
"path": "user",
"query": {
"bool": {
"must": [
{ "match": { "user.first": "Alice" }},
{ "match": { "user.last": "White" }}
]
}
}
}
}
}
当然,嵌套里面也可以继续嵌套(无限套娃),并且也可以开启跨对象(模拟对象无边界)查询。例如 include_in_root 可以将指定的字段索引到跟文档,从而开启跨对象查询。再比如可以通过 include_in_parent 来设置套娃的嵌套,等等。在这里不过多展开,有兴趣同学可以自行学习。
优点
- 对象隔离的可配置 使搜索结果符合特定场景,可以选择对象有边界或无边界来隔离对象。
- 同块存储 减少搜索开销,同块存储可以最小化读取的操作次数。
缺点
- 相比于对象关系各方面开销增长 源于同块存储,虽然表面上是一个文档,实际上是同块存储的多个文档。
3. 父子关系
两个不同的文档之间的关系,是一种一对多关系的实现。
子文档通过设置 _parent : $id 将子文档指向父文档,形成父子关系。如果确定了父子关系的两个文档,可以通过 has_parent ,用子文档找父文档。也可以通过 has_child ,用父文档找子文档。
同为一对多关系的实现,但与嵌套关系有很大不同:
- 分块存储 父子文档是两篇实实在在的、分块存储的文档,没有做任何的存储优化。所以在降低文档耦合性的同时,也增加了查询的开销。 与此同时,因为这个特性也会带来相应的优缺点。
优点
相对于嵌套关系,父子关系更加灵活,因为父子关系的建立只需要一个指针,更新其中任意一文档都不影响其他文档,所以更加适合文档增删改频繁的场景。
缺点
父子文档由于是分块存储,所以搜索时操作步骤增多,开销增大,性能下降。
4. 反规范化关系
反规范化关系,是多对多关系的一种实现。
如果在大学使用反规范化设计表结构,是要遭老师批评的,因为设计的东西不符合三范式。三范式可以说是解决数据冗余,数据有效性的保证。
但是,工作后就会发现,反规范化设计不仅仅是用起来爽,而且确实能解决性能低下的问题:通过数据冗余来减少连接(join)操作,是一种空间换效率的做法。
对于多点节点分布式架构的搜索引擎或数据库来说,反规范化可以通过冗余节剩更多的计算开销。
优点
- 多对多关系的良好实现 实际上不仅是在 nosql 中,在关系型数据库中,反规范设计也是常规操作。反规范化设计可以有效的提高频繁增删改功能的性能,如果做出良好的数据规范设计,那么在代码编写上也会相对简洁。
- 减小查询开销 充分的数据冗余使查询时连接(join)操作变少,减少查询开销。
缺点
- 冗余 虽然提升查询开销的代价是有了更多的冗余,但是相对于查询速度的下降,存储空间的冗余对于数据密集型应用来讲,显然是可以接受的。