不同于关系型数据库, 我们使用es, 一般出于性能、弹性、近实时搜索、大数据量的分析等目的。
然而, 在构建es数据模型时, 免不了会涉及到关系型数据的问题。关系型的数据在实际应用中广泛存在, 关系型数据库对此比较在行, 比如ACID支持、join查询等。es并不擅长这些,es的使用场景, 不是作为关系型数据库而存在的。
当然, 反过来关系型数据库也有不足的地方,比如比较弱的全文搜索、昂贵的join搜索开销、聚合分析等,这些却是es的专业领域。
es和大部分NoSQL数据库一样, 把数据视为平的, 不能够rollback到index之前的状态。但平的数据模型有他的好处, 比如无锁的快速索引、搜索、大数据量的节点间拓展等, 都非常符合es的使用场景。
处理关系型数据
但是, 并不是说关系型不重要,因为即使是在es中,当然无可避免要管理关系型的数据,一般来说,es 主要通过以下的方式或技巧管理:
- 应用端join
- 反规范化
- 嵌套对象
- parent/child 关联
常见的最终解决方案也来自于这些方法及其混合的使用,接下来分别介绍这些方式:
应用端join
我们可以模拟数据库的join, 把操作移到了客户端进行。
比如有以下数据:
PUT /my_index/user/1 |
如果要找用户名为“John”的博客,首先获取user.id:
GET /my_index/user/_search |
这时获取到user.id后,再去查询这个user的blogposts:
GET /my_index/blogpost/_search |
可以发现,这里的数据仍旧是规范化的,只不过要在客户端进行join,因此搜索需要进行额外的请求。
而且,上面的例子有个问题,由于现实中叫做“John”的user有很多,所以第二次请求的搜索和结果可能很大。
因此这种方法适合于第一个entity比较少的情况,最好修改变动比较少。
反规范化
如果要获得更好的性能,正如es所追求的,可以在索引的时候反模式化你的数据,使用冗余数据来避免join。
接着上面的例子,我们可以把name写入到blogpost的document中:
|
这时查询就可以用一个请求了:
GET /my_index/blogpost/_search |
这种方式最大的好处就是速度,因为避免了昂贵开销的join操作。
当然,这种方式也有缺点,一个是index的size会相对大一点,这是当然的,因为你冗余了数据。这个倒不是大问题,因为写到硬盘的数据都是高速压缩的,而且es的易拓展的。
另一个要关注的问题是,当修改时,冗余数据也需要更新。比如你改了user.name,那么blogpost中的user.name也需要更新。这个业务场景还好,因为一个用户的博客数很少超过几千篇的,使用这些这些批量更新的接口(scan-scroll和bulk)也不需要一秒。
嵌套对象
因为一个document的创建更改删除都是原子操作,那么把紧密的实体关联写入到同一个document也是有好处的。
前面我们提到,blogpost中可以冗余一个user
的对象,这个user
类型其实属于object
(和这一节介绍的不一样),它的实现其实是user.id
作为一个key,由key-value对的列表组成document来保存,那如果一个数组作为一个field是怎样索引的呢 ?
假如一个blopost可以有多个comments:
PUT /my_index/blogpost/1 |
因为没有不指定comments
的类型, 它将会是object
类型,索引里大概是这样的:
{ |
这样我们搜索某个comments.name的值时,就无法查询到对应哪个comments了,比如Alice White
和31
的关联就丢失了。所以我们需要使用一个叫nested
的类型,来代替object
作为comments的field type,具体操作可参考——Nested Object Mapping。
使用nested
类型之后,每一个nestd的对象都会被索引成一个hidden separate document
,类似这样:
{ |
虽然把nested object分开索引了,fields还保持了他们间的联系,可以进行inner object不能做的comment查询。不仅如此,nested document和root document的join非常快,接近同一个document中的速度。
这些额外的nested document是隐藏的,不能够直接访问,修改增加删除都需要重新索引整个document。
parent-child relationships
nested objects, 所有实体在同一document中(json 角度看),但使用 parent-child 的 parent 和 children 是分开的 documents.
parent-child 对比 nested objects 的好处:
- parent document可以被update而无需重新索引children
- Child documents 可以被 added, changed, or deleted,而不会影响parent和其他children。
- Child documents 可以在搜索结果中被返回
由于es维护了一个映射:哪些 parents 关联了哪些 children,也多亏了这个映射,使查询时的 joins 速度是比较快的,但是也有一个限制:parent document 及其 children 必须在同一个shard中。
这个 parent-child 的映射被保存在Doc Values(documents到terms的映射)中, 当fully hot in memory时可以快速地执行,当太大时将写回磁盘。
不过因为 Parent-child 使用了 global ordinals 来加速 joins,不管使用了 in-memory cache 或者 on-disk doc values, 当索引改变时 global ordinals 需要重新构建。shard的parents越多,rebuild所花的时间就越多。刷新后的第一次 parent-child query or aggregation之后,会触发global ordinals构建,这时可能触发一个比较明显的延时,可以使用eager_global_ordinals来优化查询时间,把开销转移到 refresh time。
因此 parent-children 比较适合这种场景,parent 有许多 children, 而不是很多 parents 和很少children。
最后
上面只是介绍了几种处理关系型数据的方式及优缺点,具体还是要结合实际的应用场景,来选择最适合的方式。
参考
https://www.elastic.co/guide/en/elasticsearch/guide/current/parent-child-performance.html
https://www.elastic.co/guide/en/elasticsearch/guide/current/relations.html
https://www.elastic.co/guide/en/elasticsearch/guide/1.x/relations.html
preload-fielddata - https://www.elastic.co/guide/en/elasticsearch/guide/current/preload-fielddata.html#global-ordinals
Doc Values - https://www.elastic.co/guide/en/elasticsearch/guide/current/docvalues.html
Inner Objects - https://www.elastic.co/guide/en/elasticsearch/guide/current/complex-core-fields.html#inner-objects
Complex Core Field Types - https://www.elastic.co/guide/en/elasticsearch/guide/current/complex-core-fields.html#object-arrays
Nested datatype - https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html
Doc Values Intro - https://www.elastic.co/guide/en/elasticsearch/guide/current/docvalues-intro.html