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:
parent
9750375412
commit
c5ff70bf43
|
@ -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": {...}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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" };
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"function_score": {
|
||||||
|
"script_score": {
|
||||||
|
"script": "_index['text']['foo'].tf()"
|
||||||
|
},
|
||||||
|
"weight": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue