From cb89a14010a7a9240459083ce2986b63b9ed3baf Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Mon, 27 Apr 2015 20:47:52 -0400 Subject: [PATCH] Add default to field_value_factor field_value_factor now takes a default that is used if the document doesn't have a value for that field. It looks like: "field_value_factor": { "field": "popularity", "missing": 1 } Closes #10841 --- .../queries/function-score-query.asciidoc | 5 +++- .../function/FieldValueFactorFunction.java | 30 +++++++++++++------ .../FieldValueFactorFunctionBuilder.java | 13 ++++++++ .../FieldValueFactorFunctionParser.java | 8 +++-- .../FunctionScoreFieldValueTests.java | 8 +++++ 5 files changed, 52 insertions(+), 12 deletions(-) diff --git a/docs/reference/query-dsl/queries/function-score-query.asciidoc b/docs/reference/query-dsl/queries/function-score-query.asciidoc index 32b7c0c386c..8b742bb088d 100644 --- a/docs/reference/query-dsl/queries/function-score-query.asciidoc +++ b/docs/reference/query-dsl/queries/function-score-query.asciidoc @@ -175,7 +175,8 @@ doing so would look like: "field_value_factor": { "field": "popularity", "factor": 1.2, - "modifier": "sqrt" + "modifier": "sqrt", + "missing": 1 } -------------------------------------------------- @@ -193,6 +194,8 @@ There are a number of options for the `field_value_factor` function: |`modifier` |Modifier to apply to the field value, can be one of: `none`, `log`, `log1p`, `log2p`, `ln`, `ln1p`, `ln2p`, `square`, `sqrt`, or `reciprocal`. Defaults to `none`. +|`missing` |Value used if the document doesn't have that field. The modifier +and factor are still applied to it as though it were read from the document. |======================================================================= Keep in mind that taking the log() of 0, or the square root of a negative number diff --git a/src/main/java/org/elasticsearch/common/lucene/search/function/FieldValueFactorFunction.java b/src/main/java/org/elasticsearch/common/lucene/search/function/FieldValueFactorFunction.java index 437e5a63b28..135cb53f65f 100644 --- a/src/main/java/org/elasticsearch/common/lucene/search/function/FieldValueFactorFunction.java +++ b/src/main/java/org/elasticsearch/common/lucene/search/function/FieldValueFactorFunction.java @@ -36,14 +36,20 @@ public class FieldValueFactorFunction extends ScoreFunction { private final String field; private final float boostFactor; private final Modifier modifier; + /** + * Value used if the document is missing the field. + */ + private final Double missing; private final IndexNumericFieldData indexFieldData; - public FieldValueFactorFunction(String field, float boostFactor, Modifier modifierType, IndexNumericFieldData indexFieldData) { + public FieldValueFactorFunction(String field, float boostFactor, Modifier modifierType, Double missing, + IndexNumericFieldData indexFieldData) { super(CombineFunction.MULT); this.field = field; this.boostFactor = boostFactor; this.modifier = modifierType; this.indexFieldData = indexFieldData; + this.missing = missing; } @Override @@ -55,26 +61,32 @@ public class FieldValueFactorFunction extends ScoreFunction { public double score(int docId, float subQueryScore) { values.setDocument(docId); final int numValues = values.count(); + double value; if (numValues > 0) { - double val = values.valueAt(0) * boostFactor; - double result = modifier.apply(val); - if (Double.isNaN(result) || Double.isInfinite(result)) { - throw new ElasticsearchException("Result of field modification [" + modifier.toString() + - "(" + val + ")] must be a number"); - } - return result; + value = values.valueAt(0); + } else if (missing != null) { + value = missing; } else { throw new ElasticsearchException("Missing value for field [" + field + "]"); } + double val = value * boostFactor; + double result = modifier.apply(val); + if (Double.isNaN(result) || Double.isInfinite(result)) { + throw new ElasticsearchException("Result of field modification [" + modifier.toString() + "(" + val + + ")] must be a number"); + } + return result; } @Override public Explanation explainScore(int docId, Explanation subQueryScore) { String modifierStr = modifier != null ? modifier.toString() : ""; + String defaultStr = missing != null ? "?:" + missing : ""; double score = score(docId, subQueryScore.getValue()); return Explanation.match( CombineFunction.toFloat(score), - "field value function: " + modifierStr + "(" + "doc['" + field + "'].value * factor=" + boostFactor + ")"); + String.format(Locale.ROOT, + "field value function: %s(doc['%s'].value%s * factor=%s)", modifierStr, field, defaultStr, boostFactor)); } }; } diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionBuilder.java b/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionBuilder.java index 34a2f8bbc67..5d38c5a5eb5 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionBuilder.java @@ -33,6 +33,7 @@ import java.util.Locale; public class FieldValueFactorFunctionBuilder extends ScoreFunctionBuilder { private String field = null; private Float factor = null; + private Double missing = null; private FieldValueFactorFunction.Modifier modifier = null; public FieldValueFactorFunctionBuilder(String fieldName) { @@ -49,6 +50,14 @@ public class FieldValueFactorFunctionBuilder extends ScoreFunctionBuilder { return this; } + /** + * Value used instead of the field value for documents that don't have that field defined. + */ + public FieldValueFactorFunctionBuilder missing(double missing) { + this.missing = missing; + return this; + } + public FieldValueFactorFunctionBuilder modifier(FieldValueFactorFunction.Modifier modifier) { this.modifier = modifier; return this; @@ -65,6 +74,10 @@ public class FieldValueFactorFunctionBuilder extends ScoreFunctionBuilder { builder.field("factor", factor); } + if (missing != null) { + builder.field("missing", missing); + } + if (modifier != null) { builder.field("modifier", modifier.toString().toLowerCase(Locale.ROOT)); } diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionParser.java b/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionParser.java index 3426dcbef3c..c5f454ef40a 100644 --- a/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionParser.java +++ b/src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionParser.java @@ -41,7 +41,8 @@ import java.util.Locale; * "field_value_factor": { * "field": "myfield", * "factor": 1.5, - * "modifier": "square" + * "modifier": "square", + * "missing": 1 * } * } * @@ -56,6 +57,7 @@ public class FieldValueFactorFunctionParser implements ScoreFunctionParser { String field = null; float boostFactor = 1; FieldValueFactorFunction.Modifier modifier = FieldValueFactorFunction.Modifier.NONE; + Double missing = null; XContentParser.Token token; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { @@ -67,6 +69,8 @@ public class FieldValueFactorFunctionParser implements ScoreFunctionParser { boostFactor = parser.floatValue(); } else if ("modifier".equals(currentFieldName)) { modifier = FieldValueFactorFunction.Modifier.valueOf(parser.text().toUpperCase(Locale.ROOT)); + } else if ("missing".equals(currentFieldName)) { + missing = parser.doubleValue(); } else { throw new QueryParsingException(parseContext.index(), NAMES[0] + " query does not support [" + currentFieldName + "]"); } @@ -84,7 +88,7 @@ public class FieldValueFactorFunctionParser implements ScoreFunctionParser { if (mapper == null) { throw new ElasticsearchException("Unable to find a field mapper for field [" + field + "]"); } - return new FieldValueFactorFunction(field, boostFactor, modifier, + return new FieldValueFactorFunction(field, boostFactor, modifier, missing, (IndexNumericFieldData)searchContext.fieldData().getForField(mapper)); } diff --git a/src/test/java/org/elasticsearch/search/functionscore/FunctionScoreFieldValueTests.java b/src/test/java/org/elasticsearch/search/functionscore/FunctionScoreFieldValueTests.java index 0d4d91b1f92..eef4ed27959 100644 --- a/src/test/java/org/elasticsearch/search/functionscore/FunctionScoreFieldValueTests.java +++ b/src/test/java/org/elasticsearch/search/functionscore/FunctionScoreFieldValueTests.java @@ -88,6 +88,14 @@ public class FunctionScoreFieldValueTests extends ElasticsearchIntegrationTest { // We are expecting an exception, because 3 has no field } + // doc 3 doesn't have a "test" field but we're defaulting it to 100 so it should be last + response = client().prepareSearch("test") + .setExplain(randomBoolean()) + .setQuery(functionScoreQuery(matchAllQuery(), + fieldValueFactorFunction("test").modifier(FieldValueFactorFunction.Modifier.RECIPROCAL).missing(100))) + .get(); + assertOrderedSearchHits(response, "1", "2", "3"); + // n divided by 0 is infinity, which should provoke an exception. try { response = client().prepareSearch("test")