Merge pull request #12156 from cbuescher/feature/query-refactoring-spannear

Query refactoring: SpanNearQueryBuilder and Parser
This commit is contained in:
Christoph Büscher 2015-07-09 17:48:48 +02:00
commit 76e9a17153
7 changed files with 253 additions and 56 deletions

View File

@ -312,8 +312,8 @@ public abstract class QueryBuilders {
return new SpanFirstQueryBuilder(match, end);
}
public static SpanNearQueryBuilder spanNearQuery() {
return new SpanNearQueryBuilder();
public static SpanNearQueryBuilder spanNearQuery(int slop) {
return new SpanNearQueryBuilder(slop);
}
public static SpanNotQueryBuilder spanNotQuery() {

View File

@ -19,70 +19,180 @@
package org.elasticsearch.index.query;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.spans.SpanNearQuery;
import org.apache.lucene.search.spans.SpanQuery;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Matches spans which are near one another. One can specify slop, the maximum number
* of intervening unmatched positions, as well as whether matches are required to be in-order.
* The span near query maps to Lucene {@link SpanNearQuery}.
*/
public class SpanNearQueryBuilder extends AbstractQueryBuilder<SpanNearQueryBuilder> implements SpanQueryBuilder<SpanNearQueryBuilder> {
public static final String NAME = "span_near";
private ArrayList<SpanQueryBuilder> clauses = new ArrayList<>();
/** Default for flag controlling whether matches are required to be in-order */
public static boolean DEFAULT_IN_ORDER = true;
private Integer slop = null;
/** Default for flag controlling whether payloads are collected */
public static boolean DEFAULT_COLLECT_PAYLOADS = true;
private Boolean inOrder;
private final ArrayList<SpanQueryBuilder> clauses = new ArrayList<>();
private Boolean collectPayloads;
private final int slop;
private boolean inOrder = DEFAULT_IN_ORDER;
private boolean collectPayloads = DEFAULT_COLLECT_PAYLOADS;
static final SpanNearQueryBuilder PROTOTYPE = new SpanNearQueryBuilder();
public SpanNearQueryBuilder clause(SpanQueryBuilder clause) {
clauses.add(clause);
return this;
}
public SpanNearQueryBuilder slop(int slop) {
/**
* @param slop controls the maximum number of intervening unmatched positions permitted
*/
public SpanNearQueryBuilder(int slop) {
this.slop = slop;
}
/**
* only used for prototype
*/
private SpanNearQueryBuilder() {
this.slop = 0;
}
/**
* @return the maximum number of intervening unmatched positions permitted
*/
public int slop() {
return this.slop;
}
public SpanNearQueryBuilder clause(SpanQueryBuilder clause) {
clauses.add(Objects.requireNonNull(clause));
return this;
}
/**
* @return the {@link SpanQueryBuilder} clauses that were set for this query
*/
public List<SpanQueryBuilder> clauses() {
return this.clauses;
}
/**
* When <code>inOrder</code> is true, the spans from each clause
* must be in the same order as in <code>clauses</code> and must be non-overlapping.
* Defaults to <code>true</code>
*/
public SpanNearQueryBuilder inOrder(boolean inOrder) {
this.inOrder = inOrder;
return this;
}
/**
* @see SpanNearQueryBuilder#inOrder(boolean))
*/
public boolean inOrder() {
return this.inOrder;
}
/**
* @param collectPayloads flag controlling whether payloads are collected
*/
public SpanNearQueryBuilder collectPayloads(boolean collectPayloads) {
this.collectPayloads = collectPayloads;
return this;
}
/**
* @see SpanNearQueryBuilder#collectPayloads(boolean))
*/
public boolean collectPayloads() {
return this.collectPayloads;
}
@Override
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
if (clauses.isEmpty()) {
throw new IllegalArgumentException("Must have at least one clause when building a spanNear query");
}
if (slop == null) {
throw new IllegalArgumentException("Must set the slop when building a spanNear query");
}
builder.startObject(NAME);
builder.startArray("clauses");
for (SpanQueryBuilder clause : clauses) {
clause.toXContent(builder, params);
}
builder.endArray();
builder.field("slop", slop.intValue());
if (inOrder != null) {
builder.field("in_order", inOrder);
}
if (collectPayloads != null) {
builder.field("collect_payloads", collectPayloads);
}
builder.field("slop", slop);
builder.field("in_order", inOrder);
builder.field("collect_payloads", collectPayloads);
printBoostAndQueryName(builder);
builder.endObject();
}
@Override
protected Query doToQuery(QueryParseContext parseContext) throws IOException {
SpanQuery[] spanQueries = new SpanQuery[clauses.size()];
for (int i = 0; i < clauses.size(); i++) {
Query query = clauses.get(i).toQuery(parseContext);
assert query instanceof SpanQuery;
spanQueries[i] = (SpanQuery) query;
}
return new SpanNearQuery(spanQueries, slop, inOrder, collectPayloads);
}
@Override
public QueryValidationException validate() {
QueryValidationException validationExceptions = null;
if (clauses.isEmpty()) {
validationExceptions = addValidationError("query must include [clauses]", validationExceptions);
}
for (SpanQueryBuilder innerClause : clauses) {
validationExceptions = validateInnerQuery(innerClause, validationExceptions);
}
return validationExceptions;
}
@Override
protected SpanNearQueryBuilder doReadFrom(StreamInput in) throws IOException {
SpanNearQueryBuilder queryBuilder = new SpanNearQueryBuilder(in.readVInt());
List<SpanQueryBuilder> clauses = in.readNamedWriteableList();
for (SpanQueryBuilder subClause : clauses) {
queryBuilder.clause(subClause);
}
queryBuilder.collectPayloads = in.readBoolean();
queryBuilder.inOrder = in.readBoolean();
return queryBuilder;
}
@Override
protected void doWriteTo(StreamOutput out) throws IOException {
out.writeVInt(slop);
out.writeNamedWriteableList(clauses);
out.writeBoolean(collectPayloads);
out.writeBoolean(inOrder);
}
@Override
protected int doHashCode() {
return Objects.hash(clauses, slop, collectPayloads, inOrder);
}
@Override
protected boolean doEquals(SpanNearQueryBuilder other) {
return Objects.equals(clauses, other.clauses) &&
Objects.equals(slop, other.slop) &&
Objects.equals(collectPayloads, other.collectPayloads) &&
Objects.equals(inOrder, other.inOrder);
}
@Override
public String getName() {
return NAME;

View File

@ -19,9 +19,6 @@
package org.elasticsearch.index.query;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.spans.SpanNearQuery;
import org.apache.lucene.search.spans.SpanQuery;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.xcontent.XContentParser;
@ -34,7 +31,7 @@ import static com.google.common.collect.Lists.newArrayList;
/**
*
*/
public class SpanNearQueryParser extends BaseQueryParserTemp {
public class SpanNearQueryParser extends BaseQueryParser {
@Inject
public SpanNearQueryParser() {
@ -46,16 +43,16 @@ public class SpanNearQueryParser extends BaseQueryParserTemp {
}
@Override
public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException {
public QueryBuilder fromXContent(QueryParseContext parseContext) throws IOException, QueryParsingException {
XContentParser parser = parseContext.parser();
float boost = AbstractQueryBuilder.DEFAULT_BOOST;
Integer slop = null;
boolean inOrder = true;
boolean collectPayloads = true;
boolean inOrder = SpanNearQueryBuilder.DEFAULT_IN_ORDER;
boolean collectPayloads = SpanNearQueryBuilder.DEFAULT_COLLECT_PAYLOADS;
String queryName = null;
List<SpanQuery> clauses = newArrayList();
List<SpanQueryBuilder> clauses = newArrayList();
String currentFieldName = null;
XContentParser.Token token;
@ -65,11 +62,11 @@ public class SpanNearQueryParser extends BaseQueryParserTemp {
} else if (token == XContentParser.Token.START_ARRAY) {
if ("clauses".equals(currentFieldName)) {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
Query query = parseContext.parseInnerQuery();
if (!(query instanceof SpanQuery)) {
QueryBuilder query = parseContext.parseInnerQueryBuilder();
if (!(query instanceof SpanQueryBuilder)) {
throw new QueryParsingException(parseContext, "spanNear [clauses] must be of type span query");
}
clauses.add((SpanQuery) query);
clauses.add((SpanQueryBuilder) query);
}
} else {
throw new QueryParsingException(parseContext, "[span_near] query does not support [" + currentFieldName + "]");
@ -92,19 +89,20 @@ public class SpanNearQueryParser extends BaseQueryParserTemp {
throw new QueryParsingException(parseContext, "[span_near] query does not support [" + currentFieldName + "]");
}
}
if (clauses.isEmpty()) {
throw new QueryParsingException(parseContext, "span_near must include [clauses]");
}
if (slop == null) {
throw new QueryParsingException(parseContext, "span_near must include [slop]");
}
SpanNearQuery query = new SpanNearQuery(clauses.toArray(new SpanQuery[clauses.size()]), slop.intValue(), inOrder, collectPayloads);
query.setBoost(boost);
if (queryName != null) {
parseContext.addNamedQuery(queryName, query);
SpanNearQueryBuilder queryBuilder = new SpanNearQueryBuilder(slop);
for (SpanQueryBuilder subQuery : clauses) {
queryBuilder.clause(subQuery);
}
return query;
queryBuilder.inOrder(inOrder);
queryBuilder.collectPayloads(collectPayloads);
queryBuilder.boost(boost);
queryBuilder.queryName(queryName);
return queryBuilder;
}
@Override

View File

@ -1384,7 +1384,7 @@ public class SimpleIndexQueryParserTests extends ElasticsearchSingleNodeTest {
@Test
public void testSpanNearQueryBuilder() throws IOException {
IndexQueryParserService queryParser = queryParser();
Query parsedQuery = queryParser.parse(spanNearQuery().clause(spanTermQuery("age", 34)).clause(spanTermQuery("age", 35)).clause(spanTermQuery("age", 36)).slop(12).inOrder(false).collectPayloads(false)).query();
Query parsedQuery = queryParser.parse(spanNearQuery(12).clause(spanTermQuery("age", 34)).clause(spanTermQuery("age", 35)).clause(spanTermQuery("age", 36)).inOrder(false).collectPayloads(false)).query();
assertThat(parsedQuery, instanceOf(SpanNearQuery.class));
SpanNearQuery spanNearQuery = (SpanNearQuery) parsedQuery;
assertThat(spanNearQuery.getClauses().length, equalTo(3));

View File

@ -0,0 +1,85 @@
/*
* 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.search.Query;
import org.apache.lucene.search.spans.SpanNearQuery;
import org.apache.lucene.search.spans.SpanQuery;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
public class SpanNearQueryBuilderTest extends BaseQueryTestCase<SpanNearQueryBuilder> {
@Override
protected Query doCreateExpectedQuery(SpanNearQueryBuilder testQueryBuilder, QueryParseContext context) throws IOException {
List<SpanQueryBuilder> clauses = testQueryBuilder.clauses();
SpanQuery[] spanQueries = new SpanQuery[clauses.size()];
for (int i = 0; i < clauses.size(); i++) {
Query query = clauses.get(i).toQuery(context);
assert query instanceof SpanQuery;
spanQueries[i] = (SpanQuery) query;
}
return new SpanNearQuery(spanQueries, testQueryBuilder.slop(), testQueryBuilder.inOrder(), testQueryBuilder.collectPayloads());
}
@Override
protected SpanNearQueryBuilder doCreateTestQueryBuilder() {
SpanNearQueryBuilder queryBuilder = new SpanNearQueryBuilder(randomIntBetween(-10, 10));
int clauses = randomIntBetween(1, 6);
// we use one random SpanTermQueryBuilder to determine same field name for subsequent clauses
String fieldName = new SpanTermQueryBuilderTest().createTestQueryBuilder().fieldName();
for (int i = 0; i < clauses; i++) {
// we need same field name in all clauses, so we only randomize value
Object value;
switch (fieldName) {
case BOOLEAN_FIELD_NAME: value = randomBoolean(); break;
case INT_FIELD_NAME: value = randomInt(); break;
case DOUBLE_FIELD_NAME: value = randomDouble(); break;
case STRING_FIELD_NAME: value = randomAsciiOfLengthBetween(1, 10); break;
default : value = randomAsciiOfLengthBetween(1, 10);
}
queryBuilder.clause(new SpanTermQueryBuilder(fieldName, value));
}
queryBuilder.inOrder(randomBoolean());
queryBuilder.collectPayloads(randomBoolean());
return queryBuilder;
}
@Test
public void testValidate() {
SpanNearQueryBuilder queryBuilder = new SpanNearQueryBuilder(1);
assertValidate(queryBuilder, 1); // empty clause list
int totalExpectedErrors = 0;
int clauses = randomIntBetween(1, 10);
for (int i = 0; i < clauses; i++) {
if (randomBoolean()) {
queryBuilder.clause(new SpanTermQueryBuilder("", "test"));
totalExpectedErrors++;
} else {
queryBuilder.clause(new SpanTermQueryBuilder("name", "value"));
}
}
assertValidate(queryBuilder, totalExpectedErrors);
}
}

View File

@ -1578,10 +1578,9 @@ public class SearchQueryTests extends ElasticsearchIntegrationTest {
assertHitCount(searchResponse, 1l);
searchResponse = client().prepareSearch("test").setQuery(
spanNearQuery()
spanNearQuery(3)
.clause(spanTermQuery("description", "foo"))
.clause(spanTermQuery("description", "other"))
.slop(3)).get();
.clause(spanTermQuery("description", "other"))).get();
assertHitCount(searchResponse, 3l);
}
@ -1628,28 +1627,28 @@ public class SearchQueryTests extends ElasticsearchIntegrationTest {
refresh();
SearchResponse searchResponse = client().prepareSearch("test")
.setQuery(spanNotQuery().include(spanNearQuery()
.setQuery(spanNotQuery().include(spanNearQuery(1)
.clause(QueryBuilders.spanTermQuery("description", "quick"))
.clause(QueryBuilders.spanTermQuery("description", "fox")).slop(1)).exclude(spanTermQuery("description", "brown"))).get();
.clause(QueryBuilders.spanTermQuery("description", "fox"))).exclude(spanTermQuery("description", "brown"))).get();
assertHitCount(searchResponse, 1l);
searchResponse = client().prepareSearch("test")
.setQuery(spanNotQuery().include(spanNearQuery()
.setQuery(spanNotQuery().include(spanNearQuery(1)
.clause(QueryBuilders.spanTermQuery("description", "quick"))
.clause(QueryBuilders.spanTermQuery("description", "fox")).slop(1)).exclude(spanTermQuery("description", "sleeping")).dist(5)).get();
.clause(QueryBuilders.spanTermQuery("description", "fox"))).exclude(spanTermQuery("description", "sleeping")).dist(5)).get();
assertHitCount(searchResponse, 1l);
searchResponse = client().prepareSearch("test")
.setQuery(spanNotQuery().include(spanNearQuery()
.setQuery(spanNotQuery().include(spanNearQuery(1)
.clause(QueryBuilders.spanTermQuery("description", "quick"))
.clause(QueryBuilders.spanTermQuery("description", "fox")).slop(1)).exclude(spanTermQuery("description", "jumped")).pre(1).post(1)).get();
.clause(QueryBuilders.spanTermQuery("description", "fox"))).exclude(spanTermQuery("description", "jumped")).pre(1).post(1)).get();
assertHitCount(searchResponse, 1l);
try {
client().prepareSearch("test")
.setQuery(spanNotQuery().include(spanNearQuery()
.setQuery(spanNotQuery().include(spanNearQuery(1)
.clause(QueryBuilders.spanTermQuery("description", "quick"))
.clause(QueryBuilders.spanTermQuery("description", "fox")).slop(1)).exclude(spanTermQuery("description", "jumped")).dist(2).pre(2)
.clause(QueryBuilders.spanTermQuery("description", "fox"))).exclude(spanTermQuery("description", "jumped")).dist(2).pre(2)
).get();
fail("ElasticsearchIllegalArgumentException should have been caught");
} catch (ElasticsearchException e) {

View File

@ -17,6 +17,11 @@ Removed setters for mandatory big/little inner span queries. Both arguments now
to be supplied at construction time already and have to be non-null. Updated
static factory methods in QueryBuilders accordingly.
==== SpanNearQueryBuilder
Removed setter for mandatory slop parameter, needs to be set in constructor now.
Updated the static factory methods in QueryBuilders accordingly.
==== QueryFilterBuilder
Removed the setter `queryName(String queryName)` since this field is not supported