aboutsummaryrefslogtreecommitdiffstats
path: root/searchlib/src/main/java/com/yahoo/searchlib/rankingexpression/RankingExpression.java
blob: a7b45feb043d9f0ab095aeacf770b9e1506c07d2 (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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.searchlib.rankingexpression;

import com.yahoo.searchlib.rankingexpression.evaluation.Context;
import com.yahoo.searchlib.rankingexpression.evaluation.Value;
import com.yahoo.searchlib.rankingexpression.parser.ParseException;
import com.yahoo.searchlib.rankingexpression.parser.RankingExpressionParser;
import com.yahoo.searchlib.rankingexpression.parser.TokenMgrException;
import com.yahoo.searchlib.rankingexpression.rule.ExpressionNode;
import com.yahoo.searchlib.rankingexpression.rule.SerializationContext;
import com.yahoo.tensor.TensorType;
import com.yahoo.tensor.evaluation.TypeContext;
import com.yahoo.text.Text;
import static com.yahoo.searchlib.rankingexpression.Reference.RANKING_EXPRESSION_WRAPPER;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Map;

/**
 * <p>A ranking expression. Ranking expressions are used to calculate a rank score for a searched instance from a set of
 * <i>rank features</i>.</p>
 *
 * <p>A ranking expression wraps a expression node tree and may also optionally have a name.</p>
 *
 * <p>The identity of a ranking expression is decided by both its name and expression tree. Two expressions which
 * looks the same in string form are the same.</p>
 *
 * <h2>Simple usage</h2>
<pre><code>
try {
    MapContext context = new MapContext();
    context.put("one", 1d);
    RankingExpression expression = new RankingExpression("10*if(i&gt;35,if(i&gt;one,if(i&gt;=670,4,8),if(i&gt;8000,5,3)),if(i==478,90,91))");
    double result = expression.evaluate(context);
   }
catch (ParseException e) {
    throw new RuntimeException(e);
}
</code></pre>
 *
 * <h3>Or, usage optimized for repeated evaluation of the same expression</h3>
<pre><code>
// Members in a class living across multiple evaluations
RankingExpression expression;
ArrayContext contextPrototype;

...

// Initialization of the above members (once)
// Create reusable, gbdt optimized expression and context.
// The expression is multithread-safe while the context created is not
try {
    RankingExpression expression = new RankingExpression("10*if(i&gt;35,if(i&gt;one,if(i&gt;=670,4,8),if(i&gt;8000,5,3)),if(i==478,90,91))");
    ArrayContext contextPrototype = new ArrayContext(expression);
    ExpressionOptimizer optimizer = new ExpressionOptimizer(); // Increases evaluation speed of gbdt form expressions by 3-4x
    OptimizationReport triviaAboutTheOptimization = optimizer.optimize(expression, contextPrototype);
}
catch (ParseException e) {
    throw new RuntimeException(e);
}

...

// Execution (many)
context = contextPrototype.clone(); // If evaluation is multithreaded - skip this if execution is single-threaded
context.put("one",1d);
double result = expression.evaluate(context);
</code></pre>
 *
 * @author Simon Thoresen Hult
 * @author bratseth
 */
public class RankingExpression implements Serializable {

    private String name = "";
    private ExpressionNode root;
    private final static String RANKEXPRESSION = RANKING_EXPRESSION_WRAPPER + "(";
    private final static String RANKINGSCRIPT = ").rankingScript";
    private final static String EXPRESSION_NAME = ").expressionName";

    /** Creates an anonymous ranking expression by consuming from the reader */
    public RankingExpression(Reader reader) throws ParseException {
        root = parse(reader);
    }

    /**
     * Creates a new ranking expression by consuming from the reader
     *
     * @param name the name of the ranking expression
     * @param reader the reader that contains the string to parse.
     * @throws ParseException if the string could not be parsed.
     */
    public RankingExpression(String name, Reader reader) throws ParseException {
        this.name = name;
        root = parse(reader);
    }

    /**
     * Creates a new ranking expression by consuming from the reader
     *
     * @param name the name of the ranking expression
     * @param expression the expression to parse.
     * @throws ParseException if the string could not be parsed.
     */
    public RankingExpression(String name, String expression) throws ParseException {
        try {
            this.name = name;
            if (expression == null || expression.length() == 0) {
                throw new IllegalArgumentException("Empty ranking expressions are not allowed");
            }
            root = parse(new StringReader(expression));
        }
        catch (ParseException e) {
            ParseException p = new ParseException("Could not parse '" + Text.truncate(expression, 50) + "'");
            p.initCause(e);
            throw p;
        }
    }

    /**
     * Creates a ranking expression from a string
     *
     * @param expression The reader that contains the string to parse.
     * @throws ParseException if the string could not be parsed.
     */
    public RankingExpression(String expression) throws ParseException {
        this("", expression);
    }

    /**
     * Creates a ranking expression from a file. For convenience, the file.getName() up to any dot becomes the name of
     * this expression.
     *
     * @param file the name of the file whose content to parse.
     * @throws ParseException           if the string could not be parsed.
     * @throws IllegalArgumentException if the file could not be found
     */
    public RankingExpression(File file) throws ParseException {
        try {
            name = file.getName().split("\\.")[0];
            root = parse(new FileReader(file));
        }
        catch (FileNotFoundException e) {
            throw new IllegalArgumentException("Could not create a ranking expression", e);
        }
    }

    /**
     * Creates a named ranking expression from an expression root node.
     */
    public RankingExpression(String name, ExpressionNode root) {
        this.name = name;
        this.root = root;
    }

    /**
     * Creates a ranking expression from an expression root node.
     *
     * @param root The root node.
     */
    public RankingExpression(ExpressionNode root) {
        this.root = root;
    }

    /**
     * Parses the content of the reader object as an expression string.
     *
     * @param reader A reader object that contains an expression string.
     * @return An expression node that corresponds to the given string.
     * @throws ParseException if the string could not be parsed.
     */
    private static ExpressionNode parse(Reader reader) throws ParseException {
        try {
            return new RankingExpressionParser(reader).rankingExpression();
        }
        catch (TokenMgrException e) {
            throw new ParseException(e.getMessage());
        }
    }

    /** Returns a deep copy of this expression */
    public RankingExpression copy() {
        try {
            return new RankingExpression(name, root.toString());
        }
        catch (ParseException e) {
            throw new RuntimeException("Programming error: Could not parse serialized expression", e);
        }
    }

    /**
     * Returns the name of this ranking expression, or "" if no name is set.
     *
     * @return The name of this expression.
     */
    public String getName() {
        return name;
    }

    /**
     * Sets the name of this ranking expression.
     *
     * @param name The name to set.
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Returns the root of the expression tree of this expression.
     *
     * @return The root node.
     */
    public ExpressionNode getRoot() {
        return root;
    }

    /**
     * Sets the root of the expression tree of this expression.
     *
     * @param root The root node to set.
     */
    public void setRoot(ExpressionNode root) {
        this.root = root;
    }

    @Override
    public int hashCode() {
        return toString().hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof RankingExpression && toString().equals(obj.toString());
    }

    @Override
    public String toString() {
        if ("".equals(name)) {
            return root.toString();
        } else {
            return name + ": " + root.toString();
        }
    }

    /**
     * Creates the necessary rank properties required to implement this expression.
     *
     * @param context context for serialization
     * @return a list of named rank properties required to implement this expression
     */
    public Map<String, String> getRankProperties(SerializationContext context) {
        Deque<String> path = new LinkedList<>();
        String serializedRoot = root.toString(new StringBuilder(), context, path, null).toString();
        Map<String, String> serializedExpressions = context.serializedFunctions();
        serializedExpressions.put(propertyName(name), serializedRoot);
        return serializedExpressions;
    }

    /**
     * Returns the rank-property name for a given expression name.
     *
     * @param expressionName the expression name to mangle.
     * @return the property name.
     */
    public static String propertyName(String expressionName) {
        return RANKEXPRESSION + expressionName + RANKINGSCRIPT;
    }
    public static String propertyExpressionName(String expressionName) {
        return RANKEXPRESSION + expressionName + EXPRESSION_NAME;
    }
    public static String extractScriptName(String propertyName) {
        if (propertyName.startsWith(RANKEXPRESSION) && propertyName.endsWith(RANKINGSCRIPT)) {
            return propertyName.substring(RANKEXPRESSION.length(), propertyName.length() - RANKINGSCRIPT.length());
        }
        return null;
    }

    /**
     * Validates the type correctness of the given expression with the given context and
     * returns the type this expression will produce from the given type context
     *
     * @throws IllegalArgumentException if this expression is not type correct in this context
     */
    public TensorType type(TypeContext<Reference> context) {
        return root.type(context);
    }

    /**
     * Returns the value of evaluating this expression over the given context.
     *
     * @param context The variable bindings to use for this evaluation.
     * @return the evaluation result.
     * @throws IllegalArgumentException if there are variables which are not bound in the given map
     */
    public Value evaluate(Context context) {
        return root.evaluate(context);
    }

    /**
     * Creates a ranking expression from a string
     *
     * @throws IllegalArgumentException if the string is not a valid ranking expression
     */
    public static RankingExpression from(String expression) {
        try {
            return new RankingExpression(expression);
        }
        catch (ParseException e) {
            throw new IllegalStateException(e);
        }
    }

}