Elasticsearch中keyword和numeric对性能的影响分析
初学者认为这两个关键字的没啥关系,一个是用于字符串的精确匹配查询,一个是数字类型的字段用在计数的场景,比如说博客的点赞数,订单金额等。
但是有些场景似乎两个关键字都可以用,比如电商场景下的订单状态,一般我们也是用数字表示不同的状态,比如1表示待支付,2表示支付成功。第一反应是用Byte(属于numeric),没有问题。但是用keyword是否可以呢?
numeric除了支持等值精确查询,还可以范围查询。但是大部分情况下我们业务场景对于订单状态的使用都是精确查询的,不会有大于某个状态或者小于某个状态这样的情况。
所以刚才说的订单状态的场景,用keyword和numeric肯定都可以满足。域名交易但是那种方案好呢?答案是keyword。对于keyword类型的term query,ES使用的是倒排索引。但是numeric类型为了能有效的支持范围查询,它的存储结构并不是倒排索引。
我们知道倒排索引在内存里维护了词典 (Term Dictionary)和文档列表(Postings List)的映射关系,倒排索引本身对于精确匹配查询是非常快的,直接从字典表找到term,然后就直接找到了posting list。
numeric类型从lucene6.0开始,使用了一种名为block KD tree的存储结构。
kd-tree(k-dimensional树的简称),是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。这种存储结构类似于mysql里的B+数,我们知道B+数这种数据结构对于范围查找的支持是非常好的。不同于mysql, Block KD tree的叶子节点存储的是一组值的集合(block),大概是512~1024个值一组。这也是为什么叫block kd tree。
Block KD tree对于范围查询,邻近搜索支持的非常好,尤其是多维空间的情况下。
每个节点有三个元素,所以这里K=3,不同于简单二叉树每个节点都是一个元素(如下面这个图)。这样就可以方便的在一个三维的空间进行范围的比较。
标准的二叉树
对于上图中的kd-tree,搜索的过程是这样的:首先和根节点比较第一项,小于往左,大于往右,第二层比较第二项,依次类推。每层参与比较的数据是不一样的。
具体的ES内部(其实是Lucene),目前的版本是基于所谓的PointValues,比如整型在Lucene内部是IntPoint类表示,还有DoublePoint等,完整的对应关系是:
Java type Lucene classint IntPointlong LongPointfloat FloatPointdouble DoublePointbyte[] BinaryPoint
而这些PointValues是基于kd-tree存储的,根据官方文档的介绍,lucene把叶子节点在磁盘是顺序存储的,这样搜索的效率就会非常高。
为啥numeric对于term精确匹配的查询性能没有keyword好
前面我们提到了IntPoint类,这个类有三个查询方法:
//构造精确查询,内部还是调用newRangeQueryQuery newExactQuery(String field, int value)
//构造一维查询,内部是调用多维查询的方法Query newRangeQuery(String field, int lowerValue, int upperValue)
//构造多维查询Query newRangeQuery(String field, int[] lowerValue, int[] upperValue)
IntPoint.java
比如我们有这样一个索引:
PUT blogs { "mappings": { "properties": { "title": { "type": "keyword" }, "content": { "type": "text" }, "status": { "type": "integer" } } }}
如果我们基于status查询,
{ "query": { "term": { "title": { "status": 2 } } }}
在lucene内部其实还是进行了一个2~2的范围查询。即便kd-tree的性能也很高,但是对于这种精确查询还是要到树上走一遭,而倒排索引相当于是直接在内存里就定位到了结果集的文档id。如果是bool组合查询的话,term还可以利用跳表,这点numeric字段也是做不到的。