elasticsearch源码分析之search查询(十一)

分布式查询

elasticsearch的搜索主要分为结构化搜索和全文检索。
结构化搜索(Structured search) 是指有关探询那些具有内在结构数据的过程。比如日期、时间和数字都是结构化的:它们有精确的格式,我们可以对这些格式进行逻辑操作。比较常见的操作包括比较数字或时间的范围,或判定两个值的大小。说白了就是类SQL检索。
全文搜索(full-text search)是怎样在全文字段中搜索到最相关的文档。
因为我们主要针对解决OLAP问题,所以此处只介绍结构化搜索。
elasticsearch整个查询是scatter/gather思想,也是多数分布式查询的套路,即:
1. master服务端(配置为node.master: true)接收客户端请求,查找对应的index、shard,分发数据请求到对应node服务端(node.data: true)
2. node端负责数据查询,返回结果到master端
3. master端把查询结果进行数据合并
上面流程是一个逻辑流程,es的具体查询过程中会分为不同的查询类型:QUERY_THEN_FETCH、QUERY_AND_FETCH(Deprecated),有不同的查询动作。
由于QUERY_AND_FETCH在5.X已经废除(使用QUERY_THEN_FETCH替代),所以这里只介绍QUERY_THEN_FETCH查询流程。

master服务端

1、接收查询请求,进行readblock检查。根据request的index构造相应的ShardsIterator,shardIterators由localShardsIterator和remoteShardIterators合并而成,用户遍历所有的shard。生成shardits会有一些查询策略,控制每个shard的查询优先次序和条件控制。

preferenceType = Preference.parse(preference);
switch (preferenceType) {
   case PREFER_NODES:
       final Set nodesIds =
               Arrays.stream(
                       preference.substring(Preference.PREFER_NODES.type().length() + 1).split(",")
               ).collect(Collectors.toSet());
       return indexShard.preferNodeActiveInitializingShardsIt(nodesIds);
   case LOCAL:
       return indexShard.preferNodeActiveInitializingShardsIt(Collections.singleton(localNodeId));
   case PRIMARY:
       return indexShard.primaryActiveInitializingShardIt();
   case REPLICA:
       return indexShard.replicaActiveInitializingShardIt();
   case PRIMARY_FIRST:
       return indexShard.primaryFirstActiveInitializingShardsIt();
   case REPLICA_FIRST:
       return indexShard.replicaFirstActiveInitializingShardsIt();
   case ONLY_LOCAL:
       return indexShard.onlyNodeActiveInitializingShardsIt(localNodeId);
   case ONLY_NODES:
       String nodeAttributes = preference.substring(Preference.ONLY_NODES.type().length() + 1);
       return indexShard.onlyNodeSelectorActiveInitializingShardsIt(nodeAttributes.split(","), nodes);
   default:
       throw new IllegalArgumentException("unknown preference [" + preferenceType + "]");
}

2、根据条件设置查询类型,根据查询类型构造出AbstractSearchAsyncAction(继承了InitialSearchPhase),异步查询action。查询类型QUERY_THEN_FETCH构造出SearchQueryThenFetchAsyncAction。start方法启动异步查询。

QUERY阶段

3、query shard阶段。如果需要查询的shard数为空,则直接返回。遍历shardits,每个shard执行query请求操作

for (final SearchShardIterator shardIt : shardsIts) {
    shardIndex++;
    final ShardRouting shard = shardIt.nextOrNull();
    if (shard != null) {
        performPhaseOnShard(shardIndex, shardIt, shard);
    } else {
        // really, no shards active in this group
        onShardFailure(shardIndex, null, null, shardIt, new NoShardAvailableActionException(shardIt.shardId()));
    }
}

4、监听所有shard query请求,成功返回回调onShardResult方法,失败返回回调onShardFailure方法。onShardResult维护了shard计数器的工作,onShardFailure维护了计数器和shard失败处理工作(失败后请求该shard的下一个副本,重新发起请求)。上面所有shard均已返回(计数器判断),则执行onPhaseDone,即executeNextPhase,进入fetch阶段。

