/*
* 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;
import com.fasterxml.jackson.core.JsonParseException;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.lucene.search.function.CombineFunction;
import org.elasticsearch.common.lucene.search.function.FieldValueFactorFunction;
import org.elasticsearch.common.lucene.search.function.FiltersFunctionScoreQuery;
import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
import org.elasticsearch.common.lucene.search.function.WeightFactorFunction;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContent;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.AbstractQueryBuilder;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryParseContext;
import org.elasticsearch.index.query.RandomQueryBuilder;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.index.query.WrapperQueryBuilder;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder.FilterFunctionBuilder;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.SearchPlugin;
import org.elasticsearch.script.MockScriptEngine;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.MultiValueMode;
import org.elasticsearch.search.internal.SearchContext;
import org.elasticsearch.test.AbstractQueryTestCase;
import org.hamcrest.Matcher;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.util.Collections.singletonList;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.index.query.QueryBuilders.functionScoreQuery;
import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
import static org.elasticsearch.index.query.QueryBuilders.termQuery;
import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.fieldValueFactorFunction;
import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.randomFunction;
import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.weightFactorFunction;
import static org.hamcrest.Matchers.closeTo;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.either;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.nullValue;
public class FunctionScoreQueryBuilderTests extends AbstractQueryTestCase<FunctionScoreQueryBuilder> {
private static final String[] SHUFFLE_PROTECTED_FIELDS = new String[] {Script.PARAMS_PARSE_FIELD.getPreferredName(),
ExponentialDecayFunctionBuilder.NAME, LinearDecayFunctionBuilder.NAME, GaussDecayFunctionBuilder.NAME};
@Override
protected Collection<Class<? extends Plugin>> getPlugins() {
return Collections.singleton(TestPlugin.class);
}
@Override
protected FunctionScoreQueryBuilder doCreateTestQueryBuilder() {
FunctionScoreQueryBuilder functionScoreQueryBuilder = createRandomFunctionScoreBuilder();
if (randomBoolean()) {
functionScoreQueryBuilder.boostMode(randomFrom(CombineFunction.values()));
}
if (randomBoolean()) {
functionScoreQueryBuilder.scoreMode(randomFrom(FiltersFunctionScoreQuery.ScoreMode.values()));
}
if (randomBoolean()) {
functionScoreQueryBuilder.maxBoost(randomFloat());
}
if (randomBoolean()) {
functionScoreQueryBuilder.setMinScore(randomFloat());
}
return functionScoreQueryBuilder;
}
@Override
protected String[] shuffleProtectedFields() {
// do not shuffle fields that may contain arbitrary content
return SHUFFLE_PROTECTED_FIELDS;
}
@Override
protected Set<String> getObjectsHoldingArbitraryContent() {
//script_score.script.params can contain arbitrary parameters. no error is expected when adding additional objects
//within the params object. Score functions get parsed in the data nodes, so they are not validated in the coord node.
return new HashSet<>(Arrays.asList(Script.PARAMS_PARSE_FIELD.getPreferredName(), ExponentialDecayFunctionBuilder.NAME,
LinearDecayFunctionBuilder.NAME, GaussDecayFunctionBuilder.NAME));
}
/**
* Creates a random function score query using only constructor params. The caller is responsible for randomizing fields set outside of
* the constructor.
*/
private static FunctionScoreQueryBuilder createRandomFunctionScoreBuilder() {
switch (randomIntBetween(0, 3)) {
case 0:
FilterFunctionBuilder[] functions = new FilterFunctionBuilder[randomIntBetween(0, 3)];
for (int i = 0; i < functions.length; i++) {
functions[i] = new FilterFunctionBuilder(RandomQueryBuilder.createQuery(random()), randomScoreFunction());
}
if (randomBoolean()) {
return new FunctionScoreQueryBuilder(RandomQueryBuilder.createQuery(random()), functions);
}
return new FunctionScoreQueryBuilder(functions);
case 1:
return new FunctionScoreQueryBuilder(randomScoreFunction());
case 2:
return new FunctionScoreQueryBuilder(RandomQueryBuilder.createQuery(random()), randomScoreFunction());
case 3:
return new FunctionScoreQueryBuilder(RandomQueryBuilder.createQuery(random()));
default:
throw new UnsupportedOperationException();
}
}
private static ScoreFunctionBuilder<?> randomScoreFunction() {
if (randomBoolean()) {
return new WeightBuilder().setWeight(randomFloat());
}
ScoreFunctionBuilder<?> functionBuilder;
switch (randomIntBetween(0, 3)) {
case 0:
DecayFunctionBuilder<?> decayFunctionBuilder = createRandomDecayFunction();
if (randomBoolean()) {
decayFunctionBuilder.setMultiValueMode(randomFrom(MultiValueMode.values()));
}
functionBuilder = decayFunctionBuilder;
break;
case 1:
FieldValueFactorFunctionBuilder fieldValueFactorFunctionBuilder = fieldValueFactorFunction(fieldValueFactorCompatibleField());
if (randomBoolean()) {
fieldValueFactorFunctionBuilder.factor(randomFloat());
}
if (randomBoolean()) {
fieldValueFactorFunctionBuilder.missing(randomDouble());
}
if (randomBoolean()) {
fieldValueFactorFunctionBuilder.modifier(randomFrom(FieldValueFactorFunction.Modifier.values()));
}
functionBuilder = fieldValueFactorFunctionBuilder;
break;
case 2:
String script = "1";
Map<String, Object> params = Collections.emptyMap();
functionBuilder = new ScriptScoreFunctionBuilder(
new Script(ScriptType.INLINE, MockScriptEngine.NAME, script, params));
break;
case 3:
RandomScoreFunctionBuilder randomScoreFunctionBuilder = new RandomScoreFunctionBuilderWithFixedSeed();
if (randomBoolean()) { // sometimes provide no seed
if (randomBoolean()) {
randomScoreFunctionBuilder.seed(randomLong());
} else if (randomBoolean()) {
randomScoreFunctionBuilder.seed(randomInt());
} else {
randomScoreFunctionBuilder.seed(randomAlphaOfLengthBetween(1, 10));
}
}
functionBuilder = randomScoreFunctionBuilder;
break;
default:
throw new UnsupportedOperationException();
}
if (randomBoolean()) {
functionBuilder.setWeight(randomFloat());
}
return functionBuilder;
}
/**
* A random field compatible with FieldValueFactor.
*/
private static String fieldValueFactorCompatibleField() {
return randomFrom(INT_FIELD_NAME, DOUBLE_FIELD_NAME, DATE_FIELD_NAME);
}
/**
* Create a random decay function setting all of its constructor parameters randomly. The caller is responsible for randomizing other
* fields.
*/
private static DecayFunctionBuilder<?> createRandomDecayFunction() {
String field = randomFrom(INT_FIELD_NAME, DOUBLE_FIELD_NAME, DATE_FIELD_NAME, GEO_POINT_FIELD_NAME);
Object origin;
Object scale;
Object offset;
switch (field) {
case GEO_POINT_FIELD_NAME:
origin = new GeoPoint(randomDouble(), randomDouble()).geohash();
scale = randomFrom(DistanceUnit.values()).toString(randomDouble());
offset = randomFrom(DistanceUnit.values()).toString(randomDouble());
break;
case DATE_FIELD_NAME:
origin = new DateTime(System.currentTimeMillis() - randomIntBetween(0, 1000000), DateTimeZone.UTC).toString();
scale = randomTimeValue(1, 1000, new String[]{"d", "h", "ms", "s", "m"});
offset = randomPositiveTimeValue();
break;
default:
origin = randomBoolean() ? randomInt() : randomFloat();
scale = randomBoolean() ? between(1, Integer.MAX_VALUE) : randomFloat() + Float.MIN_NORMAL;
offset = randomBoolean() ? between(1, Integer.MAX_VALUE) : randomFloat() + Float.MIN_NORMAL;
break;
}
offset = randomBoolean() ? null : offset;
double decay = randomDouble();
switch (randomIntBetween(0, 2)) {
case 0:
return new GaussDecayFunctionBuilder(field, origin, scale, offset, decay);
case 1:
return new ExponentialDecayFunctionBuilder(field, origin, scale, offset, decay);
case 2:
return new LinearDecayFunctionBuilder(field, origin, scale, offset, decay);
default:
throw new UnsupportedOperationException();
}
}
@Override
protected void doAssertLuceneQuery(FunctionScoreQueryBuilder queryBuilder, Query query, SearchContext context) throws IOException {
assertThat(query, either(instanceOf(FunctionScoreQuery.class)).or(instanceOf(FiltersFunctionScoreQuery.class)));
}
/**
* Overridden here to ensure the test is only run if at least one type is
* present in the mappings. Functions require the field to be
* explicitly mapped
*/
@Override
public void testToQuery() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
super.testToQuery();
}
public void testIllegalArguments() {
expectThrows(IllegalArgumentException.class, () -> new FunctionScoreQueryBuilder((QueryBuilder) null));
expectThrows(IllegalArgumentException.class, () -> new FunctionScoreQueryBuilder((ScoreFunctionBuilder<?>) null));
expectThrows(IllegalArgumentException.class, () -> new FunctionScoreQueryBuilder((FilterFunctionBuilder[]) null));
expectThrows(IllegalArgumentException.class, () -> new FunctionScoreQueryBuilder(null, randomFunction(123)));
expectThrows(IllegalArgumentException.class, () -> new FunctionScoreQueryBuilder(matchAllQuery(), (ScoreFunctionBuilder<?>) null));
expectThrows(IllegalArgumentException.class, () -> new FunctionScoreQueryBuilder(matchAllQuery(), (FilterFunctionBuilder[]) null));
expectThrows(IllegalArgumentException.class, () -> new FunctionScoreQueryBuilder(null, new FilterFunctionBuilder[0]));
expectThrows(IllegalArgumentException.class,
() -> new FunctionScoreQueryBuilder(matchAllQuery(), new FilterFunctionBuilder[] { null }));
expectThrows(IllegalArgumentException.class, () -> new FilterFunctionBuilder((ScoreFunctionBuilder<?>) null));
expectThrows(IllegalArgumentException.class, () -> new FilterFunctionBuilder(null, randomFunction(123)));
expectThrows(IllegalArgumentException.class, () -> new FilterFunctionBuilder(matchAllQuery(), null));
FunctionScoreQueryBuilder builder = new FunctionScoreQueryBuilder(matchAllQuery());
expectThrows(IllegalArgumentException.class, () -> builder.scoreMode(null));
expectThrows(IllegalArgumentException.class, () -> builder.boostMode(null));
}
public void testParseFunctionsArray() throws IOException {
String functionScoreQuery = "{\n" +
" \"function_score\":{\n" +
" \"query\":{\n" +
" \"term\":{\n" +
" \"field1\":\"value1\"\n" +
" }\n" +
" },\n" +
" \"functions\": [\n" +
" {\n" +
" \"random_score\": {\n" +
" \"seed\":123456\n" +
" },\n" +
" \"weight\": 3,\n" +
" \"filter\": {\n" +
" \"term\":{\n" +
" \"field2\":\"value2\"\n" +
" }\n" +
" }\n" +
" },\n" +
" {\n" +
" \"filter\": {\n" +
" \"term\":{\n" +
" \"field3\":\"value3\"\n" +
" }\n" +
" },\n" +
" \"weight\": 9\n" +
" },\n" +
" {\n" +
" \"gauss\": {\n" +
" \"field_name\": {\n" +
" \"origin\":0.5,\n" +
" \"scale\":0.6\n" +
" }\n" +
" }\n" +
" }\n" +
" ],\n" +
" \"boost\" : 3,\n" +
" \"score_mode\" : \"avg\",\n" +
" \"boost_mode\" : \"replace\",\n" +
" \"max_boost\" : 10\n" +
" }\n" +
"}";
QueryBuilder queryBuilder = parseQuery(functionScoreQuery);
/*
* given that we copy part of the decay functions as bytes, we test that fromXContent and toXContent both work no matter what the
* initial format was
*/
for (int i = 0; i <= XContentType.values().length; i++) {
assertThat(queryBuilder, instanceOf(FunctionScoreQueryBuilder.class));
FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) queryBuilder;
assertThat(functionScoreQueryBuilder.query(), instanceOf(TermQueryBuilder.class));
TermQueryBuilder termQueryBuilder = (TermQueryBuilder) functionScoreQueryBuilder.query();
assertThat(termQueryBuilder.fieldName(), equalTo("field1"));
assertThat(termQueryBuilder.value(), equalTo("value1"));
assertThat(functionScoreQueryBuilder.filterFunctionBuilders().length, equalTo(3));
assertThat(functionScoreQueryBuilder.filterFunctionBuilders()[0].getFilter(), instanceOf(TermQueryBuilder.class));
termQueryBuilder = (TermQueryBuilder) functionScoreQueryBuilder.filterFunctionBuilders()[0].getFilter();
assertThat(termQueryBuilder.fieldName(), equalTo("field2"));
assertThat(termQueryBuilder.value(), equalTo("value2"));
assertThat(functionScoreQueryBuilder.filterFunctionBuilders()[1].getFilter(), instanceOf(TermQueryBuilder.class));
termQueryBuilder = (TermQueryBuilder) functionScoreQueryBuilder.filterFunctionBuilders()[1].getFilter();
assertThat(termQueryBuilder.fieldName(), equalTo("field3"));
assertThat(termQueryBuilder.value(), equalTo("value3"));
assertThat(functionScoreQueryBuilder.filterFunctionBuilders()[2].getFilter(), instanceOf(MatchAllQueryBuilder.class));
assertThat(functionScoreQueryBuilder.filterFunctionBuilders()[0].getScoreFunction(),
instanceOf(RandomScoreFunctionBuilder.class));
RandomScoreFunctionBuilder randomScoreFunctionBuilder = (RandomScoreFunctionBuilder) functionScoreQueryBuilder
.filterFunctionBuilders()[0].getScoreFunction();
assertThat(randomScoreFunctionBuilder.getSeed(), equalTo(123456));
assertThat(randomScoreFunctionBuilder.getWeight(), equalTo(3f));
assertThat(functionScoreQueryBuilder.filterFunctionBuilders()[1].getScoreFunction(), instanceOf(WeightBuilder.class));
WeightBuilder weightBuilder = (WeightBuilder) functionScoreQueryBuilder.filterFunctionBuilders()[1].getScoreFunction();
assertThat(weightBuilder.getWeight(), equalTo(9f));
assertThat(functionScoreQueryBuilder.filterFunctionBuilders()[2].getScoreFunction(),
instanceOf(GaussDecayFunctionBuilder.class));
GaussDecayFunctionBuilder gaussDecayFunctionBuilder = (GaussDecayFunctionBuilder) functionScoreQueryBuilder
.filterFunctionBuilders()[2].getScoreFunction();
assertThat(gaussDecayFunctionBuilder.getFieldName(), equalTo("field_name"));
assertThat(functionScoreQueryBuilder.boost(), equalTo(3f));
assertThat(functionScoreQueryBuilder.scoreMode(), equalTo(FiltersFunctionScoreQuery.ScoreMode.AVG));
assertThat(functionScoreQueryBuilder.boostMode(), equalTo(CombineFunction.REPLACE));
assertThat(functionScoreQueryBuilder.maxBoost(), equalTo(10f));
if (i < XContentType.values().length) {
BytesReference bytes = ((AbstractQueryBuilder) queryBuilder).buildAsBytes(XContentType.values()[i]);
try (XContentParser parser = createParser(XContentType.values()[i].xContent(), bytes)) {
queryBuilder = parseQuery(parser);
}
}
}
}
public void testParseSingleFunction() throws IOException {
String functionScoreQuery = "{\n" +
" \"function_score\":{\n" +
" \"query\":{\n" +
" \"term\":{\n" +
" \"field1\":\"value1\"\n" +
" }\n" +
" },\n" +
" \"gauss\": {\n" +
" \"field_name\": {\n" +
" \"origin\":0.5,\n" +
" \"scale\":0.6\n" +
" }\n" +
" },\n" +
" \"boost\" : 3,\n" +
" \"score_mode\" : \"avg\",\n" +
" \"boost_mode\" : \"replace\",\n" +
" \"max_boost\" : 10\n" +
" }\n" +
"}";
QueryBuilder queryBuilder = parseQuery(functionScoreQuery);
/*
* given that we copy part of the decay functions as bytes, we test that fromXContent and toXContent both work no matter what the
* initial format was
*/
for (int i = 0; i <= XContentType.values().length; i++) {
assertThat(queryBuilder, instanceOf(FunctionScoreQueryBuilder.class));
FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) queryBuilder;
assertThat(functionScoreQueryBuilder.query(), instanceOf(TermQueryBuilder.class));
TermQueryBuilder termQueryBuilder = (TermQueryBuilder) functionScoreQueryBuilder.query();
assertThat(termQueryBuilder.fieldName(), equalTo("field1"));
assertThat(termQueryBuilder.value(), equalTo("value1"));
assertThat(functionScoreQueryBuilder.filterFunctionBuilders().length, equalTo(1));
assertThat(functionScoreQueryBuilder.filterFunctionBuilders()[0].getFilter(), instanceOf(MatchAllQueryBuilder.class));
assertThat(functionScoreQueryBuilder.filterFunctionBuilders()[0].getScoreFunction(),
instanceOf(GaussDecayFunctionBuilder.class));
GaussDecayFunctionBuilder gaussDecayFunctionBuilder = (GaussDecayFunctionBuilder) functionScoreQueryBuilder
.filterFunctionBuilders()[0].getScoreFunction();
assertThat(gaussDecayFunctionBuilder.getFieldName(), equalTo("field_name"));
assertThat(gaussDecayFunctionBuilder.getWeight(), nullValue());
assertThat(functionScoreQueryBuilder.boost(), equalTo(3f));
assertThat(functionScoreQueryBuilder.scoreMode(), equalTo(FiltersFunctionScoreQuery.ScoreMode.AVG));
assertThat(functionScoreQueryBuilder.boostMode(), equalTo(CombineFunction.REPLACE));
assertThat(functionScoreQueryBuilder.maxBoost(), equalTo(10f));
if (i < XContentType.values().length) {
BytesReference bytes = ((AbstractQueryBuilder) queryBuilder).buildAsBytes(XContentType.values()[i]);
try (XContentParser parser = createParser(XContentType.values()[i].xContent(), bytes)) {
queryBuilder = parseQuery(parser);
}
}
}
}
public void testProperErrorMessageWhenTwoFunctionsDefinedInQueryBody() throws IOException {
//without a functions array, we support only a single function, weight can't be associated with the function either.
String functionScoreQuery = "{\n" +
" \"function_score\": {\n" +
" \"script_score\": {\n" +
" \"script\": \"5\"\n" +
" },\n" +
" \"weight\": 2\n" +
" }\n" +
"}";
ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(functionScoreQuery));
assertThat(e.getMessage(), containsString("use [functions] array if you want to define several functions."));
}
public void testProperErrorMessageWhenTwoFunctionsDefinedInFunctionsArray() throws IOException {
String functionScoreQuery = "{\n" +
" \"function_score\":{\n" +
" \"functions\": [\n" +
" {\n" +
" \"random_score\": {\n" +
" \"seed\":123456\n" +
" },\n" +
" \"weight\": 3,\n" +
" \"script_score\": {\n" +
" \"script\": \"_index['text']['foo'].tf()\"\n" +
" },\n" +
" \"filter\": {\n" +
" \"term\":{\n" +
" \"field2\":\"value2\"\n" +
" }\n" +
" }\n" +
" }\n" +
" ]\n" +
" }\n" +
"}";
ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(functionScoreQuery));
assertThat(e.getMessage(),
containsString("failed to parse function_score functions. already found [random_score], now encountering [script_score]."));
}
public void testProperErrorMessageWhenMissingFunction() throws IOException {
String functionScoreQuery = "{\n" +
" \"function_score\":{\n" +
" \"functions\": [\n" +
" {\n" +
" \"filter\": {\n" +
" \"term\":{\n" +
" \"field2\":\"value2\"\n" +
" }\n" +
" }\n" +
" }\n" +
" ]\n" +
" }\n" +
"}";
ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(functionScoreQuery));
assertThat(e.getMessage(), containsString("an entry in functions list is missing a function."));
}
public void testWeight1fStillProducesWeightFunction() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
String queryString = jsonBuilder().startObject()
.startObject("function_score")
.startArray("functions")
.startObject()
.startObject("field_value_factor")
.field("field", INT_FIELD_NAME)
.endObject()
.field("weight", 1.0)
.endObject()
.endArray()
.endObject()
.endObject().string();
QueryBuilder query = parseQuery(queryString);
assertThat(query, instanceOf(FunctionScoreQueryBuilder.class));
FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) query;
assertThat(functionScoreQueryBuilder.filterFunctionBuilders()[0].getScoreFunction(),
instanceOf(FieldValueFactorFunctionBuilder.class));
FieldValueFactorFunctionBuilder fieldValueFactorFunctionBuilder = (FieldValueFactorFunctionBuilder) functionScoreQueryBuilder
.filterFunctionBuilders()[0].getScoreFunction();
assertThat(fieldValueFactorFunctionBuilder.fieldName(), equalTo(INT_FIELD_NAME));
assertThat(fieldValueFactorFunctionBuilder.factor(), equalTo(FieldValueFactorFunctionBuilder.DEFAULT_FACTOR));
assertThat(fieldValueFactorFunctionBuilder.modifier(), equalTo(FieldValueFactorFunctionBuilder.DEFAULT_MODIFIER));
assertThat(fieldValueFactorFunctionBuilder.getWeight(), equalTo(1f));
assertThat(fieldValueFactorFunctionBuilder.missing(), nullValue());
Query luceneQuery = query.toQuery(createShardContext());
assertThat(luceneQuery, instanceOf(FunctionScoreQuery.class));
FunctionScoreQuery functionScoreQuery = (FunctionScoreQuery) luceneQuery;
assertThat(functionScoreQuery.getFunction(), instanceOf(WeightFactorFunction.class));
WeightFactorFunction weightFactorFunction = (WeightFactorFunction) functionScoreQuery.getFunction();
assertThat(weightFactorFunction.getWeight(), equalTo(1.0f));
assertThat(weightFactorFunction.getScoreFunction(), instanceOf(FieldValueFactorFunction.class));
}
public void testProperErrorMessagesForMisplacedWeightsAndFunctions() throws IOException {
String query = jsonBuilder().startObject().startObject("function_score")
.startArray("functions")
.startObject().startObject("script_score").field("script", "3").endObject().endObject()
.endArray()
.field("weight", 2)
.endObject().endObject().string();
expectParsingException(query, "[you can either define [functions] array or a single function, not both. already "
+ "found [functions] array, now encountering [weight].]");
query = jsonBuilder().startObject().startObject("function_score")
.field("weight", 2)
.startArray("functions")
.startObject().endObject()
.endArray()
.endObject().endObject().string();
expectParsingException(query, "[you can either define [functions] array or a single function, not both. already found "
+ "[weight], now encountering [functions].]");
}
public void testMalformedThrowsException() throws IOException {
String json = "{\n" +
" \"function_score\":{\n" +
" \"query\":{\n" +
" \"term\":{\n" +
" \"name.last\":\"banon\"\n" +
" }\n" +
" },\n" +
" \"functions\": [\n" +
" {\n" +
" {\n" +
" }\n" +
" ]\n" +
" }\n" +
"}";
JsonParseException e = expectThrows(JsonParseException.class, () -> parseQuery(json));
assertThat(e.getMessage(), containsString("Unexpected character ('{"));
}
public void testCustomWeightFactorQueryBuilderWithFunctionScore() throws IOException {
Query parsedQuery = parseQuery(functionScoreQuery(termQuery("name.last", "banon"), weightFactorFunction(1.3f)))
.toQuery(createShardContext());
assertThat(parsedQuery, instanceOf(FunctionScoreQuery.class));
FunctionScoreQuery functionScoreQuery = (FunctionScoreQuery) parsedQuery;
assertThat(((TermQuery) functionScoreQuery.getSubQuery()).getTerm(), equalTo(new Term("name.last", "banon")));
assertThat((double) ((WeightFactorFunction) functionScoreQuery.getFunction()).getWeight(), closeTo(1.3, 0.001));
}
public void testCustomWeightFactorQueryBuilderWithFunctionScoreWithoutQueryGiven() throws IOException {
Query parsedQuery = parseQuery(functionScoreQuery(weightFactorFunction(1.3f))).toQuery(createShardContext());
assertThat(parsedQuery, instanceOf(FunctionScoreQuery.class));
FunctionScoreQuery functionScoreQuery = (FunctionScoreQuery) parsedQuery;
assertThat(functionScoreQuery.getSubQuery() instanceof MatchAllDocsQuery, equalTo(true));
assertThat((double) ((WeightFactorFunction) functionScoreQuery.getFunction()).getWeight(), closeTo(1.3, 0.001));
}
public void testFieldValueFactorFactorArray() throws IOException {
// don't permit an array of factors
String querySource = "{" +
" \"function_score\": {" +
" \"query\": {" +
" \"match\": {\"name\": \"foo\"}" +
" }," +
" \"functions\": [" +
" {" +
" \"field_value_factor\": {" +
" \"field\": \"test\"," +
" \"factor\": [1.2,2]" +
" }" +
" }" +
" ]" +
" }" +
"}";
expectParsingException(querySource, containsString("[field_value_factor] field 'factor' does not support lists or objects"));
}
public void testFromJson() throws IOException {
String json =
"{\n" +
" \"function_score\" : {\n" +
" \"query\" : { \"match_all\" : {} },\n" +
" \"functions\" : [ {\n" +
" \"filter\" : { \"match_all\" : {}},\n" +
" \"weight\" : 23.0,\n" +
" \"random_score\" : { }\n" +
" }, {\n" +
" \"filter\" : { \"match_all\" : {}},\n" +
" \"weight\" : 5.0\n" +
" } ],\n" +
" \"score_mode\" : \"multiply\",\n" +
" \"boost_mode\" : \"multiply\",\n" +
" \"max_boost\" : 100.0,\n" +
" \"min_score\" : 1.0,\n" +
" \"boost\" : 42.0\n" +
" }\n" +
"}";
FunctionScoreQueryBuilder parsed = (FunctionScoreQueryBuilder) parseQuery(json);
// this should be equivalent to the same with a match_all query
String expected =
"{\n" +
" \"function_score\" : {\n" +
" \"query\" : { \"match_all\" : {} },\n" +
" \"functions\" : [ {\n" +
" \"filter\" : { \"match_all\" : {}},\n" +
" \"weight\" : 23.0,\n" +
" \"random_score\" : { }\n" +
" }, {\n" +
" \"filter\" : { \"match_all\" : {}},\n" +
" \"weight\" : 5.0\n" +
" } ],\n" +
" \"score_mode\" : \"multiply\",\n" +
" \"boost_mode\" : \"multiply\",\n" +
" \"max_boost\" : 100.0,\n" +
" \"min_score\" : 1.0,\n" +
" \"boost\" : 42.0\n" +
" }\n" +
"}";
FunctionScoreQueryBuilder expectedParsed = (FunctionScoreQueryBuilder) parseQuery(json);
assertEquals(expectedParsed, parsed);
assertEquals(json, 2, parsed.filterFunctionBuilders().length);
assertEquals(json, 42, parsed.boost(), 0.0001);
assertEquals(json, 100, parsed.maxBoost(), 0.00001);
assertEquals(json, 1, parsed.getMinScore(), 0.0001);
}
@Override
public void testMustRewrite() throws IOException {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
super.testMustRewrite();
}
public void testRewrite() throws IOException {
FunctionScoreQueryBuilder functionScoreQueryBuilder =
new FunctionScoreQueryBuilder(new WrapperQueryBuilder(new TermQueryBuilder("foo", "bar").toString()))
.boostMode(CombineFunction.REPLACE)
.scoreMode(FiltersFunctionScoreQuery.ScoreMode.SUM)
.setMinScore(1)
.maxBoost(100);
FunctionScoreQueryBuilder rewrite = (FunctionScoreQueryBuilder) functionScoreQueryBuilder.rewrite(createShardContext());
assertNotSame(functionScoreQueryBuilder, rewrite);
assertEquals(rewrite.query(), new TermQueryBuilder("foo", "bar"));
assertEquals(rewrite.boostMode(), CombineFunction.REPLACE);
assertEquals(rewrite.scoreMode(), FiltersFunctionScoreQuery.ScoreMode.SUM);
assertEquals(rewrite.getMinScore(), 1f, 0.0001);
assertEquals(rewrite.maxBoost(), 100f, 0.0001);
}
public void testRewriteWithFunction() throws IOException {
QueryBuilder firstFunction = new WrapperQueryBuilder(new TermQueryBuilder("tq", "1").toString());
TermQueryBuilder secondFunction = new TermQueryBuilder("tq", "2");
QueryBuilder queryBuilder = randomBoolean() ? new WrapperQueryBuilder(new TermQueryBuilder("foo", "bar").toString())
: new TermQueryBuilder("foo", "bar");
FunctionScoreQueryBuilder functionScoreQueryBuilder = new FunctionScoreQueryBuilder(queryBuilder,
new FunctionScoreQueryBuilder.FilterFunctionBuilder[] {
new FunctionScoreQueryBuilder.FilterFunctionBuilder(firstFunction, new RandomScoreFunctionBuilder()),
new FunctionScoreQueryBuilder.FilterFunctionBuilder(secondFunction, new RandomScoreFunctionBuilder()) });
FunctionScoreQueryBuilder rewrite = (FunctionScoreQueryBuilder) functionScoreQueryBuilder.rewrite(createShardContext());
assertNotSame(functionScoreQueryBuilder, rewrite);
assertEquals(rewrite.query(), new TermQueryBuilder("foo", "bar"));
assertEquals(rewrite.filterFunctionBuilders()[0].getFilter(), new TermQueryBuilder("tq", "1"));
assertSame(rewrite.filterFunctionBuilders()[1].getFilter(), secondFunction);
}
public void testQueryMalformedArrayNotSupported() throws IOException {
String json =
"{\n" +
" \"function_score\" : {\n" +
" \"not_supported\" : []\n" +
" }\n" +
"}";
expectParsingException(json, "array [not_supported] is not supported");
}
public void testQueryMalformedFieldNotSupported() throws IOException {
String json =
"{\n" +
" \"function_score\" : {\n" +
" \"not_supported\" : \"value\"\n" +
" }\n" +
"}";
expectParsingException(json, "field [not_supported] is not supported");
}
public void testMalformedQueryFunctionFieldNotSupported() throws IOException {
String json =
"{\n" +
" \"function_score\" : {\n" +
" \"functions\" : [ {\n" +
" \"not_supported\" : 23.0\n" +
" }\n" +
" }\n" +
"}";
expectParsingException(json, "field [not_supported] is not supported");
}
public void testMalformedQueryMultipleQueryObjects() throws IOException {
//verify that an error is thrown rather than setting the query twice (https://github.com/elastic/elasticsearch/issues/16583)
String json = "{\n" +
" \"function_score\":{\n" +
" \"query\":{\n" +
" \"bool\":{\n" +
" \"must\":{\"match\":{\"field\":\"value\"}}" +
" },\n" +
" \"ignored_field_name\": {\n" +
" {\"match\":{\"field\":\"value\"}}\n" +
" }\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
expectParsingException(json, equalTo("[bool] malformed query, expected [END_OBJECT] but found [FIELD_NAME]"));
}
public void testMalformedQueryMultipleQueryElements() throws IOException {
assumeFalse("Test only makes sense if XContent parser doesn't have strict duplicate checks enabled",
XContent.isStrictDuplicateDetectionEnabled());
String json = "{\n" +
" \"function_score\":{\n" +
" \"query\":{\n" +
" \"bool\":{\n" +
" \"must\":{\"match\":{\"field\":\"value\"}}" +
" }\n" +
" },\n" +
" \"query\":{\n" +
" \"bool\":{\n" +
" \"must\":{\"match\":{\"field\":\"value\"}}" +
" }\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
expectParsingException(json, "[query] is already defined.");
}
private void expectParsingException(String json, Matcher<String> messageMatcher) {
ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(json));
assertThat(e.getMessage(), messageMatcher);
}
private void expectParsingException(String json, String message) {
expectParsingException(json, equalTo("failed to parse [function_score] query. " + message));
}
/**
* A hack on top of the normal random score function that fixed toQuery to work properly in this unit testing environment.
*/
static class RandomScoreFunctionBuilderWithFixedSeed extends RandomScoreFunctionBuilder {
public static final String NAME = "random_with_fixed_seed";
RandomScoreFunctionBuilderWithFixedSeed() {
}
/**
* Read from a stream.
*/
RandomScoreFunctionBuilderWithFixedSeed(StreamInput in) throws IOException {
super(in);
}
@Override
public String getName() {
return NAME;
}
public static RandomScoreFunctionBuilder fromXContent(QueryParseContext parseContext)
throws IOException, ParsingException {
RandomScoreFunctionBuilder builder = RandomScoreFunctionBuilder.fromXContent(parseContext);
RandomScoreFunctionBuilderWithFixedSeed replacement = new RandomScoreFunctionBuilderWithFixedSeed();
if (builder.getSeed() != null) {
replacement.seed(builder.getSeed());
}
return replacement;
}
}
public static class TestPlugin extends Plugin implements SearchPlugin {
@Override
public List<ScoreFunctionSpec<?>> getScoreFunctions() {
return singletonList(new ScoreFunctionSpec<>(RandomScoreFunctionBuilderWithFixedSeed.NAME,
RandomScoreFunctionBuilderWithFixedSeed::new, RandomScoreFunctionBuilderWithFixedSeed::fromXContent));
}
}
@Override
protected boolean isCachable(FunctionScoreQueryBuilder queryBuilder) {
FilterFunctionBuilder[] filterFunctionBuilders = queryBuilder.filterFunctionBuilders();
for (FilterFunctionBuilder builder : filterFunctionBuilders) {
if (builder.getScoreFunction() instanceof ScriptScoreFunctionBuilder) {
return false;
} else if (builder.getScoreFunction() instanceof RandomScoreFunctionBuilder
&& ((RandomScoreFunctionBuilder) builder.getScoreFunction()).getSeed() == null) {
return false;
}
}
return true;
}
}