Skip to content

Commit 5e49ee8

Browse files
authored
Drop rewriting in date_histogram (backport of #57836) (#58875)
The `date_histogram` aggregation had an optimization where it'd rewrite `time_zones` who's offset from UTC is fixed across the entire index. This rewrite is no longer needed after #56371 because we can tell that a time zone is fixed lower down in the aggregation. So this removes it.
1 parent c64e283 commit 5e49ee8

File tree

5 files changed

+27
-318
lines changed

5 files changed

+27
-318
lines changed

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

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -479,30 +479,6 @@ public Relation isFieldWithinQuery(IndexReader reader,
479479
}
480480
}
481481

482-
return isFieldWithinRange(reader, fromInclusive, toInclusive);
483-
}
484-
485-
/**
486-
* Return whether all values of the given {@link IndexReader} are within the range,
487-
* outside the range or cross the range. Unlike {@link #isFieldWithinQuery} this
488-
* accepts values that are out of the range of the {@link #resolution} of this field.
489-
* @param fromInclusive start date, inclusive
490-
* @param toInclusive end date, inclusive
491-
*/
492-
public Relation isFieldWithinRange(IndexReader reader, Instant fromInclusive, Instant toInclusive)
493-
throws IOException {
494-
return isFieldWithinRange(reader,
495-
resolution.convert(resolution.clampToValidRange(fromInclusive)),
496-
resolution.convert(resolution.clampToValidRange(toInclusive)));
497-
}
498-
499-
/**
500-
* Return whether all values of the given {@link IndexReader} are within the range,
501-
* outside the range or cross the range.
502-
* @param fromInclusive start date, inclusive, {@link Resolution#convert(Instant) converted} to the appropriate scale
503-
* @param toInclusive end date, inclusive, {@link Resolution#convert(Instant) converted} to the appropriate scale
504-
*/
505-
private Relation isFieldWithinRange(IndexReader reader, long fromInclusive, long toInclusive) throws IOException {
506482
if (PointValues.size(reader, name()) == 0) {
507483
// no points, so nothing matches
508484
return Relation.DISJOINT;

server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregationBuilder.java

Lines changed: 13 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,13 @@
1919

2020
package org.elasticsearch.search.aggregations.bucket.histogram;
2121

22-
import org.apache.lucene.index.IndexReader;
23-
import org.apache.lucene.index.LeafReaderContext;
24-
import org.apache.lucene.index.SortedNumericDocValues;
25-
import org.apache.lucene.search.DocIdSetIterator;
2622
import org.elasticsearch.common.Rounding;
2723
import org.elasticsearch.common.io.stream.StreamInput;
2824
import org.elasticsearch.common.io.stream.StreamOutput;
2925
import org.elasticsearch.common.unit.TimeValue;
3026
import org.elasticsearch.common.xcontent.ObjectParser;
3127
import org.elasticsearch.common.xcontent.XContentBuilder;
3228
import org.elasticsearch.common.xcontent.XContentParser;
33-
import org.elasticsearch.index.fielddata.IndexNumericFieldData;
34-
import org.elasticsearch.index.fielddata.LeafNumericFieldData;
35-
import org.elasticsearch.index.mapper.DateFieldMapper;
36-
import org.elasticsearch.index.mapper.MappedFieldType;
37-
import org.elasticsearch.index.mapper.MappedFieldType.Relation;
3829
import org.elasticsearch.index.query.QueryShardContext;
3930
import org.elasticsearch.search.aggregations.AggregationBuilder;
4031
import org.elasticsearch.search.aggregations.AggregatorFactories;
@@ -50,10 +41,7 @@
5041
import org.elasticsearch.search.aggregations.support.ValuesSourceType;
5142

5243
import java.io.IOException;
53-
import java.time.Instant;
5444
import java.time.ZoneId;
55-
import java.time.ZoneOffset;
56-
import java.time.zone.ZoneOffsetTransition;
5745
import java.util.HashMap;
5846
import java.util.List;
5947
import java.util.Map;
@@ -407,144 +395,32 @@ public String getType() {
407395
return NAME;
408396
}
409397

410-
/**
411-
* Returns a {@linkplain ZoneId} that functions the same as
412-
* {@link #timeZone()} on the data in the shard referred to by
413-
* {@code context}. It <strong>attempts</strong> to convert zones that
414-
* have non-fixed offsets into fixed offset zones that produce the
415-
* same results on all data in the shard.
416-
* <p>
417-
* We go about this in three phases:
418-
* <ol>
419-
* <li>A bunch of preflight checks to see if we *can* optimize it
420-
* <li>Find the any Instant in shard
421-
* <li>Find the DST transition before and after that Instant
422-
* <li>Round those into the interval
423-
* <li>Check if the rounded value include all values within shard
424-
* <li>If they do then return a fixed offset time zone because it
425-
* will return the same values for all time in the shard as the
426-
* original time zone, but faster
427-
* <li>Otherwise return the original time zone. It'll be slower, but
428-
* correct.
429-
* </ol>
430-
* <p>
431-
* NOTE: this can't be done in rewrite() because the timezone is then also used on the
432-
* coordinating node in order to generate missing buckets, which may cross a transition
433-
* even though data on the shards doesn't.
434-
*/
435-
ZoneId rewriteTimeZone(QueryShardContext context) throws IOException {
436-
final ZoneId tz = timeZone();
437-
if (tz == null || tz.getRules().isFixedOffset()) {
438-
// This time zone is already as fast as it is going to get.
439-
return tz;
440-
}
441-
if (script() != null) {
442-
// We can't be sure what dates the script will return so we don't attempt to optimize anything
443-
return tz;
444-
}
445-
if (field() == null) {
446-
// Without a field we're not going to be able to look anything up.
447-
return tz;
448-
}
449-
MappedFieldType ft = context.fieldMapper(field());
450-
if (ft == null || false == ft instanceof DateFieldMapper.DateFieldType) {
451-
// If the field is unmapped or not a date then we can't get its range.
452-
return tz;
453-
}
454-
DateFieldMapper.DateFieldType dft = (DateFieldMapper.DateFieldType) ft;
455-
final IndexReader reader = context.getIndexReader();
456-
if (reader == null) {
457-
return tz;
458-
}
459-
460-
Instant instant = null;
461-
final IndexNumericFieldData fieldData = context.getForField(ft);
462-
for (LeafReaderContext ctx : reader.leaves()) {
463-
LeafNumericFieldData leafFD = fieldData.load(ctx);
464-
SortedNumericDocValues values = leafFD.getLongValues();
465-
if (values.nextDoc() != DocIdSetIterator.NO_MORE_DOCS) {
466-
instant = Instant.ofEpochMilli(values.nextValue());
467-
break;
468-
}
469-
}
470-
if (instant == null) {
471-
return tz;
472-
}
473-
474-
ZoneOffsetTransition prevOffsetTransition = tz.getRules().previousTransition(instant);
475-
final long prevTransition;
476-
if (prevOffsetTransition != null) {
477-
prevTransition = prevOffsetTransition.getInstant().toEpochMilli();
478-
} else {
479-
prevTransition = instant.toEpochMilli();
480-
}
481-
ZoneOffsetTransition nextOffsetTransition = tz.getRules().nextTransition(instant);
482-
final long nextTransition;
483-
if (nextOffsetTransition != null) {
484-
nextTransition = nextOffsetTransition.getInstant().toEpochMilli();
485-
} else {
486-
nextTransition = Long.MAX_VALUE; // fixed time-zone after prevTransition
487-
}
488-
489-
// We need all not only values but also rounded values to be within
490-
// [prevTransition, nextTransition].
491-
final long low;
492-
493-
DateIntervalWrapper.IntervalTypeEnum intervalType = dateHistogramInterval.getIntervalType();
494-
if (intervalType.equals(DateIntervalWrapper.IntervalTypeEnum.FIXED)) {
495-
low = Math.addExact(prevTransition, dateHistogramInterval.tryIntervalAsFixedUnit().millis());
496-
} else if (intervalType.equals(DateIntervalWrapper.IntervalTypeEnum.CALENDAR)) {
497-
final Rounding.DateTimeUnit intervalAsUnit = dateHistogramInterval.tryIntervalAsCalendarUnit();
498-
final Rounding rounding = Rounding.builder(intervalAsUnit).timeZone(timeZone()).build();
499-
low = rounding.nextRoundingValue(prevTransition);
500-
} else {
501-
// We're not sure what the interval was originally (legacy) so use old behavior of assuming
502-
// calendar first, then fixed. Required because fixed/cal overlap in places ("1h")
503-
Rounding.DateTimeUnit intervalAsUnit = dateHistogramInterval.tryIntervalAsCalendarUnit();
504-
if (intervalAsUnit != null) {
505-
final Rounding rounding = Rounding.builder(intervalAsUnit).timeZone(timeZone()).build();
506-
low = rounding.nextRoundingValue(prevTransition);
507-
} else {
508-
final TimeValue intervalAsMillis = dateHistogramInterval.tryIntervalAsFixedUnit();
509-
low = Math.addExact(prevTransition, intervalAsMillis.millis());
510-
}
511-
}
512-
// rounding rounds down, so 'nextTransition' is a good upper bound
513-
final long high = nextTransition;
514-
515-
if (dft.isFieldWithinRange(
516-
reader, Instant.ofEpochMilli(low), Instant.ofEpochMilli(high - 1)) == Relation.WITHIN) {
517-
// All values in this reader have the same offset despite daylight saving times.
518-
// This is very common for location-based timezones such as Europe/Paris in
519-
// combination with time-based indices.
520-
return ZoneOffset.ofTotalSeconds(tz.getRules().getOffset(instant).getTotalSeconds());
521-
}
522-
return tz;
523-
}
524-
525398
@Override
526399
protected ValuesSourceAggregatorFactory innerBuild(QueryShardContext queryShardContext,
527400
ValuesSourceConfig config,
528401
AggregatorFactory parent,
529402
AggregatorFactories.Builder subFactoriesBuilder) throws IOException {
530403
final ZoneId tz = timeZone();
531404
final Rounding rounding = dateHistogramInterval.createRounding(tz, offset);
532-
// TODO once we optimize TimeIntervalRounding we won't need to rewrite the time zone
533-
final ZoneId rewrittenTimeZone = rewriteTimeZone(queryShardContext);
534-
final Rounding shardRounding;
535-
if (tz == rewrittenTimeZone) {
536-
shardRounding = rounding;
537-
} else {
538-
shardRounding = dateHistogramInterval.createRounding(rewrittenTimeZone, offset);
539-
}
540405

541406
ExtendedBounds roundedBounds = null;
542407
if (this.extendedBounds != null) {
543408
// parse any string bounds to longs and round
544409
roundedBounds = this.extendedBounds.parseAndValidate(name, queryShardContext, config.format()).round(rounding);
545410
}
546-
return new DateHistogramAggregatorFactory(name, config, order, keyed, minDocCount,
547-
rounding, shardRounding, roundedBounds, queryShardContext, parent, subFactoriesBuilder, metadata);
411+
return new DateHistogramAggregatorFactory(
412+
name,
413+
config,
414+
order,
415+
keyed,
416+
minDocCount,
417+
rounding,
418+
roundedBounds,
419+
queryShardContext,
420+
parent,
421+
subFactoriesBuilder,
422+
metadata
423+
);
548424
}
549425

550426
@Override

server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregatorFactory.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,26 @@ public static void registerAggregators(ValuesSourceRegistry.Builder builder) {
5454
private final long minDocCount;
5555
private final ExtendedBounds extendedBounds;
5656
private final Rounding rounding;
57-
private final Rounding shardRounding;
5857

59-
public DateHistogramAggregatorFactory(String name, ValuesSourceConfig config,
60-
BucketOrder order, boolean keyed, long minDocCount,
61-
Rounding rounding, Rounding shardRounding, ExtendedBounds extendedBounds, QueryShardContext queryShardContext,
62-
AggregatorFactory parent, AggregatorFactories.Builder subFactoriesBuilder,
63-
Map<String, Object> metadata) throws IOException {
58+
public DateHistogramAggregatorFactory(
59+
String name,
60+
ValuesSourceConfig config,
61+
BucketOrder order,
62+
boolean keyed,
63+
long minDocCount,
64+
Rounding rounding,
65+
ExtendedBounds extendedBounds,
66+
QueryShardContext queryShardContext,
67+
AggregatorFactory parent,
68+
AggregatorFactories.Builder subFactoriesBuilder,
69+
Map<String, Object> metadata
70+
) throws IOException {
6471
super(name, config, queryShardContext, parent, subFactoriesBuilder, metadata);
6572
this.order = order;
6673
this.keyed = keyed;
6774
this.minDocCount = minDocCount;
6875
this.extendedBounds = extendedBounds;
6976
this.rounding = rounding;
70-
this.shardRounding = shardRounding;
7177
}
7278

7379
public long minDocCount() {
@@ -89,7 +95,7 @@ protected Aggregator doCreateInternal(
8995
// TODO: Is there a reason not to get the prepared rounding in the supplier itself?
9096
Rounding.Prepared preparedRounding = config.getValuesSource()
9197
.roundingPreparer(queryShardContext.getIndexReader())
92-
.apply(shardRounding);
98+
.apply(rounding);
9399
return ((DateHistogramAggregationSupplier) aggregatorSupplier).build(
94100
name,
95101
factories,

server/src/test/java/org/elasticsearch/index/mapper/DateFieldTypeTests.java

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ public void testIsFieldWithinRangeEmptyReader() throws IOException {
7575
DateFieldType ft = new DateFieldType("my_date");
7676
assertEquals(Relation.DISJOINT, ft.isFieldWithinQuery(reader, "2015-10-12", "2016-04-03",
7777
randomBoolean(), randomBoolean(), null, null, context));
78-
assertEquals(Relation.DISJOINT, ft.isFieldWithinRange(reader, instant("2015-10-12"), instant("2016-04-03")));
7978
}
8079

8180
public void testIsFieldWithinQueryDateMillis() throws IOException {
@@ -109,49 +108,11 @@ public void isFieldWithinRangeTestCase(DateFieldType ft) throws IOException {
109108
doTestIsFieldWithinQuery(ft, reader, DateTimeZone.UTC, alternateFormat);
110109

111110
QueryRewriteContext context = new QueryRewriteContext(xContentRegistry(), writableRegistry(), null, () -> nowInMillis);
112-
assertEquals(Relation.INTERSECTS, ft.isFieldWithinRange(reader, instant("2015-10-09"), instant("2016-01-02")));
113-
assertEquals(Relation.INTERSECTS, ft.isFieldWithinRange(reader, instant("2016-01-02"), instant("2016-06-20")));
114-
assertEquals(Relation.INTERSECTS, ft.isFieldWithinRange(reader, instant("2016-01-02"), instant("2016-02-12")));
115-
assertEquals(Relation.DISJOINT, ft.isFieldWithinRange(reader, instant("2014-01-02"), instant("2015-02-12")));
116-
assertEquals(Relation.DISJOINT, ft.isFieldWithinRange(reader, instant("2016-05-11"), instant("2016-08-30")));
117-
assertEquals(Relation.WITHIN, ft.isFieldWithinRange(reader, instant("2015-09-25"), instant("2016-05-29")));
118-
assertEquals(Relation.WITHIN, ft.isFieldWithinRange(reader, instant("2015-10-12"), instant("2016-04-03")));
119-
assertEquals(Relation.INTERSECTS,
120-
ft.isFieldWithinRange(reader, instant("2015-10-12").plusMillis(1), instant("2016-04-03").minusMillis(1)));
121-
assertEquals(Relation.INTERSECTS,
122-
ft.isFieldWithinRange(reader, instant("2015-10-12").plusMillis(1), instant("2016-04-03")));
123-
assertEquals(Relation.INTERSECTS,
124-
ft.isFieldWithinRange(reader, instant("2015-10-12"), instant("2016-04-03").minusMillis(1)));
125-
assertEquals(Relation.INTERSECTS,
126-
ft.isFieldWithinRange(reader, instant("2015-10-12").plusNanos(1), instant("2016-04-03").minusNanos(1)));
127-
assertEquals(ft.resolution() == Resolution.NANOSECONDS ? Relation.INTERSECTS : Relation.WITHIN, // Millis round down here.
128-
ft.isFieldWithinRange(reader, instant("2015-10-12").plusNanos(1), instant("2016-04-03")));
129-
assertEquals(Relation.INTERSECTS,
130-
ft.isFieldWithinRange(reader, instant("2015-10-12"), instant("2016-04-03").minusNanos(1)));
131-
132-
// Some edge cases
133-
assertEquals(Relation.WITHIN, ft.isFieldWithinRange(reader, Instant.EPOCH, instant("2016-04-03")));
134-
assertEquals(Relation.WITHIN, ft.isFieldWithinRange(reader, Instant.ofEpochMilli(-1000), instant("2016-04-03")));
135-
assertEquals(Relation.WITHIN, ft.isFieldWithinRange(reader, Instant.ofEpochMilli(Long.MIN_VALUE), instant("2016-04-03")));
136-
assertEquals(Relation.WITHIN, ft.isFieldWithinRange(reader, instant("2015-10-12"), Instant.ofEpochMilli(Long.MAX_VALUE)));
137111

138112
// Fields with no value indexed.
139113
DateFieldType ft2 = new DateFieldType("my_date2");
140114

141115
assertEquals(Relation.DISJOINT, ft2.isFieldWithinQuery(reader, "2015-10-09", "2016-01-02", false, false, null, null, context));
142-
assertEquals(Relation.DISJOINT, ft2.isFieldWithinRange(reader, instant("2015-10-09"), instant("2016-01-02")));
143-
144-
// Fire a bunch of random values into isFieldWithinRange to make sure it doesn't crash
145-
for (int iter = 0; iter < 1000; iter++) {
146-
long min = randomLong();
147-
long max = randomLong();
148-
if (min > max) {
149-
long swap = max;
150-
max = min;
151-
min = swap;
152-
}
153-
ft.isFieldWithinRange(reader, Instant.ofEpochMilli(min), Instant.ofEpochMilli(max));
154-
}
155116

156117
IOUtils.close(reader, w, dir);
157118
}

0 commit comments

Comments
 (0)