我们在使用Elasticsearch的过程中,很多业务场景都会用到关联查询。而目前Elasticsearch支持的关联查询无非就是两种方式,一种使用嵌套(nested)和父子文档。本文主要来聊聊关于nested,Elasticsearch。
文章末尾会附上nested和父子文档的差别和使用场景。
如果大家有过一些Lucene基础的话,相信都会知道Lucene中是不支持像嵌套这种数据结构的,而Elasticsearch不过是在Lucene的基础之上,通过hack的方式做了一些修改来支持了嵌套结构,使搜索功能更佳强大,用起来更方便。那么Elasticsearch具体是如何实现的?
ElasticSearch的官方说法:
参考:https://www.elastic.co/guide/en/elasticsearch/reference/7.4/nested.html
翻译过来大概有如下几点:
- nested类型在索引里是作为Document单独存储的。nested类型可能是1个包含100数据的数组,那就是100个Document。每多1个nested类型,就多增加对应length的Document.
- 普通的query对nested字段查询无效,必须使用nested Query
- highlight高亮也需要使用专门的Query
- 对于nested类型的field个数是有限制的, 长度也是有限制的。
从官网的介绍中我们了解到,nested的存储是单独进行存储的,这其实也就解释了为什么我们明明写入了100条数据用CAT接口查看时数据条数时却显示要比100多很多的原因了。
那么为什么我们普通的检索并没有检索到nested文档的内容呢,Elasticsearch是如何把nested过滤掉的?
首先我们先看Elasticsearch是如何将nested进行分开存储的。
Elasticsearch有一个类org.elasticsearch.index.mapper.DocumentParser,是专门用来解析要索引的Document的。
源码如下:
我们进入nestedContext方法:
源码位置:
https://github.com/elastic/elasticsearch/blob/6a5bae184b80c8a0012158c217de340535e9f45c/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java
从源码中可以看出来,生成nested文档主要干了两件事:
- 指定_id值为父Doc的id,用来关联
- 指定_type值为以”__”开头的,标识特定nested 类型。
看完了nested文档的生成我们知道了nested文档是和普通文档一样被储存的,但是对外确实隐藏的,我们来看看Elasticsearch是如何做到的。
源码如下:
大家看到这里应该清楚了,查询query的判断决定是否返回nested文档的依据是文档中是否包含_primary_term字段,由此可知,nested文档是不包含_primary_term字段的,而其他文档是包含这个字段的。
_primary_term:是一个整数,每当Primary Shard发生重新分配时,比如重启,Primary选举等,_primary_term会递增1。
我们下面来看_primary_term是如何添加到字段中的。
源码如下:
源码位置:https://github.com/elastic/elasticsearch/blob/3ac6d527a1386d19008cdd08cdbfef265da30f00/server/src/main/java/org/elasticsearch/index/mapper/SeqNoFieldMapper.java
到这里相信大家都看清楚怎么回事了,那么有的同学就会说了,前面看到的type为“__”开头这个源码里没有处理逻辑呀。那是因为上面的源码是目前最新版本(7.x)的源码,大家都知道7.x版本开始已经不再需要type了。但是大家绝大多数使用的还是7.x以前的版本,笔者特意找了一下早期版本的源码,实现方式还是有些区别的。
源码如下:
/**
* Creates a new non-nested docs query
* @param indexVersionCreated the index version created since newer indices can identify a parent field more efficiently
*/
public static Query newNonNestedFilter(Version indexVersionCreated) {
if (indexVersionCreated.onOrAfter(Version.V_6_1_0)) {
// 6.1.0版本之后。 只保留有_primary_term这个元字段的query
return new DocValuesFieldExistsQuery(SeqNoFieldMapper.PRIMARY_TERM_NAME);
} else {
// 6.1.0版本之前版本
return new BooleanQuery.Builder()
.add(new MatchAllDocsQuery(), Occur.FILTER)
//过滤掉nested query
.add(newNestedFilter(), Occur.MUST_NOT)
.build();
}
}
public static Query newNestedFilter() {
// _type以“__”为前缀
return new PrefixQuery(new Term(TypeFieldMapper.NAME, new BytesRef("__")));
}
总结来说ElasticSearch对nested隐藏实现方式:
- 小于6.1.0版本中,过滤掉_type以“__”为前缀的document
- 大于等于6.1.0版本中只获取有 __primary_term Field的document
同时nested也有两个缺点:
- nested无形中增加了索引量,如果不了解具体实现,将无法很好的进行文档划分和预估。ES限制了Field个数和nested对象的size,避免无限制的扩大。
- nested Query 整体性能慢,但比parent/child Query稍快。应从业务上尽可能的避免使用NestedQuery,
对于性能要求高的场景,应该直接禁止使用。
附:nested 和 parent-child的区别以及使用场景
主要区别:
- 由于存储结构的不同,nested和parent-child的方式有不同的应用场景。
- nested 所有实体存储在同一个文档,parent-child模式,子type和父type存储在不同的文档里。
- 查询效率上nested要高于parent-child。
- 更新效率上parent-child要高于nested,更新的时候nested模式下,es会删除整个文档再创建,而parent-child只会删除你更新的文档在重新创建,不影响其他文档。
使用场景:
- nested:在少量子文档,并且不会经常改变的情况下使用。
比如:订单里面的产品,一个订单不可能会有成千上万个不同的产品,一般不会很多,并且一旦下单后,下单的产品是不可更新的。 - parent-child:在大量文档,并且会经常发生改变的情况下使用。 比如:用户的浏览记录,浏览记录会很大,并且会频繁更新
更多文章关注公众号