diff --git a/docs/reference/query-dsl/filters/range-filter.asciidoc b/docs/reference/query-dsl/filters/range-filter.asciidoc index 7399d000643..d792d1490b9 100644 --- a/docs/reference/query-dsl/filters/range-filter.asciidoc +++ b/docs/reference/query-dsl/filters/range-filter.asciidoc @@ -30,6 +30,34 @@ The `range` filter accepts the following parameters: `lte`:: Less-than or equal to `lt`:: Less-than +coming[1.4.0] + +When applied on `date` fields the `range` filter accepts also a `time_zone` parameter. +The `time_zone` parameter will be applied to your input lower and upper bounds and will +move them to UTC time based date: + +[source,js] +-------------------------------------------------- +{ + "constant_score": { + "filter": { + "range" : { + "born" : { + "gte": "2012-01-01", + "lte": "now", + "time_zone": "+1:00" + } + } + } + } +} +-------------------------------------------------- + +In the above example, `gte` will be actually moved to `2011-12-31T23:00:00` UTC date. + +NOTE: if you give a date with a timezone explicitly defined and use the `time_zone` parameter, `time_zone` will be +ignored. For example, setting `from` to `2012-01-01T00:00:00+01:00` with `"time_zone":"+10:00"` will still use `+01:00` time zone. + [float] ==== Execution diff --git a/docs/reference/query-dsl/queries/range-query.asciidoc b/docs/reference/query-dsl/queries/range-query.asciidoc index cf8a9dab465..d011996a341 100644 --- a/docs/reference/query-dsl/queries/range-query.asciidoc +++ b/docs/reference/query-dsl/queries/range-query.asciidoc @@ -29,3 +29,27 @@ The `range` query accepts the following parameters: `lt`:: Less-than `boost`:: Sets the boost value of the query, defaults to `1.0` +coming[1.4.0] + +When applied on `date` fields the `range` filter accepts also a `time_zone` parameter. +The `time_zone` parameter will be applied to your input lower and upper bounds and will +move them to UTC time based date: + +[source,js] +-------------------------------------------------- +{ + "range" : { + "born" : { + "gte": "2012-01-01", + "lte": "now", + "time_zone": "+1:00" + } + } +} +-------------------------------------------------- + +In the above example, `gte` will be actually moved to `2011-12-31T23:00:00` UTC date. + +NOTE: if you give a date with a timezone explicitly defined and use the `time_zone` parameter, `time_zone` will be +ignored. For example, setting `from` to `2012-01-01T00:00:00+01:00` with `"time_zone":"+10:00"` will still use `+01:00` time zone. + diff --git a/src/main/java/org/elasticsearch/common/joda/DateMathParser.java b/src/main/java/org/elasticsearch/common/joda/DateMathParser.java index 12d0d4236bb..d07465560d7 100644 --- a/src/main/java/org/elasticsearch/common/joda/DateMathParser.java +++ b/src/main/java/org/elasticsearch/common/joda/DateMathParser.java @@ -22,7 +22,9 @@ package org.elasticsearch.common.joda; import org.elasticsearch.ElasticsearchParseException; import org.joda.time.DateTimeZone; import org.joda.time.MutableDateTime; +import org.joda.time.format.DateTimeFormatter; +import java.io.IOException; import java.util.concurrent.TimeUnit; /** @@ -39,14 +41,26 @@ public class DateMathParser { } public long parse(String text, long now) { - return parse(text, now, false); + return parse(text, now, false, null); + } + + public long parse(String text, long now, DateTimeZone timeZone) { + return parse(text, now, false, timeZone); } public long parseRoundCeil(String text, long now) { - return parse(text, now, true); + return parse(text, now, true, null); + } + + public long parseRoundCeil(String text, long now, DateTimeZone timeZone) { + return parse(text, now, true, timeZone); } public long parse(String text, long now, boolean roundCeil) { + return parse(text, now, roundCeil, null); + } + + public long parse(String text, long now, boolean roundCeil, DateTimeZone timeZone) { long time; String mathString; if (text.startsWith("now")) { @@ -63,9 +77,9 @@ public class DateMathParser { mathString = text.substring(index + 2); } if (roundCeil) { - time = parseRoundCeilStringValue(parseString); + time = parseRoundCeilStringValue(parseString, timeZone); } else { - time = parseStringValue(parseString); + time = parseStringValue(parseString, timeZone); } } @@ -215,11 +229,29 @@ public class DateMathParser { return dateTime.getMillis(); } - private long parseStringValue(String value) { + /** + * Get a DateTimeFormatter parser applying timezone if any. + */ + public static DateTimeFormatter getDateTimeFormatterParser(FormatDateTimeFormatter dateTimeFormatter, DateTimeZone timeZone) { + if (dateTimeFormatter == null) { + return null; + } + + DateTimeFormatter parser = dateTimeFormatter.parser(); + if (timeZone != null) { + parser = parser.withZone(timeZone); + } + return parser; + } + + private long parseStringValue(String value, DateTimeZone timeZone) { try { - return dateTimeFormatter.parser().parseMillis(value); + DateTimeFormatter parser = getDateTimeFormatterParser(dateTimeFormatter, timeZone); + return parser.parseMillis(value); } catch (RuntimeException e) { try { + // When date is given as a numeric value, it's a date in ms since epoch + // By definition, it's a UTC date. long time = Long.parseLong(value); return timeUnit.toMillis(time); } catch (NumberFormatException e1) { @@ -228,14 +260,15 @@ public class DateMathParser { } } - private long parseRoundCeilStringValue(String value) { + private long parseRoundCeilStringValue(String value, DateTimeZone timeZone) { try { // we create a date time for inclusive upper range, we "include" by default the day level data // so something like 2011-01-01 will include the full first day of 2011. // we also use 1970-01-01 as the base for it so we can handle searches like 10:12:55 (just time) // since when we index those, the base is 1970-01-01 MutableDateTime dateTime = new MutableDateTime(1970, 1, 1, 23, 59, 59, 999, DateTimeZone.UTC); - int location = dateTimeFormatter.parser().parseInto(dateTime, value, 0); + DateTimeFormatter parser = getDateTimeFormatterParser(dateTimeFormatter, timeZone); + int location = parser.parseInto(dateTime, value, 0); // if we parsed all the string value, we are good if (location == value.length()) { return dateTime.getMillis(); @@ -260,4 +293,20 @@ public class DateMathParser { } } } + + public static DateTimeZone parseZone(String text) throws IOException { + int index = text.indexOf(':'); + if (index != -1) { + int beginIndex = text.charAt(0) == '+' ? 1 : 0; + // format like -02:30 + return DateTimeZone.forOffsetHoursMinutes( + Integer.parseInt(text.substring(beginIndex, index)), + Integer.parseInt(text.substring(index + 1)) + ); + } else { + // id, listed here: http://joda-time.sourceforge.net/timezones.html + return DateTimeZone.forID(text); + } + } + } diff --git a/src/main/java/org/elasticsearch/index/mapper/core/DateFieldMapper.java b/src/main/java/org/elasticsearch/index/mapper/core/DateFieldMapper.java index b7e6694513a..95d104de1ba 100644 --- a/src/main/java/org/elasticsearch/index/mapper/core/DateFieldMapper.java +++ b/src/main/java/org/elasticsearch/index/mapper/core/DateFieldMapper.java @@ -51,6 +51,7 @@ import org.elasticsearch.index.mapper.core.LongFieldMapper.CustomLongNumericFiel import org.elasticsearch.index.query.QueryParseContext; import org.elasticsearch.index.search.NumericRangeFieldDataFilter; import org.elasticsearch.index.similarity.SimilarityProvider; +import org.joda.time.DateTimeZone; import java.io.IOException; import java.util.List; @@ -298,16 +299,20 @@ public class DateFieldMapper extends NumberFieldMapper { } public long parseToMilliseconds(Object value, @Nullable QueryParseContext context, boolean includeUpper) { + return parseToMilliseconds(value, context, includeUpper, null); + } + + public long parseToMilliseconds(Object value, @Nullable QueryParseContext context, boolean includeUpper, @Nullable DateTimeZone zone) { if (value instanceof Number) { return ((Number) value).longValue(); } - long now = context == null ? System.currentTimeMillis() : context.nowInMillis(); - return includeUpper && roundCeil ? dateMathParser.parseRoundCeil(convertToString(value), now) : dateMathParser.parse(convertToString(value), now); + return parseToMilliseconds(convertToString(value), context, includeUpper, zone); } - public long parseToMilliseconds(String value, @Nullable QueryParseContext context, boolean includeUpper) { + public long parseToMilliseconds(String value, @Nullable QueryParseContext context, boolean includeUpper, @Nullable DateTimeZone zone) { long now = context == null ? System.currentTimeMillis() : context.nowInMillis(); - return includeUpper && roundCeil ? dateMathParser.parseRoundCeil(value, now) : dateMathParser.parse(value, now); + long time = includeUpper && roundCeil ? dateMathParser.parseRoundCeil(value, now, zone) : dateMathParser.parse(value, now, zone); + return time; } @Override @@ -319,58 +324,37 @@ public class DateFieldMapper extends NumberFieldMapper { @Override public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable QueryParseContext context) { + return rangeQuery(lowerTerm, upperTerm, includeLower, includeUpper, null, context); + } + + public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable DateTimeZone timeZone, @Nullable QueryParseContext context) { return NumericRangeQuery.newLongRange(names.indexName(), precisionStep, - lowerTerm == null ? null : parseToMilliseconds(lowerTerm, context), - upperTerm == null ? null : parseToMilliseconds(upperTerm, context, includeUpper), + lowerTerm == null ? null : parseToMilliseconds(lowerTerm, context, false, timeZone), + upperTerm == null ? null : parseToMilliseconds(upperTerm, context, includeUpper, timeZone), includeLower, includeUpper); } @Override public Filter rangeFilter(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable QueryParseContext context) { - return rangeFilter(lowerTerm, upperTerm, includeLower, includeUpper, context, false); + return rangeFilter(lowerTerm, upperTerm, includeLower, includeUpper, null, context, false); } - public Filter rangeFilter(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable QueryParseContext context, boolean explicitCaching) { - boolean cache = explicitCaching; - Long lowerVal = null; - Long upperVal = null; - if (lowerTerm != null) { - if (lowerTerm instanceof Number) { - lowerVal = ((Number) lowerTerm).longValue(); - } else { - String value = convertToString(lowerTerm); - cache = explicitCaching || !hasNowExpressionWithNoRounding(value); - lowerVal = parseToMilliseconds(value, context, false); - } - } - if (upperTerm != null) { - if (upperTerm instanceof Number) { - upperVal = ((Number) upperTerm).longValue(); - } else { - String value = convertToString(upperTerm); - cache = explicitCaching || !hasNowExpressionWithNoRounding(value); - upperVal = parseToMilliseconds(value, context, includeUpper); - } - } - - Filter filter = NumericRangeFilter.newLongRange( - names.indexName(), precisionStep, lowerVal, upperVal, includeLower, includeUpper - ); - if (!cache) { - // We don't cache range filter if `now` date expression is used and also when a compound filter wraps - // a range filter with a `now` date expressions. - return NoCacheFilter.wrap(filter); - } else { - return filter; - } + public Filter rangeFilter(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable DateTimeZone timeZone, @Nullable QueryParseContext context, boolean explicitCaching) { + return rangeFilter(null, lowerTerm, upperTerm, includeLower, includeUpper, timeZone, context, explicitCaching); } @Override public Filter rangeFilter(QueryParseContext parseContext, Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable QueryParseContext context) { - return rangeFilter(parseContext, lowerTerm, upperTerm, includeLower, includeUpper, context, false); + return rangeFilter(parseContext, lowerTerm, upperTerm, includeLower, includeUpper, null, context, false); } - public Filter rangeFilter(QueryParseContext parseContext, Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable QueryParseContext context, boolean explicitCaching) { + /* + * `timeZone` parameter is only applied when: + * - not null + * - the object to parse is a String (does not apply to ms since epoch which are UTC based time values) + * - the String to parse does not have already a timezone defined (ie. `2014-01-01T00:00:00+03:00`) + */ + public Filter rangeFilter(QueryParseContext parseContext, Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable DateTimeZone timeZone, @Nullable QueryParseContext context, boolean explicitCaching) { boolean cache = explicitCaching; Long lowerVal = null; Long upperVal = null; @@ -380,7 +364,7 @@ public class DateFieldMapper extends NumberFieldMapper { } else { String value = convertToString(lowerTerm); cache = explicitCaching || !hasNowExpressionWithNoRounding(value); - lowerVal = parseToMilliseconds(value, context, false); + lowerVal = parseToMilliseconds(value, context, false, timeZone); } } if (upperTerm != null) { @@ -389,13 +373,21 @@ public class DateFieldMapper extends NumberFieldMapper { } else { String value = convertToString(upperTerm); cache = explicitCaching || !hasNowExpressionWithNoRounding(value); - upperVal = parseToMilliseconds(value, context, includeUpper); + upperVal = parseToMilliseconds(value, context, includeUpper, timeZone); } } - Filter filter = NumericRangeFieldDataFilter.newLongRange( - (IndexNumericFieldData) parseContext.getForField(this), lowerVal,upperVal, includeLower, includeUpper - ); + Filter filter; + if (parseContext != null) { + filter = NumericRangeFieldDataFilter.newLongRange( + (IndexNumericFieldData) parseContext.getForField(this), lowerVal,upperVal, includeLower, includeUpper + ); + } else { + filter = NumericRangeFilter.newLongRange( + names.indexName(), precisionStep, lowerVal, upperVal, includeLower, includeUpper + ); + } + if (!cache) { // We don't cache range filter if `now` date expression is used and also when a compound filter wraps // a range filter with a `now` date expressions. diff --git a/src/main/java/org/elasticsearch/index/query/RangeFilterBuilder.java b/src/main/java/org/elasticsearch/index/query/RangeFilterBuilder.java index ae4c5b6f508..80149821438 100644 --- a/src/main/java/org/elasticsearch/index/query/RangeFilterBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/RangeFilterBuilder.java @@ -35,6 +35,7 @@ public class RangeFilterBuilder extends BaseFilterBuilder { private Object from; private Object to; + private String timeZone; private boolean includeLower = true; @@ -371,6 +372,14 @@ public class RangeFilterBuilder extends BaseFilterBuilder { return this; } + /** + * In case of date field, we can adjust the from/to fields using a timezone + */ + public RangeFilterBuilder timeZone(String timeZone) { + this.timeZone = timeZone; + return this; + } + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(RangeFilterParser.NAME); @@ -378,6 +387,9 @@ public class RangeFilterBuilder extends BaseFilterBuilder { builder.startObject(name); builder.field("from", from); builder.field("to", to); + if (timeZone != null) { + builder.field("time_zone", timeZone); + } builder.field("include_lower", includeLower); builder.field("include_upper", includeUpper); builder.endObject(); @@ -397,4 +409,4 @@ public class RangeFilterBuilder extends BaseFilterBuilder { builder.endObject(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/elasticsearch/index/query/RangeFilterParser.java b/src/main/java/org/elasticsearch/index/query/RangeFilterParser.java index 4c9c765403d..74f6c4b6425 100644 --- a/src/main/java/org/elasticsearch/index/query/RangeFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/RangeFilterParser.java @@ -22,6 +22,7 @@ package org.elasticsearch.index.query; import org.apache.lucene.search.Filter; import org.apache.lucene.search.TermRangeFilter; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.joda.DateMathParser; import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.cache.filter.support.CacheKeyFilter; @@ -29,6 +30,7 @@ import org.elasticsearch.index.mapper.FieldMapper; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.core.DateFieldMapper; import org.elasticsearch.index.mapper.core.NumberFieldMapper; +import org.joda.time.DateTimeZone; import java.io.IOException; @@ -61,6 +63,7 @@ public class RangeFilterParser implements FilterParser { Object to = null; boolean includeLower = true; boolean includeUpper = true; + DateTimeZone timeZone = null; String execution = "index"; String filterName = null; @@ -95,6 +98,8 @@ public class RangeFilterParser implements FilterParser { } else if ("lte".equals(currentFieldName) || "le".equals(currentFieldName)) { to = parser.objectBytes(); includeUpper = true; + } else if ("time_zone".equals(currentFieldName) || "timeZone".equals(currentFieldName)) { + timeZone = DateMathParser.parseZone(parser.text()); } else { throw new QueryParsingException(parseContext.index(), "[range] filter does not support [" + currentFieldName + "]"); } @@ -130,8 +135,14 @@ public class RangeFilterParser implements FilterParser { } FieldMapper mapper = smartNameFieldMappers.mapper(); if (mapper instanceof DateFieldMapper) { - filter = ((DateFieldMapper) mapper).rangeFilter(from, to, includeLower, includeUpper, parseContext, explicitlyCached); + if ((from instanceof Number || to instanceof Number) && timeZone != null) { + throw new QueryParsingException(parseContext.index(), "[range] time_zone when using ms since epoch format as it's UTC based can not be applied to [" + fieldName + "]"); + } + filter = ((DateFieldMapper) mapper).rangeFilter(from, to, includeLower, includeUpper, timeZone, parseContext, explicitlyCached); } else { + if (timeZone != null) { + throw new QueryParsingException(parseContext.index(), "[range] time_zone can not be applied to non date field [" + fieldName + "]"); + } filter = mapper.rangeFilter(from, to, includeLower, includeUpper, parseContext); } } else if ("fielddata".equals(execution)) { @@ -143,8 +154,14 @@ public class RangeFilterParser implements FilterParser { throw new QueryParsingException(parseContext.index(), "[range] filter field [" + fieldName + "] is not a numeric type"); } if (mapper instanceof DateFieldMapper) { - filter = ((DateFieldMapper) mapper).rangeFilter(parseContext, from, to, includeLower, includeUpper, parseContext, explicitlyCached); + if ((from instanceof Number || to instanceof Number) && timeZone != null) { + throw new QueryParsingException(parseContext.index(), "[range] time_zone when using ms since epoch format as it's UTC based can not be applied to [" + fieldName + "]"); + } + filter = ((DateFieldMapper) mapper).rangeFilter(parseContext, from, to, includeLower, includeUpper, timeZone, parseContext, explicitlyCached); } else { + if (timeZone != null) { + throw new QueryParsingException(parseContext.index(), "[range] time_zone can not be applied to non date field [" + fieldName + "]"); + } filter = ((NumberFieldMapper) mapper).rangeFilter(parseContext, from, to, includeLower, includeUpper, parseContext); } } else { @@ -170,4 +187,4 @@ public class RangeFilterParser implements FilterParser { } return filter; } -} \ No newline at end of file +} diff --git a/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java index 7123985c3e1..c31e3429b86 100644 --- a/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java @@ -35,6 +35,7 @@ public class RangeQueryBuilder extends BaseQueryBuilder implements MultiTermQuer private Object from; private Object to; + private String timeZone; private boolean includeLower = true; @@ -398,12 +399,23 @@ public class RangeQueryBuilder extends BaseQueryBuilder implements MultiTermQuer return this; } + /** + * In case of date field, we can adjust the from/to fields using a timezone + */ + public RangeQueryBuilder timeZone(String preZone) { + this.timeZone = preZone; + return this; + } + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(RangeQueryParser.NAME); builder.startObject(name); builder.field("from", from); builder.field("to", to); + if (timeZone != null) { + builder.field("time_zone", timeZone); + } builder.field("include_lower", includeLower); builder.field("include_upper", includeUpper); if (boost != -1) { diff --git a/src/main/java/org/elasticsearch/index/query/RangeQueryParser.java b/src/main/java/org/elasticsearch/index/query/RangeQueryParser.java index 53f34275e28..04054d9715e 100644 --- a/src/main/java/org/elasticsearch/index/query/RangeQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/RangeQueryParser.java @@ -22,9 +22,13 @@ package org.elasticsearch.index.query; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermRangeQuery; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.joda.DateMathParser; import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.mapper.FieldMapper; import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.mapper.core.DateFieldMapper; +import org.joda.time.DateTimeZone; import java.io.IOException; @@ -64,6 +68,7 @@ public class RangeQueryParser implements QueryParser { Object to = null; boolean includeLower = true; boolean includeUpper = true; + DateTimeZone timeZone = null; float boost = 1.0f; String queryName = null; @@ -94,6 +99,8 @@ public class RangeQueryParser implements QueryParser { } else if ("lte".equals(currentFieldName) || "le".equals(currentFieldName)) { to = parser.objectBytes(); includeUpper = true; + } else if ("time_zone".equals(currentFieldName) || "timeZone".equals(currentFieldName)) { + timeZone = DateMathParser.parseZone(parser.text()); } else if ("_name".equals(currentFieldName)) { queryName = parser.text(); } else { @@ -112,8 +119,20 @@ public class RangeQueryParser implements QueryParser { MapperService.SmartNameFieldMappers smartNameFieldMappers = parseContext.smartFieldMappers(fieldName); if (smartNameFieldMappers != null) { if (smartNameFieldMappers.hasMapper()) { - //LUCENE 4 UPGRADE Mapper#rangeQuery should use bytesref as well? - query = smartNameFieldMappers.mapper().rangeQuery(from, to, includeLower, includeUpper, parseContext); + FieldMapper mapper = smartNameFieldMappers.mapper(); + if (mapper instanceof DateFieldMapper) { + if ((from instanceof Number || to instanceof Number) && timeZone != null) { + throw new QueryParsingException(parseContext.index(), "[range] time_zone when using ms since epoch format as it's UTC based can not be applied to [" + fieldName + "]"); + } + query = ((DateFieldMapper) mapper).rangeQuery(from, to, includeLower, includeUpper, timeZone, parseContext); + } else { + if (timeZone != null) { + throw new QueryParsingException(parseContext.index(), "[range] time_zone can not be applied to non date field [" + fieldName + "]"); + } + //LUCENE 4 UPGRADE Mapper#rangeQuery should use bytesref as well? + query = mapper.rangeQuery(from, to, includeLower, includeUpper, parseContext); + } + } } if (query == null) { @@ -126,4 +145,4 @@ public class RangeQueryParser implements QueryParser { } return query; } -} \ No newline at end of file +} diff --git a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramParser.java b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramParser.java index 8f1dbb9d9ab..616f8a86d32 100644 --- a/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramParser.java +++ b/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramParser.java @@ -21,6 +21,7 @@ package org.elasticsearch.search.aggregations.bucket.histogram; import com.google.common.collect.ImmutableMap; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.collect.MapBuilder; +import org.elasticsearch.common.joda.DateMathParser; import org.elasticsearch.common.rounding.DateTimeUnit; import org.elasticsearch.common.rounding.Rounding; import org.elasticsearch.common.rounding.TimeZoneRounding; @@ -100,11 +101,11 @@ public class DateHistogramParser implements Aggregator.Parser { continue; } else if (token == XContentParser.Token.VALUE_STRING) { if ("time_zone".equals(currentFieldName) || "timeZone".equals(currentFieldName)) { - preZone = parseZone(parser.text()); + preZone = DateMathParser.parseZone(parser.text()); } else if ("pre_zone".equals(currentFieldName) || "preZone".equals(currentFieldName)) { - preZone = parseZone(parser.text()); + preZone = DateMathParser.parseZone(parser.text()); } else if ("post_zone".equals(currentFieldName) || "postZone".equals(currentFieldName)) { - postZone = parseZone(parser.text()); + postZone = DateMathParser.parseZone(parser.text()); } else if ("pre_offset".equals(currentFieldName) || "preOffset".equals(currentFieldName)) { preOffset = parseOffset(parser.text()); } else if ("post_offset".equals(currentFieldName) || "postOffset".equals(currentFieldName)) { @@ -220,20 +221,4 @@ public class DateHistogramParser implements Aggregator.Parser { int beginIndex = offset.charAt(0) == '+' ? 1 : 0; return TimeValue.parseTimeValue(offset.substring(beginIndex), null).millis(); } - - private DateTimeZone parseZone(String text) throws IOException { - int index = text.indexOf(':'); - if (index != -1) { - int beginIndex = text.charAt(0) == '+' ? 1 : 0; - // format like -02:30 - return DateTimeZone.forOffsetHoursMinutes( - Integer.parseInt(text.substring(beginIndex, index)), - Integer.parseInt(text.substring(index + 1)) - ); - } else { - // id, listed here: http://joda-time.sourceforge.net/timezones.html - return DateTimeZone.forID(text); - } - } - } diff --git a/src/main/java/org/elasticsearch/search/facet/datehistogram/DateHistogramFacetParser.java b/src/main/java/org/elasticsearch/search/facet/datehistogram/DateHistogramFacetParser.java index d3121656a98..79a45bfba51 100644 --- a/src/main/java/org/elasticsearch/search/facet/datehistogram/DateHistogramFacetParser.java +++ b/src/main/java/org/elasticsearch/search/facet/datehistogram/DateHistogramFacetParser.java @@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableMap; import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.joda.DateMathParser; import org.elasticsearch.common.rounding.DateTimeUnit; import org.elasticsearch.common.rounding.Rounding; import org.elasticsearch.common.rounding.TimeZoneRounding; @@ -214,19 +215,7 @@ public class DateHistogramFacetParser extends AbstractComponent implements Facet if (token == XContentParser.Token.VALUE_NUMBER) { return DateTimeZone.forOffsetHours(parser.intValue()); } else { - String text = parser.text(); - int index = text.indexOf(':'); - if (index != -1) { - int beginIndex = text.charAt(0) == '+' ? 1 : 0; - // format like -02:30 - return DateTimeZone.forOffsetHoursMinutes( - Integer.parseInt(text.substring(beginIndex, index)), - Integer.parseInt(text.substring(index + 1)) - ); - } else { - // id, listed here: http://joda-time.sourceforge.net/timezones.html - return DateTimeZone.forID(text); - } + return DateMathParser.parseZone(parser.text()); } } diff --git a/src/test/java/org/elasticsearch/index/query/IndexQueryParserFilterDateRangeTimezoneTests.java b/src/test/java/org/elasticsearch/index/query/IndexQueryParserFilterDateRangeTimezoneTests.java new file mode 100644 index 00000000000..dd774088e5d --- /dev/null +++ b/src/test/java/org/elasticsearch/index/query/IndexQueryParserFilterDateRangeTimezoneTests.java @@ -0,0 +1,111 @@ +/* + * 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 org.apache.lucene.search.NumericRangeQuery; +import org.apache.lucene.search.Query; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.compress.CompressedString; +import org.elasticsearch.common.inject.Injector; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.service.IndexService; +import org.elasticsearch.test.ElasticsearchSingleNodeTest; +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; + +import static org.elasticsearch.common.io.Streams.copyToBytesFromClasspath; +import static org.elasticsearch.common.io.Streams.copyToStringFromClasspath; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThanOrEqualTo; + +/** + * + */ +public class IndexQueryParserFilterDateRangeTimezoneTests extends ElasticsearchSingleNodeTest { + + private Injector injector; + private IndexQueryParserService queryParser; + + @Before + public void setup() throws IOException { + IndexService indexService = createIndex("test"); + injector = indexService.injector(); + + MapperService mapperService = indexService.mapperService(); + String mapping = copyToStringFromClasspath("/org/elasticsearch/index/query/mapping.json"); + mapperService.merge("person", new CompressedString(mapping), true); + mapperService.documentMapper("person").parse(new BytesArray(copyToBytesFromClasspath("/org/elasticsearch/index/query/data.json"))); + queryParser = injector.getInstance(IndexQueryParserService.class); + } + + private IndexQueryParserService queryParser() throws IOException { + return this.queryParser; + } + + @Test + public void testDateRangeFilterTimezone() throws IOException { + IndexQueryParserService queryParser = queryParser(); + String query = copyToStringFromClasspath("/org/elasticsearch/index/query/date_range_filter_timezone.json"); + queryParser.parse(query).query(); + // Sadly from NoCacheFilter, we can not access to the delegate filter so we can not check + // it's the one we are expecting + + query = copyToStringFromClasspath("/org/elasticsearch/index/query/date_range_filter_timezone_numeric_field.json"); + try { + queryParser.parse(query).query(); + fail("A Range Filter on a numeric field with a TimeZone should raise a QueryParsingException"); + } catch (QueryParsingException e) { + // We expect it + } + } + + @Test + public void testDateRangeQueryTimezone() throws IOException { + long startDate = System.currentTimeMillis(); + + IndexQueryParserService queryParser = queryParser(); + String query = copyToStringFromClasspath("/org/elasticsearch/index/query/date_range_query_timezone.json"); + Query parsedQuery = queryParser.parse(query).query(); + assertThat(parsedQuery, instanceOf(NumericRangeQuery.class)); + + // Min value was 2012-01-01 (UTC) so we need to remove one hour + DateTime min = DateTime.parse("2012-01-01T00:00:00.000+01:00"); + // Max value is when we started the test. So it should be some ms from now + DateTime max = new DateTime(startDate); + + assertThat(((NumericRangeQuery) parsedQuery).getMin().longValue(), is(min.getMillis())); + + // We should not have a big difference here (should be some ms) + assertThat(((NumericRangeQuery) parsedQuery).getMax().longValue() - max.getMillis(), lessThanOrEqualTo(60000L)); + + query = copyToStringFromClasspath("/org/elasticsearch/index/query/date_range_query_timezone_numeric_field.json"); + try { + queryParser.parse(query).query(); + fail("A Range Query on a numeric field with a TimeZone should raise a QueryParsingException"); + } catch (QueryParsingException e) { + // We expect it + } + } +} diff --git a/src/test/java/org/elasticsearch/index/query/date_range_filter_timezone.json b/src/test/java/org/elasticsearch/index/query/date_range_filter_timezone.json new file mode 100644 index 00000000000..4253018c121 --- /dev/null +++ b/src/test/java/org/elasticsearch/index/query/date_range_filter_timezone.json @@ -0,0 +1,13 @@ +{ + "constant_score": { + "filter": { + "range" : { + "born" : { + "gte": "2012-01-01", + "lte": "now", + "time_zone": "+1:00" + } + } + } + } +} diff --git a/src/test/java/org/elasticsearch/index/query/date_range_filter_timezone_numeric_field.json b/src/test/java/org/elasticsearch/index/query/date_range_filter_timezone_numeric_field.json new file mode 100644 index 00000000000..8814a7ea954 --- /dev/null +++ b/src/test/java/org/elasticsearch/index/query/date_range_filter_timezone_numeric_field.json @@ -0,0 +1,13 @@ +{ + "constant_score": { + "filter": { + "range" : { + "age" : { + "gte": "0", + "lte": "100", + "time_zone": "-1:00" + } + } + } + } +} diff --git a/src/test/java/org/elasticsearch/index/query/date_range_query_timezone.json b/src/test/java/org/elasticsearch/index/query/date_range_query_timezone.json new file mode 100644 index 00000000000..da0b94314ce --- /dev/null +++ b/src/test/java/org/elasticsearch/index/query/date_range_query_timezone.json @@ -0,0 +1,9 @@ +{ + "range" : { + "born" : { + "gte": "2012-01-01", + "lte": "now", + "time_zone": "+1:00" + } + } +} diff --git a/src/test/java/org/elasticsearch/index/query/date_range_query_timezone_numeric_field.json b/src/test/java/org/elasticsearch/index/query/date_range_query_timezone_numeric_field.json new file mode 100644 index 00000000000..61670bd60d3 --- /dev/null +++ b/src/test/java/org/elasticsearch/index/query/date_range_query_timezone_numeric_field.json @@ -0,0 +1,9 @@ +{ + "range" : { + "age" : { + "gte": "0", + "lte": "100", + "time_zone": "-1:00" + } + } +} diff --git a/src/test/java/org/elasticsearch/search/query/SimpleQueryTests.java b/src/test/java/org/elasticsearch/search/query/SimpleQueryTests.java index 94ae5749a7d..61f2b95d94c 100644 --- a/src/test/java/org/elasticsearch/search/query/SimpleQueryTests.java +++ b/src/test/java/org/elasticsearch/search/query/SimpleQueryTests.java @@ -47,7 +47,10 @@ import org.joda.time.format.ISODateTimeFormat; import org.junit.Test; import java.io.IOException; -import java.util.*; +import java.util.HashSet; +import java.util.Locale; +import java.util.Random; +import java.util.Set; import java.util.concurrent.ExecutionException; import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS; @@ -2342,6 +2345,200 @@ public class SimpleQueryTests extends ElasticsearchIntegrationTest { assertThat(statsResponse.getIndex("test").getTotal().getFilterCache().getMemorySizeInBytes(), cluster().hasFilterCache() ? greaterThan(filtercacheSize) : is(filtercacheSize)); } + @Test + public void testRangeFilterWithTimeZone() throws Exception { + assertAcked(prepareCreate("test") + .addMapping("type1", "date", "type=date")); + ensureGreen(); + + index("test", "type1", "1", "date", "2014-01-01", "num", 1); + index("test", "type1", "2", "date", "2013-12-31T23:00:00", "num", 2); + index("test", "type1", "3", "date", "2014-01-01T01:00:00", "num", 3); + // Now in UTC+1 + index("test", "type1", "4", "date", DateTime.now(DateTimeZone.forOffsetHours(1)).getMillis(), "num", 4); + + refresh(); + + SearchResponse searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.filteredQuery(matchAllQuery(), FilterBuilders.rangeFilter("date").from("2014-01-01T00:00:00").to("2014-01-01T00:59:00"))) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("1")); + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.filteredQuery(matchAllQuery(), FilterBuilders.rangeFilter("date").from("2013-12-31T23:00:00").to("2013-12-31T23:59:00"))) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("2")); + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.filteredQuery(matchAllQuery(), FilterBuilders.rangeFilter("date").from("2014-01-01T01:00:00").to("2014-01-01T01:59:00"))) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("3")); + + // We explicitly define a time zone in the from/to dates so whatever the time zone is, it won't be used + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.filteredQuery(matchAllQuery(), FilterBuilders.rangeFilter("date").from("2014-01-01T00:00:00Z").to("2014-01-01T00:59:00Z").timeZone("+10:00"))) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("1")); + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.filteredQuery(matchAllQuery(), FilterBuilders.rangeFilter("date").from("2013-12-31T23:00:00Z").to("2013-12-31T23:59:00Z").timeZone("+10:00"))) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("2")); + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.filteredQuery(matchAllQuery(), FilterBuilders.rangeFilter("date").from("2014-01-01T01:00:00Z").to("2014-01-01T01:59:00Z").timeZone("+10:00"))) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("3")); + + // We define a time zone to be applied to the filter and from/to have no time zone + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.filteredQuery(matchAllQuery(), FilterBuilders.rangeFilter("date").from("2014-01-01T03:00:00").to("2014-01-01T03:59:00").timeZone("+3:00"))) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("1")); + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.filteredQuery(matchAllQuery(), FilterBuilders.rangeFilter("date").from("2014-01-01T02:00:00").to("2014-01-01T02:59:00").timeZone("+3:00"))) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("2")); + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.filteredQuery(matchAllQuery(), FilterBuilders.rangeFilter("date").from("2014-01-01T04:00:00").to("2014-01-01T04:59:00").timeZone("+3:00"))) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("3")); + + // When we use long values, it means we have ms since epoch UTC based so we don't apply any transformation + try { + client().prepareSearch("test") + .setQuery(QueryBuilders.filteredQuery(matchAllQuery(), FilterBuilders.rangeFilter("date").from(1388534400000L).to(1388537940999L).timeZone("+1:00"))) + .get(); + fail("A Range Filter using ms since epoch with a TimeZone should raise a QueryParsingException"); + } catch (SearchPhaseExecutionException e) { + // We expect it + } + + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.filteredQuery(matchAllQuery(), FilterBuilders.rangeFilter("date").from("2014-01-01").to("2014-01-01T00:59:00").timeZone("-1:00"))) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("3")); + + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.filteredQuery(matchAllQuery(), FilterBuilders.rangeFilter("date").from("now/d-1d").timeZone("+1:00"))) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("4")); + + // A Range Filter on a numeric field with a TimeZone should raise an exception + try { + client().prepareSearch("test") + .setQuery(QueryBuilders.filteredQuery(matchAllQuery(), FilterBuilders.rangeFilter("num").from("0").to("4").timeZone("-1:00"))) + .get(); + fail("A Range Filter on a numeric field with a TimeZone should raise a QueryParsingException"); + } catch (SearchPhaseExecutionException e) { + // We expect it + } + } + + @Test + public void testRangeQueryWithTimeZone() throws Exception { + assertAcked(prepareCreate("test") + .addMapping("type1", "date", "type=date")); + ensureGreen(); + + index("test", "type1", "1", "date", "2014-01-01", "num", 1); + index("test", "type1", "2", "date", "2013-12-31T23:00:00", "num", 2); + index("test", "type1", "3", "date", "2014-01-01T01:00:00", "num", 3); + // Now in UTC+1 + index("test", "type1", "4", "date", DateTime.now(DateTimeZone.forOffsetHours(1)).getMillis(), "num", 4); + + refresh(); + + SearchResponse searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.rangeQuery("date").from("2014-01-01T00:00:00").to("2014-01-01T00:59:00")) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("1")); + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.rangeQuery("date").from("2013-12-31T23:00:00").to("2013-12-31T23:59:00")) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("2")); + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.rangeQuery("date").from("2014-01-01T01:00:00").to("2014-01-01T01:59:00")) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("3")); + + // We explicitly define a time zone in the from/to dates so whatever the time zone is, it won't be used + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.rangeQuery("date").from("2014-01-01T00:00:00Z").to("2014-01-01T00:59:00Z").timeZone("+10:00")) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("1")); + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.rangeQuery("date").from("2013-12-31T23:00:00Z").to("2013-12-31T23:59:00Z").timeZone("+10:00")) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("2")); + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.rangeQuery("date").from("2014-01-01T01:00:00Z").to("2014-01-01T01:59:00Z").timeZone("+10:00")) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("3")); + + // We define a time zone to be applied to the filter and from/to have no time zone + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.rangeQuery("date").from("2014-01-01T03:00:00").to("2014-01-01T03:59:00").timeZone("+3:00")) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("1")); + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.rangeQuery("date").from("2014-01-01T02:00:00").to("2014-01-01T02:59:00").timeZone("+3:00")) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("2")); + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.rangeQuery("date").from("2014-01-01T04:00:00").to("2014-01-01T04:59:00").timeZone("+3:00")) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("3")); + + // When we use long values, it means we have ms since epoch UTC based so we don't apply any transformation + try { + client().prepareSearch("test") + .setQuery(QueryBuilders.rangeQuery("date").from(1388534400000L).to(1388537940999L).timeZone("+1:00")) + .get(); + fail("A Range Filter using ms since epoch with a TimeZone should raise a QueryParsingException"); + } catch (SearchPhaseExecutionException e) { + // We expect it + } + + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.rangeQuery("date").from("2014-01-01").to("2014-01-01T00:59:00").timeZone("-1:00")) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("3")); + + searchResponse = client().prepareSearch("test") + .setQuery(QueryBuilders.rangeQuery("date").from("now/d-1d").timeZone("+1:00")) + .get(); + assertHitCount(searchResponse, 1l); + assertThat(searchResponse.getHits().getAt(0).getId(), is("4")); + + // A Range Filter on a numeric field with a TimeZone should raise an exception + try { + client().prepareSearch("test") + .setQuery(QueryBuilders.rangeQuery("num").from("0").to("4").timeZone("-1:00")) + .get(); + fail("A Range Filter on a numeric field with a TimeZone should raise a QueryParsingException"); + } catch (SearchPhaseExecutionException e) { + // We expect it + } + } + @Test public void testSearchEmptyDoc() { assertAcked(prepareCreate("test").setSettings("{\"index.analysis.analyzer.default.type\":\"keyword\"}"));