Skip to content

Commit 966c02b

Browse files
committed
Query refactoring: SpanMultiTermQueryBuilder and Parser
Moving the query building functionality from the parser to the builders new toQuery() method analogous to other recent query refactorings. Relates to #10217 Closes #12182
1 parent 37cdc13 commit 966c02b

File tree

6 files changed

+200
-37
lines changed

6 files changed

+200
-37
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
* Doesn't support conversion to {@link org.elasticsearch.common.xcontent.XContent} via {@link #doXContent(XContentBuilder, Params)}.
3030
*/
3131
//norelease to be removed once all queries support separate fromXContent and toQuery methods. Make AbstractQueryBuilder#toQuery final as well then.
32-
public class QueryWrappingQueryBuilder extends AbstractQueryBuilder<QueryWrappingQueryBuilder> {
32+
public class QueryWrappingQueryBuilder extends AbstractQueryBuilder<QueryWrappingQueryBuilder> implements MultiTermQueryBuilder<QueryWrappingQueryBuilder> {
3333

3434
private Query query;
3535

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

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,38 @@
1818
*/
1919
package org.elasticsearch.index.query;
2020

21+
import org.apache.lucene.search.MultiTermQuery;
22+
import org.apache.lucene.search.Query;
23+
import org.apache.lucene.search.spans.SpanMultiTermQueryWrapper;
24+
import org.elasticsearch.common.io.stream.StreamInput;
25+
import org.elasticsearch.common.io.stream.StreamOutput;
2126
import org.elasticsearch.common.xcontent.XContentBuilder;
22-
2327
import java.io.IOException;
28+
import java.util.Objects;
2429

30+
/**
31+
* Query that allows wraping a {@link MultiTermQueryBuilder} (one of wildcard, fuzzy, prefix, term, range or regexp query)
32+
* as a {@link SpanQueryBuilder} so it can be nested.
33+
*/
2534
public class SpanMultiTermQueryBuilder extends AbstractQueryBuilder<SpanMultiTermQueryBuilder> implements SpanQueryBuilder<SpanMultiTermQueryBuilder> {
2635

2736
public static final String NAME = "span_multi";
28-
private MultiTermQueryBuilder multiTermQueryBuilder;
29-
static final SpanMultiTermQueryBuilder PROTOTYPE = new SpanMultiTermQueryBuilder(null);
37+
private final MultiTermQueryBuilder multiTermQueryBuilder;
38+
static final SpanMultiTermQueryBuilder PROTOTYPE = new SpanMultiTermQueryBuilder();
3039

3140
public SpanMultiTermQueryBuilder(MultiTermQueryBuilder multiTermQueryBuilder) {
32-
this.multiTermQueryBuilder = multiTermQueryBuilder;
41+
this.multiTermQueryBuilder = Objects.requireNonNull(multiTermQueryBuilder);
42+
}
43+
44+
/**
45+
* only used for prototype
46+
*/
47+
private SpanMultiTermQueryBuilder() {
48+
this.multiTermQueryBuilder = null;
49+
}
50+
51+
public MultiTermQueryBuilder multiTermQueryBuilder() {
52+
return this.multiTermQueryBuilder;
3353
}
3454

3555
@Override
@@ -41,6 +61,42 @@ protected void doXContent(XContentBuilder builder, Params params)
4161
builder.endObject();
4262
}
4363

64+
@Override
65+
protected Query doToQuery(QueryParseContext parseContext) throws IOException {
66+
Query subQuery = multiTermQueryBuilder.toQuery(parseContext);
67+
if (subQuery instanceof MultiTermQuery == false) {
68+
throw new UnsupportedOperationException("unsupported inner query, should be " + MultiTermQuery.class.getName() +" but was "
69+
+ subQuery.getClass().getName());
70+
}
71+
return new SpanMultiTermQueryWrapper<>((MultiTermQuery) subQuery);
72+
}
73+
74+
@Override
75+
public QueryValidationException validate() {
76+
return validateInnerQuery(multiTermQueryBuilder, null);
77+
}
78+
79+
@Override
80+
protected SpanMultiTermQueryBuilder doReadFrom(StreamInput in) throws IOException {
81+
MultiTermQueryBuilder multiTermBuilder = in.readNamedWriteable();
82+
return new SpanMultiTermQueryBuilder(multiTermBuilder);
83+
}
84+
85+
@Override
86+
protected void doWriteTo(StreamOutput out) throws IOException {
87+
out.writeNamedWriteable(multiTermQueryBuilder);
88+
}
89+
90+
@Override
91+
protected int doHashCode() {
92+
return Objects.hash(multiTermQueryBuilder);
93+
}
94+
95+
@Override
96+
protected boolean doEquals(SpanMultiTermQueryBuilder other) {
97+
return Objects.equals(multiTermQueryBuilder, other.multiTermQueryBuilder);
98+
}
99+
44100
@Override
45101
public String getName() {
46102
return NAME;

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@
1818
*/
1919
package org.elasticsearch.index.query;
2020

21-
import org.apache.lucene.search.MultiTermQuery;
22-
import org.apache.lucene.search.Query;
23-
import org.apache.lucene.search.spans.SpanMultiTermQueryWrapper;
2421
import org.elasticsearch.common.Strings;
2522
import org.elasticsearch.common.inject.Inject;
2623
import org.elasticsearch.common.xcontent.XContentParser;
@@ -31,7 +28,7 @@
3128
/**
3229
*
3330
*/
34-
public class SpanMultiTermQueryParser extends BaseQueryParserTemp {
31+
public class SpanMultiTermQueryParser extends BaseQueryParser {
3532

3633
public static final String MATCH_NAME = "match";
3734

@@ -45,7 +42,7 @@ public String[] names() {
4542
}
4643

4744
@Override
48-
public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException {
45+
public QueryBuilder fromXContent(QueryParseContext parseContext) throws IOException, QueryParsingException {
4946
XContentParser parser = parseContext.parser();
5047

5148
Token token = parser.nextToken();
@@ -58,13 +55,13 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
5855
throw new QueryParsingException(parseContext, "spanMultiTerm must have [" + MATCH_NAME + "] multi term query clause");
5956
}
6057

61-
Query subQuery = parseContext.parseInnerQuery();
62-
if (!(subQuery instanceof MultiTermQuery)) {
58+
QueryBuilder subQuery = parseContext.parseInnerQueryBuilder();
59+
if (subQuery instanceof MultiTermQueryBuilder == false) {
6360
throw new QueryParsingException(parseContext, "spanMultiTerm [" + MATCH_NAME + "] must be of type multi term query");
6461
}
6562

6663
parser.nextToken();
67-
return new SpanMultiTermQueryWrapper<>((MultiTermQuery) subQuery);
64+
return new SpanMultiTermQueryBuilder((MultiTermQueryBuilder) subQuery);
6865
}
6966

7067
@Override

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.elasticsearch.index.query;
2121

2222
import com.carrotsearch.randomizedtesting.generators.RandomInts;
23+
import com.carrotsearch.randomizedtesting.generators.RandomStrings;
2324

2425
import java.util.Random;
2526

@@ -36,20 +37,37 @@ public class RandomQueryBuilder {
3637
* @return a random {@link QueryBuilder}
3738
*/
3839
public static QueryBuilder createQuery(Random r) {
39-
switch (RandomInts.randomIntBetween(r, 0, 3)) {
40+
switch (RandomInts.randomIntBetween(r, 0, 4)) {
4041
case 0:
4142
return new MatchAllQueryBuilderTest().createTestQueryBuilder();
4243
case 1:
4344
return new TermQueryBuilderTest().createTestQueryBuilder();
4445
case 2:
4546
return new IdsQueryBuilderTest().createTestQueryBuilder();
4647
case 3:
48+
return createMultiTermQuery(r);
49+
case 4:
4750
return EmptyQueryBuilder.PROTOTYPE;
4851
default:
4952
throw new UnsupportedOperationException();
5053
}
5154
}
5255

56+
/**
57+
* Create a new multi term query of a random type
58+
* @param r random seed
59+
* @return a random {@link MultiTermQueryBuilder}
60+
*/
61+
public static MultiTermQueryBuilder createMultiTermQuery(Random r) {
62+
// for now, only use String Rangequeries for MultiTerm test, numeric and date makes little sense
63+
// see issue #12123 for discussion
64+
// Prefix / Fuzzy / RegEx / Wildcard can go here later once refactored and they have random query generators
65+
RangeQueryBuilder query = new RangeQueryBuilder(BaseQueryTestCase.STRING_FIELD_NAME);
66+
query.from("a" + RandomStrings.randomAsciiOfLengthBetween(r, 1, 10));
67+
query.to("z" + RandomStrings.randomAsciiOfLengthBetween(r, 1, 10));
68+
return query;
69+
}
70+
5371
/**
5472
* Create a new invalid query of a random type
5573
* @param r random seed

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

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,32 +45,42 @@ public class RangeQueryBuilderTest extends BaseQueryTestCase<RangeQueryBuilder>
4545
protected RangeQueryBuilder doCreateTestQueryBuilder() {
4646
RangeQueryBuilder query;
4747
// switch between numeric and date ranges
48-
if (randomBoolean()) {
49-
if (randomBoolean()) {
50-
// use mapped integer field for numeric range queries
51-
query = new RangeQueryBuilder(INT_FIELD_NAME);
52-
query.from(randomIntBetween(1, 100));
53-
query.to(randomIntBetween(101, 200));
54-
} else {
55-
// use unmapped field for numeric range queries
56-
query = new RangeQueryBuilder(randomAsciiOfLengthBetween(1, 10));
57-
query.from(0.0 - randomDouble());
58-
query.to(randomDouble());
59-
}
60-
} else {
61-
// use mapped date field, using date string representation
62-
query = new RangeQueryBuilder(DATE_FIELD_NAME);
63-
query.from(new DateTime(System.currentTimeMillis() - randomIntBetween(0, 1000000), DateTimeZone.UTC).toString());
64-
query.to(new DateTime(System.currentTimeMillis() + randomIntBetween(0, 1000000), DateTimeZone.UTC).toString());
65-
// Create timestamp option only then we have a date mapper, otherwise we could trigger exception.
66-
if (createContext().mapperService().smartNameFieldType(DATE_FIELD_NAME) != null) {
48+
switch (randomIntBetween(0, 2)) {
49+
case 0:
6750
if (randomBoolean()) {
68-
query.timeZone(TIMEZONE_IDS.get(randomIntBetween(0, TIMEZONE_IDS.size() - 1)));
51+
// use mapped integer field for numeric range queries
52+
query = new RangeQueryBuilder(INT_FIELD_NAME);
53+
query.from(randomIntBetween(1, 100));
54+
query.to(randomIntBetween(101, 200));
55+
} else {
56+
// use unmapped field for numeric range queries
57+
query = new RangeQueryBuilder(randomAsciiOfLengthBetween(1, 10));
58+
query.from(0.0 - randomDouble());
59+
query.to(randomDouble());
6960
}
70-
if (randomBoolean()) {
71-
query.format("yyyy-MM-dd'T'HH:mm:ss.SSSZZ");
61+
break;
62+
case 1:
63+
// use mapped date field, using date string representation
64+
query = new RangeQueryBuilder(DATE_FIELD_NAME);
65+
query.from(new DateTime(System.currentTimeMillis() - randomIntBetween(0, 1000000), DateTimeZone.UTC).toString());
66+
query.to(new DateTime(System.currentTimeMillis() + randomIntBetween(0, 1000000), DateTimeZone.UTC).toString());
67+
// Create timestamp option only then we have a date mapper,
68+
// otherwise we could trigger exception.
69+
if (createContext().mapperService().smartNameFieldType(DATE_FIELD_NAME) != null) {
70+
if (randomBoolean()) {
71+
query.timeZone(TIMEZONE_IDS.get(randomIntBetween(0, TIMEZONE_IDS.size() - 1)));
72+
}
73+
if (randomBoolean()) {
74+
query.format("yyyy-MM-dd'T'HH:mm:ss.SSSZZ");
75+
}
7276
}
73-
}
77+
break;
78+
case 2:
79+
default:
80+
query = new RangeQueryBuilder(STRING_FIELD_NAME);
81+
query.from("a" + randomAsciiOfLengthBetween(1, 10));
82+
query.to("z" + randomAsciiOfLengthBetween(1, 10));
83+
break;
7484
}
7585
query.includeLower(randomBoolean()).includeUpper(randomBoolean());
7686
if (randomBoolean()) {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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.MultiTermQuery;
23+
import org.apache.lucene.search.Query;
24+
import org.apache.lucene.search.spans.SpanMultiTermQueryWrapper;
25+
import org.junit.Test;
26+
27+
import java.io.IOException;
28+
29+
public class SpanMultiTermQueryBuilderTest extends BaseQueryTestCase<SpanMultiTermQueryBuilder> {
30+
31+
@Override
32+
protected Query doCreateExpectedQuery(SpanMultiTermQueryBuilder testQueryBuilder, QueryParseContext context) throws IOException {
33+
Query multiTermQuery = testQueryBuilder.multiTermQueryBuilder().toQuery(context);
34+
return new SpanMultiTermQueryWrapper<>((MultiTermQuery) multiTermQuery);
35+
}
36+
37+
@Override
38+
protected SpanMultiTermQueryBuilder doCreateTestQueryBuilder() {
39+
MultiTermQueryBuilder multiTermQueryBuilder = RandomQueryBuilder.createMultiTermQuery(random());
40+
return new SpanMultiTermQueryBuilder(multiTermQueryBuilder);
41+
}
42+
43+
@Test
44+
public void testValidate() {
45+
int totalExpectedErrors = 0;
46+
MultiTermQueryBuilder multiTermQueryBuilder;
47+
if (randomBoolean()) {
48+
multiTermQueryBuilder = new RangeQueryBuilder("");
49+
totalExpectedErrors++;
50+
} else {
51+
multiTermQueryBuilder = new RangeQueryBuilder("field");
52+
}
53+
SpanMultiTermQueryBuilder queryBuilder = new SpanMultiTermQueryBuilder(multiTermQueryBuilder);
54+
assertValidate(queryBuilder, totalExpectedErrors);
55+
}
56+
57+
@Test(expected = NullPointerException.class)
58+
public void testInnerQueryNull() {
59+
new SpanMultiTermQueryBuilder(null);
60+
}
61+
62+
/**
63+
* test checks that we throw an {@link UnsupportedOperationException} if the query wrapped
64+
* by {@link SpanMultiTermQueryBuilder} does not generate a lucene {@link MultiTermQuery}.
65+
* This is currently the case for {@link RangeQueryBuilder} when the target field is mapped
66+
* to a date.
67+
*/
68+
@Test
69+
public void testUnsupportedInnerQueryType() throws IOException {
70+
QueryParseContext parseContext = createContext();
71+
// test makes only sense if date field is mapped
72+
if (parseContext.fieldMapper(DATE_FIELD_NAME) != null) {
73+
try {
74+
RangeQueryBuilder query = new RangeQueryBuilder(DATE_FIELD_NAME);
75+
new SpanMultiTermQueryBuilder(query).toQuery(createContext());
76+
fail("Exception expected, range query on date fields should not generate a lucene " + MultiTermQuery.class.getName());
77+
} catch (UnsupportedOperationException e) {
78+
assert(e.getMessage().contains("unsupported inner query, should be " + MultiTermQuery.class.getName()));
79+
}
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)