Переглянути джерело

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
Britta Weber 11 роки тому
батько
коміт
c5ff70bf43
19 змінених файлів з 806 додано та 89 видалено
  1. 25 9
      docs/reference/query-dsl/queries/function-score-query.asciidoc
  2. 5 20
      src/main/java/org/elasticsearch/common/lucene/search/function/BoostScoreFunction.java
  3. 1 2
      src/main/java/org/elasticsearch/common/lucene/search/function/ScoreFunction.java
  4. 104 0
      src/main/java/org/elasticsearch/common/lucene/search/function/WeightFactorFunction.java
  5. 2 3
      src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionBuilder.java
  6. 14 22
      src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java
  7. 30 13
      src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryParser.java
  8. 26 2
      src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilder.java
  9. 6 0
      src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilders.java
  10. 18 7
      src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorBuilder.java
  11. 1 0
      src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorParser.java
  12. 2 3
      src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionBuilder.java
  13. 3 3
      src/main/java/org/elasticsearch/index/query/functionscore/random/RandomScoreFunctionBuilder.java
  14. 3 3
      src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionBuilder.java
  15. 42 0
      src/main/java/org/elasticsearch/index/query/functionscore/weight/WeightBuilder.java
  16. 88 0
      src/test/java/org/elasticsearch/index/query/SimpleIndexQueryParserTests.java
  17. 9 0
      src/test/java/org/elasticsearch/index/query/function-score-query-causing-NPE.json
  18. 115 0
      src/test/java/org/elasticsearch/search/functionscore/FunctionScoreBackwardCompatibilityTests.java
  19. 312 2
      src/test/java/org/elasticsearch/search/functionscore/FunctionScoreTests.java

+ 25 - 9
docs/reference/query-dsl/queries/function-score-query.asciidoc

@@ -9,8 +9,7 @@ the score on a filtered set of documents.
 `function_score` provides the same functionality that
 `custom_boost_factor`, `custom_score` and
 `custom_filters_score` provided
-but furthermore adds futher scoring functionality such as
-distance and recency scoring (see description below).
+but with additional capabilities such as distance and recency scoring (see description below).
 
 ==== Using function score
 
@@ -42,10 +41,15 @@ given filter:
     "functions": [
         {
             "filter": {},
-            "FUNCTION": {}
+            "FUNCTION": {},
+            "weight": number
         },
         {
             "FUNCTION": {}
+        },
+        {
+            "filter": {},
+            "weight": number
         }
     ],
     "max_boost": number,
@@ -69,6 +73,9 @@ First, each document is scored by the defined functions. The parameter
 `max`::         maximum score is used
 `min`::         minimum score is used
 
+Because scores can be on different scales (for example, between 0 and 1 for decay functions but arbitrary for `field_value_factor`) and also because sometimes a different impact of functions on the score is desirable, the score of each function can be adjusted with a user defined `weight` (coming[1.4.0]). The `weight` can be defined per function in the `functions` array (example above) and is multiplied with the score computed by the respective function.
+If weight is given without any other function declaration, `weight` acts as a function that simply returns the `weight`.
+
 The new score can be restricted to not exceed a certain limit by setting
 the `max_boost` parameter. The default for `max_boost` is FLT_MAX.
 
@@ -126,18 +133,27 @@ Note that unlike the `custom_score` query, the
 score of the query is multiplied with the result of the script scoring. If
 you wish to inhibit this, set `"boost_mode": "replace"`
 
-===== Boost factor
+===== Weight
+
+coming[1.4.0]
 
-The `boost_factor` score allows you to multiply the score by the provided
-`boost_factor`. This can sometimes be desired since boost value set on
+The `weight` score allows you to multiply the score by the provided
+`weight`. This can sometimes be desired since boost value set on
 specific queries gets normalized, while for this score function it does
 not.
 
 [source,js]
 --------------------------------------------------
-"boost_factor" : number
+"weight" : number
 --------------------------------------------------
 
+===== Boost factor
+
+deprecated[1.4.0]
+
+Same as `weight`. Use `weight` instead.
+
+
 ===== Random
 
 The `random_score` generates scores using a hash of the `_uid` field,
@@ -490,7 +506,7 @@ becomes
 [source,js]
 --------------------------------------------------
 "function_score": {
-    "boost_factor": 5.2,
+    "weight": 5.2,
     "query": {...}
 }
 --------------------------------------------------
