Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to match_phrase query for zero_terms_query. #29598

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/reference/query-dsl/match-phrase-query.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ GET /_search
}
--------------------------------------------------
// CONSOLE

This query also accepts `zero_terms_query`, as explained in <<query-dsl-match-query, `match` query>>.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package org.elasticsearch.index.query;

import org.apache.lucene.search.Query;
import org.elasticsearch.Version;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.Strings;
Expand All @@ -28,6 +29,7 @@
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.search.MatchQuery;
import org.elasticsearch.index.search.MatchQuery.ZeroTermsQuery;

import java.io.IOException;
import java.util.Objects;
Expand All @@ -39,6 +41,7 @@
public class MatchPhraseQueryBuilder extends AbstractQueryBuilder<MatchPhraseQueryBuilder> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additions in this class are a bit messy, but I favored consistency with MatchQueryBuilder and MultiMatchQueryBuilder.

public static final String NAME = "match_phrase";
public static final ParseField SLOP_FIELD = new ParseField("slop");
public static final ParseField ZERO_TERMS_QUERY_FIELD = new ParseField("zero_terms_query");

private final String fieldName;

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

private int slop = MatchQuery.DEFAULT_PHRASE_SLOP;

private ZeroTermsQuery zeroTermsQuery = MatchQuery.DEFAULT_ZERO_TERMS_QUERY;

public MatchPhraseQueryBuilder(String fieldName, Object value) {
if (Strings.isEmpty(fieldName)) {
throw new IllegalArgumentException("[" + NAME + "] requires fieldName");
Expand All @@ -67,6 +72,9 @@ public MatchPhraseQueryBuilder(StreamInput in) throws IOException {
fieldName = in.readString();
value = in.readGenericValue();
slop = in.readVInt();
if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
zeroTermsQuery = ZeroTermsQuery.readFromStream(in);
}
analyzer = in.readOptionalString();
}

Expand All @@ -75,6 +83,9 @@ protected void doWriteTo(StreamOutput out) throws IOException {
out.writeString(fieldName);
out.writeGenericValue(value);
out.writeVInt(slop);
if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
zeroTermsQuery.writeTo(out);
}
out.writeOptionalString(analyzer);
}

Expand Down Expand Up @@ -116,6 +127,23 @@ public int slop() {
return this.slop;
}

/**
* Sets query to use in case no query terms are available, e.g. after analysis removed them.
* Defaults to {@link ZeroTermsQuery#NONE}, but can be set to
* {@link ZeroTermsQuery#ALL} instead.
*/
public MatchPhraseQueryBuilder zeroTermsQuery(ZeroTermsQuery zeroTermsQuery) {
if (zeroTermsQuery == null) {
throw new IllegalArgumentException("[" + NAME + "] requires zeroTermsQuery to be non-null");
}
this.zeroTermsQuery = zeroTermsQuery;
return this;
}

public ZeroTermsQuery zeroTermsQuery() {
return this.zeroTermsQuery;
}

@Override
public String getWriteableName() {
return NAME;
Expand All @@ -131,6 +159,7 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep
builder.field(MatchQueryBuilder.ANALYZER_FIELD.getPreferredName(), analyzer);
}
builder.field(SLOP_FIELD.getPreferredName(), slop);
builder.field(ZERO_TERMS_QUERY_FIELD.getPreferredName(), zeroTermsQuery.toString());
printBoostAndQueryName(builder);
builder.endObject();
builder.endObject();
Expand All @@ -148,14 +177,18 @@ protected Query doToQuery(QueryShardContext context) throws IOException {
matchQuery.setAnalyzer(analyzer);
}
matchQuery.setPhraseSlop(slop);
matchQuery.setZeroTermsQuery(zeroTermsQuery);

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

