Skip to content

Commit

Permalink
Add support to match_phrase query for zero_terms_query. (elastic#29598)
Browse files Browse the repository at this point in the history
(cherry picked from commit b9e1a00)
  • Loading branch information
jtibshirani committed Apr 19, 2018
1 parent 89179af commit d0e31d4
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 3 deletions.
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> {
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_6_3_0)) {
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_6_3_0)) {
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;
}
}

0 comments on commit d0e31d4

Please sign in to comment.