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
This commit is contained in:
Britta Weber 2014-08-02 10:12:37 +02:00
parent 9750375412
commit c5ff70bf43
19 changed files with 807 additions and 90 deletions

View File

@ -9,8 +9,7 @@ the score on a filtered set of documents.
`function_score` provides the same functionality that `function_score` provides the same functionality that
`custom_boost_factor`, `custom_score` and `custom_boost_factor`, `custom_score` and
`custom_filters_score` provided `custom_filters_score` provided
but furthermore adds futher scoring functionality such as but with additional capabilities such as distance and recency scoring (see description below).
distance and recency scoring (see description below).
==== Using function score ==== Using function score
@ -42,10 +41,15 @@ given filter:
"functions": [ "functions": [
{ {
"filter": {}, "filter": {},
"FUNCTION": {} "FUNCTION": {},
"weight": number
}, },
{ {
"FUNCTION": {} "FUNCTION": {}
},
{
"filter": {},
"weight": number
} }
], ],
"max_boost": number, "max_boost": number,
@ -69,6 +73,9 @@ First, each document is scored by the defined functions. The parameter
`max`:: maximum score is used `max`:: maximum score is used
`min`:: minimum 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 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. 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 score of the query is multiplied with the result of the script scoring. If
you wish to inhibit this, set `"boost_mode": "replace"` 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 coming[1.4.0]
`boost_factor`. This can sometimes be desired since boost value set on
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 specific queries gets normalized, while for this score function it does
not. not.
[source,js] [source,js]
-------------------------------------------------- --------------------------------------------------
"boost_factor" : number "weight" : number
-------------------------------------------------- --------------------------------------------------
===== Boost factor
deprecated[1.4.0]
Same as `weight`. Use `weight` instead.
===== Random ===== Random
The `random_score` generates scores using a hash of the `_uid` field, The `random_score` generates scores using a hash of the `_uid` field,
@ -490,7 +506,7 @@ becomes
[source,js] [source,js]
-------------------------------------------------- --------------------------------------------------
"function_score": { "function_score": {
"boost_factor": 5.2, "weight": 5.2,
"query": {...} "query": {...}
} }
-------------------------------------------------- --------------------------------------------------
@ -557,7 +573,7 @@ becomes:
"function_score": { "function_score": {
"functions": [ "functions": [
{ {
"boost_factor": "3", "weight": "3",
"filter": {...} "filter": {...}
}, },
{ {

View File

@ -21,12 +21,16 @@ package org.elasticsearch.common.lucene.search.function;
import org.apache.lucene.index.AtomicReaderContext; import org.apache.lucene.index.AtomicReaderContext;
import org.apache.lucene.search.Explanation; import org.apache.lucene.search.Explanation;
import org.elasticsearch.ElasticsearchIllegalArgumentException;
/** /**
* *
*/ */
@Deprecated
public class BoostScoreFunction extends ScoreFunction { 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; private final float boost;
public BoostScoreFunction(float boost) { public BoostScoreFunction(float boost) {
@ -55,28 +59,9 @@ public class BoostScoreFunction extends ScoreFunction {
return exp; 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 @Override
public String toString() { public String toString() {
return "boost[" + boost + "]"; return "boost[" + boost + "]";
} }
} }

View File

@ -28,7 +28,7 @@ import org.apache.lucene.search.Explanation;
public abstract class ScoreFunction { public abstract class ScoreFunction {
private final CombineFunction scoreCombiner; private final CombineFunction scoreCombiner;
public abstract void setNextReader(AtomicReaderContext context); public abstract void setNextReader(AtomicReaderContext context);
public abstract double score(int docId, float subQueryScore); public abstract double score(int docId, float subQueryScore);
@ -42,5 +42,4 @@ public abstract class ScoreFunction {
protected ScoreFunction(CombineFunction scoreCombiner) { protected ScoreFunction(CombineFunction scoreCombiner) {
this.scoreCombiner = scoreCombiner; this.scoreCombiner = scoreCombiner;
} }
} }

View File

@ -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");
}
}
}

View File

@ -26,7 +26,7 @@ import org.elasticsearch.search.MultiValueMode;
import java.io.IOException; import java.io.IOException;
import java.util.Locale; 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 ORIGIN = "origin";
protected static final String SCALE = "scale"; protected static final String SCALE = "scale";
@ -60,7 +60,7 @@ public abstract class DecayFunctionBuilder implements ScoreFunctionBuilder {
} }
@Override @Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { public void doXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(getName()); builder.startObject(getName());
builder.startObject(fieldName); builder.startObject(fieldName);
if (origin != null) { if (origin != null) {
@ -78,7 +78,6 @@ public abstract class DecayFunctionBuilder implements ScoreFunctionBuilder {
builder.field(DecayFunctionParser.MULTI_VALUE_MODE.getPreferredName(), multiValueMode.name()); builder.field(DecayFunctionParser.MULTI_VALUE_MODE.getPreferredName(), multiValueMode.name());
} }
builder.endObject(); builder.endObject();
return builder;
} }
public ScoreFunctionBuilder setMultiValueMode(MultiValueMode multiValueMode) { public ScoreFunctionBuilder setMultiValueMode(MultiValueMode multiValueMode) {

View File

@ -45,7 +45,7 @@ public class FunctionScoreQueryBuilder extends BaseQueryBuilder implements Boost
private Float maxBoost; private Float maxBoost;
private String scoreMode; private String scoreMode;
private String boostMode; private String boostMode;
private ArrayList<FilterBuilder> filters = new ArrayList<>(); private ArrayList<FilterBuilder> filters = new ArrayList<>();
@ -98,12 +98,12 @@ public class FunctionScoreQueryBuilder extends BaseQueryBuilder implements Boost
this.scoreMode = scoreMode; this.scoreMode = scoreMode;
return this; return this;
} }
public FunctionScoreQueryBuilder boostMode(String boostMode) { public FunctionScoreQueryBuilder boostMode(String boostMode) {
this.boostMode = boostMode; this.boostMode = boostMode;
return this; return this;
} }
public FunctionScoreQueryBuilder boostMode(CombineFunction combineFunction) { public FunctionScoreQueryBuilder boostMode(CombineFunction combineFunction) {
this.boostMode = combineFunction.getName(); this.boostMode = combineFunction.getName();
return this; return this;
@ -133,27 +133,19 @@ public class FunctionScoreQueryBuilder extends BaseQueryBuilder implements Boost
} else if (filterBuilder != null) { } else if (filterBuilder != null) {
builder.field("filter"); builder.field("filter");
filterBuilder.toXContent(builder, params); 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) { if (scoreMode != null) {
builder.field("score_mode", scoreMode); builder.field("score_mode", scoreMode);
} }

View File

@ -24,14 +24,13 @@ import com.google.common.collect.ImmutableMap.Builder;
import org.apache.lucene.search.Filter; import org.apache.lucene.search.Filter;
import org.apache.lucene.search.Query; import org.apache.lucene.search.Query;
import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.Strings; import org.elasticsearch.common.Strings;
import org.elasticsearch.common.inject.Inject; 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.Queries;
import org.elasticsearch.common.lucene.search.XConstantScoreQuery; import org.elasticsearch.common.lucene.search.XConstantScoreQuery;
import org.elasticsearch.common.lucene.search.function.CombineFunction; import org.elasticsearch.common.lucene.search.function.*;
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.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.query.QueryParseContext; import org.elasticsearch.index.query.QueryParseContext;
import org.elasticsearch.index.query.QueryParser; 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_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?"; static final String MISPLACED_BOOST_FUNCTION_MESSAGE_SUFFIX = " Did you mean \"boost\" instead?";
public static final ParseField WEIGHT_FIELD = new ParseField("weight");
@Inject @Inject
public FunctionScoreQueryParser(ScoreFunctionParserMapper funtionParserMapper) { public FunctionScoreQueryParser(ScoreFunctionParserMapper funtionParserMapper) {
this.funtionParserMapper = funtionParserMapper; this.funtionParserMapper = funtionParserMapper;
@ -62,7 +63,7 @@ public class FunctionScoreQueryParser implements QueryParser {
public String[] names() { public String[] names() {
return new String[] { NAME, Strings.toCamelCase(NAME) }; return new String[] { NAME, Strings.toCamelCase(NAME) };
} }
private static final ImmutableMap<String, CombineFunction> combineFunctionsMap; private static final ImmutableMap<String, CombineFunction> combineFunctionsMap;
static { static {
@ -116,17 +117,27 @@ public class FunctionScoreQueryParser implements QueryParser {
currentFieldName = parseFiltersAndFunctions(parseContext, parser, filterFunctions, currentFieldName); currentFieldName = parseFiltersAndFunctions(parseContext, parser, filterFunctions, currentFieldName);
functionArrayFound = true; functionArrayFound = true;
} else { } else {
// we try to parse a score function. If there is no score ScoreFunction scoreFunction;
// function for the current field name, if (currentFieldName.equals("weight")) {
// functionParserMapper.get() will throw an Exception. scoreFunction = new WeightFactorFunction(parser.floatValue());
ScoreFunctionParser currentFunctionParser = funtionParserMapper.get(parseContext.index(), currentFieldName);
singleFunctionName = currentFieldName; } 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) { if (functionArrayFound) {
String errorString = "Found \"functions\": [...] already, now encountering \"" + currentFieldName + "\"."; String errorString = "Found \"functions\": [...] already, now encountering \"" + currentFieldName + "\".";
handleMisplacedFunctionsDeclaration(errorString, 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; singleFunctionFound = true;
singleFunctionName = currentFieldName;
} }
} }
if (query == null) { if (query == null) {
@ -138,7 +149,7 @@ public class FunctionScoreQueryParser implements QueryParser {
} }
// handle cases where only one score function and no filter was // handle cases where only one score function and no filter was
// provided. In this case we create a FunctionScoreQuery. // 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); FunctionScoreQuery theQuery = new FunctionScoreQuery(query, filterFunctions.get(0).function);
if (combineFunction != null) { if (combineFunction != null) {
theQuery.setCombineFunction(combineFunction); theQuery.setCombineFunction(combineFunction);
@ -167,11 +178,12 @@ public class FunctionScoreQueryParser implements QueryParser {
} }
private String parseFiltersAndFunctions(QueryParseContext parseContext, XContentParser parser, private String parseFiltersAndFunctions(QueryParseContext parseContext, XContentParser parser,
ArrayList<FiltersFunctionScoreQuery.FilterFunction> filterFunctions, String currentFieldName) throws IOException { ArrayList<FiltersFunctionScoreQuery.FilterFunction> filterFunctions, String currentFieldName) throws IOException {
XContentParser.Token token; XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
Filter filter = null; Filter filter = null;
ScoreFunction scoreFunction = null; ScoreFunction scoreFunction = null;
Float functionWeight = null;
if (token != XContentParser.Token.START_OBJECT) { if (token != XContentParser.Token.START_OBJECT) {
throw new QueryParsingException(parseContext.index(), NAME + ": malformed query, expected a " throw new QueryParsingException(parseContext.index(), NAME + ": malformed query, expected a "
+ XContentParser.Token.START_OBJECT + " while parsing functions but got a " + token); + 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) { while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) { if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName(); currentFieldName = parser.currentName();
} else if (WEIGHT_FIELD.match(currentFieldName)) {
functionWeight = parser.floatValue();
} else { } else {
if ("filter".equals(currentFieldName)) { if ("filter".equals(currentFieldName)) {
filter = parseContext.parseInnerFilter(); filter = parseContext.parseInnerFilter();
@ -191,6 +205,9 @@ public class FunctionScoreQueryParser implements QueryParser {
} }
} }
} }
if (functionWeight != null) {
scoreFunction = new WeightFactorFunction(functionWeight, scoreFunction);
}
} }
if (filter == null) { if (filter == null) {
filter = Queries.MATCH_ALL_FILTER; filter = Queries.MATCH_ALL_FILTER;

View File

@ -20,9 +20,33 @@
package org.elasticsearch.index.query.functionscore; package org.elasticsearch.index.query.functionscore;
import org.elasticsearch.common.xcontent.ToXContent; 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;
} }

View File

@ -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.lin.LinearDecayFunctionBuilder;
import org.elasticsearch.index.query.functionscore.random.RandomScoreFunctionBuilder; import org.elasticsearch.index.query.functionscore.random.RandomScoreFunctionBuilder;
import org.elasticsearch.index.query.functionscore.script.ScriptScoreFunctionBuilder; import org.elasticsearch.index.query.functionscore.script.ScriptScoreFunctionBuilder;
import org.elasticsearch.index.query.functionscore.weight.WeightBuilder;
import java.util.Map; import java.util.Map;
@ -71,6 +72,7 @@ public class ScoreFunctionBuilders {
return (new ScriptScoreFunctionBuilder()).script(script).params(params); return (new ScriptScoreFunctionBuilder()).script(script).params(params);
} }
@Deprecated
public static FactorBuilder factorFunction(float boost) { public static FactorBuilder factorFunction(float boost) {
return (new FactorBuilder()).boostFactor(boost); return (new FactorBuilder()).boostFactor(boost);
} }
@ -78,6 +80,10 @@ public class ScoreFunctionBuilders {
public static RandomScoreFunctionBuilder randomFunction(int seed) { public static RandomScoreFunctionBuilder randomFunction(int seed) {
return (new RandomScoreFunctionBuilder()).seed(seed); return (new RandomScoreFunctionBuilder()).seed(seed);
} }
public static WeightBuilder weightFactorFunction(float weight) {
return (WeightBuilder)(new WeightBuilder().setWeight(weight));
}
public static FieldValueFactorFunctionBuilder fieldValueFactorFunction(String fieldName) { public static FieldValueFactorFunctionBuilder fieldValueFactorFunction(String fieldName) {
return new FieldValueFactorFunctionBuilder(fieldName); return new FieldValueFactorFunctionBuilder(fieldName);

View File

@ -19,18 +19,20 @@
package org.elasticsearch.index.query.functionscore.factor; 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.common.xcontent.XContentBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder;
import java.io.IOException; import java.io.IOException;
/** /**
* A query that simply applies the boost factor to another query (multiply it). * 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; private Float boostFactor;
@ -43,15 +45,24 @@ public class FactorBuilder implements ScoreFunctionBuilder {
} }
@Override @Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { public void doXContent(XContentBuilder builder, Params params) throws IOException {
if (boostFactor != null) { if (boostFactor != null) {
builder.field("boost_factor", boostFactor.floatValue()); builder.field("boost_factor", boostFactor.floatValue());
} }
return builder;
} }
@Override @Override
public String getName() { public String getName() {
return FactorParser.NAMES[0]; 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
}
} }

View File

@ -33,6 +33,7 @@ import java.io.IOException;
/** /**
* *
*/ */
@Deprecated
public class FactorParser implements ScoreFunctionParser { public class FactorParser implements ScoreFunctionParser {
public static String[] NAMES = { "boost_factor", "boostFactor" }; public static String[] NAMES = { "boost_factor", "boostFactor" };

View File

@ -30,7 +30,7 @@ import java.util.Locale;
* Builder to construct {@code field_value_factor} functions for a function * Builder to construct {@code field_value_factor} functions for a function
* score query. * score query.
*/ */
public class FieldValueFactorFunctionBuilder implements ScoreFunctionBuilder { public class FieldValueFactorFunctionBuilder extends ScoreFunctionBuilder {
private String field = null; private String field = null;
private Float factor = null; private Float factor = null;
private FieldValueFactorFunction.Modifier modifier = null; private FieldValueFactorFunction.Modifier modifier = null;
@ -55,7 +55,7 @@ public class FieldValueFactorFunctionBuilder implements ScoreFunctionBuilder {
} }
@Override @Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { public void doXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(getName()); builder.startObject(getName());
if (field != null) { if (field != null) {
builder.field("field", field); builder.field("field", field);
@ -69,6 +69,5 @@ public class FieldValueFactorFunctionBuilder implements ScoreFunctionBuilder {
builder.field("modifier", modifier.toString().toLowerCase(Locale.ROOT)); builder.field("modifier", modifier.toString().toLowerCase(Locale.ROOT));
} }
builder.endObject(); builder.endObject();
return builder;
} }
} }

View File

@ -26,7 +26,7 @@ import java.io.IOException;
/** /**
* A function that computes a random score for the matched documents * 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; private Integer seed = null;
@ -50,12 +50,12 @@ public class RandomScoreFunctionBuilder implements ScoreFunctionBuilder {
} }
@Override @Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { public void doXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(getName()); builder.startObject(getName());
if (seed != null) { if (seed != null) {
builder.field("seed", seed.intValue()); builder.field("seed", seed.intValue());
} }
return builder.endObject(); builder.endObject();
} }
} }

View File

@ -31,7 +31,7 @@ import java.util.Map;
* A function that uses a script to compute or influence the score of documents * A function that uses a script to compute or influence the score of documents
* that match with the inner query or filter. * that match with the inner query or filter.
*/ */
public class ScriptScoreFunctionBuilder implements ScoreFunctionBuilder { public class ScriptScoreFunctionBuilder extends ScoreFunctionBuilder {
private String script; private String script;
@ -80,7 +80,7 @@ public class ScriptScoreFunctionBuilder implements ScoreFunctionBuilder {
} }
@Override @Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { public void doXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(getName()); builder.startObject(getName());
builder.field("script", script); builder.field("script", script);
if (lang != null) { if (lang != null) {
@ -89,7 +89,7 @@ public class ScriptScoreFunctionBuilder implements ScoreFunctionBuilder {
if (this.params != null) { if (this.params != null) {
builder.field("params", this.params); builder.field("params", this.params);
} }
return builder.endObject(); builder.endObject();
} }
@Override @Override

View File

@ -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 {
}
}

View File

@ -34,6 +34,7 @@ import org.apache.lucene.util.CharsRef;
import org.apache.lucene.util.NumericUtils; import org.apache.lucene.util.NumericUtils;
import org.apache.lucene.util.UnicodeUtil; import org.apache.lucene.util.UnicodeUtil;
import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchIllegalArgumentException;
import org.elasticsearch.action.get.MultiGetRequest; import org.elasticsearch.action.get.MultiGetRequest;
import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.compress.CompressedString; 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.*;
import org.elasticsearch.common.lucene.search.function.BoostScoreFunction; import org.elasticsearch.common.lucene.search.function.BoostScoreFunction;
import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery; 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.ImmutableSettings;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.DistanceUnit; import org.elasticsearch.common.unit.DistanceUnit;
@ -2318,6 +2320,92 @@ public class SimpleIndexQueryParserTests extends ElasticsearchSingleNodeTest {
assertThat(filter.getTerm().toString(), equalTo("text:apache")); 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 // https://github.com/elasticsearch/elasticsearch/issues/6722
public void testEmptyBoolSubClausesIsMatchAll() throws ElasticsearchException, IOException { public void testEmptyBoolSubClausesIsMatchAll() throws ElasticsearchException, IOException {
String query = copyToStringFromClasspath("/org/elasticsearch/index/query/bool-query-with-empty-clauses-for-parsing.json"); String query = copyToStringFromClasspath("/org/elasticsearch/index/query/bool-query-with-empty-clauses-for-parsing.json");

View File

@ -0,0 +1,9 @@
{
"function_score": {
"script_score": {
"script": "_index['text']['foo'].tf()"
},
"weight": 2
}
}

View File

@ -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<IndexRequestBuilder> 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);
}
}

View File

@ -19,8 +19,15 @@
package org.elasticsearch.search.functionscore; package org.elasticsearch.search.functionscore;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchType; 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.elasticsearch.test.ElasticsearchIntegrationTest;
import org.junit.Test; import org.junit.Test;
@ -28,17 +35,27 @@ import java.io.IOException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import static org.elasticsearch.client.Requests.searchRequest; 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.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.functionScoreQuery;
import static org.elasticsearch.index.query.QueryBuilders.termQuery; import static org.elasticsearch.index.query.QueryBuilders.termQuery;
import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.*; import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.*;
import static org.elasticsearch.search.builder.SearchSourceBuilder.searchSource; import static org.elasticsearch.search.builder.SearchSourceBuilder.searchSource;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
import static org.hamcrest.Matchers.equalTo; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.*;
public class FunctionScoreTests extends ElasticsearchIntegrationTest { 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 @Test
public void testExplainQueryOnlyOnce() throws IOException, ExecutionException, InterruptedException { public void testExplainQueryOnlyOnce() throws IOException, ExecutionException, InterruptedException {
assertAcked(prepareCreate("test").addMapping( assertAcked(prepareCreate("test").addMapping(
@ -87,4 +104,297 @@ public class FunctionScoreTests extends ElasticsearchIntegrationTest {
assertThat(queryExplanationIndex, equalTo(-1)); 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));
}
} }