@Override
protected boolean doEquals(MatchPhraseQueryBuilder other) {
return Objects.equals(fieldName, other.fieldName) && Objects.equals(value, other.value) && Objects.equals(analyzer, other.analyzer)
&& Objects.equals(slop, other.slop);
return Objects.equals(fieldName, other.fieldName)
&& Objects.equals(value, other.value)
&& Objects.equals(analyzer, other.analyzer)
&& Objects.equals(slop, other.slop)
&& Objects.equals(zeroTermsQuery, other.zeroTermsQuery);
}

@Override
Expand All @@ -169,6 +202,7 @@ public static MatchPhraseQueryBuilder fromXContent(XContentParser parser) throws
float boost = AbstractQueryBuilder.DEFAULT_BOOST;
String analyzer = null;
int slop = MatchQuery.DEFAULT_PHRASE_SLOP;
ZeroTermsQuery zeroTermsQuery = MatchQuery.DEFAULT_ZERO_TERMS_QUERY;
String queryName = null;
String currentFieldName = null;
XContentParser.Token token;
Expand All @@ -192,6 +226,16 @@ public static MatchPhraseQueryBuilder fromXContent(XContentParser parser) throws
slop = parser.intValue();
} else if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
queryName = parser.text();
} else if (ZERO_TERMS_QUERY_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
String zeroTermsDocs = parser.text();
if ("none".equalsIgnoreCase(zeroTermsDocs)) {
zeroTermsQuery = ZeroTermsQuery.NONE;
} else if ("all".equalsIgnoreCase(zeroTermsDocs)) {
zeroTermsQuery = ZeroTermsQuery.ALL;
} else {
throw new ParsingException(parser.getTokenLocation(),
"Unsupported zero_terms_docs value [" + zeroTermsDocs + "]");
}
} else {
throw new ParsingException(parser.getTokenLocation(),
"[" + NAME + "] query does not support [" + currentFieldName + "]");
Expand All @@ -211,6 +255,7 @@ public static MatchPhraseQueryBuilder fromXContent(XContentParser parser) throws
MatchPhraseQueryBuilder matchQuery = new MatchPhraseQueryBuilder(fieldName, value);
matchQuery.analyzer(analyzer);
matchQuery.slop(slop);
matchQuery.zeroTermsQuery(zeroTermsQuery);
matchQuery.queryName(queryName);
matchQuery.boost(boost);
return matchQuery;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@

import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexOrDocValuesQuery;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.PhraseQuery;
import org.apache.lucene.search.PointRangeQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.index.search.MatchQuery.ZeroTermsQuery;
import org.elasticsearch.search.internal.SearchContext;
import org.elasticsearch.test.AbstractQueryTestCase;

Expand All @@ -37,6 +39,7 @@
import static org.hamcrest.CoreMatchers.either;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;

public class MatchPhraseQueryBuilderTests extends AbstractQueryTestCase<MatchPhraseQueryBuilder> {
Expand Down Expand Up @@ -68,6 +71,11 @@ protected MatchPhraseQueryBuilder doCreateTestQueryBuilder() {
if (randomBoolean()) {
matchQuery.slop(randomIntBetween(0, 10));
}

if (randomBoolean()) {
matchQuery.zeroTermsQuery(randomFrom(ZeroTermsQuery.ALL, ZeroTermsQuery.NONE));
}

return matchQuery;
}

Expand All @@ -88,6 +96,12 @@ protected Map<String, MatchPhraseQueryBuilder> getAlternateVersions() {
@Override
protected void doAssertLuceneQuery(MatchPhraseQueryBuilder queryBuilder, Query query, SearchContext context) throws IOException {
assertThat(query, notNullValue());

if (query instanceof MatchAllDocsQuery) {
assertThat(queryBuilder.zeroTermsQuery(), equalTo(ZeroTermsQuery.ALL));
return;
}

assertThat(query, either(instanceOf(BooleanQuery.class)).or(instanceOf(PhraseQuery.class))
.or(instanceOf(TermQuery.class)).or(instanceOf(PointRangeQuery.class))
.or(instanceOf(IndexOrDocValuesQuery.class)).or(instanceOf(MatchNoDocsQuery.class)));
Expand All @@ -108,7 +122,7 @@ public void testBadAnalyzer() throws IOException {
assertThat(e.getMessage(), containsString("analyzer [bogusAnalyzer] not found"));
}

public void testPhraseMatchQuery() throws IOException {
public void testFromSimpleJson() throws IOException {
String json1 = "{\n" +
" \"match_phrase\" : {\n" +
" \"message\" : \"this is a test\"\n" +
Expand All @@ -120,6 +134,7 @@ public void testPhraseMatchQuery() throws IOException {
" \"message\" : {\n" +
" \"query\" : \"this is a test\",\n" +
" \"slop\" : 0,\n" +
" \"zero_terms_query\" : \"NONE\",\n" +
" \"boost\" : 1.0\n" +
" }\n" +
" }\n" +
Expand All @@ -128,6 +143,26 @@ public void testPhraseMatchQuery() throws IOException {
checkGeneratedJson(expected, qb);
}

public void testFromJson() throws IOException {
String json = "{\n" +
" \"match_phrase\" : {\n" +
" \"message\" : {\n" +
" \"query\" : \"this is a test\",\n" +
" \"slop\" : 2,\n" +
" \"zero_terms_query\" : \"ALL\",\n" +
" \"boost\" : 1.0\n" +
" }\n" +
" }\n" +
"}";

MatchPhraseQueryBuilder parsed = (MatchPhraseQueryBuilder) parseQuery(json);
checkGeneratedJson(json, parsed);

assertEquals(json, "this is a test", parsed.value());
assertEquals(json, 2, parsed.slop());
assertEquals(json, ZeroTermsQuery.ALL, parsed.zeroTermsQuery());
}

public void testParseFailsWithMultipleFields() throws IOException {
String json = "{\n" +
" \"match_phrase\" : {\n" +
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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.search;

import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
import org.elasticsearch.action.index.IndexRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.query.MatchPhraseQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.search.MatchQuery.ZeroTermsQuery;
import org.elasticsearch.test.ESIntegTestCase;
import org.junit.Before;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;

import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;

public class MatchPhraseQueryIT extends ESIntegTestCase {
private static final String INDEX = "test";

@Before
public void setUp() throws Exception {
super.setUp();
CreateIndexRequestBuilder createIndexRequest = prepareCreate(INDEX).setSettings(
Settings.builder()
.put(indexSettings())
.put("index.analysis.analyzer.standard_stopwords.type", "standard")
.putList("index.analysis.analyzer.standard_stopwords.stopwords", "of", "the", "who"));
assertAcked(createIndexRequest);
ensureGreen();
}

public void testZeroTermsQuery() throws ExecutionException, InterruptedException {
List<IndexRequestBuilder> indexRequests = getIndexRequests();
indexRandom(true, false, indexRequests);

MatchPhraseQueryBuilder baseQuery = QueryBuilders.matchPhraseQuery("name", "the who")
.analyzer("standard_stopwords");

MatchPhraseQueryBuilder matchNoneQuery = baseQuery.zeroTermsQuery(ZeroTermsQuery.NONE);
SearchResponse matchNoneResponse = client().prepareSearch(INDEX).setQuery(matchNoneQuery).get();
assertHitCount(matchNoneResponse, 0L);

MatchPhraseQueryBuilder matchAllQuery = baseQuery.zeroTermsQuery(ZeroTermsQuery.ALL);
SearchResponse matchAllResponse = client().prepareSearch(INDEX).setQuery(matchAllQuery).get();
assertHitCount(matchAllResponse, 2L);
}


private List<IndexRequestBuilder> getIndexRequests() {
List<IndexRequestBuilder> requests = new ArrayList<>();
requests.add(client().prepareIndex(INDEX, "band").setSource("name", "the beatles"));
requests.add(client().prepareIndex(INDEX, "band").setSource("name", "led zeppelin"));
return requests;
}
}