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.
This commit is contained in:
David Pilato 2014-07-31 16:22:03 +02:00
parent d2fea5378a
commit 873a45eaba
16 changed files with 574 additions and 95 deletions

View File

@ -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

View File

@ -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.

View File

@ -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);
}
}
}

View File

@ -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.

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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) {

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}

View File

@ -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
}
}
}

View File

@ -0,0 +1,13 @@
{
"constant_score": {
"filter": {
"range" : {
"born" : {
"gte": "2012-01-01",
"lte": "now",
"time_zone": "+1:00"
}
}
}
}
}

View File

@ -0,0 +1,13 @@
{
"constant_score": {
"filter": {
"range" : {
"age" : {
"gte": "0",
"lte": "100",
"time_zone": "-1:00"
}
}
}
}
}

View File

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

View File

@ -0,0 +1,9 @@
{
"range" : {
"age" : {
"gte": "0",
"lte": "100",
"time_zone": "-1:00"
}
}
}

View File

@ -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\"}"));