Skip to content

Commit 6ae6a07

Browse files
committed
Search: add format support for date range filter and queries
When the date format is defined in mapping, you can not use another format when querying using range date query or filter. For example, this won't work: ``` DELETE /test PUT /test/t/1 { "date": "2014-01-01" } GET /test/_search { "query": { "filtered": { "filter": { "range": { "date": { "from": "01/01/2014" } } } } } } ``` It causes: ``` Caused by: org.elasticsearch.ElasticsearchParseException: failed to parse date field [01/01/2014], tried both date format [dateOptionalTime], and timestamp number ``` It could be nice if we can support at query time another date format just like we support `analyzer` at search time on String fields. Something like: ``` GET /test/_search { "query": { "filtered": { "filter": { "range": { "date": { "from": "01/01/2014", "format": "dd/MM/yyyy" } } } } } } ``` Same for queries: ``` GET /test/_search { "query": { "range": { "date": { "from": "01/01/2014", "format": "dd/MM/yyyy" } } } } ``` Closes #7189.
1 parent 6cf3713 commit 6ae6a07

File tree

10 files changed

+228
-19
lines changed

10 files changed

+228
-19
lines changed

docs/reference/query-dsl/filters/range-filter.asciidoc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ The `range` filter accepts the following parameters:
3030
`lte`:: Less-than or equal to
3131
`lt`:: Less-than
3232

33+
[float]
34+
==== Date options
35+
3336
When applied on `date` fields the `range` filter accepts also a `time_zone` parameter.
3437
The `time_zone` parameter will be applied to your input lower and upper bounds and will
3538
move them to UTC time based date:
@@ -56,6 +59,28 @@ In the above example, `gte` will be actually moved to `2011-12-31T23:00:00` UTC
5659
NOTE: if you give a date with a timezone explicitly defined and use the `time_zone` parameter, `time_zone` will be
5760
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.
5861

62+
coming[1.5.0,New feature added]
63+
64+
When applied on `date` fields the `range` filter accepts also a `format` parameter.
65+
The `format` parameter will help support another date format than the one defined in mapping:
66+
67+
[source,js]
68+
--------------------------------------------------
69+
{
70+
"constant_score": {
71+
"filter": {
72+
"range" : {
73+
"born" : {
74+
"gte": "01/01/2012",
75+
"lte": "2013",
76+
"format": "dd/MM/yyyy||yyyy"
77+
}
78+
}
79+
}
80+
}
81+
}
82+
--------------------------------------------------
83+
5984
[float]
6085
==== Execution
6186

docs/reference/query-dsl/queries/range-query.asciidoc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ The `range` query accepts the following parameters:
2929
`lt`:: Less-than
3030
`boost`:: Sets the boost value of the query, defaults to `1.0`
3131

32+
[float]
33+
==== Date options
34+
3235
When applied on `date` fields the `range` filter accepts also a `time_zone` parameter.
3336
The `time_zone` parameter will be applied to your input lower and upper bounds and will
3437
move them to UTC time based date:
@@ -51,3 +54,20 @@ In the above example, `gte` will be actually moved to `2011-12-31T23:00:00` UTC
5154
NOTE: if you give a date with a timezone explicitly defined and use the `time_zone` parameter, `time_zone` will be
5255
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.
5356

57+
coming[1.5.0,New feature added]
58+
59+
When applied on `date` fields the `range` query accepts also a `format` parameter.
60+
The `format` parameter will help support another date format than the one defined in mapping:
61+
62+
[source,js]
63+
--------------------------------------------------
64+
{
65+
"range" : {
66+
"born" : {
67+
"gte": "01/01/2012",
68+
"lte": "2013",
69+
"format": "dd/MM/yyyy||yyyy"
70+
}
71+
}
72+
}
73+
--------------------------------------------------

src/main/java/org/elasticsearch/index/mapper/core/DateFieldMapper.java

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -300,19 +300,23 @@ public long parseToMilliseconds(Object value, @Nullable QueryParseContext contex
300300
}
301301

