diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java index 9b58f472955..f89ca6425c9 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserModule.java @@ -225,6 +225,7 @@ public class IndexQueryParserModule extends AbstractModule { bindings.processXContentQueryParser(DisMaxQueryParser.NAME, DisMaxQueryParser.class); bindings.processXContentQueryParser(MatchAllQueryParser.NAME, MatchAllQueryParser.class); bindings.processXContentQueryParser(QueryStringQueryParser.NAME, QueryStringQueryParser.class); + bindings.processXContentQueryParser(BoostingQueryParser.NAME, BoostingQueryParser.class); bindings.processXContentQueryParser(BoolQueryParser.NAME, BoolQueryParser.class); bindings.processXContentQueryParser(TermQueryParser.NAME, TermQueryParser.class); bindings.processXContentQueryParser(TermsQueryParser.NAME, TermsQueryParser.class); diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/BoostingQueryBuilder.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/BoostingQueryBuilder.java new file mode 100644 index 00000000000..abad520e580 --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/BoostingQueryBuilder.java @@ -0,0 +1,95 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.xcontent; + +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.QueryBuilderException; + +import java.io.IOException; + +/** + * The BoostingQuery class can be used to effectively demote results that match a given query. + * Unlike the "NOT" clause, this still selects documents that contain undesirable terms, + * but reduces their overall score: + * + * Query balancedQuery = new BoostingQuery(positiveQuery, negativeQuery, 0.01f); + * In this scenario the positiveQuery contains the mandatory, desirable criteria which is used to + * select all matching documents, and the negativeQuery contains the undesirable elements which + * are simply used to lessen the scores. Documents that match the negativeQuery have their score + * multiplied by the supplied "boost" parameter, so this should be less than 1 to achieve a + * demoting effect + */ +public class BoostingQueryBuilder extends BaseQueryBuilder { + + private XContentQueryBuilder positiveQuery; + + private XContentQueryBuilder negativeQuery; + + private float negativeBoost = -1; + + private float boost = -1; + + public BoostingQueryBuilder() { + + } + + public BoostingQueryBuilder positive(XContentQueryBuilder positiveQuery) { + this.positiveQuery = positiveQuery; + return this; + } + + public BoostingQueryBuilder negative(XContentQueryBuilder negativeQuery) { + this.negativeQuery = negativeQuery; + return this; + } + + public BoostingQueryBuilder negativeBoost(float negativeBoost) { + this.negativeBoost = negativeBoost; + return this; + } + + public BoostingQueryBuilder boost(float boost) { + this.boost = boost; + return this; + } + + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { + if (positiveQuery == null) { + throw new QueryBuilderException("boosting query requires positive query to be set"); + } + if (negativeQuery == null) { + throw new QueryBuilderException("boosting query requires negative query to be set"); + } + if (negativeBoost == -1) { + throw new QueryBuilderException("boosting query requires negativeBoost to be set"); + } + builder.startObject(BoostingQueryParser.NAME); + builder.field("positive"); + positiveQuery.toXContent(builder, params); + builder.field("negative"); + negativeQuery.toXContent(builder, params); + + builder.field("negative_boost", negativeBoost); + + if (boost != -1) { + builder.field("boost", boost); + } + } +} \ No newline at end of file diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/BoostingQueryParser.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/BoostingQueryParser.java new file mode 100644 index 00000000000..a3a3ecba62e --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/BoostingQueryParser.java @@ -0,0 +1,93 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.xcontent; + +import org.apache.lucene.search.BoostingQuery; +import org.apache.lucene.search.Query; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.AbstractIndexComponent; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.query.QueryParsingException; +import org.elasticsearch.index.settings.IndexSettings; + +import java.io.IOException; + +/** + * + */ +public class BoostingQueryParser extends AbstractIndexComponent implements XContentQueryParser { + + public static final String NAME = "boosting"; + + @Inject public BoostingQueryParser(Index index, @IndexSettings Settings settings) { + super(index, settings); + } + + @Override public String[] names() { + return new String[]{NAME}; + } + + @Override public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException { + XContentParser parser = parseContext.parser(); + + Query positiveQuery = null; + Query negativeQuery = null; + float boost = -1; + float negativeBoost = -1; + + 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 ("positive".equals(currentFieldName)) { + positiveQuery = parseContext.parseInnerQuery(); + } else if ("negative".equals(currentFieldName)) { + negativeQuery = parseContext.parseInnerQuery(); + } + } else if (token.isValue()) { + if ("negative_boost".equals(currentFieldName) || "negativeBoost".equals(currentFieldName)) { + negativeBoost = parser.floatValue(); + } else if ("boost".equals(currentFieldName)) { + boost = parser.floatValue(); + } + } + } + + if (positiveQuery == null) { + throw new QueryParsingException(index, "[boosting] query requires 'positive' query to be set'"); + } + if (negativeQuery == null) { + throw new QueryParsingException(index, "[boosting] query requires 'negative' query to be set'"); + } + if (negativeBoost == -1) { + throw new QueryParsingException(index, "[boosting] query requires 'negative_boost' to be set'"); + } + + BoostingQuery boostingQuery = new BoostingQuery(positiveQuery, negativeQuery, negativeBoost); + if (boost != -1) { + boostingQuery.setBoost(boost); + } + return boostingQuery; + } +} \ No newline at end of file diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryBuilders.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryBuilders.java index 718849f592c..57a3027e477 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryBuilders.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryBuilders.java @@ -252,6 +252,15 @@ public abstract class QueryBuilders { return new QueryStringQueryBuilder(queryString); } + /** + * The BoostingQuery class can be used to effectively demote results that match a given query. + * Unlike the "NOT" clause, this still selects documents that contain undesirable terms, + * but reduces their overall score: + */ + public static BoostingQueryBuilder boostingQuery() { + return new BoostingQueryBuilder(); + } + /** * A Query that matches documents matching boolean combinations of other queries. */ diff --git a/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/SimpleIndexQueryParserTests.java b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/SimpleIndexQueryParserTests.java index 71cb5a00db2..93858b7b8c8 100644 --- a/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/SimpleIndexQueryParserTests.java +++ b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/SimpleIndexQueryParserTests.java @@ -713,6 +713,19 @@ public class SimpleIndexQueryParserTests { assertThat(((TermFilter) notFilter.filter()).getTerm(), equalTo(new Term("name.first", "shay1"))); } + @Test public void testBoostingQueryBuilder() throws IOException { + IndexQueryParser queryParser = queryParser(); + Query parsedQuery = queryParser.parse(boostingQuery().positive(termQuery("field1", "value1")).negative(termQuery("field1", "value2")).negativeBoost(0.2f)).query(); + assertThat(parsedQuery, instanceOf(BoostingQuery.class)); + } + + @Test public void testBoostingQuery() throws IOException { + IndexQueryParser queryParser = queryParser(); + String query = copyToStringFromClasspath("/org/elasticsearch/index/query/xcontent/boosting-query.json"); + Query parsedQuery = queryParser.parse(query).query(); + assertThat(parsedQuery, instanceOf(BoostingQuery.class)); + } + @Test public void testBoolQueryBuilder() throws IOException { IndexQueryParser queryParser = queryParser(); Query parsedQuery = queryParser.parse(boolQuery().must(termQuery("content", "test1")).must(termQuery("content", "test4")).mustNot(termQuery("content", "test2")).should(termQuery("content", "test3"))).query(); diff --git a/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/boosting-query.json b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/boosting-query.json new file mode 100644 index 00000000000..b22347debcb --- /dev/null +++ b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/boosting-query.json @@ -0,0 +1,15 @@ +{ + "boosting" : { + "positive" : { + "term" : { + "field1" : "value1" + } + }, + "negative" : { + "term" : { + "field2" : "value2" + } + }, + "negative_boost" : 0.2 + } +} \ No newline at end of file