Merge pull request #11621 from cbuescher/feature/query-refactoring-boostingquery
Query refactoring: BoostingQueryBuilder and Parser
This commit is contained in:
commit
33668a8df0
|
@ -19,9 +19,14 @@
|
||||||
|
|
||||||
package org.elasticsearch.index.query;
|
package org.elasticsearch.index.query;
|
||||||
|
|
||||||
|
import org.apache.lucene.search.Query;
|
||||||
|
import org.apache.lucene.queries.BoostingQuery;
|
||||||
|
import org.elasticsearch.common.io.stream.StreamInput;
|
||||||
|
import org.elasticsearch.common.io.stream.StreamOutput;
|
||||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The BoostingQuery class can be used to effectively demote results that match a given query.
|
* The BoostingQuery class can be used to effectively demote results that match a given query.
|
||||||
|
@ -35,7 +40,7 @@ import java.io.IOException;
|
||||||
* multiplied by the supplied "boost" parameter, so this should be less than 1 to achieve a
|
* multiplied by the supplied "boost" parameter, so this should be less than 1 to achieve a
|
||||||
* demoting effect
|
* demoting effect
|
||||||
*/
|
*/
|
||||||
public class BoostingQueryBuilder extends QueryBuilder implements BoostableQueryBuilder<BoostingQueryBuilder> {
|
public class BoostingQueryBuilder extends QueryBuilder<BoostingQueryBuilder> implements BoostableQueryBuilder<BoostingQueryBuilder> {
|
||||||
|
|
||||||
public static final String NAME = "boosting";
|
public static final String NAME = "boosting";
|
||||||
|
|
||||||
|
@ -45,34 +50,74 @@ public class BoostingQueryBuilder extends QueryBuilder implements BoostableQuery
|
||||||
|
|
||||||
private float negativeBoost = -1;
|
private float negativeBoost = -1;
|
||||||
|
|
||||||
private float boost = -1;
|
private float boost = 1.0f;
|
||||||
|
|
||||||
static final BoostingQueryBuilder PROTOTYPE = new BoostingQueryBuilder();
|
static final BoostingQueryBuilder PROTOTYPE = new BoostingQueryBuilder();
|
||||||
|
|
||||||
public BoostingQueryBuilder() {
|
public BoostingQueryBuilder() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the positive query for this boosting query.
|
||||||
|
*/
|
||||||
public BoostingQueryBuilder positive(QueryBuilder positiveQuery) {
|
public BoostingQueryBuilder positive(QueryBuilder positiveQuery) {
|
||||||
this.positiveQuery = positiveQuery;
|
this.positiveQuery = positiveQuery;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the positive query for this boosting query.
|
||||||
|
*/
|
||||||
|
public QueryBuilder positive() {
|
||||||
|
return this.positiveQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the negative query for this boosting query.
|
||||||
|
*/
|
||||||
public BoostingQueryBuilder negative(QueryBuilder negativeQuery) {
|
public BoostingQueryBuilder negative(QueryBuilder negativeQuery) {
|
||||||
this.negativeQuery = negativeQuery;
|
this.negativeQuery = negativeQuery;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the negative query for this boosting query.
|
||||||
|
*/
|
||||||
|
public QueryBuilder negative() {
|
||||||
|
return this.negativeQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the negative boost factor.
|
||||||
|
*/
|
||||||
public BoostingQueryBuilder negativeBoost(float negativeBoost) {
|
public BoostingQueryBuilder negativeBoost(float negativeBoost) {
|
||||||
this.negativeBoost = negativeBoost;
|
this.negativeBoost = negativeBoost;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the negative boost factor.
|
||||||
|
*/
|
||||||
|
public float negativeBoost() {
|
||||||
|
return this.negativeBoost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the boost factor.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public BoostingQueryBuilder boost(float boost) {
|
public BoostingQueryBuilder boost(float boost) {
|
||||||
this.boost = boost;
|
this.boost = boost;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the boost factor.
|
||||||
|
*/
|
||||||
|
public float boost() {
|
||||||
|
return this.boost;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
|
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
|
||||||
if (positiveQuery == null) {
|
if (positiveQuery == null) {
|
||||||
|
@ -81,25 +126,87 @@ public class BoostingQueryBuilder extends QueryBuilder implements BoostableQuery
|
||||||
if (negativeQuery == null) {
|
if (negativeQuery == null) {
|
||||||
throw new IllegalArgumentException("boosting query requires negative query to be set");
|
throw new IllegalArgumentException("boosting query requires negative query to be set");
|
||||||
}
|
}
|
||||||
if (negativeBoost == -1) {
|
|
||||||
throw new IllegalArgumentException("boosting query requires negativeBoost to be set");
|
|
||||||
}
|
|
||||||
builder.startObject(NAME);
|
builder.startObject(NAME);
|
||||||
builder.field("positive");
|
builder.field("positive");
|
||||||
positiveQuery.toXContent(builder, params);
|
positiveQuery.toXContent(builder, params);
|
||||||
builder.field("negative");
|
builder.field("negative");
|
||||||
negativeQuery.toXContent(builder, params);
|
negativeQuery.toXContent(builder, params);
|
||||||
|
|
||||||
builder.field("negative_boost", negativeBoost);
|
builder.field("negative_boost", negativeBoost);
|
||||||
|
|
||||||
if (boost != -1) {
|
|
||||||
builder.field("boost", boost);
|
builder.field("boost", boost);
|
||||||
}
|
|
||||||
builder.endObject();
|
builder.endObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public QueryValidationException validate() {
|
||||||
|
QueryValidationException validationException = null;
|
||||||
|
if (negativeBoost < 0) {
|
||||||
|
validationException = QueryValidationException
|
||||||
|
.addValidationError("[boosting] query requires negativeBoost to be set to positive value", validationException);
|
||||||
|
}
|
||||||
|
return validationException;
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String queryId() {
|
public String queryId() {
|
||||||
return NAME;
|
return NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query toQuery(QueryParseContext parseContext) throws QueryParsingException, IOException {
|
||||||
|
|
||||||
|
// make upstream queries ignore this query by returning `null`
|
||||||
|
// if either inner query builder is null or returns null-Query
|
||||||
|
if (positiveQuery == null || negativeQuery == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Query positive = positiveQuery.toQuery(parseContext);
|
||||||
|
Query negative = negativeQuery.toQuery(parseContext);
|
||||||
|
if (positive == null || negative == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
BoostingQuery boostingQuery = new BoostingQuery(positive, negative, negativeBoost);
|
||||||
|
boostingQuery.setBoost(boost);
|
||||||
|
return boostingQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(this.boost, this.negativeBoost, this.positiveQuery, this.negativeQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BoostingQueryBuilder other = (BoostingQueryBuilder) obj;
|
||||||
|
return Objects.equals(this.boost, other.boost) &&
|
||||||
|
Objects.equals(this.negativeBoost, other.negativeBoost) &&
|
||||||
|
Objects.equals(this.positiveQuery, other.positiveQuery) &&
|
||||||
|
Objects.equals(this.negativeQuery, other.negativeQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BoostingQueryBuilder readFrom(StreamInput in) throws IOException {
|
||||||
|
QueryBuilder positiveQuery = in.readNamedWriteable();
|
||||||
|
QueryBuilder negativeQuery = in.readNamedWriteable();
|
||||||
|
BoostingQueryBuilder boostingQuery = new BoostingQueryBuilder();
|
||||||
|
boostingQuery.positive(positiveQuery);
|
||||||
|
boostingQuery.negative(negativeQuery);
|
||||||
|
boostingQuery.boost = in.readFloat();
|
||||||
|
boostingQuery.negativeBoost = in.readFloat();
|
||||||
|
return boostingQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(StreamOutput out) throws IOException {
|
||||||
|
out.writeNamedWriteable(this.positiveQuery);
|
||||||
|
out.writeNamedWriteable(this.negativeQuery);
|
||||||
|
out.writeFloat(this.boost);
|
||||||
|
out.writeFloat(this.negativeBoost);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,6 @@
|
||||||
|
|
||||||
package org.elasticsearch.index.query;
|
package org.elasticsearch.index.query;
|
||||||
|
|
||||||
import org.apache.lucene.queries.BoostingQuery;
|
|
||||||
import org.apache.lucene.search.Query;
|
|
||||||
import org.elasticsearch.common.inject.Inject;
|
import org.elasticsearch.common.inject.Inject;
|
||||||
import org.elasticsearch.common.xcontent.XContentParser;
|
import org.elasticsearch.common.xcontent.XContentParser;
|
||||||
|
|
||||||
|
@ -29,7 +27,7 @@ import java.io.IOException;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class BoostingQueryParser extends BaseQueryParserTemp {
|
public class BoostingQueryParser extends BaseQueryParser {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public BoostingQueryParser() {
|
public BoostingQueryParser() {
|
||||||
|
@ -41,14 +39,14 @@ public class BoostingQueryParser extends BaseQueryParserTemp {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException {
|
public QueryBuilder fromXContent(QueryParseContext parseContext) throws IOException, QueryParsingException {
|
||||||
XContentParser parser = parseContext.parser();
|
XContentParser parser = parseContext.parser();
|
||||||
|
|
||||||
Query positiveQuery = null;
|
QueryBuilder positiveQuery = null;
|
||||||
boolean positiveQueryFound = false;
|
boolean positiveQueryFound = false;
|
||||||
Query negativeQuery = null;
|
QueryBuilder negativeQuery = null;
|
||||||
boolean negativeQueryFound = false;
|
boolean negativeQueryFound = false;
|
||||||
float boost = -1;
|
float boost = 1.0f;
|
||||||
float negativeBoost = -1;
|
float negativeBoost = -1;
|
||||||
|
|
||||||
String currentFieldName = null;
|
String currentFieldName = null;
|
||||||
|
@ -58,10 +56,10 @@ public class BoostingQueryParser extends BaseQueryParserTemp {
|
||||||
currentFieldName = parser.currentName();
|
currentFieldName = parser.currentName();
|
||||||
} else if (token == XContentParser.Token.START_OBJECT) {
|
} else if (token == XContentParser.Token.START_OBJECT) {
|
||||||
if ("positive".equals(currentFieldName)) {
|
if ("positive".equals(currentFieldName)) {
|
||||||
positiveQuery = parseContext.parseInnerQuery();
|
positiveQuery = parseContext.parseInnerQueryBuilder();
|
||||||
positiveQueryFound = true;
|
positiveQueryFound = true;
|
||||||
} else if ("negative".equals(currentFieldName)) {
|
} else if ("negative".equals(currentFieldName)) {
|
||||||
negativeQuery = parseContext.parseInnerQuery();
|
negativeQuery = parseContext.parseInnerQueryBuilder();
|
||||||
negativeQueryFound = true;
|
negativeQueryFound = true;
|
||||||
} else {
|
} else {
|
||||||
throw new QueryParsingException(parseContext, "[boosting] query does not support [" + currentFieldName + "]");
|
throw new QueryParsingException(parseContext, "[boosting] query does not support [" + currentFieldName + "]");
|
||||||
|
@ -83,19 +81,16 @@ public class BoostingQueryParser extends BaseQueryParserTemp {
|
||||||
if (negativeQuery == null && !negativeQueryFound) {
|
if (negativeQuery == null && !negativeQueryFound) {
|
||||||
throw new QueryParsingException(parseContext, "[boosting] query requires 'negative' query to be set'");
|
throw new QueryParsingException(parseContext, "[boosting] query requires 'negative' query to be set'");
|
||||||
}
|
}
|
||||||
if (negativeBoost == -1) {
|
if (negativeBoost < 0) {
|
||||||
throw new QueryParsingException(parseContext, "[boosting] query requires 'negative_boost' to be set'");
|
throw new QueryParsingException(parseContext, "[boosting] query requires 'negative_boost' to be set to be a positive value'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsers returned null
|
BoostingQueryBuilder boostingQuery = new BoostingQueryBuilder();
|
||||||
if (positiveQuery == null || negativeQuery == null) {
|
boostingQuery.positive(positiveQuery);
|
||||||
return null;
|
boostingQuery.negative(negativeQuery);
|
||||||
}
|
boostingQuery.negativeBoost(negativeBoost);
|
||||||
|
boostingQuery.boost(boost);
|
||||||
BoostingQuery boostingQuery = new BoostingQuery(positiveQuery, negativeQuery, negativeBoost);
|
boostingQuery.validate();
|
||||||
if (boost != -1) {
|
|
||||||
boostingQuery.setBoost(boost);
|
|
||||||
}
|
|
||||||
return boostingQuery;
|
return boostingQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,9 +60,12 @@ public abstract class QueryBuilder<QB extends QueryBuilder> extends ToXContentTo
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts this QueryBuilder to a lucene {@link Query}
|
* Converts this QueryBuilder to a lucene {@link Query}.
|
||||||
|
* Returns <tt>null</tt> if this query should be ignored in the context of
|
||||||
|
* parent queries.
|
||||||
|
*
|
||||||
* @param parseContext additional information needed to construct the queries
|
* @param parseContext additional information needed to construct the queries
|
||||||
* @return the {@link Query}
|
* @return the {@link Query} or <tt>null</tt> if this query should be ignored upstream
|
||||||
* @throws QueryParsingException
|
* @throws QueryParsingException
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
* Licensed to Elasticsearch under one or more contributor
|
||||||
|
* license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright
|
||||||
|
* ownership. Elasticsearch licenses this file to you under
|
||||||
|
* the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
* not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.elasticsearch.index.query;
|
||||||
|
|
||||||
|
import org.apache.lucene.queries.BoostingQuery;
|
||||||
|
import org.apache.lucene.search.Query;
|
||||||
|
import org.elasticsearch.common.xcontent.ToXContent;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||||
|
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
|
||||||
|
public class BoostingQueryBuilderTest extends BaseQueryTestCase<BoostingQueryBuilder> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BoostingQueryBuilder createTestQueryBuilder() {
|
||||||
|
BoostingQueryBuilder query = new BoostingQueryBuilder();
|
||||||
|
query.positive(RandomQueryBuilder.create(random()));
|
||||||
|
query.negative(RandomQueryBuilder.create(random()));
|
||||||
|
query.negativeBoost(2.0f / randomIntBetween(1, 20));
|
||||||
|
if (randomBoolean()) {
|
||||||
|
query.boost(2.0f / randomIntBetween(1, 20));
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Query createExpectedQuery(BoostingQueryBuilder queryBuilder, QueryParseContext context) throws IOException {
|
||||||
|
Query positive = queryBuilder.positive().toQuery(context);
|
||||||
|
Query negative = queryBuilder.negative().toQuery(context);
|
||||||
|
BoostingQuery boostingQuery = new BoostingQuery(positive, negative, queryBuilder.negativeBoost());
|
||||||
|
if (queryBuilder.boost() != 1.0f) {
|
||||||
|
boostingQuery.setBoost(queryBuilder.boost());
|
||||||
|
}
|
||||||
|
return boostingQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testToXContentIllegalArgumentExceptions() throws IOException {
|
||||||
|
BoostingQueryBuilder boostingQueryBuilder = new BoostingQueryBuilder();
|
||||||
|
boostingQueryBuilder.positive(new MatchAllQueryBuilder());
|
||||||
|
try {
|
||||||
|
boostingQueryBuilder.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS);
|
||||||
|
fail("Expected IllegalArgumentException because of missing negative query.");
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
boostingQueryBuilder = new BoostingQueryBuilder();
|
||||||
|
boostingQueryBuilder.negative(new MatchAllQueryBuilder());
|
||||||
|
try {
|
||||||
|
boostingQueryBuilder.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS);
|
||||||
|
fail("Expected IllegalArgumentException because of missing positive query.");
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tests that we signal upstream queries to ignore this query by returning <tt>null</tt>
|
||||||
|
* if any of the inner query builder is not set
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testInnerQueryBuilderNull() throws IOException {
|
||||||
|
BoostingQueryBuilder boostingQueryBuilder = new BoostingQueryBuilder();
|
||||||
|
if (randomBoolean()) {
|
||||||
|
boostingQueryBuilder.positive(new MatchAllQueryBuilder()).negative(null);
|
||||||
|
} else {
|
||||||
|
boostingQueryBuilder.positive(null).negative(new MatchAllQueryBuilder());
|
||||||
|
}
|
||||||
|
assertNull(boostingQueryBuilder.toQuery(createContext()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInnerQueryBuilderReturnsNull() throws IOException {
|
||||||
|
QueryBuilder noOpBuilder = new QueryBuilder<QueryBuilder>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String queryId() {
|
||||||
|
return "dummy";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Query toQuery(QueryParseContext context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
BoostingQueryBuilder boostingQueryBuilder = null;
|
||||||
|
if (randomBoolean()) {
|
||||||
|
boostingQueryBuilder = new BoostingQueryBuilder().positive(new MatchAllQueryBuilder()).negative(noOpBuilder);
|
||||||
|
} else {
|
||||||
|
boostingQueryBuilder = new BoostingQueryBuilder().positive(noOpBuilder).negative(new MatchAllQueryBuilder());
|
||||||
|
}
|
||||||
|
assertNull(boostingQueryBuilder.toQuery(createContext()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidate() {
|
||||||
|
BoostingQueryBuilder boostingQueryBuilder = new BoostingQueryBuilder();
|
||||||
|
// check for error with negative `negative boost` factor
|
||||||
|
boostingQueryBuilder.negativeBoost(-0.5f);
|
||||||
|
assertThat(boostingQueryBuilder.validate().validationErrors().size(), is(1));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue