Add time_zone setting for query_string

Query String query now supports a new `time_zone` option based on JODA time zones.
When using a range on date field, the time zone is applied.

```json
{
"query": {
  "query_string": {
    "text": "date:[2012 TO 2014]",
    "timezone": "Europe/Paris"
  }
 }
}
```

Closes #7880.
This commit is contained in:
David Pilato 2014-10-20 13:18:58 +02:00
parent 65d40468b8
commit 0ff61e1d6f
10 changed files with 106 additions and 5 deletions

View File

@ -74,6 +74,9 @@ providing text to a numeric field) to be ignored.
|`locale` | Locale that should be used for string conversions.
Defaults to `ROOT`.
|`time_zone` | Time Zone to be applied to any range query related to dates. See also
http://joda-time.sourceforge.net/api-release/org/joda/time/DateTimeZone.html[JODA timezone].
|=======================================================================
When a multi term query is being generated, one can control how it gets

View File

@ -126,6 +126,9 @@ public class MapperQueryParser extends QueryParser {
setFuzzyMinSim(settings.fuzzyMinSim());
setFuzzyPrefixLength(settings.fuzzyPrefixLength());
setLocale(settings.locale());
if (settings.timeZone() != null) {
setTimeZone(settings.timeZone().toTimeZone());
}
this.analyzeWildcard = settings.analyzeWildcard();
}

View File

@ -23,6 +23,7 @@ import com.carrotsearch.hppc.ObjectFloatOpenHashMap;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.MultiTermQuery;
import org.joda.time.DateTimeZone;
import java.util.Collection;
import java.util.List;
@ -61,7 +62,7 @@ public class QueryParserSettings {
private String minimumShouldMatch;
private boolean lenient;
private Locale locale;
private DateTimeZone timeZone;
List<String> fields = null;
Collection<String> queryTypes = null;
@ -306,6 +307,14 @@ public class QueryParserSettings {
return this.locale;
}
public void timeZone(DateTimeZone timeZone) {
this.timeZone = timeZone;
}
public DateTimeZone timeZone() {
return this.timeZone;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@ -349,6 +358,9 @@ public class QueryParserSettings {
if (locale != null ? !locale.equals(that.locale) : that.locale != null) {
return false;
}
if (timeZone != null ? !timeZone.equals(that.timeZone) : that.timeZone != null) {
return false;
}
if (Float.compare(that.tieBreaker, tieBreaker) != 0) return false;
if (useDisMax != that.useDisMax) return false;
@ -385,6 +397,7 @@ public class QueryParserSettings {
result = 31 * result + (tieBreaker != +0.0f ? Float.floatToIntBits(tieBreaker) : 0);
result = 31 * result + (useDisMax ? 1 : 0);
result = 31 * result + (locale != null ? locale.hashCode() : 0);
result = 31 * result + (timeZone != null ? timeZone.hashCode() : 0);
return result;
}
}

View File

@ -93,6 +93,8 @@ public class QueryStringQueryBuilder extends BaseQueryBuilder implements Boostab
private String queryName;
private String timeZone;
public QueryStringQueryBuilder(String queryString) {
this.queryString = queryString;
}
@ -319,6 +321,14 @@ public class QueryStringQueryBuilder extends BaseQueryBuilder implements Boostab
return this;
}
/**
* In case of date field, we can adjust the from/to fields using a timezone
*/
public QueryStringQueryBuilder timeZone(String timeZone) {
this.timeZone = timeZone;
return this;
}
@Override
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(QueryStringQueryParser.NAME);
@ -402,6 +412,9 @@ public class QueryStringQueryBuilder extends BaseQueryBuilder implements Boostab
if (locale != null) {
builder.field("locale", locale.toString());
}
if (timeZone != null) {
builder.field("time_zone", timeZone);
}
builder.endObject();
}
}

View File

