Skip to content

Commit

Permalink
Query refactoring: SpanNearQueryBuilder and Parser
Browse files Browse the repository at this point in the history
Moving the query building functionality from the parser to the builders
new toQuery() method analogous to other recent query refactorings.

Relates to #10217
  • Loading branch information
cbuescher committed Jul 9, 2015
1 parent 6c79569 commit c689e89
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,8 @@ public static SpanFirstQueryBuilder spanFirstQuery(SpanQueryBuilder match, int e
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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/** Default for flag controlling whether payloads are collected */
public static boolean DEFAULT_COLLECT_PAYLOADS = true;

private final ArrayList<SpanQueryBuilder> clauses = new ArrayList<>();

private Integer slop = null;
private final int slop;

private Boolean inOrder;
private boolean inOrder = DEFAULT_IN_ORDER;

private Boolean collectPayloads;
private boolean collectPayloads = DEFAULT_COLLECT_PAYLOADS;

static final SpanNearQueryBuilder PROTOTYPE = new SpanNearQueryBuilder();

/**
* @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(clause);
clauses.add(Objects.requireNonNull(clause));
return this;
}

public SpanNearQueryBuilder slop(int slop) {
this.slop = slop;
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,7 +31,7 @@
/**
*
*/
public class SpanNearQueryParser extends BaseQueryParserTemp {
public class SpanNearQueryParser extends BaseQueryParser {

@Inject
public SpanNearQueryParser() {
Expand All @@ -46,16 +43,16 @@ public String[] names() {
}

@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;
Expand All @@ -65,11 +62,11 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
} 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 + "]");
Expand All @@ -92,19 +89,20 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1384,7 +1384,7 @@ public void testSpanFirstQuery() throws IOException {
@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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit c689e89

Please sign in to comment.