try {
    executePhaseOnShard(shardIt, shard, new SearchActionListener(new SearchShardTarget(shard.currentNodeId(),
        shardIt.shardId(), shardIt.getClusterAlias(), shardIt.getOriginalIndices()), shardIndex) {
        @Override
        public void innerOnResponse(FirstResult result) {
                onShardResult(result, shardIt);
        }

        @Override
        public void onFailure(Exception t) {
            onShardFailure(shardIndex, shard, shard.currentNodeId(), shardIt, t);
        }
    });
} catch (ConnectTransportException | IllegalArgumentException ex) {
    onShardFailure(shardIndex, shard, shard.currentNodeId(), shardIt, ex);
}

FETCH阶段

5、FetchSearchPhase,fetch阶段。如果query阶段shard全部失败,则通过raisePhaseFailure抛出异常,否则执行FetchSearchPhase.innerRun。如果不需要进行fetch抓取(聚合查询),则直接调用finishPhase进行数据合并处理;如果需要进行fetch抓取(明细查询),则调用executeFetch进行数据抓取,返回后进行数据合并。
6、数据合并工作主要有searchPhaseController.merge完成。主要完成search hits,合并aggregations聚合和分析结果。结果返回给client。

context.onResponse(context.buildSearchResponse(response, scrollId));
...
public final SearchResponse buildSearchResponse(InternalSearchResponse internalSearchResponse, String scrollId) {
   return new SearchResponse(internalSearchResponse, scrollId, getNumShards(), successfulOps.get(),
       buildTookInMillis(), buildShardFailures());
}
...
public final void onResponse(SearchResponse response) {
    listener.onResponse(response);
}

node服务端

QUERY阶段

1、接收到master端发送来的queryaction,执行executeQueryPhase。其中SearchContext为查询阶段的上下文对象,读取某个参考时间点快照的shard(IndexReader / contextindexsearcher),支持从query阶段到fetch阶段,查询过程中主要操作该对象。

final SearchContext context = createAndPutContext(request);
final SearchOperationListener operationListener = context.indexShard().getSearchOperationListener();
context.incRef();
boolean queryPhaseSuccess = false;
try {
    context.setTask(task);
    operationListener.onPreQueryPhase(context);
    long time = System.nanoTime();
    contextProcessing(context);

    loadOrExecuteQueryPhase(request, context);

    if (context.queryResult().hasSearchContext() == false && context.scrollContext() == null) {
        freeContext(context.id());
    } else {
        contextProcessedSuccessfully(context);
    }
    final long afterQueryTime = System.nanoTime();
    queryPhaseSuccess = true;
    operationListener.onQueryPhase(context, afterQueryTime - time);
    if (request.numberOfShards() == 1) {
        return executeFetchPhase(context, operationListener, afterQueryTime);
    }
    return context.queryResult();
} catch (Exception e) {
    // execution exception can happen while loading the cache, strip it
    if (e instanceof ExecutionException) {
        e = (e.getCause() == null || e.getCause() instanceof Exception) ?
            (Exception) e.getCause() : new ElasticsearchException(e.getCause());
    }
    if (!queryPhaseSuccess) {
        operationListener.onFailedQueryPhase(context);
    }
    logger.trace("Query phase failed", e);
    processFailure(context, e);
    throw ExceptionsHelper.convertToRuntime(e);
} finally {
    cleanContext(context);
}

创建context代码

final DefaultSearchContext searchContext = new DefaultSearchContext(idGenerator.incrementAndGet(), request, shardTarget, engineSearcher, indexService, indexShard, bigArrays, threadPool.estimatedTimeInMillisCounter(), timeout, fetchPhase);

2、执行查询阶段,loadOrExecuteQueryPhase(request, context)。首先在cache里面判断是否有缓存,如果有则执行缓存查询indicesService.loadIntoContext;如果cache里面没有,执行queryPhase.execute(context),代码如下:

if (searchContext.hasOnlySuggest()) {
    suggestPhase.execute(searchContext);
    // TODO: fix this once we can fetch docs for suggestions
    searchContext.queryResult().topDocs(
            new TopDocs(0, Lucene.EMPTY_SCORE_DOCS, 0),
            new DocValueFormat[0]);
    return;
}
// Pre-process aggregations as late as possible. In the case of a DFS_Q_T_F
// request, preProcess is called on the DFS phase phase, this is why we pre-process them
// here to make sure it happens during the QUERY phase
aggregationPhase.preProcess(searchContext);

