From c5ff70bf430a81702c6e429942a8a56d0287f468 Mon Sep 17 00:00:00 2001 From: Britta Weber Date: Sat, 2 Aug 2014 10:12:37 +0200 Subject: [PATCH] function_score: add optional weight parameter per function Weights can be defined per function like this: ``` "function_score": { "functions": [ { "filter": {}, "FUNCTION": {}, "weight": number } ... ``` If `weight` is given without `FUNCTION` then `weight` behaves like `boost_factor`. This commit deprecates `boost_factor`. The following is valid: ``` POST testidx/_search { "query": { "function_score": { "weight": 2 } } } POST testidx/_search { "query": { "function_score": { "functions": [ { "weight": 2 }, ... ] } } } POST testidx/_search { "query": { "function_score": { "functions": [ { "FUNCTION": {}, "weight": 2 }, ... ] } } } POST testidx/_search { "query": { "function_score": { "functions": [ { "filter": {}, "weight": 2 }, ... ] } } } POST testidx/_search { "query": { "function_score": { "functions": [ { "filter": {}, "FUNCTION": {}, "weight": 2 }, ... ] } } } ``` The following is not valid: ``` POST testidx/_search { "query": { "function_score": { "weight": 2, "FUNCTION(including boost_factor)": 2 } } } POST testidx/_search { "query": { "function_score": { "functions": [ { "weight": 2, "boost_factor": 2 } ] } } } ```` closes #6955 closes #7137 --- .../queries/function-score-query.asciidoc | 34 +- .../search/function/BoostScoreFunction.java | 25 +- .../lucene/search/function/ScoreFunction.java | 3 +- .../search/function/WeightFactorFunction.java | 104 ++++++ .../functionscore/DecayFunctionBuilder.java | 5 +- .../FunctionScoreQueryBuilder.java | 38 +-- .../FunctionScoreQueryParser.java | 43 ++- .../functionscore/ScoreFunctionBuilder.java | 28 +- .../functionscore/ScoreFunctionBuilders.java | 6 + .../functionscore/factor/FactorBuilder.java | 25 +- .../functionscore/factor/FactorParser.java | 1 + .../FieldValueFactorFunctionBuilder.java | 5 +- .../random/RandomScoreFunctionBuilder.java | 6 +- .../script/ScriptScoreFunctionBuilder.java | 6 +- .../functionscore/weight/WeightBuilder.java | 42 +++ .../query/SimpleIndexQueryParserTests.java | 88 +++++ .../function-score-query-causing-NPE.json | 9 + ...nctionScoreBackwardCompatibilityTests.java | 115 +++++++ .../functionscore/FunctionScoreTests.java | 314 +++++++++++++++++- 19 files changed, 807 insertions(+), 90 deletions(-) create mode 100644 src/main/java/org/elasticsearch/common/lucene/search/function/WeightFactorFunction.java create mode 100644 src/main/java/org/elasticsearch/index/query/functionscore/weight/WeightBuilder.java create mode 100644 src/test/java/org/elasticsearch/index/query/function-score-query-causing-NPE.json create mode 100644 src/test/java/org/elasticsearch/search/functionscore/FunctionScoreBackwardCompatibilityTests.java diff --git a/docs/reference/query-dsl/queries/function-score-query.asciidoc b/docs/reference/query-dsl/queries/function-score-query.asciidoc index 56e2d60c4b1..9972e5f2d01 100644 --- a/docs/reference/query-dsl/queries/function-score-query.asciidoc +++ b/docs/reference/query-dsl/queries/function-score-query.asciidoc @@ -9,8 +9,7 @@ the score on a filtered set of documents. `function_score` provides the same functionality that `custom_boost_factor`, `custom_score` and `custom_filters_score` provided -but furthermore adds futher scoring functionality such as -distance and recency scoring (see description below). +but with additional capabilities such as distance and recency scoring (see description below). ==== Using function score @@ -42,10 +41,15 @@ given filter: "functions": [ { "filter": {}, - "FUNCTION": {} + "FUNCTION": {}, + "weight": number }, { "FUNCTION": {} + }, + { + "filter": {}, + "weight": number } ], "max_boost": number, @@ -69,6 +73,9 @@ First, each document is scored by the defined functions. The parameter `max`:: maximum score is used `min`:: minimum score is used +Because scores can be on different scales (for example, between 0 and 1 for decay functions but arbitrary for `field_value_factor`) and also because sometimes a different impact of functions on the score is desirable, the score of each function can be adjusted with a user defined `weight` (coming[1.4.0]). The `weight` can be defined per function in the `functions` array (example above) and is multiplied with the score computed by the respective function. +If weight is given without any other function declaration, `weight` acts as a function that simply returns the `weight`. + The new score can be restricted to not exceed a certain limit by setting the `max_boost` parameter. The default for `max_boost` is FLT_MAX. @@ -126,18 +133,27 @@ Note that unlike the `custom_score` query, the score of the query is multiplied with the result of the script scoring. If you wish to inhibit this, set `"boost_mode": "replace"` -===== Boost factor +===== Weight -The `boost_factor` score allows you to multiply the score by the provided -`boost_factor`. This can sometimes be desired since boost value set on +coming[1.4.0] + +The `weight` score allows you to multiply the score by the provided +`weight`. This can sometimes be desired since boost value set on specific queries gets normalized, while for this score function it does not. [source,js] -------------------------------------------------- -"boost_factor" : number +"weight" : number -------------------------------------------------- +===== Boost factor + +deprecated[1.4.0] + +Same as `weight`. Use `weight` instead. + + ===== Random The `random_score` generates scores using a hash of the `_uid` field, @@ -490,7 +506,7 @@ becomes [source,js] -------------------------------------------------- "function_score": { - "boost_factor": 5.2, + "weight": 5.2, "query": {...} } -------------------------------------------------- @@ -557,7 +573,7 @@ becomes: "function_score": { "functions": [ { - "boost_factor": "3", + "weight": "3", "filter": {...} }, { diff --git a/src/main/java/org/elasticsearch/common/lucene/search/function/BoostScoreFunction.java b/src/main/java/org/elasticsearch/common/lucene/search/function/BoostScoreFunction.java index 1db8fc6b757..fe50984e1a3 100644 --- a/src/main/java/org/elasticsearch/common/lucene/search/function/BoostScoreFunction.java +++ b/src/main/java/org/elasticsearch/common/lucene/search/function/BoostScoreFunction.java @@ -21,12 +21,16 @@ package org.elasticsearch.common.lucene.search.function; import org.apache.lucene.index.AtomicReaderContext; import org.apache.lucene.search.Explanation; +import org.elasticsearch.ElasticsearchIllegalArgumentException; /** * */ +@Deprecated public class BoostScoreFunction extends ScoreFunction { + public static final String BOOST_WEIGHT_ERROR_MESSAGE = "'boost_factor' and 'weight' cannot be used together. Use 'weight'."; + private final float boost; public BoostScoreFunction(float boost) { @@ -55,28 +59,9 @@ public class BoostScoreFunction extends ScoreFunction { return exp; } - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - - BoostScoreFunction that = (BoostScoreFunction) o; - - if (Float.compare(that.boost, boost) != 0) - return false; - - return true; - } - - @Override - public int hashCode() { - return (boost != +0.0f ? Float.floatToIntBits(boost) : 0); - } - @Override public String toString() { return "boost[" + boost + "]"; } + } diff --git a/src/main/java/org/elasticsearch/common/lucene/search/function/ScoreFunction.java b/src/main/java/org/elasticsearch/common/lucene/search/function/ScoreFunction.java index 9c8a3aabc5e..391e64648b3 100644 --- a/src/main/java/org/elasticsearch/common/lucene/search/function/ScoreFunction.java +++ b/src/main/java/org/elasticsearch/common/lucene/search/function/ScoreFunction.java @@ -28,7 +28,7 @@ import org.apache.lucene.search.Explanation; public abstract class ScoreFunction { private final CombineFunction scoreCombiner; - + public abstract void setNextReader(AtomicReaderContext context); public abstract double score(int docId, float subQueryScore); @@ -42,5 +42,4 @@ public abstract class ScoreFunction { protected ScoreFunction(CombineFunction scoreCombiner) { this.scoreCombiner = scoreCombiner; } - } diff --git a/src/main/java/org/elasticsearch/common/lucene/search/function/WeightFactorFunction.java b/src/main/java/org/elasticsearch/common/lucene/search/function/WeightFactorFunction.java new file mode 100644 index 00000000000..79abf028424 --- /dev/null +++ b/src/main/java/org/elasticsearch/common/lucene/search/function/WeightFactorFunction.java @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.lucene.search.function; + +import org.apache.lucene.index.AtomicReaderContext; +import org.apache.lucene.search.ComplexExplanation; +import org.apache.lucene.search.Explanation; +import org.elasticsearch.ElasticsearchIllegalArgumentException; + +/** + * + */ +public class WeightFactorFunction extends ScoreFunction { + + private static final ScoreFunction SCORE_ONE = new ScoreOne(CombineFunction.MULT); + private final ScoreFunction scoreFunction; + private float weight = 1.0f; + + public WeightFactorFunction(float weight, ScoreFunction scoreFunction) { + super(CombineFunction.MULT); + if (scoreFunction instanceof BoostScoreFunction) { + throw new ElasticsearchIllegalArgumentException(BoostScoreFunction.BOOST_WEIGHT_ERROR_MESSAGE); + } + if (scoreFunction == null) { + this.scoreFunction = SCORE_ONE; + } else { + this.scoreFunction = scoreFunction; + } + this.weight = weight; + } + + public WeightFactorFunction(float weight) { + super(CombineFunction.MULT); + this.scoreFunction = SCORE_ONE; + this.weight = weight; + } + + @Override + public void setNextReader(AtomicReaderContext context) { + scoreFunction.setNextReader(context); + } + + @Override + public double score(int docId, float subQueryScore) { + return scoreFunction.score(docId, subQueryScore) * getWeight(); + } + + @Override + public Explanation explainScore(int docId, float score) { + Explanation functionScoreExplanation; + Explanation functionExplanation = scoreFunction.explainScore(docId, score); + functionScoreExplanation = new ComplexExplanation(true, functionExplanation.getValue() * (float) getWeight(), "product of:"); + functionScoreExplanation.addDetail(functionExplanation); + functionScoreExplanation.addDetail(explainWeight()); + return functionScoreExplanation; + } + + public Explanation explainWeight() { + return new Explanation(getWeight(), "weight"); + } + + public float getWeight() { + return weight; + } + + private static class ScoreOne extends ScoreFunction { + + protected ScoreOne(CombineFunction scoreCombiner) { + super(scoreCombiner); + } + + @Override + public void setNextReader(AtomicReaderContext context) { + + } + + @Override + public double score(int docId, float subQueryScore) { + return 1.0; + } + + @Override + public Explanation explainScore(int docId, float subQueryScore) { + return new Explanation(1.0f, "constant score 1.0 - no function provided"); + } + } +} diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionBuilder.java b/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionBuilder.java index 4290f1ccc82..ce9f5231949 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionBuilder.java @@ -26,7 +26,7 @@ import org.elasticsearch.search.MultiValueMode; import java.io.IOException; import java.util.Locale; -public abstract class DecayFunctionBuilder implements ScoreFunctionBuilder { +public abstract class DecayFunctionBuilder extends ScoreFunctionBuilder { protected static final String ORIGIN = "origin"; protected static final String SCALE = "scale"; @@ -60,7 +60,7 @@ public abstract class DecayFunctionBuilder implements ScoreFunctionBuilder { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + public void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(getName()); builder.startObject(fieldName); if (origin != null) { @@ -78,7 +78,6 @@ public abstract class DecayFunctionBuilder implements ScoreFunctionBuilder { builder.field(DecayFunctionParser.MULTI_VALUE_MODE.getPreferredName(), multiValueMode.name()); } builder.endObject(); - return builder; } public ScoreFunctionBuilder setMultiValueMode(MultiValueMode multiValueMode) { diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java index 4826984d74b..0ea796fe40f 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java @@ -45,7 +45,7 @@ public class FunctionScoreQueryBuilder extends BaseQueryBuilder implements Boost private Float maxBoost; private String scoreMode; - + private String boostMode; private ArrayList filters = new ArrayList<>(); @@ -98,12 +98,12 @@ public class FunctionScoreQueryBuilder extends BaseQueryBuilder implements Boost this.scoreMode = scoreMode; return this; } - + public FunctionScoreQueryBuilder boostMode(String boostMode) { this.boostMode = boostMode; return this; } - + public FunctionScoreQueryBuilder boostMode(CombineFunction combineFunction) { this.boostMode = combineFunction.getName(); return this; @@ -133,27 +133,19 @@ public class FunctionScoreQueryBuilder extends BaseQueryBuilder implements Boost } else if (filterBuilder != null) { builder.field("filter"); filterBuilder.toXContent(builder, params); - } - // If there is only one function without a filter, we later want to - // create a FunctionScoreQuery. - // For this, we only build the scoreFunction.Tthis will be translated to - // FunctionScoreQuery in the parser. - if (filters.size() == 1 && filters.get(0) == null) { - scoreFunctions.get(0).toXContent(builder, params); - } else { // in all other cases we build the format needed for a - // FiltersFunctionScoreQuery - builder.startArray("functions"); - for (int i = 0; i < filters.size(); i++) { - builder.startObject(); - if (filters.get(i) != null) { - builder.field("filter"); - filters.get(i).toXContent(builder, params); - } - scoreFunctions.get(i).toXContent(builder, params); - builder.endObject(); - } - builder.endArray(); } + builder.startArray("functions"); + for (int i = 0; i < filters.size(); i++) { + builder.startObject(); + if (filters.get(i) != null) { + builder.field("filter"); + filters.get(i).toXContent(builder, params); + } + scoreFunctions.get(i).toXContent(builder, params); + builder.endObject(); + } + builder.endArray(); + if (scoreMode != null) { builder.field("score_mode", scoreMode); } diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryParser.java b/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryParser.java index 3bd5a05b41c..09cd5b05a0f 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryParser.java @@ -24,14 +24,13 @@ import com.google.common.collect.ImmutableMap.Builder; import org.apache.lucene.search.Filter; import org.apache.lucene.search.Query; import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.lucene.search.MatchAllDocsFilter; import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.lucene.search.XConstantScoreQuery; -import org.elasticsearch.common.lucene.search.function.CombineFunction; -import org.elasticsearch.common.lucene.search.function.FiltersFunctionScoreQuery; -import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery; -import org.elasticsearch.common.lucene.search.function.ScoreFunction; +import org.elasticsearch.common.lucene.search.function.*; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.QueryParseContext; import org.elasticsearch.index.query.QueryParser; @@ -53,6 +52,8 @@ public class FunctionScoreQueryParser implements QueryParser { static final String MISPLACED_FUNCTION_MESSAGE_PREFIX = "You can either define \"functions\":[...] or a single function, not both. "; static final String MISPLACED_BOOST_FUNCTION_MESSAGE_SUFFIX = " Did you mean \"boost\" instead?"; + public static final ParseField WEIGHT_FIELD = new ParseField("weight"); + @Inject public FunctionScoreQueryParser(ScoreFunctionParserMapper funtionParserMapper) { this.funtionParserMapper = funtionParserMapper; @@ -62,7 +63,7 @@ public class FunctionScoreQueryParser implements QueryParser { public String[] names() { return new String[] { NAME, Strings.toCamelCase(NAME) }; } - + private static final ImmutableMap combineFunctionsMap; static { @@ -116,17 +117,27 @@ public class FunctionScoreQueryParser implements QueryParser { currentFieldName = parseFiltersAndFunctions(parseContext, parser, filterFunctions, currentFieldName); functionArrayFound = true; } else { - // we try to parse a score function. If there is no score - // function for the current field name, - // functionParserMapper.get() will throw an Exception. - ScoreFunctionParser currentFunctionParser = funtionParserMapper.get(parseContext.index(), currentFieldName); - singleFunctionName = currentFieldName; + ScoreFunction scoreFunction; + if (currentFieldName.equals("weight")) { + scoreFunction = new WeightFactorFunction(parser.floatValue()); + + } else { + // we try to parse a score function. If there is no score + // function for the current field name, + // functionParserMapper.get() will throw an Exception. + scoreFunction = funtionParserMapper.get(parseContext.index(), currentFieldName).parse(parseContext, parser); + } if (functionArrayFound) { String errorString = "Found \"functions\": [...] already, now encountering \"" + currentFieldName + "\"."; handleMisplacedFunctionsDeclaration(errorString, currentFieldName); } - filterFunctions.add(new FiltersFunctionScoreQuery.FilterFunction(null, currentFunctionParser.parse(parseContext, parser))); + if (filterFunctions.size() > 0) { + String errorString = "Found function " + singleFunctionName + " already, now encountering \"" + currentFieldName + "\". Use functions[{...},...] if you want to define several functions."; + throw new ElasticsearchParseException(errorString); + } + filterFunctions.add(new FiltersFunctionScoreQuery.FilterFunction(null, scoreFunction)); singleFunctionFound = true; + singleFunctionName = currentFieldName; } } if (query == null) { @@ -138,7 +149,7 @@ public class FunctionScoreQueryParser implements QueryParser { } // handle cases where only one score function and no filter was // provided. In this case we create a FunctionScoreQuery. - if (filterFunctions.size() == 1 && filterFunctions.get(0).filter == null) { + if (filterFunctions.size() == 1 && (filterFunctions.get(0).filter == null || filterFunctions.get(0).filter instanceof MatchAllDocsFilter)) { FunctionScoreQuery theQuery = new FunctionScoreQuery(query, filterFunctions.get(0).function); if (combineFunction != null) { theQuery.setCombineFunction(combineFunction); @@ -167,11 +178,12 @@ public class FunctionScoreQueryParser implements QueryParser { } private String parseFiltersAndFunctions(QueryParseContext parseContext, XContentParser parser, - ArrayList filterFunctions, String currentFieldName) throws IOException { + ArrayList filterFunctions, String currentFieldName) throws IOException { XContentParser.Token token; while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { Filter filter = null; ScoreFunction scoreFunction = null; + Float functionWeight = null; if (token != XContentParser.Token.START_OBJECT) { throw new QueryParsingException(parseContext.index(), NAME + ": malformed query, expected a " + XContentParser.Token.START_OBJECT + " while parsing functions but got a " + token); @@ -179,6 +191,8 @@ public class FunctionScoreQueryParser implements QueryParser { while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); + } else if (WEIGHT_FIELD.match(currentFieldName)) { + functionWeight = parser.floatValue(); } else { if ("filter".equals(currentFieldName)) { filter = parseContext.parseInnerFilter(); @@ -191,6 +205,9 @@ public class FunctionScoreQueryParser implements QueryParser { } } } + if (functionWeight != null) { + scoreFunction = new WeightFactorFunction(functionWeight, scoreFunction); + } } if (filter == null) { filter = Queries.MATCH_ALL_FILTER; diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilder.java b/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilder.java index c6c5d153da1..e4fc5cdac8c 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilder.java @@ -20,9 +20,33 @@ package org.elasticsearch.index.query.functionscore; import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; -public interface ScoreFunctionBuilder extends ToXContent { +import java.io.IOException; - public String getName(); +public abstract class ScoreFunctionBuilder implements ToXContent { + public ScoreFunctionBuilder setWeight(float weight) { + this.weight = weight; + return this; + } + + private Float weight; + + public abstract String getName(); + + protected void buildWeight(XContentBuilder builder) throws IOException { + if (weight != null) { + builder.field(FunctionScoreQueryParser.WEIGHT_FIELD.getPreferredName(), weight); + } + } + + @Override + public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + buildWeight(builder); + doXContent(builder, params); + return builder; + } + + protected abstract void doXContent(XContentBuilder builder, Params params) throws IOException; } diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilders.java b/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilders.java index 8215f5388ff..be8c9cb424d 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilders.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilders.java @@ -26,6 +26,7 @@ import org.elasticsearch.index.query.functionscore.gauss.GaussDecayFunctionBuild import org.elasticsearch.index.query.functionscore.lin.LinearDecayFunctionBuilder; import org.elasticsearch.index.query.functionscore.random.RandomScoreFunctionBuilder; import org.elasticsearch.index.query.functionscore.script.ScriptScoreFunctionBuilder; +import org.elasticsearch.index.query.functionscore.weight.WeightBuilder; import java.util.Map; @@ -71,6 +72,7 @@ public class ScoreFunctionBuilders { return (new ScriptScoreFunctionBuilder()).script(script).params(params); } + @Deprecated public static FactorBuilder factorFunction(float boost) { return (new FactorBuilder()).boostFactor(boost); } @@ -78,6 +80,10 @@ public class ScoreFunctionBuilders { public static RandomScoreFunctionBuilder randomFunction(int seed) { return (new RandomScoreFunctionBuilder()).seed(seed); } + + public static WeightBuilder weightFactorFunction(float weight) { + return (WeightBuilder)(new WeightBuilder().setWeight(weight)); + } public static FieldValueFactorFunctionBuilder fieldValueFactorFunction(String fieldName) { return new FieldValueFactorFunctionBuilder(fieldName); diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorBuilder.java b/src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorBuilder.java index 941c21697c1..286b32c7f70 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorBuilder.java @@ -19,18 +19,20 @@ package org.elasticsearch.index.query.functionscore.factor; -import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder; - +import org.elasticsearch.ElasticsearchIllegalArgumentException; +import org.elasticsearch.common.lucene.search.function.BoostScoreFunction; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder; import java.io.IOException; /** * A query that simply applies the boost factor to another query (multiply it). - * - * + * + * */ -public class FactorBuilder implements ScoreFunctionBuilder { +@Deprecated +public class FactorBuilder extends ScoreFunctionBuilder { private Float boostFactor; @@ -43,15 +45,24 @@ public class FactorBuilder implements ScoreFunctionBuilder { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + public void doXContent(XContentBuilder builder, Params params) throws IOException { if (boostFactor != null) { builder.field("boost_factor", boostFactor.floatValue()); } - return builder; } @Override public String getName() { return FactorParser.NAMES[0]; } + + @Override + public ScoreFunctionBuilder setWeight(float weight) { + throw new ElasticsearchIllegalArgumentException(BoostScoreFunction.BOOST_WEIGHT_ERROR_MESSAGE); + } + + @Override + public void buildWeight(XContentBuilder builder) throws IOException { + //we do not want the weight to be written for boost_factor as it does not make sense to have it + } } \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorParser.java b/src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorParser.java index 294779cbd97..a1c8d20350d 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorParser.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorParser.java @@ -33,6 +33,7 @@ import java.io.IOException; /** * */ +@Deprecated public class FactorParser implements ScoreFunctionParser { public static String[] NAMES = { "boost_factor", "boostFactor" }; diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionBuilder.java b/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionBuilder.java index f8726253991..34a2f8bbc67 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionBuilder.java @@ -30,7 +30,7 @@ import java.util.Locale; * Builder to construct {@code field_value_factor} functions for a function * score query. */ -public class FieldValueFactorFunctionBuilder implements ScoreFunctionBuilder { +public class FieldValueFactorFunctionBuilder extends ScoreFunctionBuilder { private String field = null; private Float factor = null; private FieldValueFactorFunction.Modifier modifier = null; @@ -55,7 +55,7 @@ public class FieldValueFactorFunctionBuilder implements ScoreFunctionBuilder { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + public void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(getName()); if (field != null) { builder.field("field", field); @@ -69,6 +69,5 @@ public class FieldValueFactorFunctionBuilder implements ScoreFunctionBuilder { builder.field("modifier", modifier.toString().toLowerCase(Locale.ROOT)); } builder.endObject(); - return builder; } } diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/random/RandomScoreFunctionBuilder.java b/src/main/java/org/elasticsearch/index/query/functionscore/random/RandomScoreFunctionBuilder.java index 6acff5f9c00..1467259bd29 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/random/RandomScoreFunctionBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/random/RandomScoreFunctionBuilder.java @@ -26,7 +26,7 @@ import java.io.IOException; /** * A function that computes a random score for the matched documents */ -public class RandomScoreFunctionBuilder implements ScoreFunctionBuilder { +public class RandomScoreFunctionBuilder extends ScoreFunctionBuilder { private Integer seed = null; @@ -50,12 +50,12 @@ public class RandomScoreFunctionBuilder implements ScoreFunctionBuilder { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + public void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(getName()); if (seed != null) { builder.field("seed", seed.intValue()); } - return builder.endObject(); + builder.endObject(); } } \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionBuilder.java b/src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionBuilder.java index d11a85aa8c0..3f715512bab 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionBuilder.java @@ -31,7 +31,7 @@ import java.util.Map; * A function that uses a script to compute or influence the score of documents * that match with the inner query or filter. */ -public class ScriptScoreFunctionBuilder implements ScoreFunctionBuilder { +public class ScriptScoreFunctionBuilder extends ScoreFunctionBuilder { private String script; @@ -80,7 +80,7 @@ public class ScriptScoreFunctionBuilder implements ScoreFunctionBuilder { } @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + public void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(getName()); builder.field("script", script); if (lang != null) { @@ -89,7 +89,7 @@ public class ScriptScoreFunctionBuilder implements ScoreFunctionBuilder { if (this.params != null) { builder.field("params", this.params); } - return builder.endObject(); + builder.endObject(); } @Override diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/weight/WeightBuilder.java b/src/main/java/org/elasticsearch/index/query/functionscore/weight/WeightBuilder.java new file mode 100644 index 00000000000..50f704de810 --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/functionscore/weight/WeightBuilder.java @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.query.functionscore.weight; + +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder; + +import java.io.IOException; + +/** + * A query that multiplies the weight to the score. + */ +public class WeightBuilder extends ScoreFunctionBuilder { + + + @Override + public String getName() { + return "weight"; + } + + @Override + protected void doXContent(XContentBuilder builder, Params params) throws IOException { + } +} diff --git a/src/test/java/org/elasticsearch/index/query/SimpleIndexQueryParserTests.java b/src/test/java/org/elasticsearch/index/query/SimpleIndexQueryParserTests.java index 7488719b05c..9bf72b843bf 100644 --- a/src/test/java/org/elasticsearch/index/query/SimpleIndexQueryParserTests.java +++ b/src/test/java/org/elasticsearch/index/query/SimpleIndexQueryParserTests.java @@ -34,6 +34,7 @@ import org.apache.lucene.util.CharsRef; import org.apache.lucene.util.NumericUtils; import org.apache.lucene.util.UnicodeUtil; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchIllegalArgumentException; import org.elasticsearch.action.get.MultiGetRequest; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.compress.CompressedString; @@ -41,6 +42,7 @@ import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.lucene.search.*; import org.elasticsearch.common.lucene.search.function.BoostScoreFunction; import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery; +import org.elasticsearch.common.lucene.search.function.WeightFactorFunction; import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.DistanceUnit; @@ -2318,6 +2320,92 @@ public class SimpleIndexQueryParserTests extends ElasticsearchSingleNodeTest { assertThat(filter.getTerm().toString(), equalTo("text:apache")); } + @Test + public void testProperErrorMessageWhenTwoFunctionsDefinedInQueryBody() throws IOException { + IndexQueryParserService queryParser = queryParser(); + String query = copyToStringFromClasspath("/org/elasticsearch/index/query/function-score-query-causing-NPE.json"); + try { + queryParser.parse(query).query(); + fail("FunctionScoreQueryParser should throw an exception here because two functions in body are not allowed."); + } catch (QueryParsingException e) { + assertThat(e.getDetailedMessage(), containsString("Use functions[{...},...] if you want to define several functions.")); + } + } + + @Test + public void testWeight1fStillProducesWeighFuction() throws IOException { + IndexQueryParserService queryParser = queryParser(); + String queryString = jsonBuilder().startObject() + .startObject("function_score") + .startArray("functions") + .startObject() + .startObject("field_value_factor") + .field("field", "popularity") + .endObject() + .field("weight", 1.0) + .endObject() + .endArray() + .endObject() + .endObject().string(); + IndexService indexService = createIndex("testidx", client().admin().indices().prepareCreate("testidx") + .addMapping("doc",jsonBuilder().startObject() + .startObject("properties") + .startObject("popularity").field("type", "float").endObject() + .endObject() + .endObject())); + SearchContext.setCurrent(createSearchContext(indexService)); + Query query = queryParser.parse(queryString).query(); + assertThat(query, instanceOf(FunctionScoreQuery.class)); + assertThat(((FunctionScoreQuery) query).getFunction(), instanceOf(WeightFactorFunction.class)); + SearchContext.removeCurrent(); + } + + @Test + public void testProperErrorMessagesForMisplacedWeightsAndFunctions() throws IOException { + IndexQueryParserService queryParser = queryParser(); + String query = jsonBuilder().startObject().startObject("function_score") + .startArray("functions") + .startObject().field("weight", 2).field("boost_factor",2).endObject() + .endArray() + .endObject().endObject().string(); + try { + queryParser.parse(query).query(); + fail("Expect exception here because boost_factor must not have a weight"); + } catch (QueryParsingException e) { + assertThat(e.getDetailedMessage(), containsString(BoostScoreFunction.BOOST_WEIGHT_ERROR_MESSAGE)); + } + try { + functionScoreQuery().add(factorFunction(2.0f).setWeight(2.0f)); + fail("Expect exception here because boost_factor must not have a weight"); + } catch (ElasticsearchIllegalArgumentException e) { + assertThat(e.getDetailedMessage(), containsString(BoostScoreFunction.BOOST_WEIGHT_ERROR_MESSAGE)); + } + query = jsonBuilder().startObject().startObject("function_score") + .startArray("functions") + .startObject().field("boost_factor",2).endObject() + .endArray() + .field("weight", 2) + .endObject().endObject().string(); + try { + queryParser.parse(query).query(); + fail("Expect exception here because array of functions and one weight in body is not allowed."); + } catch (QueryParsingException e) { + assertThat(e.getDetailedMessage(), containsString("You can either define \"functions\":[...] or a single function, not both. Found \"functions\": [...] already, now encountering \"weight\".")); + } + query = jsonBuilder().startObject().startObject("function_score") + .field("weight", 2) + .startArray("functions") + .startObject().field("boost_factor",2).endObject() + .endArray() + .endObject().endObject().string(); + try { + queryParser.parse(query).query(); + fail("Expect exception here because array of functions and one weight in body is not allowed."); + } catch (QueryParsingException e) { + assertThat(e.getDetailedMessage(), containsString("You can either define \"functions\":[...] or a single function, not both. Found \"weight\" already, now encountering \"functions\": [...].")); + } + } + // https://github.com/elasticsearch/elasticsearch/issues/6722 public void testEmptyBoolSubClausesIsMatchAll() throws ElasticsearchException, IOException { String query = copyToStringFromClasspath("/org/elasticsearch/index/query/bool-query-with-empty-clauses-for-parsing.json"); diff --git a/src/test/java/org/elasticsearch/index/query/function-score-query-causing-NPE.json b/src/test/java/org/elasticsearch/index/query/function-score-query-causing-NPE.json new file mode 100644 index 00000000000..283682bd90f --- /dev/null +++ b/src/test/java/org/elasticsearch/index/query/function-score-query-causing-NPE.json @@ -0,0 +1,9 @@ +{ + "function_score": { + "script_score": { + "script": "_index['text']['foo'].tf()" + }, + "weight": 2 + } +} + diff --git a/src/test/java/org/elasticsearch/search/functionscore/FunctionScoreBackwardCompatibilityTests.java b/src/test/java/org/elasticsearch/search/functionscore/FunctionScoreBackwardCompatibilityTests.java new file mode 100644 index 00000000000..567649fcf59 --- /dev/null +++ b/src/test/java/org/elasticsearch/search/functionscore/FunctionScoreBackwardCompatibilityTests.java @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.search.functionscore; + +import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.common.geo.GeoPoint; +import org.elasticsearch.common.lucene.search.function.FieldValueFactorFunction; +import org.elasticsearch.test.ElasticsearchBackwardsCompatIntegrationTest; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import static org.elasticsearch.client.Requests.searchRequest; +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.index.query.FilterBuilders.termFilter; +import static org.elasticsearch.index.query.QueryBuilders.functionScoreQuery; +import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.*; +import static org.elasticsearch.search.builder.SearchSourceBuilder.searchSource; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.*; + +/** + */ +public class FunctionScoreBackwardCompatibilityTests extends ElasticsearchBackwardsCompatIntegrationTest { + + /** + * Simple upgrade test for function score + */ + @Test + public void testSimpleFunctionScoreParsingWorks() throws IOException, ExecutionException, InterruptedException { + + assertAcked(prepareCreate("test").addMapping( + "type1", + jsonBuilder().startObject() + .startObject("type1") + .startObject("properties") + .startObject("text") + .field("type", "string") + .endObject() + .startObject("loc") + .field("type", "geo_point") + .endObject() + .startObject("popularity") + .field("type", "float") + .endObject() + .endObject() + .endObject() + .endObject())); + ensureYellow(); + + int numDocs = 10; + String[] ids = new String[numDocs]; + List indexBuilders = new ArrayList<>(); + for (int i = 0; i < numDocs; i++) { + String id = Integer.toString(i); + indexBuilders.add(client().prepareIndex() + .setType("type1").setId(id).setIndex("test") + .setSource( + jsonBuilder().startObject() + .field("text", "value") + .startObject("loc") + .field("lat", 10 + i) + .field("lon", 20) + .endObject() + .field("popularity", 2.71828) + .endObject())); + ids[i] = id; + } + indexRandom(true, indexBuilders); + checkFunctionScoreStillWorks(ids); + logClusterState(); + boolean upgraded; + int upgradedNodesCounter = 1; + do { + logger.debug("function_score bwc: upgrading {}st node", upgradedNodesCounter++); + upgraded = backwardsCluster().upgradeOneNode(); + ensureGreen(); + logClusterState(); + checkFunctionScoreStillWorks(ids); + } while (upgraded); + logger.debug("done function_score while upgrading"); + } + + private void checkFunctionScoreStillWorks(String... ids) throws ExecutionException, InterruptedException, IOException { + SearchResponse response = client().search( + searchRequest().source( + searchSource().query( + functionScoreQuery(termFilter("text", "value")) + .add(gaussDecayFunction("loc", new GeoPoint(10, 20), "1000km")) + .add(fieldValueFactorFunction("popularity").modifier(FieldValueFactorFunction.Modifier.LN)) + .add(scriptFunction("_index['text']['value'].tf()")) + ))).actionGet(); + assertSearchResponse(response); + assertOrderedSearchHits(response, ids); + } +} diff --git a/src/test/java/org/elasticsearch/search/functionscore/FunctionScoreTests.java b/src/test/java/org/elasticsearch/search/functionscore/FunctionScoreTests.java index 3c4eec9c7e6..f6e84746fa2 100644 --- a/src/test/java/org/elasticsearch/search/functionscore/FunctionScoreTests.java +++ b/src/test/java/org/elasticsearch/search/functionscore/FunctionScoreTests.java @@ -19,8 +19,15 @@ package org.elasticsearch.search.functionscore; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchType; +import org.elasticsearch.common.geo.GeoPoint; +import org.elasticsearch.common.lucene.search.function.FieldValueFactorFunction; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder; +import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder; +import org.elasticsearch.index.query.functionscore.weight.WeightBuilder; import org.elasticsearch.test.ElasticsearchIntegrationTest; import org.junit.Test; @@ -28,17 +35,27 @@ import java.io.IOException; import java.util.concurrent.ExecutionException; import static org.elasticsearch.client.Requests.searchRequest; +import static org.elasticsearch.common.io.Streams.copyToStringFromClasspath; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.index.query.FilterBuilders.termFilter; import static org.elasticsearch.index.query.QueryBuilders.functionScoreQuery; import static org.elasticsearch.index.query.QueryBuilders.termQuery; import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.*; import static org.elasticsearch.search.builder.SearchSourceBuilder.searchSource; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.greaterThan; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; +import static org.hamcrest.Matchers.*; public class FunctionScoreTests extends ElasticsearchIntegrationTest { + static final String TYPE = "type"; + static final String INDEX = "index"; + static final String TEXT_FIELD = "text_field"; + static final String FLOAT_FIELD = "float_field"; + static final String GEO_POINT_FIELD = "geo_point_field"; + static final XContentBuilder SIMPLE_DOC; + static final XContentBuilder MAPPING_WITH_FLOAT_AND_GEO_POINT_AND_TEST_FIELD; + @Test public void testExplainQueryOnlyOnce() throws IOException, ExecutionException, InterruptedException { assertAcked(prepareCreate("test").addMapping( @@ -87,4 +104,297 @@ public class FunctionScoreTests extends ElasticsearchIntegrationTest { assertThat(queryExplanationIndex, equalTo(-1)); } + static { + XContentBuilder simpleDoc; + XContentBuilder mappingWithFloatAndGeoPointAndTestField; + try { + simpleDoc = jsonBuilder().startObject() + .field(TEXT_FIELD, "value") + .startObject(GEO_POINT_FIELD) + .field("lat", 10) + .field("lon", 20) + .endObject() + .field(FLOAT_FIELD, 2.71828) + .endObject(); + } catch (IOException e) { + throw new ElasticsearchException("Exception while initializing FunctionScoreTests", e); + } + SIMPLE_DOC = simpleDoc; + try { + + mappingWithFloatAndGeoPointAndTestField = jsonBuilder().startObject() + .startObject(TYPE) + .startObject("properties") + .startObject(TEXT_FIELD) + .field("type", "string") + .endObject() + .startObject(GEO_POINT_FIELD) + .field("type", "geo_point") + .endObject() + .startObject(FLOAT_FIELD) + .field("type", "float") + .endObject() + .endObject() + .endObject() + .endObject(); + } catch (IOException e) { + throw new ElasticsearchException("Exception while initializing FunctionScoreTests", e); + } + MAPPING_WITH_FLOAT_AND_GEO_POINT_AND_TEST_FIELD = mappingWithFloatAndGeoPointAndTestField; + } + + @Test + public void testExplain() throws IOException, ExecutionException, InterruptedException { + assertAcked(prepareCreate(INDEX).addMapping( + TYPE, MAPPING_WITH_FLOAT_AND_GEO_POINT_AND_TEST_FIELD + )); + ensureYellow(); + + index(INDEX, TYPE, "1", SIMPLE_DOC); + refresh(); + + SearchResponse responseWithWeights = client().search( + searchRequest().source( + searchSource().query( + functionScoreQuery(termFilter(TEXT_FIELD, "value").cache(false)) + .add(gaussDecayFunction(GEO_POINT_FIELD, new GeoPoint(10, 20), "1000km")) + .add(fieldValueFactorFunction(FLOAT_FIELD).modifier(FieldValueFactorFunction.Modifier.LN).setWeight(2)) + .add(scriptFunction("_index['" + TEXT_FIELD + "']['value'].tf()").setWeight(3)) + ).explain(true))).actionGet(); + + assertThat(responseWithWeights.getHits().getAt(0).getExplanation().toString(), + equalTo("5.999996 = (MATCH) function score, product of:\n 1.0 = (MATCH) ConstantScore(text_field:value), product of:\n 1.0 = boost\n 1.0 = queryNorm\n 5.999996 = (MATCH) Math.min of\n 5.999996 = (MATCH) function score, score mode [multiply]\n 1.0 = (MATCH) function score, product of:\n 1.0 = match filter: *:*\n 1.0 = (MATCH) Function for field geo_point_field:\n 1.0 = -exp(-0.5*pow(MIN of: [Math.max(arcDistance([10.0, 20.0](=doc value),[10.0, 20.0](=origin)) - 0.0(=offset), 0)],2.0)/7.213475204444817E11)\n 1.9999987 = (MATCH) function score, product of:\n 1.0 = match filter: *:*\n 1.9999987 = (MATCH) product of:\n 0.99999934 = field value function: ln(doc['float_field'].value * factor=1.0)\n 2.0 = weight\n 3.0 = (MATCH) function score, product of:\n 1.0 = match filter: *:*\n 3.0 = (MATCH) product of:\n 1.0 = script score function, computed with script:\"_index['text_field']['value'].tf()\n 3.0 = weight\n 3.4028235E38 = maxBoost\n 1.0 = queryBoost\n") + ); + responseWithWeights = client().search( + searchRequest().source( + searchSource().query( + functionScoreQuery(termFilter(TEXT_FIELD, "value").cache(false)) + .add(weightFactorFunction(4.0f)) + ).explain(true))).actionGet(); + assertThat(responseWithWeights.getHits().getAt(0).getExplanation().toString(), + equalTo("4.0 = (MATCH) function score, product of:\n 1.0 = (MATCH) ConstantScore(text_field:value), product of:\n 1.0 = boost\n 1.0 = queryNorm\n 4.0 = (MATCH) Math.min of\n 4.0 = (MATCH) product of:\n 1.0 = constant score 1.0 - no function provided\n 4.0 = weight\n 3.4028235E38 = maxBoost\n 1.0 = queryBoost\n") + ); + + } + + @Test + public void simpleWeightedFunctionsTest() throws IOException, ExecutionException, InterruptedException { + assertAcked(prepareCreate(INDEX).addMapping( + TYPE, MAPPING_WITH_FLOAT_AND_GEO_POINT_AND_TEST_FIELD + )); + ensureYellow(); + + index(INDEX, TYPE, "1", SIMPLE_DOC); + refresh(); + SearchResponse response = client().search( + searchRequest().source( + searchSource().query( + functionScoreQuery(termFilter(TEXT_FIELD, "value")) + .add(gaussDecayFunction(GEO_POINT_FIELD, new GeoPoint(10, 20), "1000km")) + .add(fieldValueFactorFunction(FLOAT_FIELD).modifier(FieldValueFactorFunction.Modifier.LN)) + .add(scriptFunction("_index['" + TEXT_FIELD + "']['value'].tf()")) + ))).actionGet(); + SearchResponse responseWithWeights = client().search( + searchRequest().source( + searchSource().query( + functionScoreQuery(termFilter(TEXT_FIELD, "value")) + .add(gaussDecayFunction(GEO_POINT_FIELD, new GeoPoint(10, 20), "1000km").setWeight(2)) + .add(fieldValueFactorFunction(FLOAT_FIELD).modifier(FieldValueFactorFunction.Modifier.LN).setWeight(2)) + .add(scriptFunction("_index['" + TEXT_FIELD + "']['value'].tf()").setWeight(2)) + ))).actionGet(); + + assertThat((double) response.getHits().getAt(0).getScore(), closeTo(1.0, 1.e-5)); + assertThat((double) responseWithWeights.getHits().getAt(0).getScore(), closeTo(8.0, 1.e-5)); + } + + @Test + public void simpleWeightedFunctionsTestWithRandomWeightsAndRandomCombineMode() throws IOException, ExecutionException, InterruptedException { + assertAcked(prepareCreate(INDEX).addMapping( + TYPE, + MAPPING_WITH_FLOAT_AND_GEO_POINT_AND_TEST_FIELD)); + ensureYellow(); + + XContentBuilder doc = jsonBuilder().startObject() + .field(TEXT_FIELD, "value") + .startObject(GEO_POINT_FIELD) + .field("lat", 10) + .field("lon", 20) + .endObject() + .field(FLOAT_FIELD, 10) + .endObject(); + index(INDEX, TYPE, "1", doc); + refresh(); + ScoreFunctionBuilder[] scoreFunctionBuilders = getScoreFunctionBuilders(); + float[] weights = createRandomWeights(scoreFunctionBuilders.length); + float[] scores = getScores(scoreFunctionBuilders); + + String scoreMode = getRandomScoreMode(); + FunctionScoreQueryBuilder withWeights = functionScoreQuery(termFilter(TEXT_FIELD, "value")).scoreMode(scoreMode); + int weightscounter = 0; + for (ScoreFunctionBuilder builder : scoreFunctionBuilders) { + withWeights.add(builder.setWeight((float) weights[weightscounter])); + weightscounter++; + } + SearchResponse responseWithWeights = client().search( + searchRequest().source(searchSource().query(withWeights)) + ).actionGet(); + + double expectedScore = computeExpectedScore(weights, scores, scoreMode); + assertThat(expectedScore / responseWithWeights.getHits().getAt(0).getScore(), closeTo(1.0, 1.e-6)); + + } + + protected float computeExpectedScore(float[] weights, float[] scores, String scoreMode) { + float expectedScore = 0.0f; + if ("multiply".equals(scoreMode)) { + expectedScore = 1.0f; + } + if ("max".equals(scoreMode)) { + expectedScore = Float.MAX_VALUE * -1.0f; + } + if ("min".equals(scoreMode)) { + expectedScore = Float.MAX_VALUE; + } + + for (int i = 0; i < weights.length; i++) { + float functionScore = weights[i] * scores[i]; + + if ("avg".equals(scoreMode)) { + expectedScore += functionScore; + } else if ("max".equals(scoreMode)) { + expectedScore = Math.max(functionScore, expectedScore); + } else if ("min".equals(scoreMode)) { + expectedScore = Math.min(functionScore, expectedScore); + } else if ("sum".equals(scoreMode)) { + expectedScore += functionScore; + } else if ("multiply".equals(scoreMode)) { + expectedScore *= functionScore; + } + + } + if ("avg".equals(scoreMode)) { + expectedScore /= (double) weights.length; + } + return expectedScore; + } + + @Test + public void simpleWeightedFunctionsTestSingleFunction() throws IOException, ExecutionException, InterruptedException { + assertAcked(prepareCreate(INDEX).addMapping( + TYPE, + MAPPING_WITH_FLOAT_AND_GEO_POINT_AND_TEST_FIELD)); + ensureYellow(); + + XContentBuilder doc = jsonBuilder().startObject() + .field(TEXT_FIELD, "value") + .startObject(GEO_POINT_FIELD) + .field("lat", 12) + .field("lon", 21) + .endObject() + .field(FLOAT_FIELD, 10) + .endObject(); + index(INDEX, TYPE, "1", doc); + refresh(); + ScoreFunctionBuilder[] scoreFunctionBuilders = getScoreFunctionBuilders(); + ScoreFunctionBuilder scoreFunctionBuilder = scoreFunctionBuilders[randomInt(3)]; + float[] weights = createRandomWeights(1); + float[] scores = getScores(scoreFunctionBuilder); + FunctionScoreQueryBuilder withWeights = functionScoreQuery(termFilter(TEXT_FIELD, "value")); + withWeights.add(scoreFunctionBuilder.setWeight(weights[0])); + + SearchResponse responseWithWeights = client().search( + searchRequest().source(searchSource().query(withWeights)) + ).actionGet(); + + assertThat((double) scores[0] * weights[0] / responseWithWeights.getHits().getAt(0).getScore(), closeTo(1.0, 1.e-6)); + + } + + private String getRandomScoreMode() { + String[] scoreModes = {"avg", "sum", "min", "max", "multiply"}; + return scoreModes[randomInt(scoreModes.length - 1)]; + } + + private float[] getScores(ScoreFunctionBuilder... scoreFunctionBuilders) { + float[] scores = new float[scoreFunctionBuilders.length]; + int scorecounter = 0; + for (ScoreFunctionBuilder builder : scoreFunctionBuilders) { + SearchResponse response = client().search( + searchRequest().source( + searchSource().query( + functionScoreQuery(termFilter(TEXT_FIELD, "value")) + .add(builder) + ))).actionGet(); + scores[scorecounter] = response.getHits().getAt(0).getScore(); + scorecounter++; + } + return scores; + } + + private float[] createRandomWeights(int size) { + float[] weights = new float[size]; + for (int i = 0; i < weights.length; i++) { + weights[i] = randomFloat() * (randomBoolean() ? 1.0f : -1.0f) * (float) randomInt(100) + 1.e-6f; + } + return weights; + } + + public ScoreFunctionBuilder[] getScoreFunctionBuilders() { + ScoreFunctionBuilder[] builders = new ScoreFunctionBuilder[4]; + builders[0] = gaussDecayFunction(GEO_POINT_FIELD, new GeoPoint(11, 20), "1000km"); + builders[1] = randomFunction(10); + builders[2] = fieldValueFactorFunction(FLOAT_FIELD).modifier(FieldValueFactorFunction.Modifier.LN); + builders[3] = scriptFunction("_index['" + TEXT_FIELD + "']['value'].tf()"); + return builders; + } + + @Test + public void checkWeightOnlyCreatesBoostFunction() throws IOException { + assertAcked(prepareCreate(INDEX).addMapping( + TYPE, + MAPPING_WITH_FLOAT_AND_GEO_POINT_AND_TEST_FIELD)); + ensureYellow(); + + index(INDEX, TYPE, "1", SIMPLE_DOC); + refresh(); + String query =jsonBuilder().startObject() + .startObject("query") + .startObject("function_score") + .startArray("functions") + .startObject() + .field("weight",2) + .endObject() + .endArray() + .endObject() + .endObject() + .endObject().string(); + SearchResponse response = client().search( + searchRequest().source(query) + ).actionGet(); + assertSearchResponse(response); + assertThat(response.getHits().getAt(0).score(), equalTo(2.0f)); + + query =jsonBuilder().startObject() + .startObject("query") + .startObject("function_score") + .field("weight",2) + .endObject() + .endObject() + .endObject().string(); + response = client().search( + searchRequest().source(query) + ).actionGet(); + assertSearchResponse(response); + assertThat(response.getHits().getAt(0).score(), equalTo(2.0f)); + response = client().search( + searchRequest().source(searchSource().query(functionScoreQuery().add(new WeightBuilder().setWeight(2.0f)))) + ).actionGet(); + assertSearchResponse(response); + assertThat(response.getHits().getAt(0).score(), equalTo(2.0f)); + response = client().search( + searchRequest().source(searchSource().query(functionScoreQuery().add(weightFactorFunction(2.0f)))) + ).actionGet(); + assertSearchResponse(response); + assertThat(response.getHits().getAt(0).score(), equalTo(2.0f)); + } }