在Elasticsearch6.0中,引入了一个新的特征,叫Index Sorting(索引排序)。用户可以将索引数据按照指定的顺序存储在硬盘上,这样在搜索排序取前N条时,不需要访问所有的匹配中的记录再进行排序,只需要访问前N条记录即可。

Lucene中的Index Sorting

很多年以前,Lucene引入了一个工具叫做IndexSorter。这个工具的作用是从一个已有的索引中取数,然后按照指定的字段排序输出到另一个索引中。当然,这个排好序的索引是不允许用户进行数据修改的,所以用户每次修改了源索引,必须要重新构建生成目标索引。IndexSorter是Lucene第一次尝试在索引阶段,直接将数据排序存储到硬盘上。

有了索引排序的概念以后,另一个“early termination(早期中断)”概念也被引入。假设你要搜索按照日期排序的前N条记录时,如果索引在硬盘上已经按照日期排好序了,那么我们只需访问匹配搜索条件的前N条记录,就可以中断请求。这就是早期中断的概念。很明显,早期中断带来了巨大的性能提升,不需要访问所有的匹配中的记录,不需要再搜索时排序。但是,IndexSorter也有问题,不能适应频繁更新的索引。这又引入了一个新的想法,在在索引Merge阶段进行排序。

Lucene的提升

Lucene会定期的刷新产生一个新的段(Segment),这个新的段包含了自从上次刷新以后新到达的所有数据。一旦刷新产生了段,数据就可以被搜索展示了。因为刷新频繁的发生,Elasticsearch 1s刷新一次,所以会产生大量的段文件,为了限制段文件的数量,Lucene会在后台定期的合并(merge)这些段文件。合并其实就是将多个小文件合并成一个大文件,为了能在merge阶段对文档重新排序,Lucene引入了一种新的merge策略。这种新的设计使得用户可以不使用IndexSorter这种静态工具就能在索引阶段对数据进行排序。由于一些段已经排序,而新刷新的段则没有排序,所以,merge的时候首先对没排序的段排序,然后与已排好序的段进行合并。

当然,在merge阶段排序,会带来索引吞吐量的降低。下面的测评显示了merge阶段的排序将总共的吞吐量降低了一倍。




es 排序条件 es如何排序_es 排序条件


https://home.apache.org/~mikemccand/lucenebench/sparseResults.html#index_throughput

性能降低的原因也很简单:merge阶段需要先对未排序的段进行排序,然后再合并。

为了减少对多个段的再排序花费,Lucene决定在索引阶段(flush)就对数据进行排序。现在,因为所有的段文件都是排好序的,merge过程更加快速了。这个在刷新阶段就排序的特性在Lucene6.5被引入,带来了65%的性能提升。

借助于Lucene的努力,Elasticsearch6.0引入了Index Sorting的概念,大大提升了搜索的速度。

搜索时的早期中断

很常见的一种搜索,按照某个字段进行排序,返回前N条记录。虽然只是获取前N条记录,Elasticsearch需要遍历所有匹配的文档,排序,然后返回前N条。尽管Doc values使得排序更加的有效,但是当匹配的数据集较大时,这个过程仍然会耗费较大的资源。

有了index sorting,我们可以提前指定文档的存储顺序,这样在搜索时,只需要访问前N条记录,就可以中断。

例如下面的图示,5.x需要访问所有的文档,排序计算前3条记录。而6.x因为提前按照顺序存储了文档,在搜索时只需访问前3条即可。


es 排序条件 es如何排序_es 排序条件_02


如何指定索引顺序

PUT scores 
{
 "settings": {
 "index": {
 "sort.field": "points",
 "sort.order": "desc"
 }
 },
 "mappings": {
 "score": {
 "properties": {
 "points": {
 "type": "long"
 },
 "playerid": {
 "type": "keyword"
 },
 "game": {
 "type": "keyword"
 }
 }
 }
 }
}

将相似的文档聚合在了一起

按照顺序存储文档,可以将相似的文档存储到一起,能够带来更好的压缩(例如差值压缩),以及更快的查询速度(不是绝对,查询速度取决于你的查询类型和查询语句)。

更加有效的And交集查询

例如下面的查询

GET players/player/_search 
{
 "size": 3,
 "track_total_hits": false,
 "query": {
 "bool": {
 "filter": [
 {
 "term": {
 "region": "eu"
 }
 },
 {
 "term": {
 "game": "dragons-lair"
 }
 },
 {
 "term": {
 "skill-rating": 9
 }
 },
 {
 "term": {
 "map": "castle"
 }
 }
 ]
 }
 }
}

Elasticsearch会按照下面的方式获取结果返回给用户:


es 排序条件 es如何排序_搜索_03


如果我们在存储时指定了顺序:

PUT players 
{
 "settings": {
 "index": {
 "sort.field": [
 "region",
 "game",
 "skill-rating",
 "map"
 ],
 "sort.order": [
 "asc",
 "asc",
 "asc",
 "asc"
 ]
 }
 },
 "mappings": {
 "player": {
 "properties": {
 "playerid": {
 "type": "keyword"
 },
 "region": {
 "type": "keyword"
 },
 "skill-rating": {
 "type": "integer"
 },
 "game": {
 "type": "keyword"
 },
 "map": {
 "type": "keyword"
 }
 }
 }
 }
}

现在文档被存放在了一起:


es 排序条件 es如何排序_搜索_04


查询获取到的数据集中在了一起,减少了随机寻址。

什么时候不建议使用索引排序

前面已经说过,在索引阶段排序会降低写入的吞吐量,如果你的应用对于写入的性能要求高,那么索引排序并不是一个好的选择。用户需要平衡查询速率和写入速率,实际测试以后做出自己的选择。