From 0a6259dc8b95733ef41593f1db5f935617de79af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Wed, 18 Jul 2018 09:12:28 +0200 Subject: [PATCH 1/2] Fix `range` queries on `_type` field for singe type indices (#31756) With the introduction of single types in 6.x, the `_type` field is no longer indexed, which leads to certain queries that were working before throw errors now. One such query is the `range` query, that, if performed on a single typer index, currently throws an IAE since the field is not indexed. This change adds special treatment for this case in the TypeFieldMapper, comparing the range queries lower and upper bound to the one existing type and either returns a MatchAllDocs or a MatchNoDocs query. Relates to #31632 Closes #31476 --- .../index/mapper/TypeFieldMapper.java | 35 ++++++++++++++++++ .../search/query/SearchQueryIT.java | 36 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java index 4160bdc38cc9d..10ccb67847c60 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java @@ -34,7 +34,10 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.TermInSetQuery; import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TermRangeQuery; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.logging.DeprecationLogger; +import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -91,6 +94,8 @@ public MetadataFieldMapper getDefault(MappedFieldType fieldType, ParserContext c static final class TypeFieldType extends StringFieldType { + private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(ESLoggerFactory.getLogger(TypeFieldType.class)); + TypeFieldType() { } @@ -174,6 +179,36 @@ public Query termsQuery(List values, QueryShardContext context) { } } + @Override + public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, QueryShardContext context) { + if (hasDocValues()) { + return new TermRangeQuery(name(), lowerTerm == null ? null : indexedValueForSearch(lowerTerm), + upperTerm == null ? null : indexedValueForSearch(upperTerm), includeLower, includeUpper); + } else { + // this means the index has a single type and the type field is implicit + DEPRECATION_LOGGER.deprecatedAndMaybeLog("range_single_type", + "Running [range] query on [_type] field for an index with a single type. As types are deprecated, this functionality will be removed in future releases."); + Query result = new MatchAllDocsQuery(); + Collection types = context.getMapperService().types(); + String type = types.iterator().hasNext() ? types.iterator().next() : null; + if (type != null) { + BytesRef typeBytes = new BytesRef(type); + if (lowerTerm != null) { + int comp = indexedValueForSearch(lowerTerm).compareTo(typeBytes); + if (comp > 0 || (comp == 0 && includeLower == false)) { + result = new MatchNoDocsQuery("[_type] was lexicographically smaller than lower bound of range"); + } + } + if (upperTerm != null) { + int comp = indexedValueForSearch(upperTerm).compareTo(typeBytes); + if (comp < 0 || (comp == 0 && includeUpper == false)) { + result = new MatchNoDocsQuery("[_type] was lexicographically greater than upper bound of range"); + } + } + } + return result; + } + } } /** diff --git a/server/src/test/java/org/elasticsearch/search/query/SearchQueryIT.java b/server/src/test/java/org/elasticsearch/search/query/SearchQueryIT.java index 367b97eb4da08..47355124d8a02 100644 --- a/server/src/test/java/org/elasticsearch/search/query/SearchQueryIT.java +++ b/server/src/test/java/org/elasticsearch/search/query/SearchQueryIT.java @@ -1858,4 +1858,40 @@ public void testRangeQueryRangeFields_24744() throws Exception { SearchResponse searchResponse = client().prepareSearch("test").setQuery(range).get(); assertHitCount(searchResponse, 1); } + + public void testRangeQueryTypeField_31476() throws Exception { + assertAcked(prepareCreate("test").addMapping("foo", "field", "type=keyword")); + + client().prepareIndex("test", "foo", "1").setSource("field", "value").get(); + refresh(); + + RangeQueryBuilder range = new RangeQueryBuilder("_type").from("ape").to("zebra"); + SearchResponse searchResponse = client().prepareSearch("test").setQuery(range).get(); + assertHitCount(searchResponse, 1); + + range = new RangeQueryBuilder("_type").from("monkey").to("zebra"); + searchResponse = client().prepareSearch("test").setQuery(range).get(); + assertHitCount(searchResponse, 0); + + range = new RangeQueryBuilder("_type").from("ape").to("donkey"); + searchResponse = client().prepareSearch("test").setQuery(range).get(); + assertHitCount(searchResponse, 0); + + range = new RangeQueryBuilder("_type").from("ape").to("foo").includeUpper(false); + searchResponse = client().prepareSearch("test").setQuery(range).get(); + assertHitCount(searchResponse, 0); + + range = new RangeQueryBuilder("_type").from("ape").to("foo").includeUpper(true); + searchResponse = client().prepareSearch("test").setQuery(range).get(); + assertHitCount(searchResponse, 1); + + range = new RangeQueryBuilder("_type").from("foo").to("zebra").includeLower(false); + searchResponse = client().prepareSearch("test").setQuery(range).get(); + assertHitCount(searchResponse, 0); + + range = new RangeQueryBuilder("_type").from("foo").to("zebra").includeLower(true); + searchResponse = client().prepareSearch("test").setQuery(range).get(); + assertHitCount(searchResponse, 1); + } + } From afde82dae3c0f13bf5c39fff61c49db1a00af4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Mon, 23 Jul 2018 11:24:53 +0200 Subject: [PATCH 2/2] iter and add unit test --- .../index/mapper/TypeFieldMapper.java | 8 ++- .../index/mapper/TypeFieldTypeTests.java | 63 +++++++++++++++---- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java index 10ccb67847c60..847cc30969826 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/TypeFieldMapper.java @@ -181,17 +181,17 @@ public Query termsQuery(List values, QueryShardContext context) { @Override public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, QueryShardContext context) { - if (hasDocValues()) { + if (context.getIndexSettings().isSingleType() == false) { return new TermRangeQuery(name(), lowerTerm == null ? null : indexedValueForSearch(lowerTerm), upperTerm == null ? null : indexedValueForSearch(upperTerm), includeLower, includeUpper); } else { // this means the index has a single type and the type field is implicit DEPRECATION_LOGGER.deprecatedAndMaybeLog("range_single_type", "Running [range] query on [_type] field for an index with a single type. As types are deprecated, this functionality will be removed in future releases."); - Query result = new MatchAllDocsQuery(); Collection types = context.getMapperService().types(); String type = types.iterator().hasNext() ? types.iterator().next() : null; if (type != null) { + Query result = new MatchAllDocsQuery(); BytesRef typeBytes = new BytesRef(type); if (lowerTerm != null) { int comp = indexedValueForSearch(lowerTerm).compareTo(typeBytes); @@ -205,8 +205,10 @@ public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower result = new MatchNoDocsQuery("[_type] was lexicographically greater than upper bound of range"); } } + return result; + } else { + return new MatchNoDocsQuery(); } - return result; } } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/TypeFieldTypeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/TypeFieldTypeTests.java index 1fdfb52b47f67..d9f68e9dce94b 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/TypeFieldTypeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/TypeFieldTypeTests.java @@ -34,14 +34,15 @@ import org.apache.lucene.search.PhraseQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TermRangeQuery; import org.apache.lucene.store.Directory; import org.apache.lucene.util.BytesRef; -import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.Version; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.test.VersionUtils; @@ -51,15 +52,17 @@ import java.util.Collections; import java.util.Set; +import static org.hamcrest.Matchers.instanceOf; + public class TypeFieldTypeTests extends FieldTypeTestCase { @Override protected MappedFieldType createDefaultFieldType() { return new TypeFieldMapper.TypeFieldType(); } - public void testTermsQueryWhenTypesAreDisabled() throws Exception { + private QueryShardContext createMockContext(Version versionFrom, Version versionTo) { QueryShardContext context = Mockito.mock(QueryShardContext.class); - Version indexVersionCreated = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); + Version indexVersionCreated = VersionUtils.randomVersionBetween(random(), versionFrom, versionTo); Settings indexSettings = Settings.builder() .put(IndexMetaData.SETTING_VERSION_CREATED, indexVersionCreated) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) @@ -70,6 +73,12 @@ public void testTermsQueryWhenTypesAreDisabled() throws Exception { Mockito.when(context.getIndexSettings()).thenReturn(mockSettings); Mockito.when(context.indexVersionCreated()).thenReturn(indexVersionCreated); + return context; + } + + public void testTermsQueryWhenTypesAreDisabled() throws Exception { + QueryShardContext context = createMockContext(Version.V_6_0_0, Version.CURRENT); + MapperService mapperService = Mockito.mock(MapperService.class); Set types = Collections.emptySet(); Mockito.when(mapperService.types()).thenReturn(types); @@ -100,16 +109,7 @@ public void testTermsQueryWhenTypesAreEnabled() throws Exception { IndexWriter w = new IndexWriter(dir, newIndexWriterConfig()); IndexReader reader = openReaderWithNewType("my_type", w); - QueryShardContext context = Mockito.mock(QueryShardContext.class); - Settings indexSettings = Settings.builder() - .put(IndexMetaData.SETTING_VERSION_CREATED, Version.V_5_6_0) // to allow for multiple types - .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0) - .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1) - .put(IndexMetaData.SETTING_INDEX_UUID, UUIDs.randomBase64UUID()) - .build(); - IndexMetaData indexMetaData = IndexMetaData.builder(IndexMetaData.INDEX_UUID_NA_VALUE).settings(indexSettings).build(); - IndexSettings mockSettings = new IndexSettings(indexMetaData, Settings.EMPTY); - Mockito.when(context.getIndexSettings()).thenReturn(mockSettings); + QueryShardContext context = createMockContext(Version.V_5_6_0, Version.V_5_6_0); // to allow for multiple types TypeFieldMapper.TypeFieldType ft = new TypeFieldMapper.TypeFieldType(); ft.setName(TypeFieldMapper.NAME); @@ -166,6 +166,43 @@ public void testTermsQueryWhenTypesAreEnabled() throws Exception { IOUtils.close(reader, w, dir); } + public void testRangeWhenTypesAreDisabled() throws Exception { + QueryShardContext context = createMockContext(Version.V_6_0_0, Version.CURRENT); + + MapperService mapperService = Mockito.mock(MapperService.class); + Set types = Collections.emptySet(); + Mockito.when(mapperService.types()).thenReturn(types); + Mockito.when(context.getMapperService()).thenReturn(mapperService); + + TypeFieldMapper.TypeFieldType ft = new TypeFieldMapper.TypeFieldType(); + ft.setName(TypeFieldMapper.NAME); + Query query = ft.rangeQuery("a_type", "z_type", randomBoolean(), randomBoolean(), context); + assertEquals(new MatchNoDocsQuery(), query); + + types = Collections.singleton("my_type"); + Mockito.when(mapperService.types()).thenReturn(types); + query = ft.rangeQuery("a_type", "z_type", randomBoolean(), randomBoolean(), context); + assertEquals(new MatchAllDocsQuery(), query); + + query = ft.rangeQuery("n_type", "z_type", randomBoolean(), randomBoolean(), context); + assertEquals(new MatchNoDocsQuery(), query); + + query = ft.rangeQuery("a_type", "l_type", randomBoolean(), randomBoolean(), context); + assertEquals(new MatchNoDocsQuery(), query); + assertWarnings("Running [range] query on [_type] field for an index with a single type. As types are deprecated, this " + + "functionality will be removed in future releases."); + } + + public void testRangeWhenTypesEnabled() throws Exception { + TypeFieldMapper.TypeFieldType ft = new TypeFieldMapper.TypeFieldType(); + ft.setName(TypeFieldMapper.NAME); + String lowerTerm = randomBoolean() ? "a_type" : null; + String upperTerm = randomBoolean() ? "z_type" : null; + QueryShardContext context = createMockContext(Version.V_5_6_0, Version.V_5_6_0); // to allow for multiple types + Query query = ft.rangeQuery(lowerTerm, upperTerm, randomBoolean(), randomBoolean(), context); + assertThat(query, instanceOf(TermRangeQuery.class)); + } + static DirectoryReader openReaderWithNewType(String type, IndexWriter writer) throws IOException { Document doc = new Document(); StringField typeField = new StringField(TypeFieldMapper.NAME, type, Store.NO);