@@ -557,7 +573,7 @@ becomes:
 "function_score": {
     "functions": [
         {
-            "boost_factor": "3",
+            "weight": "3",
             "filter": {...}
         },
         {

+ 5 - 20
src/main/java/org/elasticsearch/common/lucene/search/function/BoostScoreFunction.java

@@ -21,12 +21,16 @@ package org.elasticsearch.common.lucene.search.function;
 
 import org.apache.lucene.index.AtomicReaderContext;
 import org.apache.lucene.search.Explanation;
+import org.elasticsearch.ElasticsearchIllegalArgumentException;
 
 /**
  *
  */
+@Deprecated
 public class BoostScoreFunction extends ScoreFunction {
 
+    public static final String BOOST_WEIGHT_ERROR_MESSAGE = "'boost_factor' and 'weight' cannot be used together. Use 'weight'.";
+
     private final float boost;
 
     public BoostScoreFunction(float boost) {
@@ -55,28 +59,9 @@ public class BoostScoreFunction extends ScoreFunction {
         return exp;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o)
-            return true;
-        if (o == null || getClass() != o.getClass())
-            return false;
-
-        BoostScoreFunction that = (BoostScoreFunction) o;
-
-        if (Float.compare(that.boost, boost) != 0)
-            return false;
-
-        return true;
-    }
-
-    @Override
-    public int hashCode() {
-        return (boost != +0.0f ? Float.floatToIntBits(boost) : 0);
-    }
-
     @Override
     public String toString() {
         return "boost[" + boost + "]";
     }
+
 }

+ 1 - 2
src/main/java/org/elasticsearch/common/lucene/search/function/ScoreFunction.java

@@ -28,7 +28,7 @@ import org.apache.lucene.search.Explanation;
 public abstract class ScoreFunction {
 
     private final CombineFunction scoreCombiner;
-    
+
     public abstract void setNextReader(AtomicReaderContext context);
 
     public abstract double score(int docId, float subQueryScore);
@@ -42,5 +42,4 @@ public abstract class ScoreFunction {
     protected ScoreFunction(CombineFunction scoreCombiner) {
         this.scoreCombiner = scoreCombiner;
     }
-
 }

+ 104 - 0
src/main/java/org/elasticsearch/common/lucene/search/function/WeightFactorFunction.java

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

+ 2 - 3
src/main/java/org/elasticsearch/index/query/functionscore/DecayFunctionBuilder.java

@@ -26,7 +26,7 @@ import org.elasticsearch.search.MultiValueMode;
 import java.io.IOException;
 import java.util.Locale;
 
-public abstract class DecayFunctionBuilder implements ScoreFunctionBuilder {
+public abstract class DecayFunctionBuilder extends ScoreFunctionBuilder {
 
     protected static final String ORIGIN = "origin";
     protected static final String SCALE = "scale";
@@ -60,7 +60,7 @@ public abstract class DecayFunctionBuilder implements ScoreFunctionBuilder {
     }
 
     @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+    public void doXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject(getName());
         builder.startObject(fieldName);
         if (origin != null) {
@@ -78,7 +78,6 @@ public abstract class DecayFunctionBuilder implements ScoreFunctionBuilder {
             builder.field(DecayFunctionParser.MULTI_VALUE_MODE.getPreferredName(), multiValueMode.name());
         }
         builder.endObject();
-        return builder;
     }
 
     public ScoreFunctionBuilder setMultiValueMode(MultiValueMode multiValueMode) {

+ 14 - 22
src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java

@@ -45,7 +45,7 @@ public class FunctionScoreQueryBuilder extends BaseQueryBuilder implements Boost
     private Float maxBoost;
 
     private String scoreMode;
-    
+
     private String boostMode;
 
     private ArrayList<FilterBuilder> filters = new ArrayList<>();
@@ -98,12 +98,12 @@ public class FunctionScoreQueryBuilder extends BaseQueryBuilder implements Boost
         this.scoreMode = scoreMode;
         return this;
     }
-    
+
     public FunctionScoreQueryBuilder boostMode(String boostMode) {
         this.boostMode = boostMode;
         return this;
     }
-    
+
     public FunctionScoreQueryBuilder boostMode(CombineFunction combineFunction) {
         this.boostMode = combineFunction.getName();
         return this;
@@ -133,27 +133,19 @@ public class FunctionScoreQueryBuilder extends BaseQueryBuilder implements Boost
         } else if (filterBuilder != null) {
             builder.field("filter");
             filterBuilder.toXContent(builder, params);
-        } 
-        // If there is only one function without a filter, we later want to
-        // create a FunctionScoreQuery.
-        // For this, we only build the scoreFunction.Tthis will be translated to
-        // FunctionScoreQuery in the parser.
-        if (filters.size() == 1 && filters.get(0) == null) {
-            scoreFunctions.get(0).toXContent(builder, params);
-        } else { // in all other cases we build the format needed for a
-                 // FiltersFunctionScoreQuery
-            builder.startArray("functions");
-            for (int i = 0; i < filters.size(); i++) {
-                builder.startObject();
-                if (filters.get(i) != null) {
-                    builder.field("filter");
-                    filters.get(i).toXContent(builder, params);
-                }
-                scoreFunctions.get(i).toXContent(builder, params);
-                builder.endObject();
+        }
+        builder.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);
             }
-            builder.endArray();
+            scoreFunctions.get(i).toXContent(builder, params);
+            builder.endObject();
         }
+        builder.endArray();
+
         if (scoreMode != null) {
             builder.field("score_mode", scoreMode);
         }

+ 30 - 13
src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryParser.java

@@ -24,14 +24,13 @@ import com.google.common.collect.ImmutableMap.Builder;
 import org.apache.lucene.search.Filter;
 import org.apache.lucene.search.Query;
 import org.elasticsearch.ElasticsearchParseException;
+import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.lucene.search.MatchAllDocsFilter;
 import org.elasticsearch.common.lucene.search.Queries;
 import org.elasticsearch.common.lucene.search.XConstantScoreQuery;
-import org.elasticsearch.common.lucene.search.function.CombineFunction;
-import org.elasticsearch.common.lucene.search.function.FiltersFunctionScoreQuery;
-import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
-import org.elasticsearch.common.lucene.search.function.ScoreFunction;
+import org.elasticsearch.common.lucene.search.function.*;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.index.query.QueryParseContext;
 import org.elasticsearch.index.query.QueryParser;
@@ -53,6 +52,8 @@ public class FunctionScoreQueryParser implements QueryParser {
     static final String MISPLACED_FUNCTION_MESSAGE_PREFIX = "You can either define \"functions\":[...] or a single function, not both. ";
     static final String MISPLACED_BOOST_FUNCTION_MESSAGE_SUFFIX = " Did you mean \"boost\" instead?";
 
+    public static final ParseField WEIGHT_FIELD = new ParseField("weight");
+
     @Inject
     public FunctionScoreQueryParser(ScoreFunctionParserMapper funtionParserMapper) {
         this.funtionParserMapper = funtionParserMapper;
@@ -62,7 +63,7 @@ public class FunctionScoreQueryParser implements QueryParser {
     public String[] names() {
         return new String[] { NAME, Strings.toCamelCase(NAME) };
     }
-    
+
     private static final ImmutableMap<String, CombineFunction> combineFunctionsMap;
 
     static {
@@ -116,17 +117,27 @@ public class FunctionScoreQueryParser implements QueryParser {
                 currentFieldName = parseFiltersAndFunctions(parseContext, parser, filterFunctions, currentFieldName);
                 functionArrayFound = true;
             } else {
-                // we try to parse a score function. If there is no score
-                // function for the current field name,
-                // functionParserMapper.get() will throw an Exception.
-                ScoreFunctionParser currentFunctionParser = funtionParserMapper.get(parseContext.index(), currentFieldName);
-                singleFunctionName = currentFieldName;
+                ScoreFunction scoreFunction;
+                if (currentFieldName.equals("weight")) {
+                    scoreFunction = new WeightFactorFunction(parser.floatValue());
+
+                } else {
+                    // we try to parse a score function. If there is no score
+                    // function for the current field name,
+                    // functionParserMapper.get() will throw an Exception.
+                    scoreFunction = funtionParserMapper.get(parseContext.index(), currentFieldName).parse(parseContext, parser);
+                }
                 if (functionArrayFound) {
                     String errorString = "Found \"functions\": [...] already, now encountering \"" + currentFieldName + "\".";
                     handleMisplacedFunctionsDeclaration(errorString, currentFieldName);
                 }
-                filterFunctions.add(new FiltersFunctionScoreQuery.FilterFunction(null, currentFunctionParser.parse(parseContext, parser)));
+                if (filterFunctions.size() > 0) {
+                    String errorString = "Found function " + singleFunctionName + " already, now encountering \"" + currentFieldName + "\". Use functions[{...},...] if you want to define several functions.";
+                    throw new ElasticsearchParseException(errorString);
+                }
+                filterFunctions.add(new FiltersFunctionScoreQuery.FilterFunction(null, scoreFunction));
                 singleFunctionFound = true;
+                singleFunctionName = currentFieldName;
             }
         }
         if (query == null) {
@@ -138,7 +149,7 @@ public class FunctionScoreQueryParser implements QueryParser {
         }
         // handle cases where only one score function and no filter was
         // provided. In this case we create a FunctionScoreQuery.
-        if (filterFunctions.size() == 1 && filterFunctions.get(0).filter == null) {
+        if (filterFunctions.size() == 1 && (filterFunctions.get(0).filter == null || filterFunctions.get(0).filter instanceof MatchAllDocsFilter)) {
             FunctionScoreQuery theQuery = new FunctionScoreQuery(query, filterFunctions.get(0).function);
             if (combineFunction != null) {
                 theQuery.setCombineFunction(combineFunction);
@@ -167,11 +178,12 @@ public class FunctionScoreQueryParser implements QueryParser {
     }
 
     private String parseFiltersAndFunctions(QueryParseContext parseContext, XContentParser parser,
-            ArrayList<FiltersFunctionScoreQuery.FilterFunction> filterFunctions, String currentFieldName) throws IOException {
+                                            ArrayList<FiltersFunctionScoreQuery.FilterFunction> filterFunctions, String currentFieldName) throws IOException {
         XContentParser.Token token;
         while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
             Filter filter = null;
             ScoreFunction scoreFunction = null;
+            Float functionWeight = null;
             if (token != XContentParser.Token.START_OBJECT) {
                 throw new QueryParsingException(parseContext.index(), NAME + ": malformed query, expected a "
                         + XContentParser.Token.START_OBJECT + " while parsing functions but got a " + token);
@@ -179,6 +191,8 @@ public class FunctionScoreQueryParser implements QueryParser {
                 while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
                     if (token == XContentParser.Token.FIELD_NAME) {
                         currentFieldName = parser.currentName();
+                    } else if (WEIGHT_FIELD.match(currentFieldName)) {
+                        functionWeight = parser.floatValue();
                     } else {
                         if ("filter".equals(currentFieldName)) {
                             filter = parseContext.parseInnerFilter();
@@ -191,6 +205,9 @@ public class FunctionScoreQueryParser implements QueryParser {
                         }
                     }
                 }
+                if (functionWeight != null) {
+                    scoreFunction = new WeightFactorFunction(functionWeight, scoreFunction);
+                }
             }
             if (filter == null) {
                 filter = Queries.MATCH_ALL_FILTER;

+ 26 - 2
src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilder.java

@@ -20,9 +20,33 @@
 package org.elasticsearch.index.query.functionscore;
 
 import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
 
-public interface ScoreFunctionBuilder extends ToXContent {
+import java.io.IOException;
 
-    public String getName();
+public abstract class ScoreFunctionBuilder implements ToXContent {
 
+    public ScoreFunctionBuilder setWeight(float weight) {
+        this.weight = weight;
+        return this;
+    }
+
+    private Float weight;
+
+    public abstract String getName();
+
+    protected void buildWeight(XContentBuilder builder) throws IOException {
+        if (weight != null) {
+            builder.field(FunctionScoreQueryParser.WEIGHT_FIELD.getPreferredName(), weight);
+        }
+    }
+
+    @Override
+    public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        buildWeight(builder);
+        doXContent(builder, params);
+        return builder;
+    }
+
+    protected abstract void doXContent(XContentBuilder builder, Params params) throws IOException;
 }

+ 6 - 0
src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilders.java

@@ -26,6 +26,7 @@ import org.elasticsearch.index.query.functionscore.gauss.GaussDecayFunctionBuild
 import org.elasticsearch.index.query.functionscore.lin.LinearDecayFunctionBuilder;
 import org.elasticsearch.index.query.functionscore.random.RandomScoreFunctionBuilder;
 import org.elasticsearch.index.query.functionscore.script.ScriptScoreFunctionBuilder;
+import org.elasticsearch.index.query.functionscore.weight.WeightBuilder;
 
 import java.util.Map;
 
@@ -71,6 +72,7 @@ public class ScoreFunctionBuilders {
         return (new ScriptScoreFunctionBuilder()).script(script).params(params);
     }
 
+    @Deprecated
     public static FactorBuilder factorFunction(float boost) {
         return (new FactorBuilder()).boostFactor(boost);
     }
@@ -78,6 +80,10 @@ public class ScoreFunctionBuilders {
     public static RandomScoreFunctionBuilder randomFunction(int seed) {
         return (new RandomScoreFunctionBuilder()).seed(seed);
     }
+    
+    public static WeightBuilder weightFactorFunction(float weight) {
+        return (WeightBuilder)(new WeightBuilder().setWeight(weight));
+    }
 
     public static FieldValueFactorFunctionBuilder fieldValueFactorFunction(String fieldName) {
         return new FieldValueFactorFunctionBuilder(fieldName);

+ 18 - 7
src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorBuilder.java

@@ -19,18 +19,20 @@
 
 package org.elasticsearch.index.query.functionscore.factor;
 
-import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder;
-
+import org.elasticsearch.ElasticsearchIllegalArgumentException;
+import org.elasticsearch.common.lucene.search.function.BoostScoreFunction;
 import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder;
 
 import java.io.IOException;
 
 /**
  * A query that simply applies the boost factor to another query (multiply it).
- * 
- * 
+ *
+ *
  */
-public class FactorBuilder implements ScoreFunctionBuilder {
+@Deprecated
+public class FactorBuilder extends ScoreFunctionBuilder {
 
     private Float boostFactor;
 
@@ -43,15 +45,24 @@ public class FactorBuilder implements ScoreFunctionBuilder {
     }
 
     @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+    public void doXContent(XContentBuilder builder, Params params) throws IOException {
         if (boostFactor != null) {
             builder.field("boost_factor", boostFactor.floatValue());
         }
-        return builder;
     }
 
     @Override
     public String getName() {
         return FactorParser.NAMES[0];
     }
+
+    @Override
+    public ScoreFunctionBuilder setWeight(float weight) {
+        throw new ElasticsearchIllegalArgumentException(BoostScoreFunction.BOOST_WEIGHT_ERROR_MESSAGE);
+    }
+
+    @Override
+    public void buildWeight(XContentBuilder builder) throws IOException {
+        //we do not want the weight to be written for boost_factor as it does not make sense to have it
+    }
 }

+ 1 - 0
src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorParser.java

@@ -33,6 +33,7 @@ import java.io.IOException;
 /**
  *
  */
+@Deprecated
 public class FactorParser implements ScoreFunctionParser {
 
     public static String[] NAMES = { "boost_factor", "boostFactor" };

+ 2 - 3
src/main/java/org/elasticsearch/index/query/functionscore/fieldvaluefactor/FieldValueFactorFunctionBuilder.java

@@ -30,7 +30,7 @@ import java.util.Locale;
  * Builder to construct {@code field_value_factor} functions for a function
  * score query.
  */
-public class FieldValueFactorFunctionBuilder implements ScoreFunctionBuilder {
+public class FieldValueFactorFunctionBuilder extends ScoreFunctionBuilder {
     private String field = null;
     private Float factor = null;
     private FieldValueFactorFunction.Modifier modifier = null;
@@ -55,7 +55,7 @@ public class FieldValueFactorFunctionBuilder implements ScoreFunctionBuilder {
     }
 
     @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+    public void doXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject(getName());
         if (field != null) {
             builder.field("field", field);
@@ -69,6 +69,5 @@ public class FieldValueFactorFunctionBuilder implements ScoreFunctionBuilder {
             builder.field("modifier", modifier.toString().toLowerCase(Locale.ROOT));
         }
         builder.endObject();
-        return builder;
     }
 }

+ 3 - 3
src/main/java/org/elasticsearch/index/query/functionscore/random/RandomScoreFunctionBuilder.java

@@ -26,7 +26,7 @@ import java.io.IOException;
 /**
  * A function that computes a random score for the matched documents
  */
-public class RandomScoreFunctionBuilder implements ScoreFunctionBuilder {
+public class RandomScoreFunctionBuilder extends ScoreFunctionBuilder {
 
     private Integer seed = null;
 
@@ -50,12 +50,12 @@ public class RandomScoreFunctionBuilder implements ScoreFunctionBuilder {
     }
 
     @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+    public void doXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject(getName());
         if (seed != null) {
             builder.field("seed", seed.intValue());
         }
-        return builder.endObject();
+        builder.endObject();
     }
 
 }

+ 3 - 3
src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionBuilder.java

@@ -31,7 +31,7 @@ import java.util.Map;
  * A function that uses a script to compute or influence the score of documents
  * that match with the inner query or filter.
  */
-public class ScriptScoreFunctionBuilder implements ScoreFunctionBuilder {
+public class ScriptScoreFunctionBuilder extends ScoreFunctionBuilder {
 
     private String script;
 
@@ -80,7 +80,7 @@ public class ScriptScoreFunctionBuilder implements ScoreFunctionBuilder {
     }
 
     @Override
-    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+    public void doXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject(getName());
         builder.field("script", script);
         if (lang != null) {
@@ -89,7 +89,7 @@ public class ScriptScoreFunctionBuilder implements ScoreFunctionBuilder {
         if (this.params != null) {
             builder.field("params", this.params);
         }
-        return builder.endObject();
+        builder.endObject();
     }
 
     @Override

+ 42 - 0
src/main/java/org/elasticsearch/index/query/functionscore/weight/WeightBuilder.java

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

+ 88 - 0
src/test/java/org/elasticsearch/index/query/SimpleIndexQueryParserTests.java

@@ -34,6 +34,7 @@ import org.apache.lucene.util.CharsRef;
 import org.apache.lucene.util.NumericUtils;
 import org.apache.lucene.util.UnicodeUtil;
 import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.ElasticsearchIllegalArgumentException;
 import org.elasticsearch.action.get.MultiGetRequest;
 import org.elasticsearch.common.bytes.BytesArray;
 import org.elasticsearch.common.compress.CompressedString;
@@ -41,6 +42,7 @@ import org.elasticsearch.common.lucene.Lucene;
 import org.elasticsearch.common.lucene.search.*;
 import org.elasticsearch.common.lucene.search.function.BoostScoreFunction;
 import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
+import org.elasticsearch.common.lucene.search.function.WeightFactorFunction;
 import org.elasticsearch.common.settings.ImmutableSettings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.unit.DistanceUnit;
@@ -2318,6 +2320,92 @@ public class SimpleIndexQueryParserTests extends ElasticsearchSingleNodeTest {
         assertThat(filter.getTerm().toString(), equalTo("text:apache"));
     }
 
+    @Test
+    public void testProperErrorMessageWhenTwoFunctionsDefinedInQueryBody() throws IOException {
+        IndexQueryParserService queryParser = queryParser();
+        String query = copyToStringFromClasspath("/org/elasticsearch/index/query/function-score-query-causing-NPE.json");
+        try {
+            queryParser.parse(query).query();
+            fail("FunctionScoreQueryParser should throw an exception here because two functions in body are not allowed.");
+        } catch (QueryParsingException e) {
+            assertThat(e.getDetailedMessage(), containsString("Use functions[{...},...] if you want to define several functions."));
+        }
+    }
+
+    @Test
+    public void testWeight1fStillProducesWeighFuction() throws IOException {
+        IndexQueryParserService queryParser = queryParser();
+        String queryString = jsonBuilder().startObject()
+                .startObject("function_score")
+                .startArray("functions")
+                .startObject()
+                .startObject("field_value_factor")
+                .field("field", "popularity")
+                .endObject()
+                .field("weight", 1.0)
+                .endObject()
+                .endArray()
+                .endObject()
+                .endObject().string();
+        IndexService indexService = createIndex("testidx", client().admin().indices().prepareCreate("testidx")
+                .addMapping("doc",jsonBuilder().startObject()
+                        .startObject("properties")
+                        .startObject("popularity").field("type", "float").endObject()
+                        .endObject()
+                        .endObject()));
+        SearchContext.setCurrent(createSearchContext(indexService));
+        Query query = queryParser.parse(queryString).query();
+        assertThat(query, instanceOf(FunctionScoreQuery.class));
+        assertThat(((FunctionScoreQuery) query).getFunction(), instanceOf(WeightFactorFunction.class));
+        SearchContext.removeCurrent();
+    }
+
+    @Test
+    public void testProperErrorMessagesForMisplacedWeightsAndFunctions() throws IOException {
+        IndexQueryParserService queryParser = queryParser();
+        String query = jsonBuilder().startObject().startObject("function_score")
+                .startArray("functions")
+                .startObject().field("weight", 2).field("boost_factor",2).endObject()
+                .endArray()
+                .endObject().endObject().string();
+        try {
+            queryParser.parse(query).query();
+            fail("Expect exception here because boost_factor must not have a weight");
+        } catch (QueryParsingException e) {
+            assertThat(e.getDetailedMessage(), containsString(BoostScoreFunction.BOOST_WEIGHT_ERROR_MESSAGE));
+        }
+        try {
+            functionScoreQuery().add(factorFunction(2.0f).setWeight(2.0f));
+            fail("Expect exception here because boost_factor must not have a weight");
+        } catch (ElasticsearchIllegalArgumentException e) {
+            assertThat(e.getDetailedMessage(), containsString(BoostScoreFunction.BOOST_WEIGHT_ERROR_MESSAGE));
+        }
+        query = jsonBuilder().startObject().startObject("function_score")
+                .startArray("functions")
+                .startObject().field("boost_factor",2).endObject()
+                .endArray()
+                .field("weight", 2)
+                .endObject().endObject().string();
+        try {
+            queryParser.parse(query).query();
+            fail("Expect exception here because array of functions and one weight in body is not allowed.");
+        } catch (QueryParsingException e) {
+            assertThat(e.getDetailedMessage(), containsString("You can either define \"functions\":[...] or a single function, not both. Found \"functions\": [...] already, now encountering \"weight\"."));
+        }
+        query = jsonBuilder().startObject().startObject("function_score")
+                .field("weight", 2)
+                .startArray("functions")
+                .startObject().field("boost_factor",2).endObject()
+                .endArray()
+                .endObject().endObject().string();
+        try {
+            queryParser.parse(query).query();
+            fail("Expect exception here because array of functions and one weight in body is not allowed.");
+        } catch (QueryParsingException e) {
+            assertThat(e.getDetailedMessage(), containsString("You can either define \"functions\":[...] or a single function, not both. Found \"weight\" already, now encountering \"functions\": [...]."));
+        }
+    }
+
     // https://github.com/elasticsearch/elasticsearch/issues/6722
     public void testEmptyBoolSubClausesIsMatchAll() throws ElasticsearchException, IOException {
         String query = copyToStringFromClasspath("/org/elasticsearch/index/query/bool-query-with-empty-clauses-for-parsing.json");

+ 9 - 0
src/test/java/org/elasticsearch/index/query/function-score-query-causing-NPE.json

@@ -0,0 +1,9 @@
+{
+    "function_score": {
+      "script_score": {
+        "script": "_index['text']['foo'].tf()"
+      },
+      "weight": 2
+    }
+}
+

+ 115 - 0
src/test/java/org/elasticsearch/search/functionscore/FunctionScoreBackwardCompatibilityTests.java

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

+ 312 - 2
src/test/java/org/elasticsearch/search/functionscore/FunctionScoreTests.java

@@ -19,8 +19,15 @@
 
 package org.elasticsearch.search.functionscore;
 
+import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.action.search.SearchType;
+import org.elasticsearch.common.geo.GeoPoint;
+import org.elasticsearch.common.lucene.search.function.FieldValueFactorFunction;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
+import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder;
+import org.elasticsearch.index.query.functionscore.weight.WeightBuilder;
 import org.elasticsearch.test.ElasticsearchIntegrationTest;
 import org.junit.Test;
 
@@ -28,17 +35,27 @@ import java.io.IOException;
 import java.util.concurrent.ExecutionException;
 
 import static org.elasticsearch.client.Requests.searchRequest;
+import static org.elasticsearch.common.io.Streams.copyToStringFromClasspath;
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+import static org.elasticsearch.index.query.FilterBuilders.termFilter;
 import static org.elasticsearch.index.query.QueryBuilders.functionScoreQuery;
 import static org.elasticsearch.index.query.QueryBuilders.termQuery;
 import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.*;
 import static org.elasticsearch.search.builder.SearchSourceBuilder.searchSource;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.greaterThan;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
+import static org.hamcrest.Matchers.*;
 
 public class FunctionScoreTests extends ElasticsearchIntegrationTest {
 
+    static final String TYPE = "type";
+    static final String INDEX = "index";
+    static final String TEXT_FIELD = "text_field";
+    static final String FLOAT_FIELD = "float_field";
+    static final String GEO_POINT_FIELD = "geo_point_field";
+    static final XContentBuilder SIMPLE_DOC;
+    static final XContentBuilder MAPPING_WITH_FLOAT_AND_GEO_POINT_AND_TEST_FIELD;
+
     @Test
     public void testExplainQueryOnlyOnce() throws IOException, ExecutionException, InterruptedException {
         assertAcked(prepareCreate("test").addMapping(
@@ -87,4 +104,297 @@ public class FunctionScoreTests extends ElasticsearchIntegrationTest {
         assertThat(queryExplanationIndex, equalTo(-1));
     }
 
+    static {
+        XContentBuilder simpleDoc;
+        XContentBuilder mappingWithFloatAndGeoPointAndTestField;
+        try {
+            simpleDoc = jsonBuilder().startObject()
+                    .field(TEXT_FIELD, "value")
+                    .startObject(GEO_POINT_FIELD)
+                    .field("lat", 10)
+                    .field("lon", 20)
+                    .endObject()
+                    .field(FLOAT_FIELD, 2.71828)
+                    .endObject();
+        } catch (IOException e) {
+            throw new ElasticsearchException("Exception while initializing FunctionScoreTests", e);
+        }
+        SIMPLE_DOC = simpleDoc;
+        try {
+
+            mappingWithFloatAndGeoPointAndTestField = jsonBuilder().startObject()
+                    .startObject(TYPE)
+                    .startObject("properties")
+                    .startObject(TEXT_FIELD)
+                    .field("type", "string")
+                    .endObject()
+                    .startObject(GEO_POINT_FIELD)
+                    .field("type", "geo_point")
+                    .endObject()
+                    .startObject(FLOAT_FIELD)
+                    .field("type", "float")
+                    .endObject()
+                    .endObject()
+                    .endObject()
+                    .endObject();
+        } catch (IOException e) {
+            throw new ElasticsearchException("Exception while initializing FunctionScoreTests", e);
+        }
+        MAPPING_WITH_FLOAT_AND_GEO_POINT_AND_TEST_FIELD = mappingWithFloatAndGeoPointAndTestField;
+    }
+
+    @Test
+    public void testExplain() throws IOException, ExecutionException, InterruptedException {
+        assertAcked(prepareCreate(INDEX).addMapping(
+                TYPE, MAPPING_WITH_FLOAT_AND_GEO_POINT_AND_TEST_FIELD
+        ));
+        ensureYellow();
+
+        index(INDEX, TYPE, "1", SIMPLE_DOC);
+        refresh();
+
+        SearchResponse responseWithWeights = client().search(
+                searchRequest().source(
+                        searchSource().query(
+                                functionScoreQuery(termFilter(TEXT_FIELD, "value").cache(false))
+                                        .add(gaussDecayFunction(GEO_POINT_FIELD, new GeoPoint(10, 20), "1000km"))
+                                        .add(fieldValueFactorFunction(FLOAT_FIELD).modifier(FieldValueFactorFunction.Modifier.LN).setWeight(2))
+                                        .add(scriptFunction("_index['" + TEXT_FIELD + "']['value'].tf()").setWeight(3))
+                        ).explain(true))).actionGet();
+
+        assertThat(responseWithWeights.getHits().getAt(0).getExplanation().toString(),
+                equalTo("5.999996 = (MATCH) function score, product of:\n  1.0 = (MATCH) ConstantScore(text_field:value), product of:\n    1.0 = boost\n    1.0 = queryNorm\n  5.999996 = (MATCH) Math.min of\n    5.999996 = (MATCH) function score, score mode [multiply]\n      1.0 = (MATCH) function score, product of:\n        1.0 = match filter: *:*\n        1.0 = (MATCH) Function for field geo_point_field:\n          1.0 = -exp(-0.5*pow(MIN of: [Math.max(arcDistance([10.0, 20.0](=doc value),[10.0, 20.0](=origin)) - 0.0(=offset), 0)],2.0)/7.213475204444817E11)\n      1.9999987 = (MATCH) function score, product of:\n        1.0 = match filter: *:*\n        1.9999987 = (MATCH) product of:\n          0.99999934 = field value function: ln(doc['float_field'].value * factor=1.0)\n          2.0 = weight\n      3.0 = (MATCH) function score, product of:\n        1.0 = match filter: *:*\n        3.0 = (MATCH) product of:\n          1.0 = script score function, computed with script:\"_index['text_field']['value'].tf()\n          3.0 = weight\n    3.4028235E38 = maxBoost\n  1.0 = queryBoost\n")
+                );
+        responseWithWeights = client().search(
+                searchRequest().source(
+                        searchSource().query(
+                                functionScoreQuery(termFilter(TEXT_FIELD, "value").cache(false))
+                                        .add(weightFactorFunction(4.0f))
+                        ).explain(true))).actionGet();
+        assertThat(responseWithWeights.getHits().getAt(0).getExplanation().toString(),
+                equalTo("4.0 = (MATCH) function score, product of:\n  1.0 = (MATCH) ConstantScore(text_field:value), product of:\n    1.0 = boost\n    1.0 = queryNorm\n  4.0 = (MATCH) Math.min of\n    4.0 = (MATCH) product of:\n      1.0 = constant score 1.0 - no function provided\n      4.0 = weight\n    3.4028235E38 = maxBoost\n  1.0 = queryBoost\n")
+        );
+
+    }
+
+    @Test
+    public void simpleWeightedFunctionsTest() throws IOException, ExecutionException, InterruptedException {
+        assertAcked(prepareCreate(INDEX).addMapping(
+                TYPE, MAPPING_WITH_FLOAT_AND_GEO_POINT_AND_TEST_FIELD
+        ));
+        ensureYellow();
+
+        index(INDEX, TYPE, "1", SIMPLE_DOC);
+        refresh();
+        SearchResponse response = client().search(
+                searchRequest().source(
+                        searchSource().query(
+                                functionScoreQuery(termFilter(TEXT_FIELD, "value"))
+                                        .add(gaussDecayFunction(GEO_POINT_FIELD, new GeoPoint(10, 20), "1000km"))
+                                        .add(fieldValueFactorFunction(FLOAT_FIELD).modifier(FieldValueFactorFunction.Modifier.LN))
+                                        .add(scriptFunction("_index['" + TEXT_FIELD + "']['value'].tf()"))
+                        ))).actionGet();
+        SearchResponse responseWithWeights = client().search(
+                searchRequest().source(
+                        searchSource().query(
+                                functionScoreQuery(termFilter(TEXT_FIELD, "value"))
+                                        .add(gaussDecayFunction(GEO_POINT_FIELD, new GeoPoint(10, 20), "1000km").setWeight(2))
+                                        .add(fieldValueFactorFunction(FLOAT_FIELD).modifier(FieldValueFactorFunction.Modifier.LN).setWeight(2))
+                                        .add(scriptFunction("_index['" + TEXT_FIELD + "']['value'].tf()").setWeight(2))
+                        ))).actionGet();
+
+        assertThat((double) response.getHits().getAt(0).getScore(), closeTo(1.0, 1.e-5));
+        assertThat((double) responseWithWeights.getHits().getAt(0).getScore(), closeTo(8.0, 1.e-5));
+    }
+
+    @Test
+    public void simpleWeightedFunctionsTestWithRandomWeightsAndRandomCombineMode() throws IOException, ExecutionException, InterruptedException {
+        assertAcked(prepareCreate(INDEX).addMapping(
+                TYPE,
+                MAPPING_WITH_FLOAT_AND_GEO_POINT_AND_TEST_FIELD));
+        ensureYellow();
+
+        XContentBuilder doc = jsonBuilder().startObject()
+                .field(TEXT_FIELD, "value")
+                .startObject(GEO_POINT_FIELD)
+                .field("lat", 10)
+                .field("lon", 20)
+                .endObject()
+                .field(FLOAT_FIELD, 10)
+                .endObject();
+        index(INDEX, TYPE, "1", doc);
+        refresh();
+        ScoreFunctionBuilder[] scoreFunctionBuilders = getScoreFunctionBuilders();
+        float[] weights = createRandomWeights(scoreFunctionBuilders.length);
+        float[] scores = getScores(scoreFunctionBuilders);
+
+        String scoreMode = getRandomScoreMode();
+        FunctionScoreQueryBuilder withWeights = functionScoreQuery(termFilter(TEXT_FIELD, "value")).scoreMode(scoreMode);
+        int weightscounter = 0;
+        for (ScoreFunctionBuilder builder : scoreFunctionBuilders) {
+            withWeights.add(builder.setWeight((float) weights[weightscounter]));
+            weightscounter++;
+        }
+        SearchResponse responseWithWeights = client().search(
+                searchRequest().source(searchSource().query(withWeights))
+        ).actionGet();
+
+        double expectedScore = computeExpectedScore(weights, scores, scoreMode);
+        assertThat(expectedScore / responseWithWeights.getHits().getAt(0).getScore(), closeTo(1.0, 1.e-6));
+
+    }
+
+    protected float computeExpectedScore(float[] weights, float[] scores, String scoreMode) {
+        float expectedScore = 0.0f;
+        if ("multiply".equals(scoreMode)) {
+            expectedScore = 1.0f;
+        }
+        if ("max".equals(scoreMode)) {
+            expectedScore = Float.MAX_VALUE * -1.0f;
+        }
+        if ("min".equals(scoreMode)) {
+            expectedScore = Float.MAX_VALUE;
+        }
+
+        for (int i = 0; i < weights.length; i++) {
+            float functionScore = weights[i] * scores[i];
+
+            if ("avg".equals(scoreMode)) {
+                expectedScore += functionScore;
+            } else if ("max".equals(scoreMode)) {
+                expectedScore = Math.max(functionScore, expectedScore);
+            } else if ("min".equals(scoreMode)) {
+                expectedScore = Math.min(functionScore, expectedScore);
+            } else if ("sum".equals(scoreMode)) {
+                expectedScore += functionScore;
+            } else if ("multiply".equals(scoreMode)) {
+                expectedScore *= functionScore;
+            }
+
+        }
+        if ("avg".equals(scoreMode)) {
+            expectedScore /= (double) weights.length;
+        }
+        return expectedScore;
+    }
+
+    @Test
+    public void simpleWeightedFunctionsTestSingleFunction() throws IOException, ExecutionException, InterruptedException {
+        assertAcked(prepareCreate(INDEX).addMapping(
+                TYPE,
+                MAPPING_WITH_FLOAT_AND_GEO_POINT_AND_TEST_FIELD));
+        ensureYellow();
+
+        XContentBuilder doc = jsonBuilder().startObject()
+                .field(TEXT_FIELD, "value")
+                .startObject(GEO_POINT_FIELD)
+                .field("lat", 12)
+                .field("lon", 21)
+                .endObject()
+                .field(FLOAT_FIELD, 10)
+                .endObject();
+        index(INDEX, TYPE, "1", doc);
+        refresh();
+        ScoreFunctionBuilder[] scoreFunctionBuilders = getScoreFunctionBuilders();
+        ScoreFunctionBuilder scoreFunctionBuilder = scoreFunctionBuilders[randomInt(3)];
+        float[] weights = createRandomWeights(1);
+        float[] scores = getScores(scoreFunctionBuilder);
+        FunctionScoreQueryBuilder withWeights = functionScoreQuery(termFilter(TEXT_FIELD, "value"));
+        withWeights.add(scoreFunctionBuilder.setWeight(weights[0]));
+
+        SearchResponse responseWithWeights = client().search(
+                searchRequest().source(searchSource().query(withWeights))
+        ).actionGet();
+
+        assertThat((double) scores[0] * weights[0] / responseWithWeights.getHits().getAt(0).getScore(), closeTo(1.0, 1.e-6));
+
+    }
+
+    private String getRandomScoreMode() {
+        String[] scoreModes = {"avg", "sum", "min", "max", "multiply"};
+        return scoreModes[randomInt(scoreModes.length - 1)];
+    }
+
+    private float[] getScores(ScoreFunctionBuilder... scoreFunctionBuilders) {
+        float[] scores = new float[scoreFunctionBuilders.length];
+        int scorecounter = 0;
+        for (ScoreFunctionBuilder builder : scoreFunctionBuilders) {
+            SearchResponse response = client().search(
+                    searchRequest().source(
+                            searchSource().query(
+                                    functionScoreQuery(termFilter(TEXT_FIELD, "value"))
+                                            .add(builder)
+                            ))).actionGet();
+            scores[scorecounter] = response.getHits().getAt(0).getScore();
+            scorecounter++;
+        }
+        return scores;
+    }
+
+    private float[] createRandomWeights(int size) {
+        float[] weights = new float[size];
+        for (int i = 0; i < weights.length; i++) {
+            weights[i] = randomFloat() * (randomBoolean() ? 1.0f : -1.0f) * (float) randomInt(100) + 1.e-6f;
+        }
+        return weights;
+    }
+
+    public ScoreFunctionBuilder[] getScoreFunctionBuilders() {
+        ScoreFunctionBuilder[] builders = new ScoreFunctionBuilder[4];
+        builders[0] = gaussDecayFunction(GEO_POINT_FIELD, new GeoPoint(11, 20), "1000km");
+        builders[1] = randomFunction(10);
+        builders[2] = fieldValueFactorFunction(FLOAT_FIELD).modifier(FieldValueFactorFunction.Modifier.LN);
+        builders[3] = scriptFunction("_index['" + TEXT_FIELD + "']['value'].tf()");
+        return builders;
+    }
+
+    @Test
+    public void checkWeightOnlyCreatesBoostFunction() throws IOException {
+        assertAcked(prepareCreate(INDEX).addMapping(
+                TYPE,
+                MAPPING_WITH_FLOAT_AND_GEO_POINT_AND_TEST_FIELD));
+        ensureYellow();
+
+        index(INDEX, TYPE, "1", SIMPLE_DOC);
+        refresh();
+        String query =jsonBuilder().startObject()
+                .startObject("query")
+                .startObject("function_score")
+                .startArray("functions")
+                .startObject()
+                .field("weight",2)
+                .endObject()
+                .endArray()
+                .endObject()
+                .endObject()
+                .endObject().string();
+        SearchResponse response = client().search(
+                searchRequest().source(query)
+        ).actionGet();
+        assertSearchResponse(response);
+        assertThat(response.getHits().getAt(0).score(), equalTo(2.0f));
+
+        query =jsonBuilder().startObject()
+                .startObject("query")
+                .startObject("function_score")
+                .field("weight",2)
+                .endObject()
+                .endObject()
+                .endObject().string();
+        response = client().search(
+                searchRequest().source(query)
+        ).actionGet();
+        assertSearchResponse(response);
+        assertThat(response.getHits().getAt(0).score(), equalTo(2.0f));
+        response = client().search(
+                searchRequest().source(searchSource().query(functionScoreQuery().add(new WeightBuilder().setWeight(2.0f))))
+        ).actionGet();
+        assertSearchResponse(response);
+        assertThat(response.getHits().getAt(0).score(), equalTo(2.0f));
+        response = client().search(
+                searchRequest().source(searchSource().query(functionScoreQuery().add(weightFactorFunction(2.0f))))
+        ).actionGet();
+        assertSearchResponse(response);
+        assertThat(response.getHits().getAt(0).score(), equalTo(2.0f));
+    }
 }