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查询流程。
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方法启动异步查询。
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);
}
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);
}
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()。
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的过程,具体如下:
下节详细学习一下es的聚合查询Aggregation。