Function Score Query: make parsing stricter

Function Score Query now checks the type of token that we are parsing, which makes parsing stricter and allows to throw useful errors in case the json is malformed. It also makes code more readable as in what gets parsed when.

Closes #16583
This commit is contained in:
javanna 2016-02-11 17:59:50 +01:00 committed by Luca Cavanna
parent 53c7c09972
commit a624410450
3 changed files with 323 additions and 228 deletions

View File

@ -19,10 +19,6 @@
package org.elasticsearch.index.query.functionscore;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.Strings;
@ -39,6 +35,10 @@ import org.elasticsearch.index.query.QueryParseContext;
import org.elasticsearch.index.query.QueryParser;
import org.elasticsearch.index.query.functionscore.weight.WeightBuilder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Parser for function_score query
*/
@ -86,27 +86,12 @@ public class FunctionScoreQueryParser implements QueryParser<FunctionScoreQueryB
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if ("query".equals(currentFieldName)) {
query = parseContext.parseInnerQueryBuilder();
} else if ("score_mode".equals(currentFieldName) || "scoreMode".equals(currentFieldName)) {
scoreMode = FiltersFunctionScoreQuery.ScoreMode.fromString(parser.text());
} else if ("boost_mode".equals(currentFieldName) || "boostMode".equals(currentFieldName)) {
combineFunction = CombineFunction.fromString(parser.text());
} else if ("max_boost".equals(currentFieldName) || "maxBoost".equals(currentFieldName)) {
maxBoost = parser.floatValue();
} else if ("boost".equals(currentFieldName)) {
boost = parser.floatValue();
} else if ("_name".equals(currentFieldName)) {
queryName = parser.text();
} else if ("min_score".equals(currentFieldName) || "minScore".equals(currentFieldName)) {
minScore = parser.floatValue();
} else if ("functions".equals(currentFieldName)) {
if (singleFunctionFound) {
String errorString = "already found [" + singleFunctionName + "], now encountering [functions].";
handleMisplacedFunctionsDeclaration(parser.getTokenLocation(), errorString);
} else if (token == XContentParser.Token.START_OBJECT) {
if ("query".equals(currentFieldName)) {
if (query != null) {
throw new ParsingException(parser.getTokenLocation(), "failed to parse [{}] query. [query] is already defined.", FunctionScoreQueryBuilder.NAME);
}
functionArrayFound = true;
currentFieldName = parseFiltersAndFunctions(parseContext, parser, filterFunctionBuilders);
query = parseContext.parseInnerQueryBuilder();
} else {
if (singleFunctionFound) {
throw new ParsingException(parser.getTokenLocation(), "failed to parse [{}] query. already found function [{}], now encountering [{}]. use [functions] array if you want to define several functions.", FunctionScoreQueryBuilder.NAME, singleFunctionName, currentFieldName);
@ -118,17 +103,53 @@ public class FunctionScoreQueryParser implements QueryParser<FunctionScoreQueryB
singleFunctionFound = true;
singleFunctionName = currentFieldName;
ScoreFunctionBuilder<?> scoreFunction;
if (parseContext.parseFieldMatcher().match(currentFieldName, WEIGHT_FIELD)) {
scoreFunction = new WeightBuilder().setWeight(parser.floatValue());
} else {
// we try to parse a score function. If there is no score
// function for the current field name,
// functionParserMapper.get() will throw an Exception.
scoreFunction = functionParserMapper.get(parser.getTokenLocation(), currentFieldName).fromXContent(parseContext, parser);
}
// we try to parse a score function. If there is no score function for the current field name,
// functionParserMapper.get() may throw an Exception.
ScoreFunctionBuilder<?> scoreFunction = functionParserMapper.get(parser.getTokenLocation(), currentFieldName).fromXContent(parseContext, parser);
filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(scoreFunction));
}
} else if (token == XContentParser.Token.START_ARRAY) {
if ("functions".equals(currentFieldName)) {
if (singleFunctionFound) {
String errorString = "already found [" + singleFunctionName + "], now encountering [functions].";
handleMisplacedFunctionsDeclaration(parser.getTokenLocation(), errorString);
}
functionArrayFound = true;
currentFieldName = parseFiltersAndFunctions(parseContext, parser, filterFunctionBuilders);
} else {
throw new ParsingException(parser.getTokenLocation(), "failed to parse [{}] query. array [{}] is not supported", FunctionScoreQueryBuilder.NAME, currentFieldName);
}
} else if (token.isValue()) {
if ("score_mode".equals(currentFieldName) || "scoreMode".equals(currentFieldName)) {
scoreMode = FiltersFunctionScoreQuery.ScoreMode.fromString(parser.text());
} else if ("boost_mode".equals(currentFieldName) || "boostMode".equals(currentFieldName)) {
combineFunction = CombineFunction.fromString(parser.text());
} else if ("max_boost".equals(currentFieldName) || "maxBoost".equals(currentFieldName)) {
maxBoost = parser.floatValue();
} else if ("boost".equals(currentFieldName)) {
boost = parser.floatValue();
} else if ("_name".equals(currentFieldName)) {
queryName = parser.text();
} else if ("min_score".equals(currentFieldName) || "minScore".equals(currentFieldName)) {
minScore = parser.floatValue();
} else {
if (singleFunctionFound) {
throw new ParsingException(parser.getTokenLocation(), "failed to parse [{}] query. already found function [{}], now encountering [{}]. use [functions] array if you want to define several functions.", FunctionScoreQueryBuilder.NAME, singleFunctionName, currentFieldName);
}
if (functionArrayFound) {
String errorString = "already found [functions] array, now encountering [" + currentFieldName + "].";
handleMisplacedFunctionsDeclaration(parser.getTokenLocation(), errorString);
}
if (parseContext.parseFieldMatcher().match(currentFieldName, WEIGHT_FIELD)) {
filterFunctionBuilders.add(new FunctionScoreQueryBuilder.FilterFunctionBuilder(new WeightBuilder().setWeight(parser.floatValue())));
singleFunctionFound = true;
singleFunctionName = currentFieldName;
} else {
throw new ParsingException(parser.getTokenLocation(), "failed to parse [{}] query. field [{}] is not supported", FunctionScoreQueryBuilder.NAME, currentFieldName);
}
}
}
}
if (query == null) {
@ -167,21 +188,23 @@ public class FunctionScoreQueryParser implements QueryParser<FunctionScoreQueryB
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (parseContext.parseFieldMatcher().match(currentFieldName, WEIGHT_FIELD)) {
functionWeight = parser.floatValue();
} else {
} else if (token == XContentParser.Token.START_OBJECT) {
if ("filter".equals(currentFieldName)) {
filter = parseContext.parseInnerQueryBuilder();
} else {
if (scoreFunction != null) {
throw new ParsingException(parser.getTokenLocation(), "failed to parse function_score functions. already found [{}], now encountering [{}].", scoreFunction.getName(), currentFieldName);
}
// do not need to check null here,
// functionParserMapper throws exception if parser
// non-existent
// do not need to check null here, functionParserMapper does it already
ScoreFunctionParser functionParser = functionParserMapper.get(parser.getTokenLocation(), currentFieldName);
scoreFunction = functionParser.fromXContent(parseContext, parser);
}
} else if (token.isValue()) {
if (parseContext.parseFieldMatcher().match(currentFieldName, WEIGHT_FIELD)) {
functionWeight = parser.floatValue();
} else {
throw new ParsingException(parser.getTokenLocation(), "failed to parse [{}] query. field [{}] is not supported", FunctionScoreQueryBuilder.NAME, currentFieldName);
}
}
}
if (functionWeight != null) {

View File

@ -20,7 +20,6 @@
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;
@ -59,7 +58,6 @@ import java.util.Map;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.index.query.QueryBuilders.functionScoreQuery;
import static org.elasticsearch.index.query.QueryBuilders.termQuery;
import static org.elasticsearch.test.StreamsUtils.copyToStringFromClasspath;
import static org.hamcrest.Matchers.closeTo;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.either;
@ -552,8 +550,22 @@ public class FunctionScoreQueryBuilderTests extends AbstractQueryTestCase<Functi
}
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" +
"}";
try {
parseQuery(copyToStringFromClasspath("/org/elasticsearch/index/query/faulty-function-score-query.json"));
parseQuery(json);
fail("Expected JsonParseException");
} catch (JsonParseException e) {
assertThat(e.getMessage(), containsString("Unexpected character ('{"));
@ -630,4 +642,79 @@ public class FunctionScoreQueryBuilderTests extends AbstractQueryTestCase<Functi
assertEquals(json, 100, parsed.maxBoost(), 0.00001);
assertEquals(json, 1, parsed.getMinScore(), 0.0001);
}
public void testQueryMalformedArrayNotSupported() throws IOException {
String json =
"{\n" +
" \"function_score\" : {\n" +
" \"not_supported\" : []\n" +
" }\n" +
"}";
try {
parseQuery(json);
fail("parse should have failed");
} catch (ParsingException e) {
assertThat(e.getMessage(), containsString("array [not_supported] is not supported"));
}
}
public void testQueryMalformedFieldNotSupported() throws IOException {
String json =
"{\n" +
" \"function_score\" : {\n" +
" \"not_supported\" : \"value\"\n" +
" }\n" +
"}";
try {
parseQuery(json);
fail("parse should have failed");
} catch (ParsingException e) {
assertThat(e.getMessage(), containsString("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" +
"}";
try {
parseQuery(json);
fail("parse should have failed");
} catch (ParsingException e) {
assertThat(e.getMessage(), containsString("field [not_supported] is not supported"));
}
}
public void testMalformedQuery() 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" +
"}";
try {
parseQuery(json);
fail("parse should have failed");
} catch(ParsingException e) {
assertThat(e.getMessage(), containsString("[query] is already defined."));
}
}
}

View File

@ -1,15 +0,0 @@
{
"function_score":{
"query":{
"term":{
"name.last":"banon"
}
},
"functions": {
{
"boost_factor" : 3
}
}
}
}
}