aboutsummaryrefslogtreecommitdiffstats
path: root/container-search/src/main/java/com/yahoo/search/grouping/GroupingQueryParser.java
blob: 3ad610f6ee02d62c84b0bf80d6bb0718ac2450ad (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.search.grouping;

import com.yahoo.api.annotations.Beta;
import com.yahoo.component.chain.dependencies.After;
import com.yahoo.component.chain.dependencies.Before;
import com.yahoo.component.chain.dependencies.Provides;
import com.yahoo.processing.IllegalInputException;
import com.yahoo.processing.request.CompoundName;
import com.yahoo.search.Query;
import com.yahoo.search.Result;
import com.yahoo.search.Searcher;
import com.yahoo.search.grouping.request.GroupingOperation;
import com.yahoo.search.query.Select;
import com.yahoo.search.searchchain.Execution;
import com.yahoo.search.searchchain.PhaseNames;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.TimeZone;

/**
 * This searcher is responsible for turning the "select" parameter into a corresponding {@link GroupingRequest}. It will
 * also parse any "timezone" parameter as the timezone for time expressions such as {@link
 * com.yahoo.search.grouping.request.DayOfMonthFunction} and {@link com.yahoo.search.grouping.request.HourOfDayFunction}.
 *
 * @author Simon Thoresen Hult
 */
@After(PhaseNames.RAW_QUERY)
@Before(PhaseNames.TRANSFORMED_QUERY)
@Provides(GroupingQueryParser.SELECT_PARAMETER_PARSING)
public class GroupingQueryParser extends Searcher {

    public static final String SELECT_PARAMETER_PARSING = "SelectParameterParsing";
    public static final CompoundName PARAM_CONTINUE = CompoundName.from("continue");
    public static final CompoundName PARAM_REQUEST = CompoundName.from(Select.SELECT);
    public static final CompoundName PARAM_TIMEZONE = CompoundName.from("timezone");
    @Beta public static final CompoundName PARAM_DEFAULT_MAX_HITS = CompoundName.from("grouping.defaultMaxHits");
    @Beta public static final CompoundName PARAM_DEFAULT_MAX_GROUPS = CompoundName.from("grouping.defaultMaxGroups");
    @Beta public static final CompoundName PARAM_DEFAULT_PRECISION_FACTOR = CompoundName.from("grouping.defaultPrecisionFactor");
    @Beta public static final CompoundName GROUPING_GLOBAL_MAX_GROUPS = CompoundName.from("grouping.globalMaxGroups");
    private static final ThreadLocal<ZoneCache> zoneCache = new ThreadLocal<>();

    @Override
    public Result search(Query query, Execution execution) {
        try {
            validate(query);

            String reqParam = query.properties().getString(PARAM_REQUEST);
            if (reqParam == null) return execution.search(query);

            List<Continuation> continuations = getContinuations(query.properties().getString(PARAM_CONTINUE));
            for (GroupingOperation operation : GroupingOperation.fromStringAsList(reqParam))
                createGroupingRequestIn(query, operation, continuations);
            return execution.search(query);
        }
        catch (IllegalArgumentException e) {
            throw new IllegalInputException(e);
        }
    }

    public static void validate(Query query) {
        if (query.getHttpRequest().getProperty(GROUPING_GLOBAL_MAX_GROUPS.toString()) != null)
            throw new IllegalInputException(GROUPING_GLOBAL_MAX_GROUPS + " must be specified in a query profile.");
    }

    public static void createGroupingRequestIn(Query query, GroupingOperation operation, List<Continuation> continuations) {
        GroupingRequest request = GroupingRequest.newInstance(query);
        request.setRootOperation(operation);
        request.setTimeZone(getTimeZone(query.properties().getString(PARAM_TIMEZONE, "utc")));
        request.continuations().addAll(continuations);
        intProperty(query, PARAM_DEFAULT_MAX_GROUPS).ifPresent(request::setDefaultMaxGroups);
        intProperty(query, PARAM_DEFAULT_MAX_HITS).ifPresent(request::setDefaultMaxHits);
        longProperty(query, GROUPING_GLOBAL_MAX_GROUPS).ifPresent(request::setGlobalMaxGroups);
        doubleProperty(query, PARAM_DEFAULT_PRECISION_FACTOR).ifPresent(request::setDefaultPrecisionFactor);
    }

    private List<Continuation> getContinuations(String param) {
        if (param == null) {
            return Collections.emptyList();
        }
        List<Continuation> ret = new LinkedList<>();
        for (String str : param.split(" ")) {
            ret.add(Continuation.fromString(str));
        }
        return ret;
    }

    private static TimeZone getTimeZone(String name) {
        ZoneCache cache = zoneCache.get();
        if (cache == null) {
            cache = new ZoneCache();
            zoneCache.set(cache);
        }
        TimeZone timeZone = cache.get(name);
        if (timeZone == null) {
            timeZone = TimeZone.getTimeZone(name);
            cache.put(name, timeZone);
        }
        return timeZone;
    }

    private static OptionalInt intProperty(Query q, CompoundName name) {
        Integer val = q.properties().getInteger(name);
        return val != null ? OptionalInt.of(val) : OptionalInt.empty();
    }

    private static OptionalLong longProperty(Query q, CompoundName name) {
        Long val = q.properties().getLong(name);
        return val != null ? OptionalLong.of(val) : OptionalLong.empty();
    }

    private static OptionalDouble doubleProperty(Query q, CompoundName name) {
        Double val = q.properties().getDouble(name);
        return val != null ? OptionalDouble.of(val) : OptionalDouble.empty();
    }

    private static class ZoneCache extends LinkedHashMap<String, TimeZone> {

        ZoneCache() {
            super(16, 0.75f, true);
        }

        @Override
        protected boolean removeEldestEntry(Map.Entry<String, TimeZone> entry) {
            return size() > 128; // large enough to cache common cases
        }
    }
}