diff --git a/src/main/java/org/elasticsearch/common/lucene/search/function/BoostScoreFunction.java b/src/main/java/org/elasticsearch/common/lucene/search/function/BoostScoreFunction.java index c6416338b751a..209e7d44849fb 100644 --- a/src/main/java/org/elasticsearch/common/lucene/search/function/BoostScoreFunction.java +++ b/src/main/java/org/elasticsearch/common/lucene/search/function/BoostScoreFunction.java @@ -49,7 +49,7 @@ public float score(int docId, float subQueryScore) { } @Override - public float factor(int docId) { + public double factor(int docId) { return boost; } diff --git a/src/main/java/org/elasticsearch/common/lucene/search/function/FiltersFunctionScoreQuery.java b/src/main/java/org/elasticsearch/common/lucene/search/function/FiltersFunctionScoreQuery.java index 74aa6c426fc07..1fdcb3823c06b 100644 --- a/src/main/java/org/elasticsearch/common/lucene/search/function/FiltersFunctionScoreQuery.java +++ b/src/main/java/org/elasticsearch/common/lucene/search/function/FiltersFunctionScoreQuery.java @@ -287,7 +287,7 @@ public int nextDoc() throws IOException { @Override public float score() throws IOException { int docId = scorer.docID(); - float factor = 1.0f; + double factor = 1.0f; if (scoreMode == ScoreMode.First) { for (int i = 0; i < filterFunctions.length; i++) { if (docSets[i].get(docId)) { @@ -296,7 +296,7 @@ public float score() throws IOException { } } } else if (scoreMode == ScoreMode.Max) { - float maxFactor = Float.NEGATIVE_INFINITY; + double maxFactor = Double.NEGATIVE_INFINITY; for (int i = 0; i < filterFunctions.length; i++) { if (docSets[i].get(docId)) { maxFactor = Math.max(filterFunctions[i].function.factor(docId), maxFactor); @@ -306,7 +306,7 @@ public float score() throws IOException { factor = maxFactor; } } else if (scoreMode == ScoreMode.Min) { - float minFactor = Float.POSITIVE_INFINITY; + double minFactor = Double.POSITIVE_INFINITY; for (int i = 0; i < filterFunctions.length; i++) { if (docSets[i].get(docId)) { minFactor = Math.min(filterFunctions[i].function.factor(docId), minFactor); @@ -322,7 +322,7 @@ public float score() throws IOException { } } } else { // Avg / Total - float totalFactor = 0.0f; + double totalFactor = 0.0f; int count = 0; for (int i = 0; i < filterFunctions.length; i++) { if (docSets[i].get(docId)) { @@ -341,7 +341,7 @@ public float score() throws IOException { factor = maxBoost; } float score = scorer.score(); - return subQueryBoost * score * factor; + return (float)(subQueryBoost * score * factor); } @Override diff --git a/src/main/java/org/elasticsearch/common/lucene/search/function/FunctionScoreQuery.java b/src/main/java/org/elasticsearch/common/lucene/search/function/FunctionScoreQuery.java index bb1b499fe9f0e..3069e9198d89f 100644 --- a/src/main/java/org/elasticsearch/common/lucene/search/function/FunctionScoreQuery.java +++ b/src/main/java/org/elasticsearch/common/lucene/search/function/FunctionScoreQuery.java @@ -36,12 +36,21 @@ public class FunctionScoreQuery extends Query { Query subQuery; final ScoreFunction function; + float maxBoost = Float.MAX_VALUE; public FunctionScoreQuery(Query subQuery, ScoreFunction function) { this.subQuery = subQuery; this.function = function; } + public void setMaxBoost(float maxBoost) { + this.maxBoost = maxBoost; + } + + public float getMaxBoost() { + return this.maxBoost; + } + public Query getSubQuery() { return subQuery; } @@ -53,7 +62,9 @@ public ScoreFunction getFunction() { @Override public Query rewrite(IndexReader reader) throws IOException { Query newQ = subQuery.rewrite(reader); - if (newQ == subQuery) return this; + if (newQ == subQuery){ + return this; + } FunctionScoreQuery bq = (FunctionScoreQuery) this.clone(); bq.subQuery = newQ; return bq; @@ -101,7 +112,7 @@ public Scorer scorer(AtomicReaderContext context, boolean scoreDocsInOrder, bool return null; } function.setNextReader(context); - return new CustomBoostFactorScorer(this, subQueryScorer, function); + return new CustomBoostFactorScorer(this, subQueryScorer, function, maxBoost); } @Override @@ -121,18 +132,20 @@ public Explanation explain(AtomicReaderContext context, int doc) throws IOExcept } } - static class CustomBoostFactorScorer extends Scorer { private final float subQueryBoost; private final Scorer scorer; private final ScoreFunction function; + private final float maxBoost; - private CustomBoostFactorScorer(CustomBoostFactorWeight w, Scorer scorer, ScoreFunction function) throws IOException { + private CustomBoostFactorScorer(CustomBoostFactorWeight w, Scorer scorer, ScoreFunction function, float maxBoost) + throws IOException { super(w); this.subQueryBoost = w.getQuery().getBoost(); this.scorer = scorer; this.function = function; + this.maxBoost = maxBoost; } @Override @@ -152,7 +165,8 @@ public int nextDoc() throws IOException { @Override public float score() throws IOException { - return subQueryBoost * function.score(scorer.docID(), scorer.score()); + float factor = (float)function.score(scorer.docID(), scorer.score()); + return subQueryBoost * Math.min(maxBoost, factor); } @Override @@ -166,7 +180,6 @@ public long cost() { } } - public String toString(String field) { StringBuilder sb = new StringBuilder(); sb.append("custom score (").append(subQuery.toString(field)).append(",function=").append(function).append(')'); @@ -175,15 +188,14 @@ public String toString(String field) { } public boolean equals(Object o) { - if (getClass() != o.getClass()) return false; + if (getClass() != o.getClass()) + return false; FunctionScoreQuery other = (FunctionScoreQuery) o; - return this.getBoost() == other.getBoost() - && this.subQuery.equals(other.subQuery) - && this.function.equals(other.function); + return this.getBoost() == other.getBoost() && this.subQuery.equals(other.subQuery) && this.function.equals(other.function) + && this.maxBoost == other.maxBoost; } public int hashCode() { return subQuery.hashCode() + 31 * function.hashCode() ^ Float.floatToIntBits(getBoost()); } } - diff --git a/src/main/java/org/elasticsearch/common/lucene/search/function/ScoreFunction.java b/src/main/java/org/elasticsearch/common/lucene/search/function/ScoreFunction.java index d5d9f70b875ab..1555135c94b9a 100644 --- a/src/main/java/org/elasticsearch/common/lucene/search/function/ScoreFunction.java +++ b/src/main/java/org/elasticsearch/common/lucene/search/function/ScoreFunction.java @@ -31,7 +31,7 @@ public interface ScoreFunction { float score(int docId, float subQueryScore); - float factor(int docId); + double factor(int docId); Explanation explainScore(int docId, Explanation subQueryExpl); diff --git a/src/main/java/org/elasticsearch/common/lucene/search/function/ScriptScoreFunction.java b/src/main/java/org/elasticsearch/common/lucene/search/function/ScriptScoreFunction.java new file mode 100644 index 0000000000000..46c652a70745a --- /dev/null +++ b/src/main/java/org/elasticsearch/common/lucene/search/function/ScriptScoreFunction.java @@ -0,0 +1,86 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.Explanation; +import org.elasticsearch.script.ExplainableSearchScript; +import org.elasticsearch.script.SearchScript; + +import java.util.Map; + +public class ScriptScoreFunction implements ScoreFunction { + + private final String sScript; + + private final Map params; + + private final SearchScript script; + + public ScriptScoreFunction(String sScript, Map params, SearchScript script) { + this.sScript = sScript; + this.params = params; + this.script = script; + } + + @Override + public void setNextReader(AtomicReaderContext ctx) { + script.setNextReader(ctx); + } + + @Override + public float score(int docId, float subQueryScore) { + script.setNextDocId(docId); + script.setNextScore(subQueryScore); + return script.runAsFloat(); + } + + @Override + public double factor(int docId) { + // just the factor, so don't provide _score + script.setNextDocId(docId); + return script.runAsFloat(); + } + + @Override + public Explanation explainScore(int docId, Explanation subQueryExpl) { + Explanation exp; + if (script instanceof ExplainableSearchScript) { + script.setNextDocId(docId); + script.setNextScore(subQueryExpl.getValue()); + exp = ((ExplainableSearchScript) script).explain(subQueryExpl); + } else { + double score = score(docId, subQueryExpl.getValue()); + exp = new Explanation((float)score, "script score function: composed of:"); + exp.addDetail(subQueryExpl); + } + return exp; + } + + @Override + public Explanation explainFactor(int docId) { + return new Explanation((float)factor(docId), "script_factor"); + } + + @Override + public String toString() { + return "script[" + sScript + "], params [" + params + "]"; + } +} \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/index/query/CustomBoostFactorQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/CustomBoostFactorQueryBuilder.java index eda5a8d0c6ad0..5f398272cbc39 100644 --- a/src/main/java/org/elasticsearch/index/query/CustomBoostFactorQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/CustomBoostFactorQueryBuilder.java @@ -20,13 +20,14 @@ package org.elasticsearch.index.query; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder; import java.io.IOException; /** * A query that simply applies the boost factor to another query (multiply it). - * - * + * + * @deprecated use {@link FunctionScoreQueryBuilder} instead. */ public class CustomBoostFactorQueryBuilder extends BaseQueryBuilder { @@ -35,9 +36,11 @@ public class CustomBoostFactorQueryBuilder extends BaseQueryBuilder { private float boostFactor = -1; /** - * A query that simply applies the boost factor to another query (multiply it). - * - * @param queryBuilder The query to apply the boost factor to. + * A query that simply applies the boost factor to another query (multiply + * it). + * + * @param queryBuilder + * The query to apply the boost factor to. */ public CustomBoostFactorQueryBuilder(QueryBuilder queryBuilder) { this.queryBuilder = queryBuilder; diff --git a/src/main/java/org/elasticsearch/index/query/CustomBoostFactorQueryParser.java b/src/main/java/org/elasticsearch/index/query/CustomBoostFactorQueryParser.java index 9bd1b656fdb2c..38d1b967ae1d8 100644 --- a/src/main/java/org/elasticsearch/index/query/CustomBoostFactorQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/CustomBoostFactorQueryParser.java @@ -25,11 +25,12 @@ import org.elasticsearch.common.lucene.search.function.BoostScoreFunction; import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.functionscore.FunctionScoreQueryParser; import java.io.IOException; /** - * + * @deprecated use {@link FunctionScoreQueryParser} instead. */ public class CustomBoostFactorQueryParser implements QueryParser { @@ -41,7 +42,7 @@ public CustomBoostFactorQueryParser() { @Override public String[] names() { - return new String[]{NAME, Strings.toCamelCase(NAME)}; + return new String[] { NAME, Strings.toCamelCase(NAME) }; } @Override @@ -63,7 +64,8 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars query = parseContext.parseInnerQuery(); queryFound = true; } else { - throw new QueryParsingException(parseContext.index(), "[custom_boost_factor] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext.index(), "[custom_boost_factor] query does not support [" + + currentFieldName + "]"); } } else if (token.isValue()) { if ("boost_factor".equals(currentFieldName) || "boostFactor".equals(currentFieldName)) { @@ -71,7 +73,8 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars } else if ("boost".equals(currentFieldName)) { boost = parser.floatValue(); } else { - throw new QueryParsingException(parseContext.index(), "[custom_boost_factor] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext.index(), "[custom_boost_factor] query does not support [" + + currentFieldName + "]"); } } } diff --git a/src/main/java/org/elasticsearch/index/query/CustomFiltersScoreQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/CustomFiltersScoreQueryBuilder.java index b119dacf03d37..359a7fa75e1a0 100644 --- a/src/main/java/org/elasticsearch/index/query/CustomFiltersScoreQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/CustomFiltersScoreQueryBuilder.java @@ -22,13 +22,17 @@ import com.google.common.collect.Maps; import gnu.trove.list.array.TFloatArrayList; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder; import java.io.IOException; import java.util.ArrayList; import java.util.Map; /** - * A query that uses a filters with a script associated with them to compute the score. + * A query that uses a filters with a script associated with them to compute the + * score. + * + * @deprecated use {@link FunctionScoreQueryBuilder} instead. */ public class CustomFiltersScoreQueryBuilder extends BaseQueryBuilder implements BoostableQueryBuilder { @@ -108,8 +112,9 @@ public CustomFiltersScoreQueryBuilder maxBoost(float maxBoost) { } /** - * Sets the boost for this query. Documents matching this query will (in addition to the normal - * weightings) have their score multiplied by the boost provided. + * Sets the boost for this query. Documents matching this query will (in + * addition to the normal weightings) have their score multiplied by the + * boost provided. */ public CustomFiltersScoreQueryBuilder boost(float boost) { this.boost = boost; diff --git a/src/main/java/org/elasticsearch/index/query/CustomFiltersScoreQueryParser.java b/src/main/java/org/elasticsearch/index/query/CustomFiltersScoreQueryParser.java index e2f432ecc8fc9..4de65d20f75b9 100644 --- a/src/main/java/org/elasticsearch/index/query/CustomFiltersScoreQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/CustomFiltersScoreQueryParser.java @@ -27,7 +27,9 @@ import org.elasticsearch.common.lucene.search.function.BoostScoreFunction; import org.elasticsearch.common.lucene.search.function.FiltersFunctionScoreQuery; import org.elasticsearch.common.lucene.search.function.ScoreFunction; +import org.elasticsearch.common.lucene.search.function.ScriptScoreFunction; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.functionscore.FunctionScoreQueryParser; import org.elasticsearch.script.SearchScript; import java.io.IOException; @@ -35,7 +37,7 @@ import java.util.Map; /** - * + * @deprecated use {@link FunctionScoreQueryParser} instead. */ public class CustomFiltersScoreQueryParser implements QueryParser { @@ -47,7 +49,7 @@ public CustomFiltersScoreQueryParser() { @Override public String[] names() { - return new String[]{NAME, Strings.toCamelCase(NAME)}; + return new String[] { NAME, Strings.toCamelCase(NAME) }; } @Override @@ -79,7 +81,8 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars } else if ("params".equals(currentFieldName)) { vars = parser.map(); } else { - throw new QueryParsingException(parseContext.index(), "[custom_filters_score] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext.index(), "[custom_filters_score] query does not support [" + + currentFieldName + "]"); } } else if (token == XContentParser.Token.START_ARRAY) { if ("filters".equals(currentFieldName)) { @@ -106,10 +109,12 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars } } if (script == null && fboost == -1) { - throw new QueryParsingException(parseContext.index(), "[custom_filters_score] missing 'script' or 'boost' in filters array element"); + throw new QueryParsingException(parseContext.index(), + "[custom_filters_score] missing 'script' or 'boost' in filters array element"); } if (!filterFound) { - throw new QueryParsingException(parseContext.index(), "[custom_filters_score] missing 'filter' in filters array element"); + throw new QueryParsingException(parseContext.index(), + "[custom_filters_score] missing 'filter' in filters array element"); } if (filter != null) { filters.add(filter); @@ -118,7 +123,8 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars } } } else { - throw new QueryParsingException(parseContext.index(), "[custom_filters_score] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext.index(), "[custom_filters_score] query does not support [" + + currentFieldName + "]"); } } else if (token.isValue()) { if ("lang".equals(currentFieldName)) { @@ -140,12 +146,14 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars } else if ("first".equals(sScoreMode)) { scoreMode = FiltersFunctionScoreQuery.ScoreMode.First; } else { - throw new QueryParsingException(parseContext.index(), "[custom_filters_score] illegal score_mode [" + sScoreMode + "]"); + throw new QueryParsingException(parseContext.index(), "[custom_filters_score] illegal score_mode [" + sScoreMode + + "]"); } } else if ("max_boost".equals(currentFieldName) || "maxBoost".equals(currentFieldName)) { maxBoost = parser.floatValue(); } else { - throw new QueryParsingException(parseContext.index(), "[custom_filters_score] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext.index(), "[custom_filters_score] query does not support [" + + currentFieldName + "]"); } } } @@ -169,7 +177,7 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars String script = scripts.get(i); if (script != null) { SearchScript searchScript = parseContext.scriptService().search(parseContext.lookup(), scriptLang, script, vars); - scoreFunction = new CustomScoreQueryParser.ScriptScoreFunction(script, vars, searchScript); + scoreFunction = new ScriptScoreFunction(script, vars, searchScript); } else { scoreFunction = new BoostScoreFunction(boosts.get(i)); } diff --git a/src/main/java/org/elasticsearch/index/query/CustomScoreQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/CustomScoreQueryBuilder.java index fa9784af4028b..3ec904d04e9aa 100644 --- a/src/main/java/org/elasticsearch/index/query/CustomScoreQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/CustomScoreQueryBuilder.java @@ -26,7 +26,10 @@ import java.util.Map; /** - * A query that uses a script to compute or influence the score of documents that match with the inner query or filter. + * A query that uses a script to compute or influence the score of documents + * that match with the inner query or filter. + * + * @deprecated use {@link FunctionScoreQueryBuilder} instead. */ public class CustomScoreQueryBuilder extends BaseQueryBuilder implements BoostableQueryBuilder { @@ -43,10 +46,13 @@ public class CustomScoreQueryBuilder extends BaseQueryBuilder implements Boostab private Map params = null; /** - * Constructs a query that defines how the scores are computed or influenced for documents that match with the - * specified query by a custom defined script. - * - * @param queryBuilder The query that defines what documents are custom scored by this query + * Constructs a query that defines how the scores are computed or influenced + * for documents that match with the specified query by a custom defined + * script. + * + * @param queryBuilder + * The query that defines what documents are custom scored by + * this query */ public CustomScoreQueryBuilder(QueryBuilder queryBuilder) { this.queryBuilder = queryBuilder; @@ -54,9 +60,12 @@ public CustomScoreQueryBuilder(QueryBuilder queryBuilder) { } /** - * Constructs a query that defines how documents are scored that match with the specified filter. - * - * @param filterBuilder The filter that decides with documents are scored by this query. + * Constructs a query that defines how documents are scored that match with + * the specified filter. + * + * @param filterBuilder + * The filter that decides with documents are scored by this + * query. */ public CustomScoreQueryBuilder(FilterBuilder filterBuilder) { this.filterBuilder = filterBuilder; @@ -103,8 +112,9 @@ public CustomScoreQueryBuilder param(String key, Object value) { } /** - * Sets the boost for this query. Documents matching this query will (in addition to the normal - * weightings) have their score multiplied by the boost provided. + * Sets the boost for this query. Documents matching this query will (in + * addition to the normal weightings) have their score multiplied by the + * boost provided. */ public CustomScoreQueryBuilder boost(float boost) { this.boost = boost; diff --git a/src/main/java/org/elasticsearch/index/query/CustomScoreQueryParser.java b/src/main/java/org/elasticsearch/index/query/CustomScoreQueryParser.java index 97675d1213043..30336d20522e0 100644 --- a/src/main/java/org/elasticsearch/index/query/CustomScoreQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/CustomScoreQueryParser.java @@ -19,24 +19,21 @@ package org.elasticsearch.index.query; -import org.apache.lucene.index.AtomicReaderContext; -import org.apache.lucene.search.Explanation; import org.apache.lucene.search.Filter; import org.apache.lucene.search.Query; import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.lucene.search.XConstantScoreQuery; import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery; -import org.elasticsearch.common.lucene.search.function.ScoreFunction; +import org.elasticsearch.common.lucene.search.function.ScriptScoreFunction; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.script.ExplainableSearchScript; import org.elasticsearch.script.SearchScript; import java.io.IOException; import java.util.Map; /** - * + * @deprecated use {@link FunctionScoreQueryParser} instead. */ public class CustomScoreQueryParser implements QueryParser { @@ -48,7 +45,7 @@ public CustomScoreQueryParser() { @Override public String[] names() { - return new String[]{NAME, Strings.toCamelCase(NAME)}; + return new String[] { NAME, Strings.toCamelCase(NAME) }; } @Override @@ -78,7 +75,8 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars } else if ("params".equals(currentFieldName)) { vars = parser.map(); } else { - throw new QueryParsingException(parseContext.index(), "[custom_score] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext.index(), "[custom_score] query does not support [" + currentFieldName + + "]"); } } else if (token.isValue()) { if ("script".equals(currentFieldName)) { @@ -88,7 +86,8 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars } else if ("boost".equals(currentFieldName)) { boost = parser.floatValue(); } else { - throw new QueryParsingException(parseContext.index(), "[custom_score] query does not support [" + currentFieldName + "]"); + throw new QueryParsingException(parseContext.index(), "[custom_score] query does not support [" + currentFieldName + + "]"); } } } @@ -114,64 +113,4 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars functionScoreQuery.setBoost(boost); return functionScoreQuery; } - - public static class ScriptScoreFunction implements ScoreFunction { - - private final String sScript; - - private final Map params; - - private final SearchScript script; - - public ScriptScoreFunction(String sScript, Map params, SearchScript script) { - this.sScript = sScript; - this.params = params; - this.script = script; - } - - @Override - public void setNextReader(AtomicReaderContext ctx) { - //LUCENE 4 UPGRADE should this pass on a ARC or just and atomic reader? - script.setNextReader(ctx); - } - - @Override - public float score(int docId, float subQueryScore) { - script.setNextDocId(docId); - script.setNextScore(subQueryScore); - return script.runAsFloat(); - } - - @Override - public float factor(int docId) { - // just the factor, so don't provide _score - script.setNextDocId(docId); - return script.runAsFloat(); - } - - @Override - public Explanation explainScore(int docId, Explanation subQueryExpl) { - Explanation exp; - if (script instanceof ExplainableSearchScript) { - script.setNextDocId(docId); - script.setNextScore(subQueryExpl.getValue()); - exp = ((ExplainableSearchScript) script).explain(subQueryExpl); - } else { - float score = score(docId, subQueryExpl.getValue()); - exp = new Explanation(score, "script score function: composed of:"); - exp.addDetail(subQueryExpl); - } - return exp; - } - - @Override - public Explanation explainFactor(int docId) { - return new Explanation(factor(docId), "scriptFactor"); - } - - @Override - public String toString() { - return "script[" + sScript + "], params [" + params + "]"; - } - } } \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/index/query/QueryBuilders.java b/src/main/java/org/elasticsearch/index/query/QueryBuilders.java index 56d1abc2e068c..29e0d557ba2c3 100644 --- a/src/main/java/org/elasticsearch/index/query/QueryBuilders.java +++ b/src/main/java/org/elasticsearch/index/query/QueryBuilders.java @@ -19,10 +19,11 @@ package org.elasticsearch.index.query; -import java.util.Collection; - import org.elasticsearch.common.Nullable; import org.elasticsearch.common.geo.builders.ShapeBuilder; +import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder; + +import java.util.Collection; /** * A static factory for simple "import static" usage. @@ -506,6 +507,7 @@ public static ConstantScoreQueryBuilder constantScoreQuery(QueryBuilder queryBui * A query that simply applies the boost fact to the wrapped query (multiplies it). * * @param queryBuilder The query to apply the boost factor to. + * @deprecated use {@link #functionScoreQuery(QueryBuilder)} instead */ public static CustomBoostFactorQueryBuilder customBoostFactorQuery(QueryBuilder queryBuilder) { return new CustomBoostFactorQueryBuilder(queryBuilder); @@ -515,6 +517,7 @@ public static CustomBoostFactorQueryBuilder customBoostFactorQuery(QueryBuilder * A query that allows to define a custom scoring script. * * @param queryBuilder The query to custom score + * @deprecated use {@link #functionScoreQuery(QueryBuilder)} instead */ public static CustomScoreQueryBuilder customScoreQuery(QueryBuilder queryBuilder) { return new CustomScoreQueryBuilder(queryBuilder); @@ -525,15 +528,38 @@ public static CustomScoreQueryBuilder customScoreQuery(QueryBuilder queryBuilder * with the specified filter. * * @param filterBuilder The filter that defines which documents are scored by a script. + * @deprecated use {@link #functionScoreQuery(QueryBuilder)} instead */ public static CustomScoreQueryBuilder customScoreQuery(FilterBuilder filterBuilder) { return new CustomScoreQueryBuilder(filterBuilder); } - + + /** + * @deprecated use {@link #functionScoreQuery(QueryBuilder)} instead + */ public static CustomFiltersScoreQueryBuilder customFiltersScoreQuery(QueryBuilder queryBuilder) { return new CustomFiltersScoreQueryBuilder(queryBuilder); } + /** + * A query that allows to define a custom scoring function. + * + * @param queryBuilder The query to custom score + * @param scoreFunctionBuilder The score function used to re-score the query + */ + public static FunctionScoreQueryBuilder functionScoreQuery(QueryBuilder queryBuilder) { + return new FunctionScoreQueryBuilder(queryBuilder); + } + + /** + * A query that allows to define a custom scoring function. + * + * @param filterBuilder The query to custom score + * @param scoreFunctionBuilder The score function used to re-score the query + */ + public static FunctionScoreQueryBuilder functionScoreQuery(FilterBuilder filterBuilder) { + return new FunctionScoreQueryBuilder(filterBuilder); + } /** * A more like this query that finds documents that are "like" the provided {@link MoreLikeThisQueryBuilder#likeText(String)} * which is checked against the fields the query is constructed with. diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreModule.java b/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreModule.java new file mode 100644 index 0000000000000..4cb2fc72e9762 --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreModule.java @@ -0,0 +1,53 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.google.common.collect.Lists; +import org.elasticsearch.common.inject.AbstractModule; +import org.elasticsearch.common.inject.multibindings.Multibinder; +import org.elasticsearch.index.query.functionscore.factor.FactorParser; +import org.elasticsearch.index.query.functionscore.script.ScriptScoreFunctionParser; + +import java.util.List; + +/** + * + */ +public class FunctionScoreModule extends AbstractModule { + + private List> parsers = Lists.newArrayList(); + + public FunctionScoreModule() { + registerParser(FactorParser.class); + registerParser(ScriptScoreFunctionParser.class); + } + + public void registerParser(Class parser) { + parsers.add(parser); + } + + @Override + protected void configure() { + Multibinder parserMapBinder = Multibinder.newSetBinder(binder(), ScoreFunctionParser.class); + for (Class clazz : parsers) { + parserMapBinder.addBinding().to(clazz); + } + bind(ScoreFunctionParserMapper.class); + } +} diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java new file mode 100644 index 0000000000000..ff2d96848ef09 --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java @@ -0,0 +1,136 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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 org.elasticsearch.ElasticSearchException; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.BaseQueryBuilder; +import org.elasticsearch.index.query.BoostableQueryBuilder; +import org.elasticsearch.index.query.FilterBuilder; +import org.elasticsearch.index.query.QueryBuilder; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * A query that uses a filters with a script associated with them to compute the + * score. + */ +public class FunctionScoreQueryBuilder extends BaseQueryBuilder implements BoostableQueryBuilder { + + private final QueryBuilder queryBuilder; + + private final FilterBuilder filterBuilder; + + private Float boost; + + private Float maxBoost; + + private String scoreMode; + + private ArrayList filters = new ArrayList(); + private ArrayList scoreFunctions = new ArrayList(); + + public FunctionScoreQueryBuilder(QueryBuilder queryBuilder) { + this.queryBuilder = queryBuilder; + this.filterBuilder = null; + } + + public FunctionScoreQueryBuilder(FilterBuilder filterBuilder) { + this.filterBuilder = filterBuilder; + this.queryBuilder = null; + } + + public FunctionScoreQueryBuilder add(FilterBuilder filter, ScoreFunctionBuilder scoreFunctionBuilder) { + this.filters.add(filter); + this.scoreFunctions.add(scoreFunctionBuilder); + return this; + } + + public FunctionScoreQueryBuilder add(ScoreFunctionBuilder scoreFunctionBuilder) { + this.filters.add(null); + this.scoreFunctions.add(scoreFunctionBuilder); + return this; + } + + public FunctionScoreQueryBuilder scoreMode(String scoreMode) { + this.scoreMode = scoreMode; + return this; + } + + public FunctionScoreQueryBuilder maxBoost(float maxBoost) { + this.maxBoost = maxBoost; + return this; + } + + /** + * Sets the boost for this query. Documents matching this query will (in + * addition to the normal weightings) have their score multiplied by the + * boost provided. + */ + public FunctionScoreQueryBuilder boost(float boost) { + this.boost = boost; + return this; + } + + @Override + protected void doXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(FunctionScoreQueryParser.NAME); + if (queryBuilder != null) { + builder.field("query"); + queryBuilder.toXContent(builder, params); + } else if (filterBuilder != null) { + builder.field("filter"); + filterBuilder.toXContent(builder, params); + } else { + throw new ElasticSearchException(FunctionScoreQueryParser.NAME + + " builder requires that either a filter or a query is defined!"); + } + // 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(); + builder.field("filter"); + filters.get(i).toXContent(builder, params); + scoreFunctions.get(i).toXContent(builder, params); + builder.endObject(); + } + builder.endArray(); + } + if (scoreMode != null) { + builder.field("score_mode", scoreMode); + } + if (maxBoost != null) { + builder.field("max_boost", maxBoost); + } + if (boost != null) { + builder.field("boost", boost); + } + + builder.endObject(); + } +} \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryParser.java b/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryParser.java new file mode 100644 index 0000000000000..44812cdb0c143 --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryParser.java @@ -0,0 +1,169 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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 org.apache.lucene.search.Filter; +import org.apache.lucene.search.Query; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.lucene.search.MatchAllDocsFilter; +import org.elasticsearch.common.lucene.search.XConstantScoreQuery; +import org.elasticsearch.common.lucene.search.function.FiltersFunctionScoreQuery; +import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery; +import org.elasticsearch.common.lucene.search.function.ScoreFunction; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.QueryParseContext; +import org.elasticsearch.index.query.QueryParser; +import org.elasticsearch.index.query.QueryParsingException; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * + */ +public class FunctionScoreQueryParser implements QueryParser { + + public static final String NAME = "function_score"; + ScoreFunctionParserMapper funtionParserMapper; + + @Inject + public FunctionScoreQueryParser(ScoreFunctionParserMapper funtionParserMapper) { + this.funtionParserMapper = funtionParserMapper; + } + + @Override + public String[] names() { + return new String[] { NAME, Strings.toCamelCase(NAME) }; + } + + @Override + public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException { + XContentParser parser = parseContext.parser(); + + Query query = null; + float boost = 1.0f; + + FiltersFunctionScoreQuery.ScoreMode scoreMode = FiltersFunctionScoreQuery.ScoreMode.Multiply; + ArrayList filterFunctions = new ArrayList(); + float maxBoost = Float.MAX_VALUE; + + String currentFieldName = null; + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if ("query".equals(currentFieldName)) { + query = parseContext.parseInnerQuery(); + } else if ("filter".equals(currentFieldName)) { + query = new XConstantScoreQuery(parseContext.parseInnerFilter()); + } else if ("score_mode".equals(currentFieldName) || "scoreMode".equals(currentFieldName)) { + scoreMode = parseScoreMode(parseContext, parser); + } else if ("max_boost".equals(currentFieldName) || "maxBoost".equals(currentFieldName)) { + maxBoost = parser.floatValue(); + } else if ("boost".equals(currentFieldName)) { + boost = parser.floatValue(); + } else if ("functions".equals(currentFieldName)) { + currentFieldName = parseFiltersAndFunctions(parseContext, parser, filterFunctions, currentFieldName); + } else { + // we tru to parse a score function. If there is no score + // function for the current field name, + // funtionParserMapper.get() will throw an Exception. + filterFunctions.add(new FiltersFunctionScoreQuery.FilterFunction(null, funtionParserMapper.get(parseContext.index(), + currentFieldName).parse(parseContext, parser))); + } + } + if (query == null) { + throw new QueryParsingException(parseContext.index(), NAME + " requires 'query' field"); + } + // if all filter elements returned null, just use the query + if (filterFunctions.isEmpty()) { + return query; + } + // 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) { + FunctionScoreQuery theQuery = new FunctionScoreQuery(query, filterFunctions.get(0).function); + theQuery.setBoost(boost); + theQuery.setMaxBoost(maxBoost); + return theQuery; + // in all other cases we create a FiltersFunctionScoreQuery. + } else { + FiltersFunctionScoreQuery functionScoreQuery = new FiltersFunctionScoreQuery(query, scoreMode, + filterFunctions.toArray(new FiltersFunctionScoreQuery.FilterFunction[filterFunctions.size()]), maxBoost); + functionScoreQuery.setBoost(boost); + return functionScoreQuery; + } + } + + private String parseFiltersAndFunctions(QueryParseContext parseContext, XContentParser parser, + ArrayList filterFunctions, String currentFieldName) throws IOException { + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + Filter filter = null; + ScoreFunction scoreFunction = 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); + } else { + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else { + if ("filter".equals(currentFieldName)) { + filter = parseContext.parseInnerFilter(); + } else { + // do not need to check null here, + // funtionParserMapper throws exception if parser + // non-existent + ScoreFunctionParser functionParser = funtionParserMapper.get(parseContext.index(), currentFieldName); + scoreFunction = functionParser.parse(parseContext, parser); + } + } + } + } + if (filter == null) { + filter = new MatchAllDocsFilter(); + } + filterFunctions.add(new FiltersFunctionScoreQuery.FilterFunction(filter, scoreFunction)); + + } + return currentFieldName; + } + + private FiltersFunctionScoreQuery.ScoreMode parseScoreMode(QueryParseContext parseContext, XContentParser parser) throws IOException { + String scoreMode = parser.text(); + if ("avg".equals(scoreMode)) { + return FiltersFunctionScoreQuery.ScoreMode.Avg; + } else if ("max".equals(scoreMode)) { + return FiltersFunctionScoreQuery.ScoreMode.Max; + } else if ("min".equals(scoreMode)) { + return FiltersFunctionScoreQuery.ScoreMode.Min; + } else if ("total".equals(scoreMode)) { + return FiltersFunctionScoreQuery.ScoreMode.Total; + } else if ("multiply".equals(scoreMode)) { + return FiltersFunctionScoreQuery.ScoreMode.Multiply; + } else if ("first".equals(scoreMode)) { + return FiltersFunctionScoreQuery.ScoreMode.First; + } else { + throw new QueryParsingException(parseContext.index(), NAME + " illegal score_mode [" + scoreMode + "]"); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilder.java b/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilder.java new file mode 100644 index 0000000000000..89e57db55ba5d --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionBuilder.java @@ -0,0 +1,28 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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 org.elasticsearch.common.xcontent.ToXContent; + +public interface ScoreFunctionBuilder extends ToXContent { + + public String getName(); + +} diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionParser.java b/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionParser.java new file mode 100644 index 0000000000000..07ff8a0f4a008 --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionParser.java @@ -0,0 +1,40 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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 org.elasticsearch.common.lucene.search.function.ScoreFunction; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.QueryParseContext; +import org.elasticsearch.index.query.QueryParsingException; + +import java.io.IOException; + +public interface ScoreFunctionParser { + + public ScoreFunction parse(QueryParseContext parseContext, XContentParser parser) throws IOException, QueryParsingException; + + /** + * Returns the name of the function, for example "linear", "gauss" etc. This + * name is used for registering the parser in + * {@link FunctionScoreQueryParser}. + * */ + public String[] getNames(); + +} diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionParserMapper.java b/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionParserMapper.java new file mode 100644 index 0000000000000..ad8f139446640 --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/functionscore/ScoreFunctionParserMapper.java @@ -0,0 +1,57 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.google.common.collect.ImmutableMap; +import org.elasticsearch.common.collect.MapBuilder; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.query.QueryParsingException; + +import java.util.Set; + +public class ScoreFunctionParserMapper { + + protected ImmutableMap functionParsers; + + @Inject + public ScoreFunctionParserMapper(Set parsers) { + MapBuilder builder = MapBuilder.newMapBuilder(); + for (ScoreFunctionParser scoreFunctionParser : parsers) { + for (String name : scoreFunctionParser.getNames()) { + builder.put(name, scoreFunctionParser); + } + } + this.functionParsers = builder.immutableMap(); + } + + public ScoreFunctionParser get(Index index, String parserName) { + ScoreFunctionParser functionParser = get(parserName); + if (functionParser == null) { + throw new QueryParsingException(index, "No function with the name [" + parserName + "] is registered."); + } + return functionParser; + } + + private ScoreFunctionParser get(String parserName) { + return functionParsers.get(parserName); + } + +} diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorBuilder.java b/src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorBuilder.java new file mode 100644 index 0000000000000..a2a1bbc964df9 --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorBuilder.java @@ -0,0 +1,57 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.factor; + +import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder; + +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * A query that simply applies the boost factor to another query (multiply it). + * + * + */ +public class FactorBuilder implements ScoreFunctionBuilder { + + private Float boostFactor; + + /** + * Sets the boost factor for this query. + */ + public FactorBuilder boostFactor(float boost) { + this.boostFactor = new Float(boost); + return this; + } + + @Override + public XContentBuilder toXContent(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]; + } +} \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorParser.java b/src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorParser.java new file mode 100644 index 0000000000000..1ed86ac8e5e9a --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/functionscore/factor/FactorParser.java @@ -0,0 +1,54 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.factor; + +import org.elasticsearch.index.query.functionscore.ScoreFunctionParser; + +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.lucene.search.function.BoostScoreFunction; +import org.elasticsearch.common.lucene.search.function.ScoreFunction; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.QueryParseContext; +import org.elasticsearch.index.query.QueryParsingException; + +import java.io.IOException; + +/** + * + */ +public class FactorParser implements ScoreFunctionParser { + + public static String[] NAMES = { "boost_factor", "boostFactor" }; + + @Inject + public FactorParser() { + } + + @Override + public ScoreFunction parse(QueryParseContext parseContext, XContentParser parser) throws IOException, QueryParsingException { + float boostFactor = parser.floatValue(); + return new BoostScoreFunction(boostFactor); + } + + @Override + public String[] getNames() { + return NAMES; + } +} diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionBuilder.java b/src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionBuilder.java new file mode 100644 index 0000000000000..c006689c98d20 --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionBuilder.java @@ -0,0 +1,99 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.script; + +import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder; + +import com.google.common.collect.Maps; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Map; + +/** + * A query 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 { + + private String script; + + private String lang; + + private Map params = null; + + public ScriptScoreFunctionBuilder() { + + } + + public ScriptScoreFunctionBuilder script(String script) { + this.script = script; + return this; + } + + /** + * Sets the language of the script. + */ + public ScriptScoreFunctionBuilder lang(String lang) { + this.lang = lang; + return this; + } + + /** + * Additional parameters that can be provided to the script. + */ + public ScriptScoreFunctionBuilder params(Map params) { + if (this.params == null) { + this.params = params; + } else { + this.params.putAll(params); + } + return this; + } + + /** + * Additional parameters that can be provided to the script. + */ + public ScriptScoreFunctionBuilder param(String key, Object value) { + if (params == null) { + params = Maps.newHashMap(); + } + params.put(key, value); + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(getName()); + builder.field("script", script); + if (lang != null) { + builder.field("lang", lang); + } + if (this.params != null) { + builder.field("params", this.params); + } + return builder.endObject(); + } + + @Override + public String getName() { + return ScriptScoreFunctionParser.NAMES[0]; + } +} \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionParser.java b/src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionParser.java new file mode 100644 index 0000000000000..0bd00b3c23bba --- /dev/null +++ b/src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionParser.java @@ -0,0 +1,92 @@ +/* + * Licensed to ElasticSearch and Shay Banon 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.script; + +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.lucene.search.function.ScoreFunction; +import org.elasticsearch.common.lucene.search.function.ScriptScoreFunction; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.query.QueryParseContext; +import org.elasticsearch.index.query.QueryParsingException; +import org.elasticsearch.index.query.functionscore.ScoreFunctionParser; +import org.elasticsearch.script.SearchScript; + +import java.io.IOException; +import java.util.Map; + +/** + * + */ +public class ScriptScoreFunctionParser implements ScoreFunctionParser { + + public static String[] NAMES = { "script_score", "scriptScore" }; + + @Inject + public ScriptScoreFunctionParser() { + } + + @Override + public String[] getNames() { + return NAMES; + } + + @Override + public ScoreFunction parse(QueryParseContext parseContext, XContentParser parser) throws IOException, QueryParsingException { + + String script = null; + String scriptLang = null; + Map vars = null; + + String currentFieldName = null; + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_OBJECT) { + if ("params".equals(currentFieldName)) { + vars = parser.map(); + } else { + throw new QueryParsingException(parseContext.index(), NAMES[0] + " query does not support [" + currentFieldName + "]"); + } + } else if (token.isValue()) { + if ("script".equals(currentFieldName)) { + script = parser.text(); + } else if ("lang".equals(currentFieldName)) { + scriptLang = parser.text(); + } else { + throw new QueryParsingException(parseContext.index(), NAMES[0] + " query does not support [" + currentFieldName + "]"); + } + } + } + + if (script == null) { + throw new QueryParsingException(parseContext.index(), NAMES[0] + " requires 'script' field"); + } + + SearchScript searchScript; + try { + searchScript = parseContext.scriptService().search(parseContext.lookup(), scriptLang, script, vars); + return new ScriptScoreFunction(script, vars, searchScript); + } catch (Exception e) { + throw new QueryParsingException(parseContext.index(), NAMES[0] + " the script could not be loaded", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java b/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java index d49a82994b9e5..46e70cf96ecd4 100644 --- a/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java +++ b/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java @@ -24,6 +24,7 @@ import org.elasticsearch.common.inject.AbstractModule; import org.elasticsearch.common.inject.multibindings.Multibinder; import org.elasticsearch.index.query.*; +import org.elasticsearch.index.query.functionscore.FunctionScoreQueryParser; import java.util.Set; @@ -104,6 +105,7 @@ protected void configure() { qpBinders.addBinding().to(IndicesQueryParser.class).asEagerSingleton(); qpBinders.addBinding().to(CommonTermsQueryParser.class).asEagerSingleton(); qpBinders.addBinding().to(SpanMultiTermQueryParser.class).asEagerSingleton(); + qpBinders.addBinding().to(FunctionScoreQueryParser.class).asEagerSingleton(); if (ShapesAvailability.JTS_AVAILABLE) { qpBinders.addBinding().to(GeoShapeQueryParser.class).asEagerSingleton(); diff --git a/src/main/java/org/elasticsearch/search/SearchModule.java b/src/main/java/org/elasticsearch/search/SearchModule.java index c5dd3b4197b69..2d05bff5f6d78 100644 --- a/src/main/java/org/elasticsearch/search/SearchModule.java +++ b/src/main/java/org/elasticsearch/search/SearchModule.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.inject.AbstractModule; import org.elasticsearch.common.inject.Module; import org.elasticsearch.common.inject.SpawnModules; +import org.elasticsearch.index.query.functionscore.FunctionScoreModule; import org.elasticsearch.search.action.SearchServiceTransportAction; import org.elasticsearch.search.controller.SearchPhaseController; import org.elasticsearch.search.dfs.DfsPhase; @@ -46,7 +47,7 @@ public class SearchModule extends AbstractModule implements SpawnModules { @Override public Iterable spawnModules() { - return ImmutableList.of(new TransportSearchModule(), new FacetModule(), new HighlightModule(), new SuggestModule()); + return ImmutableList.of(new TransportSearchModule(), new FacetModule(), new HighlightModule(), new SuggestModule(), new FunctionScoreModule()); } @Override diff --git a/src/test/java/org/elasticsearch/test/integration/search/basic/TransportTwoNodesSearchTests.java b/src/test/java/org/elasticsearch/test/integration/search/basic/TransportTwoNodesSearchTests.java index 96d5405b6acd6..cc0435c7bf4f6 100644 --- a/src/test/java/org/elasticsearch/test/integration/search/basic/TransportTwoNodesSearchTests.java +++ b/src/test/java/org/elasticsearch/test/integration/search/basic/TransportTwoNodesSearchTests.java @@ -19,6 +19,8 @@ package org.elasticsearch.test.integration.search.basic; + + import com.google.common.base.Charsets; import com.google.common.collect.Sets; import org.elasticsearch.ElasticSearchException; @@ -29,6 +31,7 @@ import org.elasticsearch.client.Requests; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.index.query.functionscore.script.ScriptScoreFunctionBuilder; import org.elasticsearch.search.Scroll; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.builder.SearchSourceBuilder; @@ -409,4 +412,29 @@ public void testFailedMultiSearchWithWrongQuery() throws Exception { logger.info("Done Testing failed search"); } + + + @Test + public void testFailedMultiSearchWithWrongQuery_withFunctionScore() throws Exception { + prepareData(); + + logger.info("Start Testing failed multi search with a wrong query"); + + MultiSearchResponse response = client().prepareMultiSearch() + // Add custom score query with missing script + .add(client().prepareSearch("test").setQuery(QueryBuilders.functionScoreQuery(QueryBuilders.termQuery("nid", 1)).add(new ScriptScoreFunctionBuilder()))) + .add(client().prepareSearch("test").setQuery(QueryBuilders.termQuery("nid", 2))) + .add(client().prepareSearch("test").setQuery(QueryBuilders.matchAllQuery())) + .execute().actionGet(); + assertThat(response.getResponses().length, equalTo(3)); + assertThat(response.getResponses()[0].getFailureMessage(), notNullValue()); + + assertThat(response.getResponses()[1].getFailureMessage(), nullValue()); + assertThat(response.getResponses()[1].getResponse().getHits().hits().length, equalTo(1)); + + assertThat(response.getResponses()[2].getFailureMessage(), nullValue()); + assertThat(response.getResponses()[2].getResponse().getHits().hits().length, equalTo(10)); + + logger.info("Done Testing failed search"); + } } diff --git a/src/test/java/org/elasticsearch/test/integration/search/child/SimpleChildQuerySearchTests.java b/src/test/java/org/elasticsearch/test/integration/search/child/SimpleChildQuerySearchTests.java index b88ace8dca996..7b368338ef46f 100644 --- a/src/test/java/org/elasticsearch/test/integration/search/child/SimpleChildQuerySearchTests.java +++ b/src/test/java/org/elasticsearch/test/integration/search/child/SimpleChildQuerySearchTests.java @@ -19,6 +19,8 @@ package org.elasticsearch.test.integration.search.child; + + import org.elasticsearch.ElasticSearchException; import org.elasticsearch.action.count.CountResponse; import org.elasticsearch.action.index.IndexRequestBuilder; @@ -28,6 +30,7 @@ import org.elasticsearch.common.Priority; import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.index.query.functionscore.script.ScriptScoreFunctionBuilder; import org.elasticsearch.search.facet.terms.TermsFacet; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.search.sort.SortOrder; @@ -53,8 +56,7 @@ public class SimpleChildQuerySearchTests extends AbstractSharedClusterTest { @Test public void multiLevelChild() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); - + client().admin().indices().prepareCreate("test") .setSettings( ImmutableSettings.settingsBuilder() @@ -144,7 +146,6 @@ public void multiLevelChild() throws Exception { @Test // see #2744 public void test2744() throws ElasticSearchException, IOException { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test") .setSettings( @@ -395,7 +396,6 @@ public void testCachingBug_withFqueryFilter() throws Exception { @Test public void testHasParentFilter() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test") .setSettings( ImmutableSettings.settingsBuilder() @@ -457,7 +457,6 @@ public void testHasParentFilter() throws Exception { @Test public void simpleChildQueryWithFlush() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test") .setSettings( @@ -557,7 +556,6 @@ public void simpleChildQueryWithFlush() throws Exception { @Test public void simpleChildQueryWithFlushAnd3Shards() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test").setSettings( ImmutableSettings.settingsBuilder() @@ -656,7 +654,6 @@ public void simpleChildQueryWithFlushAnd3Shards() throws Exception { @Test public void testScopedFacet() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test") .setSettings( @@ -707,7 +704,6 @@ public void testScopedFacet() throws Exception { @Test public void testDeletedParent() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test") .setSettings( @@ -773,7 +769,6 @@ public void testDeletedParent() throws Exception { @Test public void testDfsSearchType() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test").setSettings( ImmutableSettings.settingsBuilder() @@ -813,7 +808,6 @@ public void testDfsSearchType() throws Exception { @Test public void testFixAOBEIfTopChildrenIsWrappedInMusNotClause() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test").setSettings(ImmutableSettings.settingsBuilder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0)).execute().actionGet(); client().admin().cluster().prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForGreenStatus().execute().actionGet(); @@ -839,7 +833,6 @@ public void testFixAOBEIfTopChildrenIsWrappedInMusNotClause() throws Exception { @Test public void testTopChildrenReSearchBug() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test") .setSettings( @@ -893,7 +886,6 @@ public void testTopChildrenReSearchBug() throws Exception { @Test public void testHasChildAndHasParentFailWhenSomeSegmentsDontContainAnyParentOrChildDocs() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test").setSettings( ImmutableSettings.settingsBuilder() @@ -936,7 +928,6 @@ public void testHasChildAndHasParentFailWhenSomeSegmentsDontContainAnyParentOrCh @Test public void testCountApiUsage() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test") .setSettings( @@ -992,7 +983,6 @@ public void testCountApiUsage() throws Exception { @Test public void testScoreForParentChildQueries() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test") .addMapping("child", jsonBuilder() @@ -1018,79 +1008,8 @@ public void testScoreForParentChildQueries() throws Exception { ).execute().actionGet(); client().admin().cluster().prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForGreenStatus().execute().actionGet(); - // Parent 1 and its children - client().prepareIndex("test", "parent", "1") - .setSource("p_field", "p_value1") - .execute().actionGet(); - client().prepareIndex("test", "child", "1") - .setSource("c_field1", 1, "c_field2", 0) - .setParent("1").execute().actionGet(); - client().prepareIndex("test", "child", "2") - .setSource("c_field1", 1, "c_field2", 0) - .setParent("1").execute().actionGet(); - client().prepareIndex("test", "child", "3") - .setSource("c_field1", 2, "c_field2", 0) - .setParent("1").execute().actionGet(); - client().prepareIndex("test", "child", "4") - .setSource("c_field1", 2, "c_field2", 0) - .setParent("1").execute().actionGet(); - client().prepareIndex("test", "child", "5") - .setSource("c_field1", 1, "c_field2", 1) - .setParent("1").execute().actionGet(); - client().prepareIndex("test", "child", "6") - .setSource("c_field1", 1, "c_field2", 2) - .setParent("1").execute().actionGet(); - - // Parent 2 and its children - client().prepareIndex("test", "parent", "2") - .setSource("p_field", "p_value2") - .execute().actionGet(); - client().prepareIndex("test", "child", "7") - .setSource("c_field1", 3, "c_field2", 0) - .setParent("2").execute().actionGet(); - client().prepareIndex("test", "child", "8") - .setSource("c_field1", 1, "c_field2", 1) - .setParent("2").execute().actionGet(); - client().prepareIndex("test", "child", "9") - .setSource("c_field1", 1, "c_field2", 1) - .setParent("p").execute().actionGet(); - client().prepareIndex("test", "child", "10") - .setSource("c_field1", 1, "c_field2", 1) - .setParent("2").execute().actionGet(); - client().prepareIndex("test", "child", "11") - .setSource("c_field1", 1, "c_field2", 1) - .setParent("2").execute().actionGet(); - client().prepareIndex("test", "child", "12") - .setSource("c_field1", 1, "c_field2", 2) - .setParent("2").execute().actionGet(); - - // Parent 3 and its children - client().prepareIndex("test", "parent", "3") - .setSource("p_field1", "p_value3", "p_field2", 5) - .execute().actionGet(); - client().prepareIndex("test", "child", "13") - .setSource("c_field1", 4, "c_field2", 0, "c_field3", 0) - .setParent("3").execute().actionGet(); - client().prepareIndex("test", "child", "14") - .setSource("c_field1", 1, "c_field2", 1, "c_field3", 1) - .setParent("3").execute().actionGet(); - client().prepareIndex("test", "child", "15") - .setSource("c_field1", 1, "c_field2", 2, "c_field3", 2) - .setParent("3").execute().actionGet(); - client().prepareIndex("test", "child", "16") - .setSource("c_field1", 1, "c_field2", 2, "c_field3", 3) - .setParent("3").execute().actionGet(); - client().prepareIndex("test", "child", "17") - .setSource("c_field1", 1, "c_field2", 2, "c_field3", 4) - .setParent("3").execute().actionGet(); - client().prepareIndex("test", "child", "18") - .setSource("c_field1", 1, "c_field2", 2, "c_field3", 5) - .setParent("3").execute().actionGet(); - client().prepareIndex("test", "child1", "1") - .setSource("c_field1", 1, "c_field2", 2, "c_field3", 6) - .setParent("3").execute().actionGet(); - - client().admin().indices().prepareRefresh().execute().actionGet(); + indexRandom("test", false, createDocBuilders().toArray(new IndexRequestBuilder[0])); + refresh(); SearchResponse response = client().prepareSearch("test") .setQuery( @@ -1179,10 +1098,164 @@ public void testScoreForParentChildQueries() throws Exception { assertThat(response.getHits().hits()[6].score(), equalTo(5f)); } + List createDocBuilders() { + List indexBuilders = new ArrayList(); + // Parent 1 and its children + indexBuilders.add(new IndexRequestBuilder(client()).setType("parent").setId("1").setIndex("test").setSource("p_field", "p_value1")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("1").setIndex("test").setSource("c_field1", 1, "c_field2", 0).setParent("1")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("2").setIndex("test").setSource("c_field1", 1, "c_field2", 0).setParent("1")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("3").setIndex("test").setSource("c_field1", 2, "c_field2", 0).setParent("1")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("4").setIndex("test").setSource("c_field1", 2, "c_field2", 0).setParent("1")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("5").setIndex("test").setSource("c_field1", 1, "c_field2", 1).setParent("1")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("6").setIndex("test").setSource("c_field1", 1, "c_field2", 2).setParent("1")); + + + // Parent 2 and its children + indexBuilders.add(new IndexRequestBuilder(client()).setType("parent").setId("2").setIndex("test").setSource("p_field", "p_value2")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("7").setIndex("test").setSource("c_field1", 3, "c_field2", 0).setParent("2")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("8").setIndex("test").setSource("c_field1", 1, "c_field2", 1).setParent("2")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("9").setIndex("test").setSource("c_field1", 1, "c_field2", 1).setParent("p")); //why "p"???? + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("10").setIndex("test").setSource("c_field1", 1, "c_field2", 1).setParent("2")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("11").setIndex("test").setSource("c_field1", 1, "c_field2", 1).setParent("2")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("12").setIndex("test").setSource("c_field1", 1, "c_field2", 2).setParent("2")); + + + + // Parent 3 and its children + + indexBuilders.add(new IndexRequestBuilder(client()).setType("parent").setId("3").setIndex("test").setSource("p_field1", "p_value3", "p_field2", 5)); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("13").setIndex("test").setSource("c_field1", 4, "c_field2", 0, "c_field3", 0).setParent("3")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("14").setIndex("test").setSource("c_field1", 1, "c_field2", 1, "c_field3", 1).setParent("3")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("15").setIndex("test").setSource("c_field1", 1, "c_field2", 2, "c_field3", 2).setParent("3")); //why "p"???? + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("16").setIndex("test").setSource("c_field1", 1, "c_field2", 2, "c_field3", 3).setParent("3")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("17").setIndex("test").setSource("c_field1", 1, "c_field2", 2, "c_field3", 4).setParent("3")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child").setId("18").setIndex("test").setSource("c_field1", 1, "c_field2", 2, "c_field3", 5).setParent("3")); + indexBuilders.add(new IndexRequestBuilder(client()).setType("child1").setId("1").setIndex("test").setSource("c_field1", 1, "c_field2", 2, "c_field3", 6).setParent("3")); + + + return indexBuilders; + } + + + @Test + public void testScoreForParentChildQueries_withFunctionScore() throws Exception { + + client().admin().indices().prepareCreate("test") + .addMapping("child", jsonBuilder() + .startObject() + .startObject("type") + .startObject("_parent") + .field("type", "parent") + .endObject() + .endObject() + .endObject() + ).addMapping("child1", jsonBuilder() + .startObject() + .startObject("type") + .startObject("_parent") + .field("type", "parent") + .endObject() + .endObject() + .endObject() + ).setSettings( + ImmutableSettings.settingsBuilder() + .put("index.number_of_shards", 2) + .put("index.number_of_replicas", 0) + ).execute().actionGet(); + client().admin().cluster().prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForGreenStatus().execute().actionGet(); + + indexRandom("test", false, createDocBuilders().toArray(new IndexRequestBuilder[0])); + refresh(); + SearchResponse response = client().prepareSearch("test") + .setQuery( + QueryBuilders.hasChildQuery( + "child", + QueryBuilders.functionScoreQuery( + matchQuery("c_field2", 0) + ).add(new ScriptScoreFunctionBuilder().script("doc['c_field1'].value") + )).scoreType("sum") + ) + .execute().actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("1")); + assertThat(response.getHits().hits()[0].score(), equalTo(6f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(4f)); + assertThat(response.getHits().hits()[2].id(), equalTo("2")); + assertThat(response.getHits().hits()[2].score(), equalTo(3f)); + + response = client().prepareSearch("test") + .setQuery( + QueryBuilders.hasChildQuery( + "child", + QueryBuilders.functionScoreQuery( + matchQuery("c_field2", 0) + ).add(new ScriptScoreFunctionBuilder().script("doc['c_field1'].value") + )).scoreType("max") + ) + .execute().actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("3")); + assertThat(response.getHits().hits()[0].score(), equalTo(4f)); + assertThat(response.getHits().hits()[1].id(), equalTo("2")); + assertThat(response.getHits().hits()[1].score(), equalTo(3f)); + assertThat(response.getHits().hits()[2].id(), equalTo("1")); + assertThat(response.getHits().hits()[2].score(), equalTo(2f)); + + response = client().prepareSearch("test") + .setQuery( + QueryBuilders.hasChildQuery( + "child", + QueryBuilders.functionScoreQuery( + matchQuery("c_field2", 0) + ).add(new ScriptScoreFunctionBuilder().script("doc['c_field1'].value") + )).scoreType("avg") + ) + .execute().actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("3")); + assertThat(response.getHits().hits()[0].score(), equalTo(4f)); + assertThat(response.getHits().hits()[1].id(), equalTo("2")); + assertThat(response.getHits().hits()[1].score(), equalTo(3f)); + assertThat(response.getHits().hits()[2].id(), equalTo("1")); + assertThat(response.getHits().hits()[2].score(), equalTo(1.5f)); + + response = client().prepareSearch("test") + .setQuery( + QueryBuilders.hasParentQuery( + "parent", + QueryBuilders.functionScoreQuery( + matchQuery("p_field1", "p_value3") + ).add(new ScriptScoreFunctionBuilder().script("doc['p_field2'].value") + )).scoreType("score") + ) + .addSort(SortBuilders.fieldSort("c_field3")) + .addSort(SortBuilders.scoreSort()) + .execute().actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(7l)); + assertThat(response.getHits().hits()[0].id(), equalTo("13")); + assertThat(response.getHits().hits()[0].score(), equalTo(5f)); + assertThat(response.getHits().hits()[1].id(), equalTo("14")); + assertThat(response.getHits().hits()[1].score(), equalTo(5f)); + assertThat(response.getHits().hits()[2].id(), equalTo("15")); + assertThat(response.getHits().hits()[2].score(), equalTo(5f)); + assertThat(response.getHits().hits()[3].id(), equalTo("16")); + assertThat(response.getHits().hits()[3].score(), equalTo(5f)); + assertThat(response.getHits().hits()[4].id(), equalTo("17")); + assertThat(response.getHits().hits()[4].score(), equalTo(5f)); + assertThat(response.getHits().hits()[5].id(), equalTo("18")); + assertThat(response.getHits().hits()[5].score(), equalTo(5f)); + assertThat(response.getHits().hits()[6].id(), equalTo("1")); + assertThat(response.getHits().hits()[6].score(), equalTo(5f)); + } + @Test // https://github.com/elasticsearch/elasticsearch/issues/2536 public void testParentChildQueriesCanHandleNoRelevantTypesInIndex() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test") .addMapping("parent", jsonBuilder() @@ -1242,7 +1315,6 @@ public void testParentChildQueriesCanHandleNoRelevantTypesInIndex() throws Excep @Test public void testHasChildAndHasParentFilter_withFilter() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test").setSettings( ImmutableSettings.settingsBuilder() @@ -1287,7 +1359,6 @@ public void testHasChildAndHasParentFilter_withFilter() throws Exception { @Test public void testSimpleQueryRewrite() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test").setSettings( ImmutableSettings.settingsBuilder() @@ -1364,7 +1435,6 @@ public void testSimpleQueryRewrite() throws Exception { @Test // See also issue: https://github.com/elasticsearch/elasticsearch/issues/3144 public void testReIndexingParentAndChildDocuments() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test") .setSettings( @@ -1444,7 +1514,6 @@ public void testReIndexingParentAndChildDocuments() throws Exception { @Test // See also issue: https://github.com/elasticsearch/elasticsearch/issues/3203 public void testHasChildQueryWithMinimumScore() throws Exception { - client().admin().indices().prepareDelete().execute().actionGet(); client().admin().indices().prepareCreate("test") .setSettings( diff --git a/src/test/java/org/elasticsearch/test/integration/search/customscore/CustomScoreSearchTests.java b/src/test/java/org/elasticsearch/test/integration/search/customscore/CustomScoreSearchTests.java index 5691563750f59..fa2c55190bca3 100644 --- a/src/test/java/org/elasticsearch/test/integration/search/customscore/CustomScoreSearchTests.java +++ b/src/test/java/org/elasticsearch/test/integration/search/customscore/CustomScoreSearchTests.java @@ -19,6 +19,8 @@ package org.elasticsearch.test.integration.search.customscore; + + import org.apache.lucene.search.Explanation; import org.elasticsearch.ElasticSearchException; import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; @@ -26,10 +28,13 @@ import org.elasticsearch.action.search.SearchType; import org.elasticsearch.common.Priority; import org.elasticsearch.index.query.FilterBuilders; +import org.elasticsearch.index.query.functionscore.factor.FactorBuilder; +import org.elasticsearch.index.query.functionscore.script.ScriptScoreFunctionBuilder; import org.elasticsearch.test.integration.AbstractSharedClusterTest; import org.junit.Test; import java.io.IOException; +import java.util.Arrays; import static org.elasticsearch.client.Requests.*; import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; @@ -123,6 +128,76 @@ public void testScoreExplainBug_2283() throws Exception { assertThat(explanation.getDetails()[1].getDetails()[2].getValue(), equalTo(2f)); } + + + @Test + public void testScoreExplainBug_2283_withFunctionScore() throws Exception { + client().admin().indices().prepareDelete().execute().actionGet(); + client().admin().indices().prepareCreate("test").setSettings(settingsBuilder().put("index.number_of_shards", 1)).execute().actionGet(); + ClusterHealthResponse healthResponse = client().admin().cluster().prepareHealth("test").setWaitForYellowStatus().execute().actionGet(); + assertThat(healthResponse.isTimedOut(), equalTo(false)); + + client().prepareIndex("test", "type", "1").setSource("field", "value1", "color", "red").execute().actionGet(); + client().prepareIndex("test", "type", "2").setSource("field", "value2", "color", "blue").execute().actionGet(); + client().prepareIndex("test", "type", "3").setSource("field", "value3", "color", "red").execute().actionGet(); + client().prepareIndex("test", "type", "4").setSource("field", "value4", "color", "blue").execute().actionGet(); + + client().admin().indices().prepareRefresh().execute().actionGet(); + + SearchResponse searchResponse = client().prepareSearch("test") + .setQuery(functionScoreQuery(matchAllQuery()).scoreMode("first").add(termFilter("field", "value4"), new ScriptScoreFunctionBuilder().script("2")).add(termFilter("field", "value2"), new ScriptScoreFunctionBuilder().script("3"))) + .setExplain(true) + .execute().actionGet(); + + assertThat(Arrays.toString(searchResponse.getShardFailures()), searchResponse.getFailedShards(), equalTo(0)); + + assertThat(searchResponse.getHits().totalHits(), equalTo(4l)); + assertThat(searchResponse.getHits().getAt(0).id(), equalTo("2")); + assertThat(searchResponse.getHits().getAt(0).score(), equalTo(3.0f)); + logger.info("--> Hit[0] {} Explanation:\n {}", searchResponse.getHits().getAt(0).id(), searchResponse.getHits().getAt(0).explanation()); + Explanation explanation = searchResponse.getHits().getAt(0).explanation(); + assertNotNull(explanation); + assertThat(explanation.isMatch(), equalTo(true)); + assertThat(explanation.getValue(), equalTo(3f)); + assertThat(explanation.getDescription(), equalTo("custom score, score mode [first]")); + + assertThat(explanation.getDetails().length, equalTo(2)); + assertThat(explanation.getDetails()[0].isMatch(), equalTo(true)); + assertThat(explanation.getDetails()[0].getValue(), equalTo(1f)); + assertThat(explanation.getDetails()[0].getDetails().length, equalTo(2)); + assertThat(explanation.getDetails()[1].isMatch(), equalTo(true)); + assertThat(explanation.getDetails()[1].getValue(), equalTo(3f)); + assertThat(explanation.getDetails()[1].getDetails().length, equalTo(3)); + + // Same query but with boost + searchResponse = client().prepareSearch("test") + .setQuery(functionScoreQuery(matchAllQuery()).scoreMode("first").add(termFilter("field", "value4"), new ScriptScoreFunctionBuilder().script("2")).add(termFilter("field", "value2"), new ScriptScoreFunctionBuilder().script("3")).boost(2)) + .setExplain(true) + .execute().actionGet(); + + assertThat(Arrays.toString(searchResponse.getShardFailures()), searchResponse.getFailedShards(), equalTo(0)); + + assertThat(searchResponse.getHits().totalHits(), equalTo(4l)); + assertThat(searchResponse.getHits().getAt(0).id(), equalTo("2")); + assertThat(searchResponse.getHits().getAt(0).score(), equalTo(6f)); + logger.info("--> Hit[0] {} Explanation:\n {}", searchResponse.getHits().getAt(0).id(), searchResponse.getHits().getAt(0).explanation()); + explanation = searchResponse.getHits().getAt(0).explanation(); + assertNotNull(explanation); + assertThat(explanation.isMatch(), equalTo(true)); + assertThat(explanation.getValue(), equalTo(6f)); + assertThat(explanation.getDescription(), equalTo("custom score, score mode [first]")); + + assertThat(explanation.getDetails().length, equalTo(2)); + assertThat(explanation.getDetails()[0].isMatch(), equalTo(true)); + assertThat(explanation.getDetails()[0].getValue(), equalTo(1f)); + assertThat(explanation.getDetails()[0].getDetails().length, equalTo(2)); + assertThat(explanation.getDetails()[1].isMatch(), equalTo(true)); + assertThat(explanation.getDetails()[1].getValue(), equalTo(6f)); + assertThat(explanation.getDetails()[1].getDetails().length, equalTo(3)); + assertThat(explanation.getDetails()[1].getDetails()[2].getDescription(), equalTo("queryBoost")); + assertThat(explanation.getDetails()[1].getDetails()[2].getValue(), equalTo(2f)); + } + @Test public void testMultiValueCustomScriptBoost() throws ElasticSearchException, IOException { client().admin().indices().prepareDelete().execute().actionGet(); @@ -223,6 +298,108 @@ public void testMultiValueCustomScriptBoost() throws ElasticSearchException, IOE assertThat(response.getHits().getAt(1).id(), equalTo("1")); } + + + @Test + public void testMultiValueCustomScriptBoost_withFunctionScore() throws ElasticSearchException, IOException { + client().admin().indices().prepareDelete().execute().actionGet(); + + client().admin().indices().prepareCreate("test") + .setSettings(settingsBuilder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0)) + .addMapping("type", jsonBuilder().startObject().startObject("type").startObject("properties") + .startObject("snum").field("type", "string").endObject() + .startObject("dnum").field("type", "double").endObject() + .startObject("slnum").field("type", "long").endObject() + .startObject("gp").field("type", "geo_point").endObject() + .endObject().endObject().endObject()) + .execute().actionGet(); + client().admin().cluster().prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForGreenStatus().execute().actionGet(); + + String[] values = new String[100]; + String[] gp = new String[100]; + + long[] lValues = new long[100]; + double[] dValues = new double[100]; + int offset = 1; + for (int i = 0; i < values.length; i++) { + values[i] = ""+ (i + offset); + gp[i] = ""+ (i + offset) + ","+ (i + offset); + lValues[i] = (i + offset); + dValues[i] = (i + offset); + } + client().index(indexRequest("test").type("type1").id("1") + .source(jsonBuilder().startObject().field("test", "value check") + .field("snum", values) + .field("dnum", dValues) + .field("lnum", lValues) + .field("gp", gp) + .endObject())).actionGet(); + offset++; + for (int i = 0; i < values.length; i++) { + values[i] = ""+ (i + offset); + gp[i] = ""+ (i + offset) + ","+ (i + offset); + lValues[i] = (i + offset); + dValues[i] = (i + offset); + } + client().index(indexRequest("test").type("type1").id("2") + .source(jsonBuilder().startObject().field("test", "value check") + .field("snum", values) + .field("dnum", dValues) + .field("lnum", lValues) + .field("gp", gp) + .endObject())).actionGet(); + client().admin().indices().refresh(refreshRequest()).actionGet(); + + logger.info("running min(doc['num1'].value)"); + SearchResponse response = client().search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value")).add(new ScriptScoreFunctionBuilder() + .script("c_min = 1000; foreach (x : doc['snum'].values) { c_min = min(Integer.parseInt(x), c_min) } return c_min")))) + ).actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation()); + assertThat(response.getHits().getAt(0).id(), equalTo("2")); + assertThat(response.getHits().getAt(1).id(), equalTo("1")); + + response = client().search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value")).add(new ScriptScoreFunctionBuilder() + .script("c_min = 1000; foreach (x : doc['lnum'].values) { c_min = min(x, c_min) } return c_min")))) + ).actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation()); + assertThat(response.getHits().getAt(0).id(), equalTo("2")); + assertThat(response.getHits().getAt(1).id(), equalTo("1")); + + response = client().search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value")).add(new ScriptScoreFunctionBuilder() + .script("c_min = 1000; foreach (x : doc['dnum'].values) { c_min = min(x, c_min) } return c_min")))) + ).actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation()); + assertThat(response.getHits().getAt(0).id(), equalTo("2")); + assertThat(response.getHits().getAt(1).id(), equalTo("1")); + + response = client().search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value")).add(new ScriptScoreFunctionBuilder() + .script("c_min = 1000; foreach (x : doc['gp'].values) { c_min = min(x.lat, c_min) } return c_min")))) + ).actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation()); + assertThat(response.getHits().getAt(0).id(), equalTo("2")); + assertThat(response.getHits().getAt(1).id(), equalTo("1")); + } + @Test public void testCustomScriptBoost() throws Exception { client().admin().indices().prepareDelete().execute().actionGet(); @@ -323,6 +500,108 @@ public void testCustomScriptBoost() throws Exception { assertThat(response.getHits().getAt(1).score(), equalTo(4f)); // _score is always 1 } + + + @Test + public void testCustomScriptBoost_withFunctionScore() throws Exception { + client().admin().indices().prepareDelete().execute().actionGet(); + client().admin().indices().prepareCreate("test").setSettings(settingsBuilder().put("index.number_of_shards", 1)).execute().actionGet(); + + client().index(indexRequest("test").type("type1").id("1") + .source(jsonBuilder().startObject().field("test", "value beck").field("num1", 1.0f).endObject())).actionGet(); + client().index(indexRequest("test").type("type1").id("2") + .source(jsonBuilder().startObject().field("test", "value check").field("num1", 2.0f).endObject())).actionGet(); + client().admin().indices().refresh(refreshRequest()).actionGet(); + + logger.info("--- QUERY_THEN_FETCH"); + + logger.info("running doc['num1'].value"); + SearchResponse response = client().search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value")).add(new ScriptScoreFunctionBuilder().script("doc['num1'].value")))) + ).actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation()); + assertThat(response.getHits().getAt(0).id(), equalTo("2")); + assertThat(response.getHits().getAt(1).id(), equalTo("1")); + + logger.info("running -doc['num1'].value"); + response = client().search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value")).add(new ScriptScoreFunctionBuilder().script("-doc['num1'].value")))) + ).actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation()); + assertThat(response.getHits().getAt(0).id(), equalTo("1")); + assertThat(response.getHits().getAt(1).id(), equalTo("2")); + + + logger.info("running pow(doc['num1'].value, 2)"); + response = client().search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value")).add(new ScriptScoreFunctionBuilder().script("pow(doc['num1'].value, 2)")))) + ).actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation()); + assertThat(response.getHits().getAt(0).id(), equalTo("2")); + assertThat(response.getHits().getAt(1).id(), equalTo("1")); + + logger.info("running max(doc['num1'].value, 1)"); + response = client().search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value")).add(new ScriptScoreFunctionBuilder().script("max(doc['num1'].value, 1d)")))) + ).actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation()); + assertThat(response.getHits().getAt(0).id(), equalTo("2")); + assertThat(response.getHits().getAt(1).id(), equalTo("1")); + + logger.info("running doc['num1'].value * _score"); + response = client().search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value")).add(new ScriptScoreFunctionBuilder().script("doc['num1'].value * _score")))) + ).actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation()); + assertThat(response.getHits().getAt(0).id(), equalTo("2")); + assertThat(response.getHits().getAt(1).id(), equalTo("1")); + + logger.info("running param1 * param2 * _score"); + response = client().search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(functionScoreQuery(termQuery("test", "value")).add(new ScriptScoreFunctionBuilder().script("param1 * param2 * _score").param("param1", 2).param("param2", 2)))) + ).actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation()); + assertSearchHits(response, "1", "2"); + + + logger.info("running param1 * param2 * _score with filter instead of query"); + response = client().search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(functionScoreQuery(termFilter("test", "value")).add(new ScriptScoreFunctionBuilder().script("param1 * param2 * _score").param("param1", 2).param("param2", 2)))) + ).actionGet(); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.getHits().getAt(0).id(), response.getHits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.getHits().getAt(1).id(), response.getHits().getAt(1).explanation()); + assertSearchHits(response, "1", "2"); + assertThat(response.getHits().getAt(0).score(), equalTo(4f)); // _score is always 1 + assertThat(response.getHits().getAt(1).score(), equalTo(4f)); // _score is always 1 + } + @Test public void testTriggerBooleanScorer() throws Exception { client().admin().indices().prepareDelete().execute().actionGet(); @@ -342,6 +621,26 @@ public void testTriggerBooleanScorer() throws Exception { assertThat(searchResponse.getHits().totalHits(), equalTo(4l)); } + + + @Test + public void testTriggerBooleanScorer_withFunctionScore() throws Exception { + client().admin().indices().prepareDelete().execute().actionGet(); + client().admin().indices().prepareCreate("test").setSettings(settingsBuilder().put("index.number_of_shards", 1)).execute().actionGet(); + + client().prepareIndex("test", "type", "1").setSource("field", "value1", "color", "red").execute().actionGet(); + client().prepareIndex("test", "type", "2").setSource("field", "value2", "color", "blue").execute().actionGet(); + client().prepareIndex("test", "type", "3").setSource("field", "value3", "color", "red").execute().actionGet(); + client().prepareIndex("test", "type", "4").setSource("field", "value4", "color", "blue").execute().actionGet(); + client().admin().indices().prepareRefresh().execute().actionGet(); + SearchResponse searchResponse = client().prepareSearch("test") + .setQuery(functionScoreQuery(fuzzyQuery("field", "value")).add(FilterBuilders.idsFilter("type").addIds("1"), new FactorBuilder().boostFactor( 3))) + .execute().actionGet(); + assertThat(Arrays.toString(searchResponse.getShardFailures()), searchResponse.getFailedShards(), equalTo(0)); + + assertThat(searchResponse.getHits().totalHits(), equalTo(4l)); + } + @Test public void testCustomFiltersScore() throws Exception { client().admin().indices().prepareDelete().execute().actionGet(); @@ -520,4 +819,159 @@ public void testCustomFiltersScore() throws Exception { assertThat(searchResponse.getHits().getAt(3).id(), equalTo("2")); assertThat(searchResponse.getHits().getAt(3).score(), equalTo(searchResponse.getHits().getAt(3).explanation().getValue())); } + + + @Test + public void testCustomFiltersScore_withFunctionScore() throws Exception { + client().admin().indices().prepareDelete().execute().actionGet(); + client().admin().indices().prepareCreate("test").setSettings(settingsBuilder().put("index.number_of_shards", 1)).execute().actionGet(); + + client().prepareIndex("test", "type", "1").setSource("field", "value1", "color", "red").execute().actionGet(); + client().prepareIndex("test", "type", "2").setSource("field", "value2", "color", "blue").execute().actionGet(); + client().prepareIndex("test", "type", "3").setSource("field", "value3", "color", "red").execute().actionGet(); + client().prepareIndex("test", "type", "4").setSource("field", "value4", "color", "blue").execute().actionGet(); + + client().admin().indices().prepareRefresh().execute().actionGet(); + + SearchResponse searchResponse = client().prepareSearch("test") + .setQuery(functionScoreQuery(matchAllQuery()).add(termFilter("field", "value4"), new ScriptScoreFunctionBuilder().script("2")).add(termFilter("field", "value2"), new ScriptScoreFunctionBuilder().script("3"))) + .setExplain(true) + .execute().actionGet(); + + assertThat(Arrays.toString(searchResponse.getShardFailures()), searchResponse.getFailedShards(), equalTo(0)); + + assertThat(searchResponse.getHits().totalHits(), equalTo(4l)); + assertThat(searchResponse.getHits().getAt(0).id(), equalTo("2")); + assertThat(searchResponse.getHits().getAt(0).score(), equalTo(3.0f)); + logger.info("--> Hit[0] {} Explanation {}", searchResponse.getHits().getAt(0).id(), searchResponse.getHits().getAt(0).explanation()); + assertThat(searchResponse.getHits().getAt(1).id(), equalTo("4")); + assertThat(searchResponse.getHits().getAt(1).score(), equalTo(2.0f)); + assertThat(searchResponse.getHits().getAt(2).id(), anyOf(equalTo("1"), equalTo("3"))); + assertThat(searchResponse.getHits().getAt(2).score(), equalTo(1.0f)); + assertThat(searchResponse.getHits().getAt(3).id(), anyOf(equalTo("1"), equalTo("3"))); + assertThat(searchResponse.getHits().getAt(3).score(), equalTo(1.0f)); + + searchResponse = client().prepareSearch("test") + .setQuery(functionScoreQuery(matchAllQuery()).add(termFilter("field", "value4"), new FactorBuilder().boostFactor( 2)).add(termFilter("field", "value2"), new FactorBuilder().boostFactor( 3))) + .setExplain(true) + .execute().actionGet(); + + assertThat(Arrays.toString(searchResponse.getShardFailures()), searchResponse.getFailedShards(), equalTo(0)); + + assertThat(searchResponse.getHits().totalHits(), equalTo(4l)); + assertThat(searchResponse.getHits().getAt(0).id(), equalTo("2")); + assertThat(searchResponse.getHits().getAt(0).score(), equalTo(3.0f)); + logger.info("--> Hit[0] {} Explanation {}", searchResponse.getHits().getAt(0).id(), searchResponse.getHits().getAt(0).explanation()); + assertThat(searchResponse.getHits().getAt(1).id(), equalTo("4")); + assertThat(searchResponse.getHits().getAt(1).score(), equalTo(2.0f)); + assertThat(searchResponse.getHits().getAt(2).id(), anyOf(equalTo("1"), equalTo("3"))); + assertThat(searchResponse.getHits().getAt(2).score(), equalTo(1.0f)); + assertThat(searchResponse.getHits().getAt(3).id(), anyOf(equalTo("1"), equalTo("3"))); + assertThat(searchResponse.getHits().getAt(3).score(), equalTo(1.0f)); + + searchResponse = client().prepareSearch("test") + .setQuery(functionScoreQuery(matchAllQuery()).scoreMode("total").add(termFilter("field", "value4"), new FactorBuilder().boostFactor( 2)).add(termFilter("field", "value1"), new FactorBuilder().boostFactor( 3)).add(termFilter("color", "red"), new FactorBuilder().boostFactor( 5))) + .setExplain(true) + .execute().actionGet(); + + assertThat(Arrays.toString(searchResponse.getShardFailures()), searchResponse.getFailedShards(), equalTo(0)); + assertThat(searchResponse.getHits().totalHits(), equalTo(4l)); + assertThat(searchResponse.getHits().getAt(0).id(), equalTo("1")); + assertThat(searchResponse.getHits().getAt(0).score(), equalTo(8.0f)); + logger.info("--> Hit[0] {} Explanation {}", searchResponse.getHits().getAt(0).id(), searchResponse.getHits().getAt(0).explanation()); + + searchResponse = client().prepareSearch("test") + .setQuery(functionScoreQuery(matchAllQuery()).scoreMode("max").add(termFilter("field", "value4"), new FactorBuilder().boostFactor( 2)).add(termFilter("field", "value1"), new FactorBuilder().boostFactor( 3)).add(termFilter("color", "red"), new FactorBuilder().boostFactor( 5))) + .setExplain(true) + .execute().actionGet(); + + assertThat(Arrays.toString(searchResponse.getShardFailures()), searchResponse.getFailedShards(), equalTo(0)); + assertThat(searchResponse.getHits().totalHits(), equalTo(4l)); + assertThat(searchResponse.getHits().getAt(0).id(), anyOf(equalTo("1"), equalTo("3"))); // could be both depending on the order of the docs internally (lucene order) + assertThat(searchResponse.getHits().getAt(0).score(), equalTo(5.0f)); + logger.info("--> Hit[0] {} Explanation {}", searchResponse.getHits().getAt(0).id(), searchResponse.getHits().getAt(0).explanation()); + + searchResponse = client().prepareSearch("test") + .setQuery(functionScoreQuery(matchAllQuery()).scoreMode("avg").add(termFilter("field", "value4"), new FactorBuilder().boostFactor( 2)).add(termFilter("field", "value1"), new FactorBuilder().boostFactor( 3)).add(termFilter("color", "red"), new FactorBuilder().boostFactor( 5))) + .setExplain(true) + .execute().actionGet(); + + assertThat(Arrays.toString(searchResponse.getShardFailures()), searchResponse.getFailedShards(), equalTo(0)); + assertThat(searchResponse.getHits().totalHits(), equalTo(4l)); + assertThat(searchResponse.getHits().getAt(0).id(), equalTo("3")); + assertThat(searchResponse.getHits().getAt(0).score(), equalTo(5.0f)); + logger.info("--> Hit[0] {} Explanation {}", searchResponse.getHits().getAt(0).id(), searchResponse.getHits().getAt(0).explanation()); + assertThat(searchResponse.getHits().getAt(1).id(), equalTo("1")); + assertThat(searchResponse.getHits().getAt(1).score(), equalTo(4.0f)); + logger.info("--> Hit[1] {} Explanation {}", searchResponse.getHits().getAt(1).id(), searchResponse.getHits().getAt(1).explanation()); + + searchResponse = client().prepareSearch("test") + .setQuery(functionScoreQuery(matchAllQuery()).scoreMode("min").add(termFilter("field", "value4"), new FactorBuilder().boostFactor( 2)).add(termFilter("field", "value1"), new FactorBuilder().boostFactor( 3)).add(termFilter("color", "red"), new FactorBuilder().boostFactor( 5))) + .setExplain(true) + .execute().actionGet(); + + assertThat(Arrays.toString(searchResponse.getShardFailures()), searchResponse.getFailedShards(), equalTo(0)); + assertThat(searchResponse.getHits().totalHits(), equalTo(4l)); + assertThat(searchResponse.getHits().getAt(0).id(), equalTo("3")); + assertThat(searchResponse.getHits().getAt(0).score(), equalTo(5.0f)); + logger.info("--> Hit[0] {} Explanation {}", searchResponse.getHits().getAt(0).id(), searchResponse.getHits().getAt(0).explanation()); + assertThat(searchResponse.getHits().getAt(1).id(), equalTo("1")); + assertThat(searchResponse.getHits().getAt(1).score(), equalTo(3.0f)); + assertThat(searchResponse.getHits().getAt(2).id(), equalTo("4")); + assertThat(searchResponse.getHits().getAt(2).score(), equalTo(2.0f)); + assertThat(searchResponse.getHits().getAt(3).id(), equalTo("2")); + assertThat(searchResponse.getHits().getAt(3).score(), equalTo(1.0f)); + + searchResponse = client().prepareSearch("test") + .setQuery(functionScoreQuery(matchAllQuery()).scoreMode("multiply").add(termFilter("field", "value4"), new FactorBuilder().boostFactor( 2)).add(termFilter("field", "value1"), new FactorBuilder().boostFactor( 3)).add(termFilter("color", "red"), new FactorBuilder().boostFactor( 5))) + .setExplain(true) + .execute().actionGet(); + + assertThat(Arrays.toString(searchResponse.getShardFailures()), searchResponse.getFailedShards(), equalTo(0)); + assertThat(searchResponse.getHits().totalHits(), equalTo(4l)); + assertThat(searchResponse.getHits().getAt(0).id(), equalTo("1")); + assertThat(searchResponse.getHits().getAt(0).score(), equalTo(15.0f)); + logger.info("--> Hit[0] {} Explanation {}", searchResponse.getHits().getAt(0).id(), searchResponse.getHits().getAt(0).explanation()); + assertThat(searchResponse.getHits().getAt(1).id(), equalTo("3")); + assertThat(searchResponse.getHits().getAt(1).score(), equalTo(5.0f)); + assertThat(searchResponse.getHits().getAt(2).id(), equalTo("4")); + assertThat(searchResponse.getHits().getAt(2).score(), equalTo(2.0f)); + assertThat(searchResponse.getHits().getAt(3).id(), equalTo("2")); + assertThat(searchResponse.getHits().getAt(3).score(), equalTo(1.0f)); + + searchResponse = client().prepareSearch("test") + .setQuery(functionScoreQuery(termsQuery("field", "value1", "value2", "value3", "value4")).scoreMode("first").add(termFilter("field", "value4"), new FactorBuilder().boostFactor( 2)).add(termFilter("field", "value3"), new FactorBuilder().boostFactor( 3)).add(termFilter("field", "value2"), new FactorBuilder().boostFactor( 4))) + .setExplain(true) + .execute().actionGet(); + + assertThat(Arrays.toString(searchResponse.getShardFailures()), searchResponse.getFailedShards(), equalTo(0)); + assertThat(searchResponse.getHits().totalHits(), equalTo(4l)); + assertThat(searchResponse.getHits().getAt(0).id(), equalTo("2")); + assertThat(searchResponse.getHits().getAt(0).score(), equalTo(searchResponse.getHits().getAt(0).explanation().getValue())); + logger.info("--> Hit[0] {} Explanation {}", searchResponse.getHits().getAt(0).id(), searchResponse.getHits().getAt(0).explanation()); + assertThat(searchResponse.getHits().getAt(1).id(), equalTo("3")); + assertThat(searchResponse.getHits().getAt(1).score(), equalTo(searchResponse.getHits().getAt(1).explanation().getValue())); + assertThat(searchResponse.getHits().getAt(2).id(), equalTo("4")); + assertThat(searchResponse.getHits().getAt(2).score(), equalTo(searchResponse.getHits().getAt(2).explanation().getValue())); + assertThat(searchResponse.getHits().getAt(3).id(), equalTo("1")); + assertThat(searchResponse.getHits().getAt(3).score(), equalTo(searchResponse.getHits().getAt(3).explanation().getValue())); + + + searchResponse = client().prepareSearch("test") + .setQuery(functionScoreQuery(termsQuery("field", "value1", "value2", "value3", "value4")).scoreMode("multiply").add(termFilter("field", "value4"), new FactorBuilder().boostFactor( 2)).add(termFilter("field", "value1"), new FactorBuilder().boostFactor( 3)).add(termFilter("color", "red"), new FactorBuilder().boostFactor( 5))) + .setExplain(true) + .execute().actionGet(); + + assertThat(Arrays.toString(searchResponse.getShardFailures()), searchResponse.getFailedShards(), equalTo(0)); + assertThat(searchResponse.getHits().totalHits(), equalTo(4l)); + assertThat(searchResponse.getHits().getAt(0).id(), equalTo("1")); + assertThat(searchResponse.getHits().getAt(0).score(), equalTo(searchResponse.getHits().getAt(0).explanation().getValue())); + logger.info("--> Hit[0] {} Explanation {}", searchResponse.getHits().getAt(0).id(), searchResponse.getHits().getAt(0).explanation()); + assertThat(searchResponse.getHits().getAt(1).id(), equalTo("3")); + assertThat(searchResponse.getHits().getAt(1).score(), equalTo(searchResponse.getHits().getAt(1).explanation().getValue())); + assertThat(searchResponse.getHits().getAt(2).id(), equalTo("4")); + assertThat(searchResponse.getHits().getAt(2).score(), equalTo(searchResponse.getHits().getAt(2).explanation().getValue())); + assertThat(searchResponse.getHits().getAt(3).id(), equalTo("2")); + assertThat(searchResponse.getHits().getAt(3).score(), equalTo(searchResponse.getHits().getAt(3).explanation().getValue())); + } } diff --git a/src/test/java/org/elasticsearch/test/integration/search/rescore/QueryRescorerTests.java b/src/test/java/org/elasticsearch/test/integration/search/rescore/QueryRescorerTests.java index 731ad8515aee9..bda2d0318b154 100644 --- a/src/test/java/org/elasticsearch/test/integration/search/rescore/QueryRescorerTests.java +++ b/src/test/java/org/elasticsearch/test/integration/search/rescore/QueryRescorerTests.java @@ -19,6 +19,8 @@ package org.elasticsearch.test.integration.search.rescore; + + import org.apache.lucene.util.English; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchType; @@ -28,6 +30,7 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.index.query.functionscore.script.ScriptScoreFunctionBuilder; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.rescore.RescoreBuilder; @@ -37,7 +40,6 @@ import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.*; -import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; @@ -443,4 +445,111 @@ public void testScoring() throws Exception { } } + + + @Test + public void testScoring_withFunctionScore() throws Exception { + client().admin() + .indices() + .prepareCreate("test") + .addMapping( + "type1", + jsonBuilder().startObject().startObject("type1").startObject("properties").startObject("field1") + .field("index", "not_analyzed").field("type", "string").endObject().endObject().endObject().endObject()) + .setSettings(ImmutableSettings.settingsBuilder()).execute().actionGet(); + ensureGreen(); + int numDocs = 1000; + + for (int i = 0; i < numDocs; i++) { + client().prepareIndex("test", "type1", String.valueOf(i)).setSource("field1", English.intToEnglish(i)).execute().actionGet(); + } + + flush(); + optimize(); // make sure we don't have a background merge running + refresh(); + ensureGreen(); + + String[] scoreModes = new String[]{ "max", "min", "avg", "total", "multiply", "" }; + float primaryWeight = 1.1f; + float secondaryWeight = 1.6f; + + for (String scoreMode: scoreModes) { + for (int i = 0; i < numDocs - 4; i++) { + String[] intToEnglish = new String[] { English.intToEnglish(i), English.intToEnglish(i + 1), English.intToEnglish(i + 2), English.intToEnglish(i + 3) }; + + QueryRescorer rescoreQuery = RescoreBuilder + .queryRescorer( + QueryBuilders.boolQuery() + .disableCoord(true) + .should(QueryBuilders.functionScoreQuery(QueryBuilders.termQuery("field1", intToEnglish[0])).add(new ScriptScoreFunctionBuilder().script("5.0f"))) + .should(QueryBuilders.functionScoreQuery(QueryBuilders.termQuery("field1", intToEnglish[1])).add(new ScriptScoreFunctionBuilder().script("7.0f"))) + .should(QueryBuilders.functionScoreQuery(QueryBuilders.termQuery("field1", intToEnglish[3])).add(new ScriptScoreFunctionBuilder().script("0.0f")))) + .setQueryWeight(primaryWeight) + .setRescoreQueryWeight(secondaryWeight); + + if (!"".equals(scoreMode)) { + rescoreQuery.setScoreMode(scoreMode); + } + + SearchResponse rescored = client() + .prepareSearch() + .setPreference("test") // ensure we hit the same shards for tie-breaking + .setQuery(QueryBuilders.boolQuery() + .disableCoord(true) + .should(QueryBuilders.functionScoreQuery(QueryBuilders.termQuery("field1", intToEnglish[0])).add(new ScriptScoreFunctionBuilder().script("2.0f"))) + .should(QueryBuilders.functionScoreQuery(QueryBuilders.termQuery("field1", intToEnglish[1])).add(new ScriptScoreFunctionBuilder().script("3.0f"))) + .should(QueryBuilders.functionScoreQuery(QueryBuilders.termQuery("field1", intToEnglish[2])).add(new ScriptScoreFunctionBuilder().script("5.0f"))) + .should(QueryBuilders.functionScoreQuery(QueryBuilders.termQuery("field1", intToEnglish[3])).add(new ScriptScoreFunctionBuilder().script("0.2f")))) + .setFrom(0) + .setSize(10) + .setRescorer(rescoreQuery) + .setRescoreWindow(50).execute().actionGet(); + + assertHitCount(rescored, 4); + + if ("total".equals(scoreMode) || "".equals(scoreMode)) { + assertFirstHit(rescored, hasId(String.valueOf(i + 1))); + assertSecondHit(rescored, hasId(String.valueOf(i))); + assertThirdHit(rescored, hasId(String.valueOf(i + 2))); + assertThat(rescored.getHits().getHits()[0].getScore(), equalTo(3.0f * primaryWeight + 7.0f * secondaryWeight)); + assertThat(rescored.getHits().getHits()[1].getScore(), equalTo(2.0f * primaryWeight + 5.0f * secondaryWeight)); + assertThat(rescored.getHits().getHits()[2].getScore(), equalTo(5.0f * primaryWeight)); + assertThat(rescored.getHits().getHits()[3].getScore(), equalTo(0.2f * primaryWeight + 0.0f * secondaryWeight)); + } else if ("max".equals(scoreMode)) { + assertFirstHit(rescored, hasId(String.valueOf(i + 1))); + assertSecondHit(rescored, hasId(String.valueOf(i))); + assertThirdHit(rescored, hasId(String.valueOf(i + 2))); + assertThat(rescored.getHits().getHits()[0].getScore(), equalTo(7.0f * secondaryWeight)); + assertThat(rescored.getHits().getHits()[1].getScore(), equalTo(5.0f * secondaryWeight)); + assertThat(rescored.getHits().getHits()[2].getScore(), equalTo(5.0f * primaryWeight)); + assertThat(rescored.getHits().getHits()[3].getScore(), equalTo(0.2f * primaryWeight)); + } else if ("min".equals(scoreMode)) { + assertFirstHit(rescored, hasId(String.valueOf(i + 2))); + assertSecondHit(rescored, hasId(String.valueOf(i + 1))); + assertThirdHit(rescored, hasId(String.valueOf(i))); + assertThat(rescored.getHits().getHits()[0].getScore(), equalTo(5.0f * primaryWeight)); + assertThat(rescored.getHits().getHits()[1].getScore(), equalTo(3.0f * primaryWeight)); + assertThat(rescored.getHits().getHits()[2].getScore(), equalTo(2.0f * primaryWeight)); + assertThat(rescored.getHits().getHits()[3].getScore(), equalTo(0.0f * secondaryWeight)); + } else if ("avg".equals(scoreMode)) { + assertFirstHit(rescored, hasId(String.valueOf(i + 1))); + assertSecondHit(rescored, hasId(String.valueOf(i + 2))); + assertThirdHit(rescored, hasId(String.valueOf(i))); + assertThat(rescored.getHits().getHits()[0].getScore(), equalTo((3.0f * primaryWeight + 7.0f * secondaryWeight) / 2.0f)); + assertThat(rescored.getHits().getHits()[1].getScore(), equalTo(5.0f * primaryWeight)); + assertThat(rescored.getHits().getHits()[2].getScore(), equalTo((2.0f * primaryWeight + 5.0f * secondaryWeight) / 2.0f)); + assertThat(rescored.getHits().getHits()[3].getScore(), equalTo((0.2f * primaryWeight) / 2.0f)); + } else if ("multiply".equals(scoreMode)) { + assertFirstHit(rescored, hasId(String.valueOf(i + 1))); + assertSecondHit(rescored, hasId(String.valueOf(i))); + assertThirdHit(rescored, hasId(String.valueOf(i + 2))); + assertThat(rescored.getHits().getHits()[0].getScore(), equalTo(3.0f * primaryWeight * 7.0f * secondaryWeight)); + assertThat(rescored.getHits().getHits()[1].getScore(), equalTo(2.0f * primaryWeight * 5.0f * secondaryWeight)); + assertThat(rescored.getHits().getHits()[2].getScore(), equalTo(5.0f * primaryWeight)); + assertThat(rescored.getHits().getHits()[3].getScore(), equalTo(0.2f * primaryWeight * 0.0f * secondaryWeight)); + } + } + } + } + } diff --git a/src/test/java/org/elasticsearch/test/integration/search/sort/SimpleSortTests.java b/src/test/java/org/elasticsearch/test/integration/search/sort/SimpleSortTests.java index ee4bea346649e..670230f0537ca 100644 --- a/src/test/java/org/elasticsearch/test/integration/search/sort/SimpleSortTests.java +++ b/src/test/java/org/elasticsearch/test/integration/search/sort/SimpleSortTests.java @@ -19,6 +19,8 @@ package org.elasticsearch.test.integration.search.sort; + + import org.elasticsearch.ElasticSearchException; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.search.SearchPhaseExecutionException; @@ -28,6 +30,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.text.Text; import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.index.query.functionscore.script.ScriptScoreFunctionBuilder; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.sort.ScriptSortBuilder; import org.elasticsearch.search.sort.SortBuilders; @@ -179,6 +182,39 @@ public void testScoreSortDirection() throws Exception { assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("1")); } + + + @Test + public void testScoreSortDirection_withFunctionScore() throws Exception { + prepareCreate("test").setSettings(randomSettingsBuilder().put("index.number_of_shards", 1)).execute().actionGet(); + ensureGreen(); + + client().prepareIndex("test", "type", "1").setSource("field", 2).execute().actionGet(); + client().prepareIndex("test", "type", "2").setSource("field", 1).execute().actionGet(); + client().prepareIndex("test", "type", "3").setSource("field", 0).execute().actionGet(); + + refresh(); + + SearchResponse searchResponse = client().prepareSearch("test").setQuery(functionScoreQuery(matchAllQuery()).add(new ScriptScoreFunctionBuilder().script("_source.field"))).execute().actionGet(); + assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("1")); + assertThat(searchResponse.getHits().getAt(1).score(), Matchers.lessThan(searchResponse.getHits().getAt(0).score())); + assertThat(searchResponse.getHits().getAt(1).getId(), equalTo("2")); + assertThat(searchResponse.getHits().getAt(2).score(), Matchers.lessThan(searchResponse.getHits().getAt(1).score())); + assertThat(searchResponse.getHits().getAt(2).getId(), equalTo("3")); + + searchResponse = client().prepareSearch("test").setQuery(functionScoreQuery(matchAllQuery()).add(new ScriptScoreFunctionBuilder().script("_source.field"))).addSort("_score", SortOrder.DESC).execute().actionGet(); + assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("1")); + assertThat(searchResponse.getHits().getAt(1).score(), Matchers.lessThan(searchResponse.getHits().getAt(0).score())); + assertThat(searchResponse.getHits().getAt(1).getId(), equalTo("2")); + assertThat(searchResponse.getHits().getAt(2).score(), Matchers.lessThan(searchResponse.getHits().getAt(1).score())); + assertThat(searchResponse.getHits().getAt(2).getId(), equalTo("3")); + + searchResponse = client().prepareSearch("test").setQuery(functionScoreQuery(matchAllQuery()).add(new ScriptScoreFunctionBuilder().script("_source.field"))).addSort("_score", SortOrder.DESC).execute().actionGet(); + assertThat(searchResponse.getHits().getAt(2).getId(), equalTo("3")); + assertThat(searchResponse.getHits().getAt(1).getId(), equalTo("2")); + assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("1")); + } + @Test public void testIssue2986() { prepareCreate("test").setSettings(getSettings()).execute().actionGet(); diff --git a/src/test/java/org/elasticsearch/test/unit/index/aliases/IndexAliasesServiceTests.java b/src/test/java/org/elasticsearch/test/unit/index/aliases/IndexAliasesServiceTests.java index 10d6642364f07..ea2027ad89f54 100644 --- a/src/test/java/org/elasticsearch/test/unit/index/aliases/IndexAliasesServiceTests.java +++ b/src/test/java/org/elasticsearch/test/unit/index/aliases/IndexAliasesServiceTests.java @@ -41,6 +41,7 @@ import org.elasticsearch.index.query.FilterBuilder; import org.elasticsearch.index.query.IndexQueryParserModule; import org.elasticsearch.index.query.IndexQueryParserService; +import org.elasticsearch.index.query.functionscore.FunctionScoreModule; import org.elasticsearch.index.settings.IndexSettingsModule; import org.elasticsearch.index.similarity.SimilarityModule; import org.elasticsearch.indices.InvalidAliasNameException; @@ -77,6 +78,7 @@ public static IndexQueryParserService newIndexQueryParserService() { new SettingsModule(ImmutableSettings.Builder.EMPTY_SETTINGS), new IndexEngineModule(ImmutableSettings.Builder.EMPTY_SETTINGS), new IndexCacheModule(ImmutableSettings.Builder.EMPTY_SETTINGS), + new FunctionScoreModule(), new AbstractModule() { @Override protected void configure() { diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/SimpleIndexQueryParserTests.java b/src/test/java/org/elasticsearch/test/unit/index/query/SimpleIndexQueryParserTests.java index d8bc2f4f7d2b2..18a158418ba2b 100644 --- a/src/test/java/org/elasticsearch/test/unit/index/query/SimpleIndexQueryParserTests.java +++ b/src/test/java/org/elasticsearch/test/unit/index/query/SimpleIndexQueryParserTests.java @@ -19,6 +19,8 @@ package org.elasticsearch.test.unit.index.query; + + import com.google.common.collect.Lists; import org.apache.lucene.index.Term; import org.apache.lucene.queries.BoostingQuery; @@ -53,10 +55,9 @@ import org.elasticsearch.index.engine.IndexEngineModule; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.MapperServiceModule; -import org.elasticsearch.index.query.IndexQueryParserModule; -import org.elasticsearch.index.query.IndexQueryParserService; -import org.elasticsearch.index.query.ParsedQuery; -import org.elasticsearch.index.query.QueryStringQueryBuilder; +import org.elasticsearch.index.query.*; +import org.elasticsearch.index.query.functionscore.FunctionScoreModule; +import org.elasticsearch.index.query.functionscore.factor.FactorBuilder; import org.elasticsearch.index.search.NumericRangeFieldDataFilter; import org.elasticsearch.index.search.geo.GeoDistanceFilter; import org.elasticsearch.index.search.geo.GeoPolygonFilter; @@ -67,6 +68,7 @@ import org.elasticsearch.script.ScriptModule; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.threadpool.ThreadPoolModule; +import org.hamcrest.Matchers; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -115,6 +117,7 @@ public static void setupQueryParser() throws IOException { new SimilarityModule(settings), new IndexQueryParserModule(settings), new IndexNameModule(index), + new FunctionScoreModule(), new AbstractModule() { @Override protected void configure() { @@ -1463,6 +1466,19 @@ public void testCustomBoostFactorQueryBuilder() throws IOException { } + + + @Test + public void testCustomBoostFactorQueryBuilder_withFunctionScore() throws IOException { + IndexQueryParserService queryParser = queryParser(); + Query parsedQuery = queryParser.parse(functionScoreQuery(termQuery("name.last", "banon")).add(new FactorBuilder().boostFactor(1.3f))).query(); + assertThat(parsedQuery, instanceOf(FunctionScoreQuery.class)); + FunctionScoreQuery functionScoreQuery = (FunctionScoreQuery) parsedQuery; + assertThat(((TermQuery) functionScoreQuery.getSubQuery()).getTerm(), equalTo(new Term("name.last", "banon"))); + assertThat((double) ((BoostScoreFunction) functionScoreQuery.getFunction()).getBoost(), closeTo(1.3, 0.001)); + } + + @Test public void testCustomBoostFactorQuery() throws IOException { IndexQueryParserService queryParser = queryParser(); @@ -2225,4 +2241,23 @@ public void testCommonTermsQuery3() throws IOException { assertThat(ectQuery.getHighFreqMinimumNumberShouldMatch(), nullValue()); assertThat(ectQuery.getLowFreqMinimumNumberShouldMatch(), equalTo("2")); } + + @Test(expected = QueryParsingException.class) + public void assureMalformedThrowsException() throws IOException { + IndexQueryParserService queryParser; + queryParser = queryParser(); + String query; + query = copyToStringFromClasspath("/org/elasticsearch/test/unit/index/query/faulty-function-score-query.json"); + Query parsedQuery = queryParser.parse(query).query(); + } + + @Test + public void testFilterParsing() throws IOException { + IndexQueryParserService queryParser; + queryParser = queryParser(); + String query; + query = copyToStringFromClasspath("/org/elasticsearch/test/unit/index/query/function-filter-score-query.json"); + Query parsedQuery = queryParser.parse(query).query(); + assertThat((double)(parsedQuery.getBoost()), Matchers.closeTo(3.0, 1.e-7)); + } } diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/faulty-function-score-query.json b/src/test/java/org/elasticsearch/test/unit/index/query/faulty-function-score-query.json new file mode 100644 index 0000000000000..07f906c87a6bd --- /dev/null +++ b/src/test/java/org/elasticsearch/test/unit/index/query/faulty-function-score-query.json @@ -0,0 +1,15 @@ +{ + "function_score":{ + "query":{ + "term":{ + "name.last":"banon" + } + }, + "functions": { + { + "boost_factor" : 3 + } + } + } + } +} \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/function-filter-score-query.json b/src/test/java/org/elasticsearch/test/unit/index/query/function-filter-score-query.json new file mode 100644 index 0000000000000..e78c54973a6a7 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/unit/index/query/function-filter-score-query.json @@ -0,0 +1,30 @@ + + +{ + "function_score":{ + "query":{ + "term":{ + "name.last":"banon" + } + }, + "functions": [ + { + "boost_factor": 3, + "filter": { + term:{ + "name.last":"banon" + } + } + }, + { + "boost_factor": 3 + }, + { + "boost_factor": 3 + } + ], + "boost" : 3, + "score_mode" : "avg", + "max_boost" : 10 + } +} \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/guice/IndexQueryParserModuleTests.java b/src/test/java/org/elasticsearch/test/unit/index/query/guice/IndexQueryParserModuleTests.java index 696a8f9a3d6d1..bbcfb307abfdd 100644 --- a/src/test/java/org/elasticsearch/test/unit/index/query/guice/IndexQueryParserModuleTests.java +++ b/src/test/java/org/elasticsearch/test/unit/index/query/guice/IndexQueryParserModuleTests.java @@ -35,6 +35,7 @@ import org.elasticsearch.index.engine.IndexEngineModule; import org.elasticsearch.index.query.IndexQueryParserModule; import org.elasticsearch.index.query.IndexQueryParserService; +import org.elasticsearch.index.query.functionscore.FunctionScoreModule; import org.elasticsearch.index.settings.IndexSettingsModule; import org.elasticsearch.index.similarity.SimilarityModule; import org.elasticsearch.indices.query.IndicesQueriesModule; @@ -77,6 +78,7 @@ public void testCustomInjection() { new SimilarityModule(settings), new IndexQueryParserModule(settings), new IndexNameModule(index), + new FunctionScoreModule(), new AbstractModule() { @Override protected void configure() { diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/plugin/IndexQueryParserPlugin2Tests.java b/src/test/java/org/elasticsearch/test/unit/index/query/plugin/IndexQueryParserPlugin2Tests.java index b271192c443e6..e2a26e6ce95f7 100644 --- a/src/test/java/org/elasticsearch/test/unit/index/query/plugin/IndexQueryParserPlugin2Tests.java +++ b/src/test/java/org/elasticsearch/test/unit/index/query/plugin/IndexQueryParserPlugin2Tests.java @@ -36,6 +36,7 @@ import org.elasticsearch.index.engine.IndexEngineModule; import org.elasticsearch.index.query.IndexQueryParserModule; import org.elasticsearch.index.query.IndexQueryParserService; +import org.elasticsearch.index.query.functionscore.FunctionScoreModule; import org.elasticsearch.index.settings.IndexSettingsModule; import org.elasticsearch.index.similarity.SimilarityModule; import org.elasticsearch.indices.query.IndicesQueriesModule; @@ -75,6 +76,7 @@ public void testCustomInjection() { new SimilarityModule(settings), queryParserModule, new IndexNameModule(index), + new FunctionScoreModule(), new AbstractModule() { @Override protected void configure() { diff --git a/src/test/java/org/elasticsearch/test/unit/index/query/plugin/IndexQueryParserPluginTests.java b/src/test/java/org/elasticsearch/test/unit/index/query/plugin/IndexQueryParserPluginTests.java index a4a60270c8d72..1c4f4d9ad4156 100644 --- a/src/test/java/org/elasticsearch/test/unit/index/query/plugin/IndexQueryParserPluginTests.java +++ b/src/test/java/org/elasticsearch/test/unit/index/query/plugin/IndexQueryParserPluginTests.java @@ -36,6 +36,7 @@ import org.elasticsearch.index.engine.IndexEngineModule; import org.elasticsearch.index.query.IndexQueryParserModule; import org.elasticsearch.index.query.IndexQueryParserService; +import org.elasticsearch.index.query.functionscore.FunctionScoreModule; import org.elasticsearch.index.settings.IndexSettingsModule; import org.elasticsearch.index.similarity.SimilarityModule; import org.elasticsearch.indices.query.IndicesQueriesModule; @@ -84,6 +85,7 @@ public void processXContentFilterParsers(XContentFilterParsersBindings bindings) queryParserModule, new IndexNameModule(index), new CodecModule(settings), + new FunctionScoreModule(), new AbstractModule() { @Override protected void configure() {