Skip to content

Commit 69c7e73

Browse files
authored
Drop rewriting in date_histogram (#57836)
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 a4d3035 commit 69c7e73

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
@@ -473,30 +473,6 @@ public Relation isFieldWithinQuery(IndexReader reader,
473473
}
474474
}
475475

476-
return isFieldWithinRange(reader, fromInclusive, toInclusive);
477-
}
478-
479-
/**
480-
* Return whether all values of the given {@link IndexReader} are within the range,
481-
* outside the range or cross the range. Unlike {@link #isFieldWithinQuery} this
482-
* accepts values that are out of the range of the {@link #resolution} of this field.
483-
* @param fromInclusive start date, inclusive
484-
* @param toInclusive end date, inclusive
485-
*/
486-
public Relation isFieldWithinRange(IndexReader reader, Instant fromInclusive, Instant toInclusive)
487-
throws IOException {
488-
return isFieldWithinRange(reader,
489-
resolution.convert(resolution.clampToValidRange(fromInclusive)),
490-
resolution.convert(resolution.clampToValidRange(toInclusive)));
491-
}
492-
493-
/**
494-
* Return whether all values of the given {@link IndexReader} are within the range,
495-
* outside the range or cross the range.
496-
* @param fromInclusive start date, inclusive, {@link Resolution#convert(Instant) converted} to the appropriate scale
497-
* @param toInclusive end date, inclusive, {@link Resolution#convert(Instant) converted} to the appropriate scale
498-
*/
499-
private Relation isFieldWithinRange(IndexReader reader, long fromInclusive, long toInclusive) throws IOException {
500476
if (PointValues.size(reader, name()) == 0) {
501477
// no points, so nothing matches
502478
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.List;
5846
import java.util.Map;
5947
import java.util.Objects;
@@ -401,144 +389,32 @@ public String getType() {
401389
return NAME;
402390
}
403391

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

535400
ExtendedBounds roundedBounds = null;
536401
if (this.extendedBounds != null) {
537402
// parse any string bounds to longs and round
538403
roundedBounds = this.extendedBounds.parseAndValidate(name, queryShardContext, config.format()).round(rounding);
539404
}
540-
return new DateHistogramAggregatorFactory(name, config, order, keyed, minDocCount,
541-
rounding, shardRounding, roundedBounds, queryShardContext, parent, subFactoriesBuilder, metadata);
405+
return new DateHistogramAggregatorFactory(
406+
name,
407+
config,
408+
order,
409+
keyed,
410+
minDocCount,
411+
rounding,
412+
roundedBounds,
413+
queryShardContext,
414+
parent,
415+
subFactoriesBuilder,
416+
metadata
417+
);
542418
}
543419

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