aboutsummaryrefslogtreecommitdiffstats
path: root/container-search/src/main/java/com/yahoo/search/yql/MinimalQueryInserter.java
blob: ee4f931d532e441585ad6c5cc3e7b67d95af29aa (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
136
137
138
139
140
141
142
143
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.search.yql;

import com.yahoo.api.annotations.Beta;
import com.yahoo.component.annotation.Inject;
import com.yahoo.language.Linguistics;
import com.yahoo.language.simple.SimpleLinguistics;
import com.yahoo.processing.IllegalInputException;
import com.yahoo.search.Query;
import com.yahoo.search.Result;
import com.yahoo.search.Searcher;
import com.yahoo.processing.request.CompoundName;
import com.yahoo.search.grouping.GroupingQueryParser;
import com.yahoo.search.query.QueryTree;
import com.yahoo.search.query.parser.Parsable;
import com.yahoo.search.query.parser.ParserEnvironment;
import com.yahoo.search.query.parser.ParserFactory;
import com.yahoo.search.result.ErrorMessage;
import com.yahoo.search.searchchain.Execution;
import com.yahoo.search.searchchain.PhaseNames;
import com.yahoo.yolean.Exceptions;
import com.yahoo.yolean.chain.After;
import com.yahoo.yolean.chain.Before;
import com.yahoo.yolean.chain.Provides;

import java.util.logging.Logger;

/**
 * Minimal combinator for YQL+ syntax and heuristically parsed user queries.
 *
 * @author Steinar Knutsen
 */
// TODO: The query model should do this
@Beta
@Provides(MinimalQueryInserter.EXTERNAL_YQL)
@Before(PhaseNames.TRANSFORMED_QUERY)
@After("com.yahoo.prelude.statistics.StatisticsSearcher")
public class MinimalQueryInserter extends Searcher {

    public static final String EXTERNAL_YQL = "ExternalYql";

    public static final CompoundName YQL = CompoundName.from("yql");

    private static final CompoundName MAX_HITS = CompoundName.from("maxHits");
    private static final CompoundName MAX_OFFSET = CompoundName.from("maxOffset");
    private static final Logger log = Logger.getLogger(MinimalQueryInserter.class.getName());

    @Inject
    public MinimalQueryInserter(Linguistics linguistics) {
        // Warmup is needed to avoid a large 400ms init cost during first execution of yql code.
        warmup(linguistics);
    }

    public MinimalQueryInserter() {
        this(new SimpleLinguistics());
    }

    static boolean warmup() {
        return warmup(new SimpleLinguistics());
    }

    private static boolean warmup(Linguistics linguistics) {
        Query query = new Query("search/?yql=select%20*%20from%20sources%20where%20title%20contains%20'xyz'");
        Result result = insertQuery(query, new ParserEnvironment().setLinguistics(linguistics));
        if (result != null) {
            log.warning("Warmup code trigger an error. Error = " + result);
            return false;
        }
        if ( ! "select * from sources where title contains \"xyz\"".equals(query.yqlRepresentation())) {
            log.warning("Warmup code generated unexpected yql: " + query.yqlRepresentation());
            return false;
        }
        return true;
    }

    @Override
    public Result search(Query query, Execution execution) {
        try {
            if (query.properties().get(YQL) == null) return execution.search(query);
            Result result = insertQuery(query, ParserEnvironment.fromExecutionContext(execution.context()));
            return (result == null) ? execution.search(query) : result;
        }
        catch (IllegalArgumentException e) {
            throw new IllegalInputException("Illegal YQL query", e);
        }
    }

    private static Result insertQuery(Query query, ParserEnvironment env) {
        YqlParser parser = (YqlParser) ParserFactory.newInstance(Query.Type.YQL, env);
        parser.setQueryParser(false);
        parser.setUserQuery(query);
        QueryTree newTree;
        try {
            Parsable parsable = Parsable.fromQueryModel(query.getModel()).setQuery(query.properties().getString(YQL));
            newTree = parser.parse(parsable);
        } catch (RuntimeException e) {
            return new Result(query, ErrorMessage.createInvalidQueryParameter("Could not create query from YQL: " +
                                                                              Exceptions.toMessageString(e),
                                                                              e));
        }
        if (parser.getOffset() != null) {
            int maxHits = query.properties().getInteger(MAX_HITS);
            int maxOffset = query.properties().getInteger(MAX_OFFSET);
            if (parser.getOffset() > maxOffset) {
                return new Result(query,
                                  ErrorMessage.createInvalidQueryParameter("Requested offset " + parser.getOffset() +
                                                                           ", but the max offset allowed is " +
                                                                           maxOffset + "."));
            }
            if (parser.getHits() > maxHits) {
                return new Result(query,
                                  ErrorMessage.createInvalidQueryParameter("Requested " + parser.getHits() +
                                                                           " hits returned, but max hits allowed is " +
                                                                           maxHits + "."));
            }
        }
        query.getModel().getQueryTree().setRoot(newTree.getRoot());
        query.getPresentation().getSummaryFields().addAll(parser.getYqlSummaryFields());

        GroupingQueryParser.validate(query);
        for (VespaGroupingStep step : parser.getGroupingSteps())
            GroupingQueryParser.createGroupingRequestIn(query, step.getOperation(), step.continuations());

        if (parser.getYqlSources().size() == 0) {
            query.getModel().getSources().clear();
        } else {
            query.getModel().getSources().addAll(parser.getYqlSources());
        }
        if (parser.getOffset() != null) {
            query.setOffset(parser.getOffset());
            query.setHits(parser.getHits());
        }
        if (parser.getTimeout() != null) {
            query.setTimeout(parser.getTimeout().longValue());
        }
        if (parser.getSorting() != null) {
            query.getRanking().setSorting(parser.getSorting());
        }
        query.trace("YQL+ query parsed", true, 2);
        return null;
    }

}