带着疑问学源码,第二篇:Elasticsearch 搜索
代码分析基于:https://github.com/jiankunking/elasticsearch
Elasticsearch 7.10.2+
目的
在看源码之前先梳理一下,自己对于检索流程疑惑的点:
当索引是按照日期拆分之后,在使用-* 检索,会不会通过索引层面的时间配置直接跳过无关索引?使用*会对性能造成多大的影响?
源码分析
第二部分是代码分析的过程,不想看的朋友可以跳过直接看第三部分总结。
分析的话,咱们就以_search操作为主线。
在RestSearchAction可以看到:
- 路由注册
- 请求参数转换
真正执行的是TransportSearchAction,类图如下:
1 | TransportSearchAction doExecute => |
下面先看一下:
1 | private void executeRequest(Task task, SearchRequest searchRequest, |
下面再看一下executeSearch:
1 | private void executeSearch(SearchTask task, SearchTimeProvider timeProvider, SearchRequest searchRequest, |
在看searchAsyncAction之前先看一下AbstractSearchAsyncAction的继承及实现类:
searchAsyncAction主要是生成查询的请求,也就是AbstractSearchAsyncAction的实例:
1 | private AbstractSearchAsyncAction<? extends SearchPhaseResult> searchAsyncAction( |
获取到具体的SearchAsyncAction之后具体的执行是通过run()来调用各个实现类具体的执行了:
1 | /** |
因为默认的搜索类型是QUERY_THEN_FETCH,那么下面看一下SearchQueryThenFetchAsyncAction,在SearchQueryThenFetchAsyncAction中没有重写run(),所以真正执行的还是父类AbstractSearchAsyncAction中的run(),下面看下:
1 | @Override |
通过performPhaseOnShard,来进行具体某个shard的搜索:
1 | protected void performPhaseOnShard(final int shardIndex, final SearchShardIterator shardIt, final SearchShardTarget shard) { |
每个分片在执行完毕Query子任务后,通过节点间通信,回调AbstractSearchAsyncAction类中的onShardResult方法,把查询结果记录在协调节点保存的数组结构results中,并增加计数:
1 | /** |
当返回结果的分片数等于预期的总分片数时,协调节点会进入当前Phase的结束处理,启动下一个阶段Fetch Phase的执行。注意,ES中只需要一个分片执行成功,就会进行后续Phase处理得到部分结果,当然它会在结果中提示用户实际有多少分片执行成功。
onPhaseDone会调用executeNextPhase方法进入下一个阶段,从而开始进入Fetch 阶段。
1 | /** |
下面看一下FetchSearchPhase中的run():
1 | @Override |
从代码在哪个可以看到Fetch后的结果保存到了counter中,而counter是定义在innerRun内:
1 | final CountedCollector<FetchSearchResult> counter = new CountedCollector<>(fetchResults, |
fetchResults用于存储从某个shard收集的结果,每收到一个shard数据就执行一次counter.countDown()。当所有shard收集完成之后,countDown会触发执行finishPhase:
1 | // FetchSearchPhase类中 |
获取查询结果之后,进入ExpandSearchPhase类中的run():
1 | // 主要判断是否启用字段折叠(field collapsing),根据需要实现字段折叠, |
看到这里还是没有发现针对-*有什么特殊的优化,还是会根据检索条件遍历符合条件的所有索引及其shard。那下面看那一下具体获取数据的时候有没有什么特殊处理,也就是data node 在Query、Fetch阶段有没有什么特殊的优化?
下面看一下SearchTransportService下的[sendExecuteQuery]https://github.com/jiankunking/elasticsearch/blob/master/server/src/main/java/org/elasticsearch/action/search/SearchTransportService.java#L139)
1 | public void sendExecuteQuery(Transport.Connection connection, final ShardSearchRequest request, SearchTask task, |
通过请求路径QUERY_ACTION_NAME可以在SearchTransportService中找到对应的处理函数searchService.executeQueryPhase:
1 | transportService.registerRequestHandler(QUERY_ACTION_NAME, ThreadPool.Names.SAME, ShardSearchRequest::new, |
下面具体看一下执行:
1 | public void executeQueryPhase(ShardSearchRequest request, boolean keepStatesInContext, |
先略过cache部分,重点看一下QueryPhase类中的execute:
1 | public void execute(SearchContext searchContext) throws QueryPhaseExecutionException { |
searchWithCollectorManager与searchWithCollector都是调用ContextIndexSearcher类中的search调用Lucene接口进行查询。
到目前为止都没发现,针对-*查询都没有任何优化。
唯一有希望进行优化的地方就是通过luece检索shard的时候,会进行优化,事实上会的:
数据节点判断某个 Range 查询与分片是否存在交集,依赖于 Lucene 的一个重要特性:PointValues 。在早期的版本中,数值类型在 Lucene 中被转换成字符串存入倒排索引,但是由于范围查询效率比较低,从 Lucene 6.0开始,对于数值类型使用 BKD-Tree 来建立索引,内部实现为 PointValues。PointValues原本用于地理位置场景,但它在多维、一维数值查询上的表现也很出色,因此原先的数值字段(IntField,LongField,FloatField,DoubleField)被替换为(IntPoint,LongPoint,FloatPoint,DoublePoint)
关于 BKD-Tree 的性质请参阅其他资料,暂且只需要知道 Lucene为每个字段单独建立索引,对于数值字段生成 BKD-Tree,一个新的 segment 生成时会产生一个新的.dim和.dii文件。最重要的,可以获取到这个 segment 中数值字段的最大值和最小值,为 pre-filter 提供了基础。当 segment 被 reader 打开的时候,Lucene 内部的 BKDReader 会将最大值和最小值读取出来保存到类成员变量,因此每个 segment 中,每个数值字段的最大最小值都是常驻 JVM 内存的。
既然每个 segment 记录了数值字段的取值范围,获取shard 级别的范围就轻而易举:PointValues.getMaxPackedValue(),PointValues.getMinPackedValue(),函数遍历全部的 segment 分别计算最大值和最小值,然后根据查询条件判断是否存在交集。
题外话:BKD-Tree 的每个节点都记录了节点自己的maxPackedValue、minPackedValue。
总结
elasticsearch针对-*检索不会在索引、shard层面优化,但会在检索具体shard的时候,通过luece的特性来快速调过一些不符合条件的shard。因此在shard数不是很多的情况下使用-*消耗的是一些RPC调用,对于性能影响不大。
对于luece部分我也不熟悉,后面有时间学习一下。