Skip to content

Commit 254887a

Browse files
committed
Merge pull request elastic#12156 from cbuescher/feature/query-refactoring-spannear
Query refactoring: SpanNearQueryBuilder and Parser
2 parents 02bd04d + 0c357c8 commit 254887a

File tree

7 files changed

+251
-54
lines changed

7 files changed

+251
-54
lines changed

core/src/main/java/org/elasticsearch/index/query/QueryBuilders.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,8 @@ public static SpanFirstQueryBuilder spanFirstQuery(SpanQueryBuilder match, int e
312312
return new SpanFirstQueryBuilder(match, end);
313313
}
314314

315-
public static SpanNearQueryBuilder spanNearQuery() {
316-
return new SpanNearQueryBuilder();
315+
public static SpanNearQueryBuilder spanNearQuery(int slop) {
316+
return new SpanNearQueryBuilder(slop);
317317
}
318318

319319
public static SpanNotQueryBuilder spanNotQuery() {

core/src/main/java/org/elasticsearch/index/query/SpanNearQueryBuilder.java

Lines changed: 131 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,70 +19,180 @@
1919

2020
package org.elasticsearch.index.query;
2121

22+
import org.apache.lucene.search.Query;
23+
import org.apache.lucene.search.spans.SpanNearQuery;
24+
import org.apache.lucene.search.spans.SpanQuery;
25+
import org.elasticsearch.common.io.stream.StreamInput;
26+
import org.elasticsearch.common.io.stream.StreamOutput;
2227
import org.elasticsearch.common.xcontent.XContentBuilder;
2328

2429
import java.io.IOException;
2530
import java.util.ArrayList;
31+
import java.util.List;
32+
import java.util.Objects;
2633

34+
/**
35+
* Matches spans which are near one another. One can specify slop, the maximum number
36+
* of intervening unmatched positions, as well as whether matches are required to be in-order.
37+
* The span near query maps to Lucene {@link SpanNearQuery}.
38+
*/
2739
public class SpanNearQueryBuilder extends AbstractQueryBuilder<SpanNearQueryBuilder> implements SpanQueryBuilder<SpanNearQueryBuilder> {
2840

2941
public static final String NAME = "span_near";
3042

31-
private ArrayList<SpanQueryBuilder> clauses = new ArrayList<>();
43+
/** Default for flag controlling whether matches are required to be in-order */
44+
public static boolean DEFAULT_IN_ORDER = true;
45+
46+
/** Default for flag controlling whether payloads are collected */
47+
public static boolean DEFAULT_COLLECT_PAYLOADS = true;
48+
49+
private final ArrayList<SpanQueryBuilder> clauses = new ArrayList<>();
3250

33-
private Integer slop = null;
51+
private final int slop;
3452

35-
private Boolean inOrder;
53+
private boolean inOrder = DEFAULT_IN_ORDER;
3654

37-
private Boolean collectPayloads;
55+
private boolean collectPayloads = DEFAULT_COLLECT_PAYLOADS;
3856

3957
static final SpanNearQueryBuilder PROTOTYPE = new SpanNearQueryBuilder();
4058

59+
/**
60+
* @param slop controls the maximum number of intervening unmatched positions permitted
61+
*/
62+
public SpanNearQueryBuilder(int slop) {
63+
this.slop = slop;
64+
}
65+
66+
/**
67+
* only used for prototype
68+
*/
69+
private SpanNearQueryBuilder() {
70+
this.slop = 0;
71+
}
72+
73+
/**
74+
* @return the maximum number of intervening unmatched positions permitted
75+
*/
76+
public int slop() {
77+
return this.slop;
78+
}
79+
4180
public SpanNearQueryBuilder clause(SpanQueryBuilder clause) {
42-
clauses.add(clause);
81+
clauses.add(Objects.requireNonNull(clause));
4382
return this;
4483
}
4584

46-
public SpanNearQueryBuilder slop(int slop) {
47-
this.slop = slop;
48-
return this;
85+
/**
86+
* @return the {@link SpanQueryBuilder} clauses that were set for this query
87+
*/
88+
public List<SpanQueryBuilder> clauses() {
89+
return this.clauses;
4990
}
5091

92+
/**
93+
* When <code>inOrder</code> is true, the spans from each clause
94+
* must be in the same order as in <code>clauses</code> and must be non-overlapping.
95+
* Defaults to <code>true</code>
96+
*/
5197
public SpanNearQueryBuilder inOrder(boolean inOrder) {
5298
this.inOrder = inOrder;
5399
return this;
54100
}
55101

102+
/**
103+
* @see SpanNearQueryBuilder#inOrder(boolean))
104+
*/
105+
public boolean inOrder() {
106+
return this.inOrder;
107+
}
108+
109+
/**
110+
* @param collectPayloads flag controlling whether payloads are collected
111+
*/
56112
public SpanNearQueryBuilder collectPayloads(boolean collectPayloads) {
57113
this.collectPayloads = collectPayloads;
58114
return this;
59115
}
60116

117+
/**
118+
* @see SpanNearQueryBuilder#collectPayloads(boolean))
119+
*/
120+
public boolean collectPayloads() {
121+
return this.collectPayloads;
122+
}
123+
61124
@Override
62125
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
63-
if (clauses.isEmpty()) {
64-
throw new IllegalArgumentException("Must have at least one clause when building a spanNear query");
65-
}
66-
if (slop == null) {
67-
throw new IllegalArgumentException("Must set the slop when building a spanNear query");
68-
}
69126
builder.startObject(NAME);
70127
builder.startArray("clauses");
71128
for (SpanQueryBuilder clause : clauses) {
72129
clause.toXContent(builder, params);
73130
}
74131
builder.endArray();
75-
builder.field("slop", slop.intValue());
76-
if (inOrder != null) {
77-
builder.field("in_order", inOrder);
78-
}
79-
if (collectPayloads != null) {
80-
builder.field("collect_payloads", collectPayloads);
81-
}
132+
builder.field("slop", slop);
133+
builder.field("in_order", inOrder);
134+
builder.field("collect_payloads", collectPayloads);
82135
printBoostAndQueryName(builder);
83136
builder.endObject();
84137
}
85138

139+
@Override
140+
protected Query doToQuery(QueryParseContext parseContext) throws IOException {
141+
SpanQuery[] spanQueries = new SpanQuery[clauses.size()];
142+
for (int i = 0; i < clauses.size(); i++) {
143+
Query query = clauses.get(i).toQuery(parseContext);
144+
assert query instanceof SpanQuery;
145+
spanQueries[i] = (SpanQuery) query;
146+
}
147+
return new SpanNearQuery(spanQueries, slop, inOrder, collectPayloads);
148+
}
149+
150+
@Override
151+
public QueryValidationException validate() {
152+
QueryValidationException validationExceptions = null;
153+
if (clauses.isEmpty()) {
154+
validationExceptions = addValidationError("query must include [clauses]", validationExceptions);
155+
}
156+
for (SpanQueryBuilder innerClause : clauses) {
157+
validationExceptions = validateInnerQuery(innerClause, validationExceptions);
158+
}
159+
return validationExceptions;
160+
}
161+
162+
@Override
163+
protected SpanNearQueryBuilder doReadFrom(StreamInput in) throws IOException {
164+
SpanNearQueryBuilder queryBuilder = new SpanNearQueryBuilder(in.readVInt());
165+
List<SpanQueryBuilder> clauses = in.readNamedWriteableList();
166+
for (SpanQueryBuilder subClause : clauses) {
167+
queryBuilder.clause(subClause);
168+
}
169+
queryBuilder.collectPayloads = in.readBoolean();
170+
queryBuilder.inOrder = in.readBoolean();
171+
return queryBuilder;
172+
173+
}
174+
175+
@Override
176+
protected void doWriteTo(StreamOutput out) throws IOException {
177+
out.writeVInt(slop);
178+
out.writeNamedWriteableList(clauses);
179+
out.writeBoolean(collectPayloads);
180+
out.writeBoolean(inOrder);
181+
}
182+
183+
@Override
184+
protected int doHashCode() {
185+
return Objects.hash(clauses, slop, collectPayloads, inOrder);
186+
}
187+
188+
@Override
189+
protected boolean doEquals(SpanNearQueryBuilder other) {
190+
return Objects.equals(clauses, other.clauses) &&
191+
Objects.equals(slop, other.slop) &&
192+
Objects.equals(collectPayloads, other.collectPayloads) &&
193+
Objects.equals(inOrder, other.inOrder);
194+
}
195+
86196
@Override
87197
public String getName() {
88198
return NAME;

core/src/main/java/org/elasticsearch/index/query/SpanNearQueryParser.java

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@
1919

2020
package org.elasticsearch.index.query;
2121

22-
import org.apache.lucene.search.Query;
23-
import org.apache.lucene.search.spans.SpanNearQuery;
24-
import org.apache.lucene.search.spans.SpanQuery;
2522
import org.elasticsearch.common.Strings;
2623
import org.elasticsearch.common.inject.Inject;
2724
import org.elasticsearch.common.xcontent.XContentParser;
@@ -34,7 +31,7 @@
3431
/**
3532
*
3633
*/
37-
public class SpanNearQueryParser extends BaseQueryParserTemp {
34+
public class SpanNearQueryParser extends BaseQueryParser {
3835

3936
@Inject
4037
public SpanNearQueryParser() {
@@ -46,16 +43,16 @@ public String[] names() {
4643
}
4744

4845
@Override
49-
public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException {
46+
public QueryBuilder fromXContent(QueryParseContext parseContext) throws IOException, QueryParsingException {
5047
XContentParser parser = parseContext.parser();
5148

5249
float boost = AbstractQueryBuilder.DEFAULT_BOOST;
5350
Integer slop = null;
54-
boolean inOrder = true;
55-
boolean collectPayloads = true;
51+
boolean inOrder = SpanNearQueryBuilder.DEFAULT_IN_ORDER;
52+
boolean collectPayloads = SpanNearQueryBuilder.DEFAULT_COLLECT_PAYLOADS;
5653
String queryName = null;
5754

58-
List<SpanQuery> clauses = newArrayList();
55+
List<SpanQueryBuilder> clauses = newArrayList();
5956

6057
String currentFieldName = null;
6158
XContentParser.Token token;
@@ -65,11 +62,11 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
6562
} else if (token == XContentParser.Token.START_ARRAY) {
6663
if ("clauses".equals(currentFieldName)) {
6764
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
68-
Query query = parseContext.parseInnerQuery();
69-
if (!(query instanceof SpanQuery)) {
65+
QueryBuilder query = parseContext.parseInnerQueryBuilder();
66+
if (!(query instanceof SpanQueryBuilder)) {
7067
throw new QueryParsingException(parseContext, "spanNear [clauses] must be of type span query");
7168
}
72-
clauses.add((SpanQuery) query);
69+
clauses.add((SpanQueryBuilder) query);
7370
}
7471
} else {
7572
throw new QueryParsingException(parseContext, "[span_near] query does not support [" + currentFieldName + "]");
@@ -92,19 +89,20 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
9289
throw new QueryParsingException(parseContext, "[span_near] query does not support [" + currentFieldName + "]");
9390
}
9491
}
95-
if (clauses.isEmpty()) {
96-
throw new QueryParsingException(parseContext, "span_near must include [clauses]");
97-
}
92+
9893
if (slop == null) {
9994
throw new QueryParsingException(parseContext, "span_near must include [slop]");
10095
}
10196

102-
SpanNearQuery query = new SpanNearQuery(clauses.toArray(new SpanQuery[clauses.size()]), slop.intValue(), inOrder, collectPayloads);
103-
query.setBoost(boost);
104-
if (queryName != null) {
105-
parseContext.addNamedQuery(queryName, query);
97+
SpanNearQueryBuilder queryBuilder = new SpanNearQueryBuilder(slop);
98+
for (SpanQueryBuilder subQuery : clauses) {
99+
queryBuilder.clause(subQuery);
106100
}
107-
return query;
101+
queryBuilder.inOrder(inOrder);
102+
queryBuilder.collectPayloads(collectPayloads);
103+
queryBuilder.boost(boost);
104+
queryBuilder.queryName(queryName);
105+
return queryBuilder;
108106
}
109107

110108
@Override

core/src/test/java/org/elasticsearch/index/query/SimpleIndexQueryParserTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1384,7 +1384,7 @@ public void testSpanFirstQuery() throws IOException {
13841384
@Test
13851385
public void testSpanNearQueryBuilder() throws IOException {
13861386
IndexQueryParserService queryParser = queryParser();
1387-
Query parsedQuery = queryParser.parse(spanNearQuery().clause(spanTermQuery("age", 34)).clause(spanTermQuery("age", 35)).clause(spanTermQuery("age", 36)).slop(12).inOrder(false).collectPayloads(false)).query();
1387+
Query parsedQuery = queryParser.parse(spanNearQuery(12).clause(spanTermQuery("age", 34)).clause(spanTermQuery("age", 35)).clause(spanTermQuery("age", 36)).inOrder(false).collectPayloads(false)).query();
13881388
assertThat(parsedQuery, instanceOf(SpanNearQuery.class));
13891389
SpanNearQuery spanNearQuery = (SpanNearQuery) parsedQuery;
13901390
assertThat(spanNearQuery.getClauses().length, equalTo(3));
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.index.query;
21+
22+
import org.apache.lucene.search.Query;
23+
import org.apache.lucene.search.spans.SpanNearQuery;
24+
import org.apache.lucene.search.spans.SpanQuery;
25+
import org.junit.Test;
26+
27+
import java.io.IOException;
28+
import java.util.List;
29+
30+
public class SpanNearQueryBuilderTest extends BaseQueryTestCase<SpanNearQueryBuilder> {
31+
32+
@Override
33+
protected Query doCreateExpectedQuery(SpanNearQueryBuilder testQueryBuilder, QueryParseContext context) throws IOException {
34+
List<SpanQueryBuilder> clauses = testQueryBuilder.clauses();
35+
SpanQuery[] spanQueries = new SpanQuery[clauses.size()];
36+
for (int i = 0; i < clauses.size(); i++) {
37+
Query query = clauses.get(i).toQuery(context);
38+
assert query instanceof SpanQuery;
39+
spanQueries[i] = (SpanQuery) query;
40+
}
41+
return new SpanNearQuery(spanQueries, testQueryBuilder.slop(), testQueryBuilder.inOrder(), testQueryBuilder.collectPayloads());
42+
43+
}
44+
45+
@Override
46+
protected SpanNearQueryBuilder doCreateTestQueryBuilder() {
47+
SpanNearQueryBuilder queryBuilder = new SpanNearQueryBuilder(randomIntBetween(-10, 10));
48+
int clauses = randomIntBetween(1, 6);
49+
// we use one random SpanTermQueryBuilder to determine same field name for subsequent clauses
50+
String fieldName = new SpanTermQueryBuilderTest().createTestQueryBuilder().fieldName();
51+
for (int i = 0; i < clauses; i++) {
52+
// we need same field name in all clauses, so we only randomize value
53+
Object value;
54+
switch (fieldName) {
55+
case BOOLEAN_FIELD_NAME: value = randomBoolean(); break;
56+
case INT_FIELD_NAME: value = randomInt(); break;
57+
case DOUBLE_FIELD_NAME: value = randomDouble(); break;
58+
case STRING_FIELD_NAME: value = randomAsciiOfLengthBetween(1, 10); break;
59+
default : value = randomAsciiOfLengthBetween(1, 10);
60+
}
61+
queryBuilder.clause(new SpanTermQueryBuilder(fieldName, value));
62+
}
63+
queryBuilder.inOrder(randomBoolean());
64+
queryBuilder.collectPayloads(randomBoolean());
65+
return queryBuilder;
66+
}
67+
68+
@Test
69+
public void testValidate() {
70+
SpanNearQueryBuilder queryBuilder = new SpanNearQueryBuilder(1);
71+
assertValidate(queryBuilder, 1); // empty clause list
72+
73+
int totalExpectedErrors = 0;
74+
int clauses = randomIntBetween(1, 10);
75+
for (int i = 0; i < clauses; i++) {
76+
if (randomBoolean()) {
77+
queryBuilder.clause(new SpanTermQueryBuilder("", "test"));
78+
totalExpectedErrors++;
79+
} else {
80+
queryBuilder.clause(new SpanTermQueryBuilder("name", "value"));
81+
}
82+
}
83+
assertValidate(queryBuilder, totalExpectedErrors);
84+
}
85+
}

0 commit comments

Comments
 (0)