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
`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": {...}
},
{

View File

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

View File

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

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.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) {

View File

@ -45,7 +45,7 @@ public class FunctionScoreQueryBuilder extends BaseQueryBuilder implements Boost
private Float maxBoost;
private String scoreMode;
private String boostMode;
private ArrayList<FilterBuilder> 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);
}

View File

@ -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<String, CombineFunction> 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<FiltersFunctionScoreQuery.FilterFunction> filterFunctions, String currentFieldName) throws IOException {
ArrayList<FiltersFunctionScoreQuery.FilterFunction> 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;

View File

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

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.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);

View File

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

View File

@ -33,6 +33,7 @@ import java.io.IOException;
/**
*
*/
@Deprecated
public class FactorParser implements ScoreFunctionParser {
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
* 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;
}
}

View File

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

View File

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

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.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");

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