Adds SpanGapQueryBuilder in the query DSL (#28636)

This change adds the support for a `span_gap` query inside the span query DSL.
This commit is contained in:
Chandan83 2018-04-13 18:21:03 +05:30 committed by Jim Ferenczi
parent 5dcfdb09cb
commit 782517b452
5 changed files with 341 additions and 8 deletions

View File

@ -24,9 +24,11 @@ import org.apache.lucene.search.spans.SpanNearQuery;
import org.apache.lucene.search.spans.SpanQuery;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.Strings;
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.XContentLocation;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
@ -203,18 +205,54 @@ public class SpanNearQueryBuilder extends AbstractQueryBuilder<SpanNearQueryBuil
@Override
protected Query doToQuery(QueryShardContext context) throws IOException {
if (clauses.size() == 1) {
Query query = clauses.get(0).toQuery(context);
SpanQueryBuilder queryBuilder = clauses.get(0);
boolean isGap = queryBuilder instanceof SpanGapQueryBuilder;
Query query = null;
if (!isGap) {
query = queryBuilder.toQuery(context);
assert query instanceof SpanQuery;
}
if (clauses.size() == 1) {
assert !isGap;
return query;
}
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;
String spanNearFieldName = null;
if (isGap) {
spanNearFieldName = ((SpanGapQueryBuilder) queryBuilder).fieldName();
} else {
spanNearFieldName = ((SpanQuery) query).getField();
}
return new SpanNearQuery(spanQueries, slop, inOrder);
SpanNearQuery.Builder builder = new SpanNearQuery.Builder(spanNearFieldName, inOrder);
builder.setSlop(slop);
/*
* Lucene SpanNearQuery throws exceptions for certain use cases like adding gap to a
* unordered SpanNearQuery. Should ES have the same checks or wrap those thrown exceptions?
*/
if (isGap) {
int gap = ((SpanGapQueryBuilder) queryBuilder).width();
builder.addGap(gap);
} else {
builder.addClause((SpanQuery) query);
}
for (int i = 1; i < clauses.size(); i++) {
queryBuilder = clauses.get(i);
isGap = queryBuilder instanceof SpanGapQueryBuilder;
if (isGap) {
String fieldName = ((SpanGapQueryBuilder) queryBuilder).fieldName();
if (!spanNearFieldName.equals(fieldName)) {
throw new IllegalArgumentException("[span_near] clauses must have same field");
}
int gap = ((SpanGapQueryBuilder) queryBuilder).width();
builder.addGap(gap);
} else {
query = clauses.get(i).toQuery(context);
assert query instanceof SpanQuery;
builder.addClause((SpanQuery)query);
}
}
return builder.build();
}
@Override
@ -233,4 +271,168 @@ public class SpanNearQueryBuilder extends AbstractQueryBuilder<SpanNearQueryBuil
public String getWriteableName() {
return NAME;
}
/**
* SpanGapQueryBuilder enables gaps in a SpanNearQuery.
* Since, SpanGapQuery is private to SpanNearQuery, SpanGapQueryBuilder cannot
* be used to generate a Query (SpanGapQuery) like another QueryBuilder.
* Instead, it just identifies a span_gap clause so that SpanNearQuery.addGap(int)
* can be invoked for it.
* This QueryBuilder is only applicable as a clause in SpanGapQueryBuilder but
* yet to enforce this restriction.
*/
public static class SpanGapQueryBuilder implements SpanQueryBuilder {
public static final String NAME = "span_gap";
/** Name of field to match against. */
private final String fieldName;
/** Width of the gap introduced. */
private final int width;
/**
* Constructs a new SpanGapQueryBuilder term query.
*
* @param fieldName The name of the field
* @param width The width of the gap introduced
*/
public SpanGapQueryBuilder(String fieldName, int width) {
if (Strings.isEmpty(fieldName)) {
throw new IllegalArgumentException("[span_gap] field name is null or empty");
}
//lucene has not coded any restriction on value of width.
//to-do : find if theoretically it makes sense to apply restrictions.
this.fieldName = fieldName;
this.width = width;
}
/**
* Read from a stream.
*/
public SpanGapQueryBuilder(StreamInput in) throws IOException {
fieldName = in.readString();
width = in.readInt();
}
/**
* @return fieldName The name of the field
*/
public String fieldName() {
return fieldName;
}
/**
* @return width The width of the gap introduced
*/
public int width() {
return width;
}
@Override
public Query toQuery(QueryShardContext context) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public Query toFilter(QueryShardContext context) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public String queryName() {
throw new UnsupportedOperationException();
}
@Override
public QueryBuilder queryName(String queryName) {
throw new UnsupportedOperationException();
}
@Override
public float boost() {
throw new UnsupportedOperationException();
}
@Override
public QueryBuilder boost(float boost) {
throw new UnsupportedOperationException();
}
@Override
public String getName() {
return NAME;
}
@Override
public String getWriteableName() {
return NAME;
}
@Override
public final void writeTo(StreamOutput out) throws IOException {
out.writeString(fieldName);
out.writeInt(width);
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.startObject(getName());
builder.field(fieldName, width);
builder.endObject();
builder.endObject();
return builder;
}
public static SpanGapQueryBuilder fromXContent(XContentParser parser) throws IOException {
String fieldName = null;
int width = 0;
String currentFieldName = null;
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
throwParsingExceptionOnMultipleFields(NAME, parser.getTokenLocation(), fieldName, currentFieldName);
fieldName = currentFieldName;
} else if (token.isValue()) {
width = parser.intValue();
}
}
SpanGapQueryBuilder result = new SpanGapQueryBuilder(fieldName, width);
return result;
}
@Override
public final boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
SpanGapQueryBuilder other = (SpanGapQueryBuilder) obj;
return Objects.equals(fieldName, other.fieldName) &&
Objects.equals(width, other.width);
}
@Override
public final int hashCode() {
return Objects.hash(getClass(), fieldName, width);
}
@Override
public final String toString() {
return Strings.toString(this, true, true);
}
//copied from AbstractQueryBuilder
protected static void throwParsingExceptionOnMultipleFields(String queryName, XContentLocation contentLocation,
String processedFieldName, String currentFieldName) {
if (processedFieldName != null) {
throw new ParsingException(contentLocation, "[" + queryName + "] query doesn't support multiple fields, found ["
+ processedFieldName + "] and [" + currentFieldName + "]");
}
}
}
}

