Skip to content

Commit b9e1a00

Browse files
authored
Add support to match_phrase query for zero_terms_query. (elastic#29598)
1 parent 00d88a5 commit b9e1a00

File tree

4 files changed

+162
-3
lines changed

4 files changed

+162
-3
lines changed

docs/reference/query-dsl/match-phrase-query.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,5 @@ GET /_search
3939
}
4040
--------------------------------------------------
4141
// CONSOLE
42+
43+
This query also accepts `zero_terms_query`, as explained in <<query-dsl-match-query, `match` query>>.

server/src/main/java/org/elasticsearch/index/query/MatchPhraseQueryBuilder.java

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

2222
import org.apache.lucene.search.Query;
23+
import org.elasticsearch.Version;
2324
import org.elasticsearch.common.ParseField;
2425
import org.elasticsearch.common.ParsingException;
2526
import org.elasticsearch.common.Strings;
@@ -28,6 +29,7 @@
2829
import org.elasticsearch.common.xcontent.XContentBuilder;
2930
import org.elasticsearch.common.xcontent.XContentParser;
3031
import org.elasticsearch.index.search.MatchQuery;
32+
import org.elasticsearch.index.search.MatchQuery.ZeroTermsQuery;
3133

3234
import java.io.IOException;
3335
import java.util.Objects;
@@ -39,6 +41,7 @@
3941
public class MatchPhraseQueryBuilder extends AbstractQueryBuilder<MatchPhraseQueryBuilder> {
4042
public static final String NAME = "match_phrase";
4143
public static final ParseField SLOP_FIELD = new ParseField("slop");
44+
public static final ParseField ZERO_TERMS_QUERY_FIELD = new ParseField("zero_terms_query");
4245

4346
private final String fieldName;
4447

@@ -48,6 +51,8 @@ public class MatchPhraseQueryBuilder extends AbstractQueryBuilder<MatchPhraseQue
4851

4952
private int slop = MatchQuery.DEFAULT_PHRASE_SLOP;
5053

54+
private ZeroTermsQuery zeroTermsQuery = MatchQuery.DEFAULT_ZERO_TERMS_QUERY;
55+
5156
public MatchPhraseQueryBuilder(String fieldName, Object value) {
5257
if (Strings.isEmpty(fieldName)) {
5358
throw new IllegalArgumentException("[" + NAME + "] requires fieldName");
@@ -67,6 +72,9 @@ public MatchPhraseQueryBuilder(StreamInput in) throws IOException {
6772
fieldName = in.readString();
6873
value = in.readGenericValue();
6974
slop = in.readVInt();
75+
if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
76+
zeroTermsQuery = ZeroTermsQuery.readFromStream(in);
77+
}
7078
analyzer = in.readOptionalString();
7179
}
7280

@@ -75,6 +83,9 @@ protected void doWriteTo(StreamOutput out) throws IOException {
7583
out.writeString(fieldName);
7684
out.writeGenericValue(value);
7785
out.writeVInt(slop);
86+
if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
87+
zeroTermsQuery.writeTo(out);
88+
}
7889
out.writeOptionalString(analyzer);
7990
}
8091

@@ -116,6 +127,23 @@ public int slop() {
116127
return this.slop;
117128
}
118129

130+
/**
131+
* Sets query to use in case no query terms are available, e.g. after analysis removed them.
132+
* Defaults to {@link ZeroTermsQuery#NONE}, but can be set to
133+
* {@link ZeroTermsQuery#ALL} instead.
134+
*/
135+
public MatchPhraseQueryBuilder zeroTermsQuery(ZeroTermsQuery zeroTermsQuery) {
136+
if (zeroTermsQuery == null) {
137+
throw new IllegalArgumentException("[" + NAME + "] requires zeroTermsQuery to be non-null");
138+
}
139+
this.zeroTermsQuery = zeroTermsQuery;
140+
return this;
141+
}
142+
143+
public ZeroTermsQuery zeroTermsQuery() {
144+
return this.zeroTermsQuery;
145+
}
146+
119147
@Override
120148
public String getWriteableName() {
121149
return NAME;
@@ -131,6 +159,7 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep
131159
builder.field(MatchQueryBuilder.ANALYZER_FIELD.getPreferredName(), analyzer);
132160
}
133161
builder.field(SLOP_FIELD.getPreferredName(), slop);
162+
builder.field(ZERO_TERMS_QUERY_FIELD.getPreferredName(), zeroTermsQuery.toString());
134163
printBoostAndQueryName(builder);
135164
builder.endObject();
136165
builder.endObject();
@@ -148,14 +177,18 @@ protected Query doToQuery(QueryShardContext context) throws IOException {
148177
matchQuery.setAnalyzer(analyzer);
149178
}
150179
matchQuery.setPhraseSlop(slop);
180+
matchQuery.setZeroTermsQuery(zeroTermsQuery);
151181

152182
return matchQuery.parse(MatchQuery.Type.PHRASE, fieldName, value);
153183
}
154184

