zoukankan      html  css  js  c++  java
  • ES查询流程源码解析

    一些基础知识

    早先ES的HTTP协议支持还是依赖Jetty的,现在不管是Rest还是RPC都是直接基于Netty了。

    另外值得一提的是,ES 是使用Google的Guice 进行模块管理,所以了解Guice的基本使用方式有助于你了解ES的代码组织。

    ES 的启动类是 org.elasticsearch.bootstrap.Bootstrap。在这里进行一些配置和环境初始化后会启动org.elasticsearch.node.Node。Node 的概念还是蛮重要的,节点的意思,也就是一个ES实例。RPC 和 Http的对应的监听启动都由在该类完成。

    Node 属性里有一个很重要的对象,叫client,类型是 NodeClient,我们知道ES是一个集群,所以每个Node都需要和其他的Nodes 进行交互,这些交互则依赖于NodeClient来完成。所以这个对象会在大部分对象中传递,完成相关的交互。

    先简要说下:

    • NettyTransport 对应RPC 协议支持
    • NettyHttpServerTransport 则对应HTTP协议支持

    Rest 模块解析

    首先,NettyHttpServerTransport 会负责进行监听Http请求。通过配置http.netty.http.blocking_server 你可以选择是Nio还是传统的阻塞式服务。默认是NIO。该类在配置pipeline的时候,最后添加了HttpRequestHandler,所以具体的接受到请求后的处理逻辑就由该类来完成了。

    pipeline.addLast("handler", requestHandler);

    HttpRequestHandler 实现了标准的 messageReceived(ChannelHandlerContext ctx, MessageEvent e) 方法,在该方法中,HttpRequestHandler 会回调NettyHttpServerTransport.dispatchRequest方法,而该方法会调用HttpServerAdapter.dispatchRequest,接着又会调用HttpServer.internalDispatchRequest方法(额,好吧,我承认嵌套挺深有点深):

    
    
    public void internalDispatchRequest(final HttpRequest request, final HttpChannel channel) {
    
    String rawPath = request.rawPath();
    
    if (rawPath.startsWith("/_plugin/")) {
    
    RestFilterChain filterChain = restController.filterChain(pluginSiteFilter);
    
    filterChain.continueProcessing(request, channel);
    
    return;
    
    } else if (rawPath.equals("/favicon.ico")) {
    
    handleFavicon(request, channel);
    
    return;
    
    }
    
    restController.dispatchRequest(request, channel);
    
    }

    这个方法里我们看到了plugin等被有限处理。最后请求又被转发给 RestController。

    RestController 大概类似一个微型的Controller层框架,实现了:

    1. 存储了 Method + Path -> Controller 的关系
    2. 提供了注册关系的方法
    3. 执行Controller的功能。

    那么各个Controller(Action) 是怎么注册到RestController中的呢?在ES中,Rest*Action 命名的类的都是提供http服务的,他们会在RestActionModule 中被初始化,对应的构造方法会注入RestController实例,接着在构造方法中,这些Action会调用controller.registerHandler 将自己注册到RestController。典型的样子是这样的:

    
    
    @Inject
    
    public RestSearchAction(Settings settings, RestController controller, Client client) {
    
    super(settings, controller, client);
    
    controller.registerHandler(GET, "/_search", this);
    
    controller.registerHandler(POST, "/_search", this);
    
    controller.registerHandler(GET, "/{index}/_search", this);

    每个Rest*Action 都会实现一个handleRequest方法。该方法接入实际的逻辑处理。

    
    @Override
    
    public void handleRequest(final RestRequest request, final RestChannel channel, final Client client) {
    
    SearchRequest searchRequest;
    
    searchRequest = RestSearchAction.parseSearchRequest(request, parseFieldMatcher);
    
    client.search(searchRequest, new RestStatusToXContentListener<SearchResponse>(channel));
    
    }

    首先是会把 请求封装成一个SearchRequest对象,然后交给 NodeClient 执行。

    如果用过ES的NodeClient Java API,你会发现,其实上面这些东西就是为了暴露NodeClient API 的功能,使得你可以通过HTTP的方式调用。

    Transport*Action,两层映射关系解析

    我们先跑个题,在ES中,Transport*Action 是比较核心的类集合。这里至少有两组映射关系。

    • Action -> Transport*Action
    • TransportAction -> TransportHandler

    第一层映射关系由类似下面的代码在ActionModule中完成:

     registerAction(PutMappingAction.INSTANCE,  TransportPutMappingAction.class);

    第二层映射则在类似 SearchServiceTransportAction 中维护。目前看来,第二层映射只有在查询相关的功能才有,如下:

    transportService.registerRequestHandler(FREE_CONTEXT_SCROLL_ACTION_NAME, ScrollFreeContextRequest.class, ThreadPool.Names.SAME, new FreeContextTransportHandler<>());

    SearchServiceTransportAction 可以看做是SearchService进一步封装。其他的Transport*Action 则只调用对应的Service 来完成实际的操作。

    对应的功能是,可以通过Action 找到对应的TransportAction,这些TransportAction 如果是query类,则会调用SearchServiceTransportAction,并且通过第二层映射找到对应的Handler,否则可能就直接通过对应的Service完成操作。

    下面关于RPC调用解析这块,我们会以查询为例。

    RPC 模块解析

    前面我们提到,Rest接口最后会调用NodeClient来完成后续的请求。对应的代码为:

    
    public <Request extends ActionRequest, Response extends ActionResponse, RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder>> void doExecute(Action<Request, Response, RequestBuilder> action, Request request, ActionListener<Response> listener) {
    
    TransportAction<Request, Response> transportAction = actions.get(action);
    
    if (transportAction == null) {
    
    throw new IllegalStateException("failed to find action [" + action + "] to execute");
    
    }
    
    transportAction.execute(request, listener);
    
    }

    这里的action 就是我们提到的第一层映射,找到Transport*Action.如果是查询,则会找到TransportSearchAction。调用对应的doExecute 方法,接着根据searchRequest.searchType找到要执行的实际代码。下面是默认的:

    else if (searchRequest.searchType() == SearchType.QUERY_THEN_FETCH) {    queryThenFetchAction.execute(searchRequest, listener);}

    我们看到Transport*Action 是可以嵌套的,这里调用了TransportSearchQueryThenFetchAction.doExecute

    
    @Overrideprotected void doExecute(SearchRequest searchRequest, ActionListener<SearchResponse> listener) {
    
    new AsyncAction(searchRequest, listener).start();
    
    }

    在AsyncAction中完成三个步骤:

    1. query
    2. fetch
    3. merge

    为了分析方便,我们只分析第一个步骤。

    
    @Overrideprotected void sendExecuteFirstPhase(
    
    DiscoveryNode node,
    
    ShardSearchTransportRequest request,
    
    ActionListener<QuerySearchResultProvider> listener) {
    
    searchService.sendExecuteQuery(node, request, listener);
    
    }

    这是AsyncAction 中执行query的代码。我们知道ES是一个集群,所以query 必然要发到多个节点去,如何知道某个索引对应的Shard 所在的节点呢?这个是在AsyncAction的父类中完成,该父类分析完后会回调子类中的对应的方法来完成,譬如上面的sendExecuteFirstPhase 方法。

    说这个是因为需要让你知道,上面贴出来的代码只是针对一个节点的查询结果,但其实最终多个节点都会通过相同的方式进行调用。所以才会有第三个环节 merge操作,合并多个节点返回的结果。

    searchService.sendExecuteQuery(node, request, listener);

    其实会调用transportService的sendRequest方法。大概值得分析的地方有两个:

    
    
    if (node.equals(localNode)) {
    
    sendLocalRequest(requestId, action, request);
    
    } else {
    
    transport.sendRequest(node, requestId, action, request, options);
    
    }

    我们先分析,如果是本地的节点,则sendLocalRequest是怎么执行的。如果你跑到senLocalRequest里去看,很简单,其实就是:

    reg.getHandler().messageReceived(request, channel);

    reg 其实就是前面我们提到的第二个映射,不过这个映射其实还包含了使用什么线程池等信息,我们在前面没有说明。这里 reg.getHandler == SearchServiceTransportAction.SearchQueryTransportHandler,所以messageReceived 方法对应的逻辑是:

    
    
    QuerySearchResultProvider result = searchService.executeQueryPhase(request);
    
    channel.sendResponse(result);

    这里,我们终于看到searchService。 在searchService里,就是整儿八景的Lucene相关查询了。这个我们后面的系列文章会做详细分析。

    如果不是本地节点,则会由NettyTransport.sendRequest 发出远程请求。
    假设当前请求的节点是A,被请求的节点是B,则B的入口为MessageChannelHandler.messageReceived。在NettyTransport中你可以看到最后添加的pipeline里就有MessageChannelHandler。我们跑进去messageReceived 看看,你会发现基本就是一些协议解析,核心方法是handleRequest,接着就和本地差不多了,我提取了关键的几行代码:

    
    final RequestHandlerRegistry reg = transportServiceAdapter.getRequestHandler(action);
    
    threadPool.executor(reg.getExecutor()).execute(new RequestHandler(reg, request, transportChannel));
    
    这里被RequestHandler包了一层,其实内部执行的就是本地的那个。RequestHandler 的run方法是这样的:
    
    
    protected void doRun() throws Exception { reg.getHandler().messageReceived(request, transportChannel);
    
    }

    这个就和前面的sendLocalRequest里的一模一样了。

    总结

    到目前为止,我们知道整个ES的Rest/RPC 的起点是从哪里开始的。RPC对应的endpoint 是MessageChannelHandler,在NettyTransport 被注册。Rest 接口的七点则在NettyHttpServerTransport,经过层层代理,最终在RestController中被执行具体的Action。 Action 的所有执行都会被委托给NodeClient。 NodeClient的功能执行单元是各种Transport*Action。对于查询类请求,还多了一层映射关系。

    分布式查询

    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<String> 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<FirstResult>(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端。完成。

    elasticsearch源码分析之search模块(server端) 

    继续接着上一篇的来说啊,当client端将search的请求发送到某一个node之后,剩下的事情就是server端来处理了,具体包括哪些步骤呢?

    过程

    一、首先我们来看看接收地方其实就是在org.elasticsearch.action.search.TransportSearchAction中,收到请求之后会判断请求的index的shard是否只有一个,如果是一个的话,那么会强制将请求的type设置为QUERY_AND_FETCH,因为所以的事情在此shard上就能够做完了。所以如果设置了routing,而让请求落在了一个shard上时,搜索的效率会高很多的原因。

    二、根据不同的type来确定不同的处理方式,这里补充一下,上一篇可能忘记说了,search的type一般来说分为“DFS_QUERY_THEN_FETCH、QUERY_THEN_FETCH、DFS_QUERY_AND_FETCH、QUERY_AND_FETCH”这四种,还有“SCAN、COUNT”在ES2.X里面其实已经被舍弃掉了。我们一般都是用的默认的QUERY_THEN_FETCH,上面说的一个shard的除外。所以本篇就只讨论这种情况了。

    三、得到搜索的index所涉及的shard,并依次执行: 1、获取该shard所在的node并执行sendExecuteFirstPhase,实际上是向node发送了一个“QUERY”的请求:

    transportService.sendRequest(node, QUERY_ACTION_NAME, request, new ActionListenerResponseHandler<QuerySearchResultProvider>(listener) {
        @Override
        public QuerySearchResult newInstance() {
            return new QuerySearchResult();
        }
    });
    

    2、node接收到"QUERY"的请求之后,执行executeQueryPhase首先是创建一个search的context,

    SearchContext context = new DefaultSearchContext(idGenerator.incrementAndGet(), request, shardTarget, engineSearcher, indexService, indexShard, scriptService, pageCacheRecycler, bigArrays, threadPool.estimatedTimeInMillisCounter(), parseFieldMatcher, defaultSearchTimeout);
    

    创建的具体过程就不详细说了,之后做的事情还是有parseSource、对size做判断(2.X里面最大不超过10000,可以通过配置文件配置)、……

    最重要的其实是loadOrExecuteQueryPhase(request, context, queryPhase);,具体的内容是首先从cache里面执行query,如果cache里面没有找到,才会执行queryPhase:queryPhase.execute(context);;里面的处理逻辑就比较复杂了,但是最重要的是searcher.search(query, collector);,其实是调用了Lucene里面IndexSeartcher的search方法。

    3、如此一来,第一阶段的query已经做完了,,接下来便是fetch的执行,入口在onFirstPhaseResult这里,在底层同样是向node发送一个“FETCH”请求咯:

    4、node接收到“fetch”请求之后,执行executeFetchPhase:

    fetch的核心代码如下:

    。。。

    大意就是轮流通过之前query结果中的docid,然后创建出InternalSearchHit的集合,并将之放在fetchResult中context.fetchResult().hits(new InternalSearchHits(hits, context.queryResult().topDocs().totalHits, context.queryResult().topDocs().getMaxScore()));,并将之返回到发送fetch的node。

    四、到目前为止,该获取的数据都已经拿到了,现在要做的则是要把个node的返回结果做merge,merge的操作由SearchPhaseController来控制:

    final InternalSearchResponse internalResponse = searchPhaseController.merge(sortedShardList, firstResults,
            fetchResults, request);
    

    具体的过程就不细说了,大体就是该排序的就做排序,有aggs的就做aggs……

    五、通过listener将上面的结果返回:listener.onResponse(new SearchResponse(internalResponse, scrollId, expectedSuccessfulOps, successfulOps.get(), buildTookInMillis(), buildShardFailures()));给发出接收search请求的node,也就是上一篇说道的client。

    正因为当初对未来做了太多的憧憬,所以对现在的自己尤其失望。生命中曾经有过的所有灿烂,终究都需要用寂寞来偿还。
  • 相关阅读:
    [内核编程] 串口过滤
    大数问题(高精度运算)
    [内核编程] 实战调试,第一个内核程序
    [内核编程] Windebug双机调试环境搭建
    树状数组
    mysql常用语句
    解决maven工程无法创建src/main/java包名的方法
    java Date中方法toLocaleString过时的替代方案
    Eclipse 无法编译,提示“错误: 找不到或无法加载主类”
    Eclipse如何设置编译文件.class输出路径
  • 原文地址:https://www.cnblogs.com/candlia/p/11920038.html
Copyright © 2011-2022 走看看