View File

@ -260,6 +260,7 @@ import java.util.function.Function;
import static java.util.Collections.unmodifiableMap;
import static java.util.Objects.requireNonNull;
import static org.elasticsearch.index.query.SpanNearQueryBuilder.SpanGapQueryBuilder;
/**
* Sets up things that can be done at search time like queries, aggregations, and suggesters.
@ -741,6 +742,7 @@ public class SearchModule {
FieldMaskingSpanQueryBuilder::fromXContent));
registerQuery(new QuerySpec<>(SpanFirstQueryBuilder.NAME, SpanFirstQueryBuilder::new, SpanFirstQueryBuilder::fromXContent));
registerQuery(new QuerySpec<>(SpanNearQueryBuilder.NAME, SpanNearQueryBuilder::new, SpanNearQueryBuilder::fromXContent));
registerQuery(new QuerySpec<>(SpanGapQueryBuilder.NAME, SpanGapQueryBuilder::new, SpanGapQueryBuilder::fromXContent));
registerQuery(new QuerySpec<>(SpanOrQueryBuilder.NAME, SpanOrQueryBuilder::new, SpanOrQueryBuilder::fromXContent));
registerQuery(new QuerySpec<>(MoreLikeThisQueryBuilder.NAME, MoreLikeThisQueryBuilder::new,
MoreLikeThisQueryBuilder::fromXContent));

View File

@ -0,0 +1,127 @@
/*
* 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.SpanBoostQuery;
import org.apache.lucene.search.spans.SpanNearQuery;
import org.apache.lucene.search.spans.SpanQuery;
import org.apache.lucene.search.spans.SpanTermQuery;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.search.internal.SearchContext;
import org.elasticsearch.test.AbstractQueryTestCase;
import java.io.IOException;
import java.util.Iterator;
import static org.elasticsearch.index.query.SpanNearQueryBuilder.SpanGapQueryBuilder;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.either;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
/*
* SpanGapQueryBuilder, unlike other QBs, is not used to build a Query. Therefore, it is not suited
* to test pattern of AbstractQueryTestCase. Since it is only used in SpanNearQueryBuilder, its test cases
* are same as those of later with SpanGapQueryBuilder included as clauses.
*/
public class SpanGapQueryBuilderTests extends AbstractQueryTestCase<SpanNearQueryBuilder> {
@Override
protected SpanNearQueryBuilder doCreateTestQueryBuilder() {
SpanTermQueryBuilder[] spanTermQueries = new SpanTermQueryBuilderTests().createSpanTermQueryBuilders(randomIntBetween(1, 6));
SpanNearQueryBuilder queryBuilder = new SpanNearQueryBuilder(spanTermQueries[0], randomIntBetween(-10, 10));
for (int i = 1; i < spanTermQueries.length; i++) {
SpanTermQueryBuilder termQB = spanTermQueries[i];
queryBuilder.addClause(termQB);
if (i % 2 == 1) {
SpanGapQueryBuilder gapQB = new SpanGapQueryBuilder(termQB.fieldName(), randomIntBetween(1,2));
queryBuilder.addClause(gapQB);
}
}
queryBuilder.inOrder(true);
return queryBuilder;
}
@Override
protected void doAssertLuceneQuery(SpanNearQueryBuilder queryBuilder, Query query, SearchContext context) throws IOException {
assertThat(query, either(instanceOf(SpanNearQuery.class))
.or(instanceOf(SpanTermQuery.class))
.or(instanceOf(SpanBoostQuery.class))
.or(instanceOf(MatchAllQueryBuilder.class)));
if (query instanceof SpanNearQuery) {
SpanNearQuery spanNearQuery = (SpanNearQuery) query;
assertThat(spanNearQuery.getSlop(), equalTo(queryBuilder.slop()));
assertThat(spanNearQuery.isInOrder(), equalTo(queryBuilder.inOrder()));
assertThat(spanNearQuery.getClauses().length, equalTo(queryBuilder.clauses().size()));
Iterator<SpanQueryBuilder> spanQueryBuilderIterator = queryBuilder.clauses().iterator();
for (SpanQuery spanQuery : spanNearQuery.getClauses()) {
SpanQueryBuilder spanQB = spanQueryBuilderIterator.next();
if (spanQB instanceof SpanGapQueryBuilder) continue;
assertThat(spanQuery, equalTo(spanQB.toQuery(context.getQueryShardContext())));
}
} else if (query instanceof SpanTermQuery || query instanceof SpanBoostQuery) {
assertThat(queryBuilder.clauses().size(), equalTo(1));
assertThat(query, equalTo(queryBuilder.clauses().get(0).toQuery(context.getQueryShardContext())));
}
}
public void testIllegalArguments() {
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new SpanGapQueryBuilder(null, 1));
assertEquals("[span_gap] field name is null or empty", e.getMessage());
}
public void testFromJson() throws IOException {
String json =
"{\n" +
" \"span_near\" : {\n" +
" \"clauses\" : [ {\n" +
" \"span_term\" : {\n" +
" \"field\" : {\n" +
" \"value\" : \"value1\",\n" +
" \"boost\" : 1.0\n" +
" }\n" +
" }\n" +
" }, {\n" +
" \"span_gap\" : {\n" +
" \"field\" : 2" +
" }\n" +
" }, {\n" +
" \"span_term\" : {\n" +
" \"field\" : {\n" +
" \"value\" : \"value3\",\n" +
" \"boost\" : 1.0\n" +
" }\n" +
" }\n" +
" } ],\n" +
" \"slop\" : 12,\n" +
" \"in_order\" : false,\n" +
" \"boost\" : 1.0\n" +
" }\n" +
"}";
SpanNearQueryBuilder parsed = (SpanNearQueryBuilder) parseQuery(json);
checkGeneratedJson(json, parsed);
assertEquals(json, 3, parsed.clauses().size());
assertEquals(json, 12, parsed.slop());
assertEquals(json, false, parsed.inOrder());
}
}

View File

@ -184,4 +184,5 @@ public class SpanNearQueryBuilderTests extends AbstractQueryTestCase<SpanNearQue
() -> parseQuery(json));
assertThat(e.getMessage(), containsString("[span_near] query does not support [collect_payloads]"));
}
}

View File

@ -324,6 +324,7 @@ public class SearchModuleTests extends ModuleTestCase {
"simple_query_string",
"span_containing",
"span_first",
"span_gap",
"span_multi",
"span_near",
"span_not",