302302
public long parseToMilliseconds(Object value, @Nullable QueryParseContext context, boolean includeUpper) {
303-
return parseToMilliseconds(value, context, includeUpper, null);
303+
return parseToMilliseconds(value, context, includeUpper, null, dateMathParser);
304304
}
305305

306-
public long parseToMilliseconds(Object value, @Nullable QueryParseContext context, boolean includeUpper, @Nullable DateTimeZone zone) {
306+
public long parseToMilliseconds(Object value, @Nullable QueryParseContext context, boolean includeUpper, @Nullable DateTimeZone zone, @Nullable DateMathParser forcedDateParser) {
307307
if (value instanceof Number) {
308308
return ((Number) value).longValue();
309309
}
310-
return parseToMilliseconds(convertToString(value), context, includeUpper, zone);
310+
return parseToMilliseconds(convertToString(value), context, includeUpper, zone, forcedDateParser);
311311
}
312312

313-
public long parseToMilliseconds(String value, @Nullable QueryParseContext context, boolean includeUpper, @Nullable DateTimeZone zone) {
313+
public long parseToMilliseconds(String value, @Nullable QueryParseContext context, boolean includeUpper, @Nullable DateTimeZone zone, @Nullable DateMathParser forcedDateParser) {
314314
long now = context == null ? System.currentTimeMillis() : context.nowInMillis();
315-
long time = includeUpper && roundCeil ? dateMathParser.parseRoundCeil(value, now, zone) : dateMathParser.parse(value, now, zone);
315+
DateMathParser dateParser = dateMathParser;
316+
if (forcedDateParser != null) {
317+
dateParser = forcedDateParser;
318+
}
319+
long time = includeUpper && roundCeil ? dateParser.parseRoundCeil(value, now, zone) : dateParser.parse(value, now, zone);
316320
return time;
317321
}
318322

@@ -325,28 +329,28 @@ public Filter termFilter(Object value, @Nullable QueryParseContext context) {
325329

326330
@Override
327331
public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable QueryParseContext context) {
328-
return rangeQuery(lowerTerm, upperTerm, includeLower, includeUpper, null, context);
332+
return rangeQuery(lowerTerm, upperTerm, includeLower, includeUpper, null, null, context);
329333
}
330334

331-
public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable DateTimeZone timeZone, @Nullable QueryParseContext context) {
335+
public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable DateTimeZone timeZone, @Nullable DateMathParser forcedDateParser, @Nullable QueryParseContext context) {
332336
return NumericRangeQuery.newLongRange(names.indexName(), precisionStep,
333-
lowerTerm == null ? null : parseToMilliseconds(lowerTerm, context, false, timeZone),
334-
upperTerm == null ? null : parseToMilliseconds(upperTerm, context, includeUpper, timeZone),
337+
lowerTerm == null ? null : parseToMilliseconds(lowerTerm, context, false, timeZone, forcedDateParser == null ? dateMathParser : forcedDateParser),
338+
upperTerm == null ? null : parseToMilliseconds(upperTerm, context, includeUpper, timeZone, forcedDateParser == null ? dateMathParser : forcedDateParser),
335339
includeLower, includeUpper);
336340
}
337341

338342
@Override
339343
public Filter rangeFilter(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable QueryParseContext context) {
340-
return rangeFilter(lowerTerm, upperTerm, includeLower, includeUpper, null, context, null);
344+
return rangeFilter(lowerTerm, upperTerm, includeLower, includeUpper, null, null, context, null);
341345
}
342346

343-
public Filter rangeFilter(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable DateTimeZone timeZone, @Nullable QueryParseContext context, @Nullable Boolean explicitCaching) {
344-
return rangeFilter(null, lowerTerm, upperTerm, includeLower, includeUpper, timeZone, context, explicitCaching);
347+
public Filter rangeFilter(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable DateTimeZone timeZone, @Nullable DateMathParser forcedDateParser, @Nullable QueryParseContext context, @Nullable Boolean explicitCaching) {
348+
return rangeFilter(null, lowerTerm, upperTerm, includeLower, includeUpper, timeZone, forcedDateParser, context, explicitCaching);
345349
}
346350

347351
@Override
348352
public Filter rangeFilter(QueryParseContext parseContext, Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable QueryParseContext context) {
349-
return rangeFilter(parseContext, lowerTerm, upperTerm, includeLower, includeUpper, null, context, null);
353+
return rangeFilter(parseContext, lowerTerm, upperTerm, includeLower, includeUpper, null, null, context, null);
350354
}
351355

352356
/*
@@ -355,7 +359,7 @@ public Filter rangeFilter(QueryParseContext parseContext, Object lowerTerm, Obje
355359
* - the object to parse is a String (does not apply to ms since epoch which are UTC based time values)
356360
* - the String to parse does not have already a timezone defined (ie. `2014-01-01T00:00:00+03:00`)
357361
*/
358-
public Filter rangeFilter(QueryParseContext parseContext, Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable DateTimeZone timeZone, @Nullable QueryParseContext context, @Nullable Boolean explicitCaching) {
362+
public Filter rangeFilter(QueryParseContext parseContext, Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, @Nullable DateTimeZone timeZone, @Nullable DateMathParser forcedDateParser, @Nullable QueryParseContext context, @Nullable Boolean explicitCaching) {
359363
boolean cache;
360364
boolean cacheable = true;
361365
Long lowerVal = null;
@@ -366,7 +370,7 @@ public Filter rangeFilter(QueryParseContext parseContext, Object lowerTerm, Obje
366370
} else {
367371
String value = convertToString(lowerTerm);
368372
cacheable = !hasDateExpressionWithNoRounding(value);
369-
lowerVal = parseToMilliseconds(value, context, false, timeZone);
373+
lowerVal = parseToMilliseconds(value, context, false, timeZone, forcedDateParser);
370374
}
371375
}
372376
if (upperTerm != null) {
@@ -375,7 +379,7 @@ public Filter rangeFilter(QueryParseContext parseContext, Object lowerTerm, Obje
375379
} else {
376380
String value = convertToString(upperTerm);
377381
cacheable = cacheable && !hasDateExpressionWithNoRounding(value);
378-
upperVal = parseToMilliseconds(value, context, includeUpper, timeZone);
382+
upperVal = parseToMilliseconds(value, context, includeUpper, timeZone, forcedDateParser);
379383
}
380384
}
381385

src/main/java/org/elasticsearch/index/query/RangeFilterParser.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.apache.lucene.search.TermRangeFilter;
2424
import org.elasticsearch.common.inject.Inject;
2525
import org.elasticsearch.common.joda.DateMathParser;
26+
import org.elasticsearch.common.joda.Joda;
2627
import org.elasticsearch.common.lucene.BytesRefs;
2728
import org.elasticsearch.common.xcontent.XContentParser;
2829
import org.elasticsearch.index.cache.filter.support.CacheKeyFilter;
@@ -64,6 +65,7 @@ public Filter parse(QueryParseContext parseContext) throws IOException, QueryPar
6465
boolean includeLower = true;
6566
boolean includeUpper = true;
6667
DateTimeZone timeZone = null;
68+
DateMathParser forcedDateParser = null;
6769
String execution = "index";
6870

6971
String filterName = null;
@@ -100,6 +102,8 @@ public Filter parse(QueryParseContext parseContext) throws IOException, QueryPar
100102
includeUpper = true;
101103
} else if ("time_zone".equals(currentFieldName) || "timeZone".equals(currentFieldName)) {
102104
timeZone = DateMathParser.parseZone(parser.text());
105+
} else if ("format".equals(currentFieldName)) {
106+
forcedDateParser = new DateMathParser(Joda.forPattern(parser.text()), DateFieldMapper.Defaults.TIME_UNIT);
103107
} else {
104108
throw new QueryParsingException(parseContext.index(), "[range] filter does not support [" + currentFieldName + "]");
105109
}
@@ -138,7 +142,7 @@ public Filter parse(QueryParseContext parseContext) throws IOException, QueryPar
138142
if ((from instanceof Number || to instanceof Number) && timeZone != null) {
139143
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 + "]");
140144
}
141-
filter = ((DateFieldMapper) mapper).rangeFilter(from, to, includeLower, includeUpper, timeZone, parseContext, explicitlyCached);
145+
filter = ((DateFieldMapper) mapper).rangeFilter(from, to, includeLower, includeUpper, timeZone, forcedDateParser, parseContext, explicitlyCached);
142146
} else {
143147
if (timeZone != null) {
144148
throw new QueryParsingException(parseContext.index(), "[range] time_zone can not be applied to non date field [" + fieldName + "]");
@@ -157,7 +161,7 @@ public Filter parse(QueryParseContext parseContext) throws IOException, QueryPar
157161
if ((from instanceof Number || to instanceof Number) && timeZone != null) {
158162
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 + "]");
159163
}
160-
filter = ((DateFieldMapper) mapper).rangeFilter(parseContext, from, to, includeLower, includeUpper, timeZone, parseContext, explicitlyCached);
164+
filter = ((DateFieldMapper) mapper).rangeFilter(parseContext, from, to, includeLower, includeUpper, timeZone, forcedDateParser, parseContext, explicitlyCached);
161165
} else {
162166
if (timeZone != null) {
163167
throw new QueryParsingException(parseContext.index(), "[range] time_zone can not be applied to non date field [" + fieldName + "]");

src/main/java/org/elasticsearch/index/query/RangeQueryParser.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.apache.lucene.search.TermRangeQuery;
2424
import org.elasticsearch.common.inject.Inject;
2525
import org.elasticsearch.common.joda.DateMathParser;
26+
import org.elasticsearch.common.joda.Joda;
2627
import org.elasticsearch.common.lucene.BytesRefs;
2728
import org.elasticsearch.common.xcontent.XContentParser;
2829
import org.elasticsearch.index.mapper.FieldMapper;
@@ -69,6 +70,7 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
6970
boolean includeLower = true;
7071
boolean includeUpper = true;
7172
DateTimeZone timeZone = null;
73+
DateMathParser forcedDateParser = null;
7274
float boost = 1.0f;
7375
String queryName = null;
7476

@@ -103,6 +105,8 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
103105
timeZone = DateMathParser.parseZone(parser.text());
104106
} else if ("_name".equals(currentFieldName)) {
105107
queryName = parser.text();
108+
} else if ("format".equals(currentFieldName)) {
109+
forcedDateParser = new DateMathParser(Joda.forPattern(parser.text()), DateFieldMapper.Defaults.TIME_UNIT);
106110
} else {
107111
throw new QueryParsingException(parseContext.index(), "[range] query does not support [" + currentFieldName + "]");
108112
}
@@ -124,7 +128,7 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
124128
if ((from instanceof Number || to instanceof Number) && timeZone != null) {
125129
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 + "]");
126130
}
127-
query = ((DateFieldMapper) mapper).rangeQuery(from, to, includeLower, includeUpper, timeZone, parseContext);
131+
query = ((DateFieldMapper) mapper).rangeQuery(from, to, includeLower, includeUpper, timeZone, forcedDateParser, parseContext);
128132
} else {
129133
if (timeZone != null) {
130134
throw new QueryParsingException(parseContext.index(), "[range] time_zone can not be applied to non date field [" + fieldName + "]");
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.index.query;
21+
22+
23+
import org.apache.lucene.search.NumericRangeQuery;
24+
import org.apache.lucene.search.Query;
25+
import org.elasticsearch.common.bytes.BytesArray;
26+
import org.elasticsearch.common.compress.CompressedString;
27+
import org.elasticsearch.common.inject.Injector;
28+
import org.elasticsearch.index.mapper.MapperService;
29+
import org.elasticsearch.index.service.IndexService;
30+
import org.elasticsearch.test.ElasticsearchSingleNodeTest;
31+
import org.joda.time.DateTime;
32+
import org.junit.Before;
33+
import org.junit.Test;
34+
35+
import java.io.IOException;
36+
37+
import static org.elasticsearch.common.io.Streams.copyToBytesFromClasspath;
38+
import static org.elasticsearch.common.io.Streams.copyToStringFromClasspath;
39+
import static org.hamcrest.Matchers.*;
40+
41+
/**
42+
*
43+
*/
44+
public class IndexQueryParserFilterDateRangeFormatTests extends ElasticsearchSingleNodeTest {
45+
46+
private Injector injector;
47+
private IndexQueryParserService queryParser;
48+
49+
@Before
50+
public void setup() throws IOException {
51+
IndexService indexService = createIndex("test");
52+
injector = indexService.injector();
53+
54+
MapperService mapperService = indexService.mapperService();
55+
String mapping = copyToStringFromClasspath("/org/elasticsearch/index/query/mapping.json");
56+
mapperService.merge("person", new CompressedString(mapping), true);
57+
mapperService.documentMapper("person").parse(new BytesArray(copyToBytesFromClasspath("/org/elasticsearch/index/query/data.json")));
58+
queryParser = injector.getInstance(IndexQueryParserService.class);
59+
}
60+
61+
private IndexQueryParserService queryParser() throws IOException {
62+
return this.queryParser;
63+
}
64+
65+
@Test
66+
public void testDateRangeFilterFormat() throws IOException {
67+
IndexQueryParserService queryParser = queryParser();
68+
String query = copyToStringFromClasspath("/org/elasticsearch/index/query/date_range_filter_format.json");
69+
queryParser.parse(query).query();
70+
// Sadly from NoCacheFilter, we can not access to the delegate filter so we can not check
71+
// it's the one we are expecting
72+
73+
// Test Invalid format
74+
query = copyToStringFromClasspath("/org/elasticsearch/index/query/date_range_filter_format_invalid.json");
75+
try {
76+
queryParser.parse(query).query();
77+
fail("A Range Filter with a specific format but with an unexpected date should raise a QueryParsingException");
78+
} catch (QueryParsingException e) {
79+
// We expect it
80+
}
81+
}
82+
83+
@Test
84+
public void testDateRangeQueryFormat() throws IOException {
85+
IndexQueryParserService queryParser = queryParser();
86+
// We test 01/01/2012 from gte and 2030 for lt
87+
String query = copyToStringFromClasspath("/org/elasticsearch/index/query/date_range_query_format.json");
88+
Query parsedQuery = queryParser.parse(query).query();
89+
assertThat(parsedQuery, instanceOf(NumericRangeQuery.class));
90+
91+
// Min value was 01/01/2012 (dd/MM/yyyy)
92+
DateTime min = DateTime.parse("2012-01-01T00:00:00.000+00");
93+
assertThat(((NumericRangeQuery) parsedQuery).getMin().longValue(), is(min.getMillis()));
94+
95+
// Max value was 2030 (yyyy)
96+
DateTime max = DateTime.parse("2030-01-01T00:00:00.000+00");
97+
assertThat(((NumericRangeQuery) parsedQuery).getMax().longValue(), is(max.getMillis()));
98+
99+
// Test Invalid format
100+
query = copyToStringFromClasspath("/org/elasticsearch/index/query/date_range_query_format_invalid.json");
101+
try {
102+
queryParser.parse(query).query();
103+
fail("A Range Query with a specific format but with an unexpected date should raise a QueryParsingException");
104+
} catch (QueryParsingException e) {
105+
// We expect it
106+
}
107+
}
108+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"constant_score": {
3+
"filter": {
4+
"range" : {
5+
"born" : {
6+
"gte": "01/01/2012",
7+
"lt": "2030",
8+
"format": "dd/MM/yyyy||yyyy"
9+
}
10+
}
11+
}
12+
}
13+
}

0 commit comments

Comments
 (0)