Browse Source

Search: add time zone setting for relative date math in range filter/query

Filters and Queries now supports `time_zone` parameter which defines which time zone should be applied to the query or filter to convert it to UTC time based value.

When applied on `date` fields the `range` filter and queries accept 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"
                }
            }
        }
    }
}

{
    "range" : {
        "born" : {
            "gte": "2012-01-01",
            "lte": "now",
            "time_zone": "+1:00"
        }
    }
}
--------------------------------------------------

In the above examples, `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.

Closes #3729.
David Pilato 11 years ago
parent
commit
873a45eaba

+ 28 - 0
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
 

+ 24 - 0
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.
+

+ 57 - 8
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);
+        }
+    }
+
 }

+ 39 - 47
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<Long> {
     }
 
     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<Long> {
 
     @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<Long> {
             } 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<Long> {
             } 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.

+ 13 - 1
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();
     }
-}
+}

+ 20 - 3
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;
     }
-}
+}

+ 12 - 0
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) {

+ 22 - 3
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;
     }
-}
+}

+ 4 - 19
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);
-        }
-    }
-
 }

+ 2 - 13
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());
         }
     }
 

+ 111 - 0
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
+        }
+    }
+}

+ 13 - 0
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"
+                }
+            }
+        }
+    }
+}

+ 13 - 0
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"
+                }
+            }
+        }
+    }
+}

+ 9 - 0
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"
+        }
+    }
+}

+ 9 - 0
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"
+        }
+    }
+}

+ 198 - 1
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\"}"));