boolean rescore = execute(searchContext, searchContext.searcher());

if (rescore) { // only if we do a regular search
    rescorePhase.execute(searchContext);
}
suggestPhase.execute(searchContext);
aggregationPhase.execute(searchContext);

if (searchContext.getProfilers() != null) {
    ProfileShardResult shardResults = SearchProfileShardResults
            .buildShardResults(searchContext.getProfilers());
    searchContext.queryResult().profileResults(shardResults);
}

3、其中execute是对索引进行查询,调用lucene的searcher.search(query, collector)。还支持聚合查询,aggregationPhase.execute(searchContext)(下节介绍)。
4、最终返回context.queryResult()。

FETCH阶段

1、接收到来自master端的fetchquery,执行executeFetchPhase。首先通过request寻找SearchContext,findContext(request.id(), request)。

final SearchContext context = findContext(request.id(), request);
final SearchOperationListener operationListener = context.indexShard().getSearchOperationListener();
context.incRef();
try {
    context.setTask(task);
    contextProcessing(context);
    if (request.lastEmittedDoc() != null) {
        context.scrollContext().lastEmittedDoc = request.lastEmittedDoc();
    }
    context.docIdsToLoad(request.docIds(), 0, request.docIdsSize());
    operationListener.onPreFetchPhase(context);
    long time = System.nanoTime();
    fetchPhase.execute(context);
    if (fetchPhaseShouldFreeContext(context)) {
        freeContext(request.id());
    } else {
        contextProcessedSuccessfully(context);
    }
    operationListener.onFetchPhase(context, System.nanoTime() - time);
    return context.fetchResult();
} catch (Exception e) {
    operationListener.onFailedFetchPhase(context);
    logger.trace("Fetch phase failed", e);
    processFailure(context, e);
    throw ExceptionsHelper.convertToRuntime(e);
} finally {
    cleanContext(context);
}

2、核心的查询方法是fetchPhase.execute(context)。主要是轮流通过上轮query结果中的docsIds,创建SearchHit[]集合,最后放在fetchResult中。

for (int index = 0; index < context.docIdsToLoadSize(); index++) {
    ...
    final SearchHit searchHit;
    try {
        int rootDocId = findRootDocumentIfNested(context, subReaderContext, subDocId);
        if (rootDocId != -1) {
            searchHit = createNestedSearchHit(context, docId, subDocId, rootDocId, fieldNames, fieldNamePatterns, subReaderContext);
        } else {
            searchHit = createSearchHit(context, fieldsVisitor, docId, subDocId, subReaderContext);
        }
    } catch (IOException e) {
        throw ExceptionsHelper.convertToElastic(e);
    }

    hits[index] = searchHit;
    hitContext.reset(searchHit, subReaderContext, subDocId, context.searcher());
    for (FetchSubPhase fetchSubPhase : fetchSubPhases) {
        fetchSubPhase.hitExecute(context, hitContext);
    }
}

for (FetchSubPhase fetchSubPhase : fetchSubPhases) {
    fetchSubPhase.hitsExecute(context, hits);
}

context.fetchResult().hits(new SearchHits(hits, context.queryResult().getTotalHits(), context.queryResult().getMaxScore()));

3、释放SearchContext,freeContext。该释放有两类情况:1是在masterquer端如果命中该shard(需要该shard执行fetch),则执行fetch完成之后(如上介绍);2是没有命中该shard,则在master端会发送释放context的请求到指定节点,进行释放。
4、fetch查询结果返回给master端。完成。

总结

ES整个查询过程是scatter/gather的过程,具体如下:

Created with Raphaël 2.1.0 client端 client端 master端 master端 node端 node端 SearchRequest ShardSearchRequest SearchPhaseResult ShardFetchRequest FetchPhaseResult SearchResponse

下节详细学习一下es的聚合查询Aggregation。

你可能感兴趣的:(elasticsearch)