private Result performSearch(Query query, Execution execution, Map<Integer, Grouping> groupingMap) {
        // Determine how many passes to perform.
        int lastPass = 0;
        for (Grouping grouping : groupingMap.values()) {
            if ( ! grouping.useSinglePass()) {
                lastPass = Math.max(lastPass, grouping.getLevels().size());
            }
        }

        // Perform multi-pass query to complete all grouping requests.
        Item origRoot = query.getModel().getQueryTree().getRoot();
        Result ret = null;
        Item baseRoot = origRoot;
        if (lastPass > 0) {
            baseRoot = origRoot.clone();
        }
        if (query.getTrace().isTraceable(3)) {
            query.trace("Grouping in " + (lastPass + 1) + " passes. SessionId='" + query.getSessionId() + "'.", 3);
        }
        for (int pass = 0; pass <= lastPass; ++pass) {
            boolean firstPass = (pass == 0);
            List<Grouping> passList = getGroupingListForPassN(groupingMap, pass);
            if (passList.isEmpty()) {
                throw new RuntimeException("No grouping request for pass " + pass + ", bug!");
            }
            if (log.isLoggable(Level.FINE)) {
                for (Grouping grouping : passList) {
                    log.log(Level.FINE, "Pass(" + pass + "), Grouping(" + grouping.getId() + "): " + grouping);
                }
            }
            Item passRoot;
            if (firstPass) {
                passRoot = origRoot;     // Use original query the first time.
            } else if (pass == lastPass) {
                passRoot = baseRoot; // Has already been cloned once, use this for last pass.
            } else {
                // noinspection ConstantConditions
                passRoot = baseRoot.clone();
            }
            if (query.getTrace().isTraceable(4) && query.getGroupingSessionCache()) {
                query.trace("Grouping with session cache '" + query.getGroupingSessionCache() + "' enabled for pass #" + pass + ".", 4);
            }
            if (origRoot != passRoot) {
                query.getModel().getQueryTree().setRoot(passRoot);
            }
            setGroupingList(query, passList);
            Result passResult = execution.search(query);
            Map<Integer, Grouping> passGroupingMap = mergeGroupingResults(passResult);
            mergeGroupingMaps(groupingMap, passGroupingMap);
            if (firstPass) {
                ret = passResult;
            } else {
                ret.hits().addErrorsFrom(passResult.hits());
            }
        }
        if (log.isLoggable(Level.FINE)) {
            for (Grouping grouping : groupingMap.values()) {
                log.log(Level.FINE, "Result Grouping(" + grouping.getId() + "): " + grouping);
            }
        }
        return ret;
    }
Performs the actual search passes to complete all the given {@link Grouping} requests. This method uses the
grouping map argument as both an input and an output variable, as the contained {@link Grouping} objects are
updates as results arrive from the back end.

@param query       the query to execute
@param execution   the execution context used to run the queries
@param groupingMap the map of grouping requests to perform
@return the search result to pass back from this searcher
container-search/src/main/java/com/yahoo/search/grouping/vespa/GroupingExecutor.java