소스 검색

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.
David Pilato 11 년 전
부모
커밋
0ff61e1d6f

+ 3 - 0
docs/reference/query-dsl/queries/query-string-query.asciidoc

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

+ 3 - 0
src/main/java/org/apache/lucene/queryparser/classic/MapperQueryParser.java

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

+ 14 - 1
src/main/java/org/apache/lucene/queryparser/classic/QueryParserSettings.java

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

+ 13 - 0
src/main/java/org/elasticsearch/index/query/QueryStringQueryBuilder.java

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

+ 7 - 0
src/main/java/org/elasticsearch/index/query/QueryStringQueryParser.java

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

+ 15 - 0
src/test/java/org/elasticsearch/index/query/SimpleIndexQueryParserTests.java

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

+ 6 - 0
src/test/java/org/elasticsearch/index/query/query-timezone-incorrect.json

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

+ 6 - 0
src/test/java/org/elasticsearch/index/query/query-timezone.json

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

+ 21 - 4
src/test/java/org/elasticsearch/search/query/SimpleQueryTests.java

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

+ 18 - 0
src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java

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