@ -28,6 +28,7 @@ import org.apache.lucene.search.Query;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.joda.DateMathParser;
import org.elasticsearch.common.lucene.search.Queries;
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.common.settings.Settings;
@ -191,6 +192,12 @@ public class QueryStringQueryParser implements QueryParser {
} else if ("locale".equals(currentFieldName)) {
String localeStr = parser.text();
qpSettings.locale(LocaleUtils.parse(localeStr));
} else if ("time_zone".equals(currentFieldName)) {
try {
qpSettings.timeZone(DateMathParser.parseZone(parser.text()));
} catch (IllegalArgumentException e) {
throw new QueryParsingException(parseContext.index(), "[query_string] time_zone [" + parser.text() + "] is unknown");
}
} else if ("_name".equals(currentFieldName)) {
queryName = parser.text();
} else {

View File

@ -250,6 +250,21 @@ public class SimpleIndexQueryParserTests extends ElasticsearchSingleNodeTest {
assertThat((double) disjuncts.get(1).getBoost(), closeTo(1, 0.01));
}
@Test
public void testQueryStringTimezone() throws Exception {
IndexQueryParserService queryParser = queryParser();
String query = copyToStringFromClasspath("/org/elasticsearch/index/query/query-timezone.json");
Query parsedQuery = queryParser.parse(query).query();
assertThat(parsedQuery, instanceOf(TermRangeQuery.class));
try {
queryParser.parse(copyToStringFromClasspath("/org/elasticsearch/index/query/query-timezone-incorrect.json"));
fail("we expect a QueryParsingException as we are providing an unknown time_zome");
} catch (QueryParsingException e) {
// We expect this one
}
}
@Test
public void testMatchAllBuilder() throws Exception {
IndexQueryParserService queryParser = queryParser();

View File

@ -0,0 +1,6 @@
{
"query_string":{
"time_zone":"This timezone does not exist",
"query":"date:[2012 TO 2014]"
}
}

View File

@ -0,0 +1,6 @@
{
"query_string":{
"time_zone":"Europe/Paris",
"query":"date:[2012 TO 2014]"
}
}

View File

@ -47,10 +47,7 @@ import org.joda.time.format.ISODateTimeFormat;
import org.junit.Test;
import java.io.IOException;
import java.util.HashSet;
import java.util.Locale;
import java.util.Random;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ExecutionException;
import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_REPLICAS;
@ -557,6 +554,26 @@ public class SimpleQueryTests extends ElasticsearchIntegrationTest {
}
}
@Test // https://github.com/elasticsearch/elasticsearch/issues/7880
public void testDateRangeInQueryStringWithTimeZone_7880() {
//the mapping needs to be provided upfront otherwise we are not sure how many failures we get back
//as with dynamic mappings some shards might be lacking behind and parse a different query
assertAcked(prepareCreate("test").addMapping(
"type", "past", "type=date"
));
ensureGreen();
DateTimeZone timeZone = randomDateTimeZone();
String now = ISODateTimeFormat.dateTime().print(new DateTime(timeZone));
logger.info(" --> Using time_zone [{}], now is [{}]", timeZone.getID(), now);
client().prepareIndex("test", "type", "1").setSource("past", now).get();
refresh();
SearchResponse searchResponse = client().prepareSearch().setQuery(queryString("past:[now-1m/m TO now+1m/m]")
.timeZone(timeZone.getID())).get();
assertHitCount(searchResponse, 1l);
}
@Test
public void typeFilterTypeIndexedTests() throws Exception {
typeFilterTests("not_analyzed");

View File

@ -102,6 +102,7 @@ import org.elasticsearch.search.SearchService;
import org.elasticsearch.test.client.RandomizingClient;
import org.elasticsearch.test.disruption.ServiceDisruptionScheme;
import org.hamcrest.Matchers;
import org.joda.time.DateTimeZone;
import org.junit.*;
import java.io.IOException;
@ -1676,6 +1677,23 @@ public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase
return randomFrom(Arrays.asList("paged_bytes", "fst", "doc_values"));
}
/**
* Returns a random JODA Time Zone based on Java Time Zones
*/
public static DateTimeZone randomDateTimeZone() {
DateTimeZone timeZone;
// It sounds like some Java Time Zones are unknown by JODA. For example: Asia/Riyadh88
// We need to fallback in that case to a known time zone
try {
timeZone = DateTimeZone.forTimeZone(randomTimeZone());
} catch (IllegalArgumentException e) {
timeZone = DateTimeZone.forOffsetHours(randomIntBetween(-12, 12));
}
return timeZone;
}
protected NumShards getNumShards(String index) {
MetaData metaData = client().admin().cluster().prepareState().get().getState().metaData();
assertThat(metaData.hasIndex(index), equalTo(true));