155185
@Override
156186
protected boolean doEquals(MatchPhraseQueryBuilder other) {
157-
return Objects.equals(fieldName, other.fieldName) && Objects.equals(value, other.value) && Objects.equals(analyzer, other.analyzer)
158-
&& Objects.equals(slop, other.slop);
187+
return Objects.equals(fieldName, other.fieldName)
188+
&& Objects.equals(value, other.value)
189+
&& Objects.equals(analyzer, other.analyzer)
190+
&& Objects.equals(slop, other.slop)
191+
&& Objects.equals(zeroTermsQuery, other.zeroTermsQuery);
159192
}
160193

161194
@Override
@@ -169,6 +202,7 @@ public static MatchPhraseQueryBuilder fromXContent(XContentParser parser) throws
169202
float boost = AbstractQueryBuilder.DEFAULT_BOOST;
170203
String analyzer = null;
171204
int slop = MatchQuery.DEFAULT_PHRASE_SLOP;
205+
ZeroTermsQuery zeroTermsQuery = MatchQuery.DEFAULT_ZERO_TERMS_QUERY;
172206
String queryName = null;
173207
String currentFieldName = null;
174208
XContentParser.Token token;
@@ -192,6 +226,16 @@ public static MatchPhraseQueryBuilder fromXContent(XContentParser parser) throws
192226
slop = parser.intValue();
193227
} else if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
194228
queryName = parser.text();
229+
} else if (ZERO_TERMS_QUERY_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
230+
String zeroTermsDocs = parser.text();
231+
if ("none".equalsIgnoreCase(zeroTermsDocs)) {
232+
zeroTermsQuery = ZeroTermsQuery.NONE;
233+
} else if ("all".equalsIgnoreCase(zeroTermsDocs)) {
234+
zeroTermsQuery = ZeroTermsQuery.ALL;
235+
} else {
236+
throw new ParsingException(parser.getTokenLocation(),
237+
"Unsupported zero_terms_docs value [" + zeroTermsDocs + "]");
238+
}
195239
} else {
196240
throw new ParsingException(parser.getTokenLocation(),
197241
"[" + NAME + "] query does not support [" + currentFieldName + "]");
@@ -211,6 +255,7 @@ public static MatchPhraseQueryBuilder fromXContent(XContentParser parser) throws
211255
MatchPhraseQueryBuilder matchQuery = new MatchPhraseQueryBuilder(fieldName, value);
212256
matchQuery.analyzer(analyzer);
213257
matchQuery.slop(slop);
258+
matchQuery.zeroTermsQuery(zeroTermsQuery);
214259
matchQuery.queryName(queryName);
215260
matchQuery.boost(boost);
216261
return matchQuery;

server/src/test/java/org/elasticsearch/index/query/MatchPhraseQueryBuilderTests.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121

2222
import org.apache.lucene.search.BooleanQuery;
2323
import org.apache.lucene.search.IndexOrDocValuesQuery;
24+
import org.apache.lucene.search.MatchAllDocsQuery;
2425
import org.apache.lucene.search.MatchNoDocsQuery;
2526
import org.apache.lucene.search.PhraseQuery;
2627
import org.apache.lucene.search.PointRangeQuery;
2728
import org.apache.lucene.search.Query;
2829
import org.apache.lucene.search.TermQuery;
2930
import org.elasticsearch.common.ParsingException;
31+
import org.elasticsearch.index.search.MatchQuery.ZeroTermsQuery;
3032
import org.elasticsearch.search.internal.SearchContext;
3133
import org.elasticsearch.test.AbstractQueryTestCase;
3234

@@ -37,6 +39,7 @@
3739
import static org.hamcrest.CoreMatchers.either;
3840
import static org.hamcrest.CoreMatchers.instanceOf;
3941
import static org.hamcrest.Matchers.containsString;
42+
import static org.hamcrest.Matchers.equalTo;
4043
import static org.hamcrest.Matchers.notNullValue;
4144

4245
public class MatchPhraseQueryBuilderTests extends AbstractQueryTestCase<MatchPhraseQueryBuilder> {
@@ -68,6 +71,11 @@ protected MatchPhraseQueryBuilder doCreateTestQueryBuilder() {
6871
if (randomBoolean()) {
6972
matchQuery.slop(randomIntBetween(0, 10));
7073
}
74+
75+
if (randomBoolean()) {
76+
matchQuery.zeroTermsQuery(randomFrom(ZeroTermsQuery.ALL, ZeroTermsQuery.NONE));
77+
}
78+
7179
return matchQuery;
7280
}
7381

@@ -88,6 +96,12 @@ protected Map<String, MatchPhraseQueryBuilder> getAlternateVersions() {
8896
@Override
8997
protected void doAssertLuceneQuery(MatchPhraseQueryBuilder queryBuilder, Query query, SearchContext context) throws IOException {
9098
assertThat(query, notNullValue());
99+
100+
if (query instanceof MatchAllDocsQuery) {
101+
assertThat(queryBuilder.zeroTermsQuery(), equalTo(ZeroTermsQuery.ALL));
102+
return;
103+
}
104+
91105
assertThat(query, either(instanceOf(BooleanQuery.class)).or(instanceOf(PhraseQuery.class))
92106
.or(instanceOf(TermQuery.class)).or(instanceOf(PointRangeQuery.class))
93107
.or(instanceOf(IndexOrDocValuesQuery.class)).or(instanceOf(MatchNoDocsQuery.class)));
@@ -108,7 +122,7 @@ public void testBadAnalyzer() throws IOException {
108122
assertThat(e.getMessage(), containsString("analyzer [bogusAnalyzer] not found"));
109123
}
110124

111-
public void testPhraseMatchQuery() throws IOException {
125+
public void testFromSimpleJson() throws IOException {
112126
String json1 = "{\n" +
113127
" \"match_phrase\" : {\n" +
114128
" \"message\" : \"this is a test\"\n" +
@@ -120,6 +134,7 @@ public void testPhraseMatchQuery() throws IOException {
120134
" \"message\" : {\n" +
121135
" \"query\" : \"this is a test\",\n" +
122136
" \"slop\" : 0,\n" +
137+
" \"zero_terms_query\" : \"NONE\",\n" +
123138
" \"boost\" : 1.0\n" +
124139
" }\n" +
125140
" }\n" +
@@ -128,6 +143,26 @@ public void testPhraseMatchQuery() throws IOException {
128143
checkGeneratedJson(expected, qb);
129144
}
130145

146+
public void testFromJson() throws IOException {
147+
String json = "{\n" +
148+
" \"match_phrase\" : {\n" +
149+
" \"message\" : {\n" +
150+
" \"query\" : \"this is a test\",\n" +
151+
" \"slop\" : 2,\n" +
152+
" \"zero_terms_query\" : \"ALL\",\n" +
153+
" \"boost\" : 1.0\n" +
154+
" }\n" +
155+
" }\n" +
156+
"}";
157+
158+
MatchPhraseQueryBuilder parsed = (MatchPhraseQueryBuilder) parseQuery(json);
159+
checkGeneratedJson(json, parsed);
160+
161+
assertEquals(json, "this is a test", parsed.value());
162+
assertEquals(json, 2, parsed.slop());
163+
assertEquals(json, ZeroTermsQuery.ALL, parsed.zeroTermsQuery());
164+
}
165+
131166
public void testParseFailsWithMultipleFields() throws IOException {
132167
String json = "{\n" +
133168
" \"match_phrase\" : {\n" +
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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.search;
21+
22+
import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
23+
import org.elasticsearch.action.index.IndexRequestBuilder;
24+
import org.elasticsearch.action.search.SearchResponse;
25+
import org.elasticsearch.common.settings.Settings;
26+
import org.elasticsearch.index.query.MatchPhraseQueryBuilder;
27+
import org.elasticsearch.index.query.QueryBuilders;
28+
import org.elasticsearch.index.search.MatchQuery.ZeroTermsQuery;
29+
import org.elasticsearch.test.ESIntegTestCase;
30+
import org.junit.Before;
31+
32+
import java.util.ArrayList;
33+
import java.util.List;
34+
import java.util.concurrent.ExecutionException;
35+
36+
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
37+
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
38+
39+
public class MatchPhraseQueryIT extends ESIntegTestCase {
40+
private static final String INDEX = "test";
41+
42+
@Before
43+
public void setUp() throws Exception {
44+
super.setUp();
45+
CreateIndexRequestBuilder createIndexRequest = prepareCreate(INDEX).setSettings(
46+
Settings.builder()
47+
.put(indexSettings())
48+
.put("index.analysis.analyzer.standard_stopwords.type", "standard")
49+
.putList("index.analysis.analyzer.standard_stopwords.stopwords", "of", "the", "who"));
50+
assertAcked(createIndexRequest);
51+
ensureGreen();
52+
}
53+
54+
public void testZeroTermsQuery() throws ExecutionException, InterruptedException {
55+
List<IndexRequestBuilder> indexRequests = getIndexRequests();
56+
indexRandom(true, false, indexRequests);
57+
58+
MatchPhraseQueryBuilder baseQuery = QueryBuilders.matchPhraseQuery("name", "the who")
59+
.analyzer("standard_stopwords");
60+
61+
MatchPhraseQueryBuilder matchNoneQuery = baseQuery.zeroTermsQuery(ZeroTermsQuery.NONE);
62+
SearchResponse matchNoneResponse = client().prepareSearch(INDEX).setQuery(matchNoneQuery).get();
63+
assertHitCount(matchNoneResponse, 0L);
64+
65+
MatchPhraseQueryBuilder matchAllQuery = baseQuery.zeroTermsQuery(ZeroTermsQuery.ALL);
66+
SearchResponse matchAllResponse = client().prepareSearch(INDEX).setQuery(matchAllQuery).get();
67+
assertHitCount(matchAllResponse, 2L);
68+
}
69+
70+
71+
private List<IndexRequestBuilder> getIndexRequests() {
72+
List<IndexRequestBuilder> requests = new ArrayList<>();
73+
requests.add(client().prepareIndex(INDEX, "band").setSource("name", "the beatles"));
74+
requests.add(client().prepareIndex(INDEX, "band").setSource("name", "led zeppelin"));
75+
return requests;
76+
}
77+
}

0 commit comments

Comments
 (0)