/* * 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.query; import com.carrotsearch.randomizedtesting.generators.RandomPicks; import org.apache.lucene.document.IntPoint; import org.apache.lucene.document.LongPoint; import org.apache.lucene.search.IndexOrDocValuesQuery; import org.apache.lucene.search.PointRangeQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermRangeQuery; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MappedFieldType.Relation; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.test.AbstractQueryTestCase; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.chrono.ISOChronology; import java.io.IOException; import java.util.HashMap; import java.util.Map; import static org.elasticsearch.index.query.QueryBuilders.rangeQuery; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.sameInstance; public class RangeQueryBuilderTests extends AbstractQueryTestCase<RangeQueryBuilder> { @Override protected RangeQueryBuilder doCreateTestQueryBuilder() { RangeQueryBuilder query; // switch between numeric and date ranges switch (randomIntBetween(0, 2)) { case 0: // use mapped integer field for numeric range queries query = new RangeQueryBuilder(randomBoolean() ? INT_FIELD_NAME : INT_RANGE_FIELD_NAME); query.from(randomIntBetween(1, 100)); query.to(randomIntBetween(101, 200)); break; case 1: // use mapped date field, using date string representation query = new RangeQueryBuilder(randomBoolean() ? DATE_FIELD_NAME : DATE_RANGE_FIELD_NAME); query.from(new DateTime(System.currentTimeMillis() - randomIntBetween(0, 1000000), DateTimeZone.UTC).toString()); query.to(new DateTime(System.currentTimeMillis() + randomIntBetween(0, 1000000), DateTimeZone.UTC).toString()); // Create timestamp option only then we have a date mapper, // otherwise we could trigger exception. if (createShardContext().getMapperService().fullName(DATE_FIELD_NAME) != null) { if (randomBoolean()) { query.timeZone(randomDateTimeZone().getID()); } if (randomBoolean()) { query.format("yyyy-MM-dd'T'HH:mm:ss.SSSZZ"); } } if (query.fieldName().equals(DATE_RANGE_FIELD_NAME)) { query.relation(RandomPicks.randomFrom(random(), ShapeRelation.values()).getRelationName()); } break; case 2: default: query = new RangeQueryBuilder(STRING_FIELD_NAME); query.from("a" + randomAlphaOfLengthBetween(1, 10)); query.to("z" + randomAlphaOfLengthBetween(1, 10)); break; } query.includeLower(randomBoolean()).includeUpper(randomBoolean()); if (randomBoolean()) { query.from(null); } if (randomBoolean()) { query.to(null); } return query; } @Override protected Map<String, RangeQueryBuilder> getAlternateVersions() { Map<String, RangeQueryBuilder> alternateVersions = new HashMap<>(); RangeQueryBuilder rangeQueryBuilder = new RangeQueryBuilder(INT_FIELD_NAME); rangeQueryBuilder.from(randomIntBetween(1, 100)).to(randomIntBetween(101, 200)); rangeQueryBuilder.includeLower(randomBoolean()); rangeQueryBuilder.includeUpper(randomBoolean()); String query = "{\n" + " \"range\":{\n" + " \"" + INT_FIELD_NAME + "\": {\n" + " \"" + (rangeQueryBuilder.includeLower() ? "gte" : "gt") + "\": " + rangeQueryBuilder.from() + ",\n" + " \"" + (rangeQueryBuilder.includeUpper() ? "lte" : "lt") + "\": " + rangeQueryBuilder.to() + "\n" + " }\n" + " }\n" + "}"; alternateVersions.put(query, rangeQueryBuilder); return alternateVersions; } @Override protected void doAssertLuceneQuery(RangeQueryBuilder queryBuilder, Query query, SearchContext context) throws IOException { if (getCurrentTypes().length == 0 || (queryBuilder.fieldName().equals(DATE_FIELD_NAME) == false && queryBuilder.fieldName().equals(INT_FIELD_NAME) == false && queryBuilder.fieldName().equals(DATE_RANGE_FIELD_NAME) == false && queryBuilder.fieldName().equals(INT_RANGE_FIELD_NAME) == false)) { assertThat(query, instanceOf(TermRangeQuery.class)); TermRangeQuery termRangeQuery = (TermRangeQuery) query; assertThat(termRangeQuery.getField(), equalTo(queryBuilder.fieldName())); assertThat(termRangeQuery.getLowerTerm(), equalTo(BytesRefs.toBytesRef(queryBuilder.from()))); assertThat(termRangeQuery.getUpperTerm(), equalTo(BytesRefs.toBytesRef(queryBuilder.to()))); assertThat(termRangeQuery.includesLower(), equalTo(queryBuilder.includeLower())); assertThat(termRangeQuery.includesUpper(), equalTo(queryBuilder.includeUpper())); } else if (queryBuilder.fieldName().equals(DATE_FIELD_NAME)) { assertThat(query, instanceOf(IndexOrDocValuesQuery.class)); query = ((IndexOrDocValuesQuery) query).getIndexQuery(); assertThat(query, instanceOf(PointRangeQuery.class)); MapperService mapperService = context.getQueryShardContext().getMapperService(); MappedFieldType mappedFieldType = mapperService.fullName(DATE_FIELD_NAME); final Long fromInMillis; final Long toInMillis; // we have to normalize the incoming value into milliseconds since it could be literally anything if (mappedFieldType instanceof DateFieldMapper.DateFieldType) { fromInMillis = queryBuilder.from() == null ? null : ((DateFieldMapper.DateFieldType) mappedFieldType).parseToMilliseconds(queryBuilder.from(), queryBuilder.includeLower(), queryBuilder.getDateTimeZone(), queryBuilder.getForceDateParser(), context.getQueryShardContext()); toInMillis = queryBuilder.to() == null ? null : ((DateFieldMapper.DateFieldType) mappedFieldType).parseToMilliseconds(queryBuilder.to(), queryBuilder.includeUpper(), queryBuilder.getDateTimeZone(), queryBuilder.getForceDateParser(), context.getQueryShardContext()); } else { fromInMillis = toInMillis = null; fail("unexpected mapped field type: [" + mappedFieldType.getClass() + "] " + mappedFieldType.toString()); } Long min = fromInMillis; Long max = toInMillis; long minLong, maxLong; if (min == null) { minLong = Long.MIN_VALUE; } else { minLong = min.longValue(); if (queryBuilder.includeLower() == false && minLong != Long.MAX_VALUE) { minLong++; } } if (max == null) { maxLong = Long.MAX_VALUE; } else { maxLong = max.longValue(); if (queryBuilder.includeUpper() == false && maxLong != Long.MIN_VALUE) { maxLong--; } } assertEquals(LongPoint.newRangeQuery(DATE_FIELD_NAME, minLong, maxLong), query); } else if (queryBuilder.fieldName().equals(INT_FIELD_NAME)) { assertThat(query, instanceOf(IndexOrDocValuesQuery.class)); query = ((IndexOrDocValuesQuery) query).getIndexQuery(); assertThat(query, instanceOf(PointRangeQuery.class)); Integer min = (Integer) queryBuilder.from(); Integer max = (Integer) queryBuilder.to(); int minInt, maxInt; if (min == null) { minInt = Integer.MIN_VALUE; } else { minInt = min.intValue(); if (queryBuilder.includeLower() == false && minInt != Integer.MAX_VALUE) { minInt++; } } if (max == null) { maxInt = Integer.MAX_VALUE; } else { maxInt = max.intValue(); if (queryBuilder.includeUpper() == false && maxInt != Integer.MIN_VALUE) { maxInt--; } } } else if (queryBuilder.fieldName().equals(DATE_RANGE_FIELD_NAME) || queryBuilder.fieldName().equals(INT_RANGE_FIELD_NAME)) { // todo can't check RangeFieldQuery because its currently package private (this will change) } else { throw new UnsupportedOperationException(); } } public void testIllegalArguments() { expectThrows(IllegalArgumentException.class, () -> new RangeQueryBuilder((String) null)); expectThrows(IllegalArgumentException.class, () -> new RangeQueryBuilder("")); RangeQueryBuilder rangeQueryBuilder = new RangeQueryBuilder("test"); expectThrows(IllegalArgumentException.class, () -> rangeQueryBuilder.timeZone(null)); expectThrows(IllegalArgumentException.class, () -> rangeQueryBuilder.timeZone("badID")); expectThrows(IllegalArgumentException.class, () -> rangeQueryBuilder.format(null)); expectThrows(IllegalArgumentException.class, () -> rangeQueryBuilder.format("badFormat")); } /** * Specifying a timezone together with a numeric range query should throw an exception. */ public void testToQueryNonDateWithTimezone() throws QueryShardException, IOException { RangeQueryBuilder query = new RangeQueryBuilder(INT_FIELD_NAME); query.from(1).to(10).timeZone("UTC"); QueryShardException e = expectThrows(QueryShardException.class, () -> query.toQuery(createShardContext())); assertThat(e.getMessage(), containsString("[range] time_zone can not be applied")); } /** * Specifying a timezone together with an unmapped field should throw an exception. */ public void testToQueryUnmappedWithTimezone() throws QueryShardException, IOException { RangeQueryBuilder query = new RangeQueryBuilder("bogus_field"); query.from(1).to(10).timeZone("UTC"); QueryShardException e = expectThrows(QueryShardException.class, () -> query.toQuery(createShardContext())); assertThat(e.getMessage(), containsString("[range] time_zone can not be applied")); } public void testToQueryNumericField() throws IOException { assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); Query parsedQuery = rangeQuery(INT_FIELD_NAME).from(23).to(54).includeLower(true).includeUpper(false).toQuery(createShardContext()); // since age is automatically registered in data, we encode it as numeric assertThat(parsedQuery, instanceOf(IndexOrDocValuesQuery.class)); parsedQuery = ((IndexOrDocValuesQuery) parsedQuery).getIndexQuery(); assertThat(parsedQuery, instanceOf(PointRangeQuery.class)); assertEquals(IntPoint.newRangeQuery(INT_FIELD_NAME, 23, 53), parsedQuery); } public void testDateRangeQueryFormat() throws IOException { assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); // We test 01/01/2012 from gte and 2030 for lt String query = "{\n" + " \"range\" : {\n" + " \"" + DATE_FIELD_NAME + "\" : {\n" + " \"gte\": \"01/01/2012\",\n" + " \"lt\": \"2030\",\n" + " \"format\": \"dd/MM/yyyy||yyyy\"\n" + " }\n" + " }\n" + "}"; Query parsedQuery = parseQuery(query).toQuery(createShardContext()); assertThat(parsedQuery, instanceOf(IndexOrDocValuesQuery.class)); parsedQuery = ((IndexOrDocValuesQuery) parsedQuery).getIndexQuery(); assertThat(parsedQuery, instanceOf(PointRangeQuery.class)); assertEquals(LongPoint.newRangeQuery(DATE_FIELD_NAME, DateTime.parse("2012-01-01T00:00:00.000+00").getMillis(), DateTime.parse("2030-01-01T00:00:00.000+00").getMillis() - 1), parsedQuery); // Test Invalid format final String invalidQuery = "{\n" + " \"range\" : {\n" + " \"" + DATE_FIELD_NAME + "\" : {\n" + " \"gte\": \"01/01/2012\",\n" + " \"lt\": \"2030\",\n" + " \"format\": \"yyyy\"\n" + " }\n" + " }\n" + "}"; expectThrows(ElasticsearchParseException.class, () -> parseQuery(invalidQuery).toQuery(createShardContext())); } public void testDateRangeBoundaries() throws IOException { assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); String query = "{\n" + " \"range\" : {\n" + " \"" + DATE_FIELD_NAME + "\" : {\n" + " \"gte\": \"2014-11-05||/M\",\n" + " \"lte\": \"2014-12-08||/d\"\n" + " }\n" + " }\n" + "}\n"; Query parsedQuery = parseQuery(query).toQuery(createShardContext()); assertThat(parsedQuery, instanceOf(IndexOrDocValuesQuery.class)); parsedQuery = ((IndexOrDocValuesQuery) parsedQuery).getIndexQuery(); assertThat(parsedQuery, instanceOf(PointRangeQuery.class)); assertEquals(LongPoint.newRangeQuery(DATE_FIELD_NAME, DateTime.parse("2014-11-01T00:00:00.000+00").getMillis(), DateTime.parse("2014-12-08T23:59:59.999+00").getMillis()), parsedQuery); query = "{\n" + " \"range\" : {\n" + " \"" + DATE_FIELD_NAME + "\" : {\n" + " \"gt\": \"2014-11-05||/M\",\n" + " \"lt\": \"2014-12-08||/d\"\n" + " }\n" + " }\n" + "}"; parsedQuery = parseQuery(query).toQuery(createShardContext()); assertThat(parsedQuery, instanceOf(IndexOrDocValuesQuery.class)); parsedQuery = ((IndexOrDocValuesQuery) parsedQuery).getIndexQuery(); assertThat(parsedQuery, instanceOf(PointRangeQuery.class)); assertEquals(LongPoint.newRangeQuery(DATE_FIELD_NAME, DateTime.parse("2014-11-30T23:59:59.999+00").getMillis() + 1, DateTime.parse("2014-12-08T00:00:00.000+00").getMillis() - 1), parsedQuery); } public void testDateRangeQueryTimezone() throws IOException { assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); String query = "{\n" + " \"range\" : {\n" + " \"" + DATE_FIELD_NAME + "\" : {\n" + " \"gte\": \"2012-01-01\",\n" + " \"lte\": \"now\",\n" + " \"time_zone\": \"+01:00\"\n" + " }\n" + " }\n" + "}"; QueryShardContext context = createShardContext(); Query parsedQuery = parseQuery(query).toQuery(context); assertThat(parsedQuery, instanceOf(IndexOrDocValuesQuery.class)); parsedQuery = ((IndexOrDocValuesQuery) parsedQuery).getIndexQuery(); assertThat(parsedQuery, instanceOf(PointRangeQuery.class)); // TODO what else can we assert query = "{\n" + " \"range\" : {\n" + " \"" + INT_FIELD_NAME + "\" : {\n" + " \"gte\": \"0\",\n" + " \"lte\": \"100\",\n" + " \"time_zone\": \"-01:00\"\n" + " }\n" + " }\n" + "}"; QueryBuilder queryBuilder = parseQuery(query); expectThrows(QueryShardException.class, () -> queryBuilder.toQuery(createShardContext())); } public void testFromJson() throws IOException { String json = "{\n" + " \"range\" : {\n" + " \"timestamp\" : {\n" + " \"from\" : \"2015-01-01 00:00:00\",\n" + " \"to\" : \"now\",\n" + " \"include_lower\" : true,\n" + " \"include_upper\" : true,\n" + " \"time_zone\" : \"+01:00\",\n" + " \"boost\" : 1.0\n" + " }\n" + " }\n" + "}"; RangeQueryBuilder parsed = (RangeQueryBuilder) parseQuery(json); checkGeneratedJson(json, parsed); assertEquals(json, "2015-01-01 00:00:00", parsed.from()); assertEquals(json, "now", parsed.to()); } public void testNamedQueryParsing() throws IOException { String json = "{\n" + " \"range\" : {\n" + " \"timestamp\" : {\n" + " \"from\" : \"2015-01-01 00:00:00\",\n" + " \"to\" : \"now\",\n" + " \"boost\" : 1.0,\n" + " \"_name\" : \"my_range\"\n" + " }\n" + " }\n" + "}"; assertNotNull(parseQuery(json)); final String deprecatedJson = "{\n" + " \"range\" : {\n" + " \"timestamp\" : {\n" + " \"from\" : \"2015-01-01 00:00:00\",\n" + " \"to\" : \"now\",\n" + " \"boost\" : 1.0\n" + " },\n" + " \"_name\" : \"my_range\"\n" + " }\n" + "}"; assertNotNull(parseQuery(deprecatedJson)); assertWarnings("Deprecated field [_name] used, replaced by [query name is not supported in short version of range query]"); } public void testRewriteDateToMatchAll() throws IOException { String fieldName = randomAlphaOfLengthBetween(1, 20); RangeQueryBuilder query = new RangeQueryBuilder(fieldName) { @Override protected MappedFieldType.Relation getRelation(QueryRewriteContext queryRewriteContext) throws IOException { return Relation.WITHIN; } }; DateTime queryFromValue = new DateTime(2015, 1, 1, 0, 0, 0, ISOChronology.getInstanceUTC()); DateTime queryToValue = new DateTime(2016, 1, 1, 0, 0, 0, ISOChronology.getInstanceUTC()); query.from(queryFromValue); query.to(queryToValue); QueryShardContext queryShardContext = createShardContext(); QueryBuilder rewritten = query.rewrite(queryShardContext); assertThat(rewritten, instanceOf(RangeQueryBuilder.class)); RangeQueryBuilder rewrittenRange = (RangeQueryBuilder) rewritten; assertThat(rewrittenRange.fieldName(), equalTo(fieldName)); assertThat(rewrittenRange.from(), equalTo(null)); assertThat(rewrittenRange.to(), equalTo(null)); } public void testRewriteDateToMatchAllWithTimezoneAndFormat() throws IOException { String fieldName = randomAlphaOfLengthBetween(1, 20); RangeQueryBuilder query = new RangeQueryBuilder(fieldName) { @Override protected MappedFieldType.Relation getRelation(QueryRewriteContext queryRewriteContext) throws IOException { return Relation.WITHIN; } }; DateTime queryFromValue = new DateTime(2015, 1, 1, 0, 0, 0, ISOChronology.getInstanceUTC()); DateTime queryToValue = new DateTime(2016, 1, 1, 0, 0, 0, ISOChronology.getInstanceUTC()); query.from(queryFromValue); query.to(queryToValue); query.timeZone(randomFrom(DateTimeZone.getAvailableIDs())); query.format("yyyy-MM-dd"); QueryShardContext queryShardContext = createShardContext(); QueryBuilder rewritten = query.rewrite(queryShardContext); assertThat(rewritten, instanceOf(RangeQueryBuilder.class)); RangeQueryBuilder rewrittenRange = (RangeQueryBuilder) rewritten; assertThat(rewrittenRange.fieldName(), equalTo(fieldName)); assertThat(rewrittenRange.from(), equalTo(null)); assertThat(rewrittenRange.to(), equalTo(null)); assertThat(rewrittenRange.timeZone(), equalTo(null)); assertThat(rewrittenRange.format(), equalTo(null)); } public void testRewriteDateToMatchNone() throws IOException { String fieldName = randomAlphaOfLengthBetween(1, 20); RangeQueryBuilder query = new RangeQueryBuilder(fieldName) { @Override protected MappedFieldType.Relation getRelation(QueryRewriteContext queryRewriteContext) throws IOException { return Relation.DISJOINT; } }; DateTime queryFromValue = new DateTime(2015, 1, 1, 0, 0, 0, ISOChronology.getInstanceUTC()); DateTime queryToValue = new DateTime(2016, 1, 1, 0, 0, 0, ISOChronology.getInstanceUTC()); query.from(queryFromValue); query.to(queryToValue); QueryShardContext queryShardContext = createShardContext(); QueryBuilder rewritten = query.rewrite(queryShardContext); assertThat(rewritten, instanceOf(MatchNoneQueryBuilder.class)); } public void testRewriteDateToSame() throws IOException { String fieldName = randomAlphaOfLengthBetween(1, 20); RangeQueryBuilder query = new RangeQueryBuilder(fieldName) { @Override protected MappedFieldType.Relation getRelation(QueryRewriteContext queryRewriteContext) throws IOException { return Relation.INTERSECTS; } }; DateTime queryFromValue = new DateTime(2015, 1, 1, 0, 0, 0, ISOChronology.getInstanceUTC()); DateTime queryToValue = new DateTime(2016, 1, 1, 0, 0, 0, ISOChronology.getInstanceUTC()); query.from(queryFromValue); query.to(queryToValue); QueryShardContext queryShardContext = createShardContext(); QueryBuilder rewritten = query.rewrite(queryShardContext); assertThat(rewritten, sameInstance(query)); } public void testRewriteOpenBoundsToSame() throws IOException { String fieldName = randomAlphaOfLengthBetween(1, 20); RangeQueryBuilder query = new RangeQueryBuilder(fieldName) { @Override protected MappedFieldType.Relation getRelation(QueryRewriteContext queryRewriteContext) throws IOException { return Relation.INTERSECTS; } }; QueryShardContext queryShardContext = createShardContext(); QueryBuilder rewritten = query.rewrite(queryShardContext); assertThat(rewritten, sameInstance(query)); } public void testParseFailsWithMultipleFields() throws IOException { String json = "{\n" + " \"range\": {\n" + " \"age\": {\n" + " \"gte\": 30,\n" + " \"lte\": 40\n" + " },\n" + " \"price\": {\n" + " \"gte\": 10,\n" + " \"lte\": 30\n" + " }\n" + " }\n" + " }"; ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(json)); assertEquals("[range] query doesn't support multiple fields, found [age] and [price]", e.getMessage()); } public void testParseFailsWithMultipleFieldsWhenOneIsDate() throws IOException { String json = "{\n" + " \"range\": {\n" + " \"age\": {\n" + " \"gte\": 30,\n" + " \"lte\": 40\n" + " },\n" + " \"" + DATE_FIELD_NAME + "\": {\n" + " \"gte\": \"2016-09-13 05:01:14\"\n" + " }\n" + " }\n" + " }"; ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(json)); assertEquals("[range] query doesn't support multiple fields, found [age] and [" + DATE_FIELD_NAME + "]", e.getMessage()); } }