Skip to content

Commit 53b6583

Browse files
authored
Decode max and min optimization more carefully (#52336) (#52358)
Fixes the the no-query optimization for `min` and `max` aggregations for `date_nanos` fields by delegating decoding dates "through" their `resolution` member. Closes #52220
1 parent 51e74be commit 53b6583

File tree

3 files changed

+65
-52
lines changed

3 files changed

+65
-52
lines changed

server/src/main/java/org/elasticsearch/search/aggregations/metrics/MaxAggregator.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx,
9898
if (pointConverter != null) {
9999
Number segMax = findLeafMaxValue(ctx.reader(), pointField, pointConverter);
100100
if (segMax != null) {
101-
/**
101+
/*
102102
* There is no parent aggregator (see {@link MinAggregator#getPointReaderOrNull}
103103
* so the ordinal for the bucket is always 0.
104104
*/

server/src/main/java/org/elasticsearch/search/aggregations/metrics/MinAggregator.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx,
103103
if (pointConverter != null) {
104104
Number segMin = findLeafMinValue(ctx.reader(), pointField, pointConverter);
105105
if (segMin != null) {
106-
/**
106+
/*
107107
* There is no parent aggregator (see {@link MinAggregator#getPointReaderOrNull}
108108
* so the ordinal for the bucket is always 0.
109109
*/
@@ -190,7 +190,12 @@ static Function<byte[], Number> getPointReaderOrNull(SearchContext context, Aggr
190190
if (fieldType instanceof NumberFieldMapper.NumberFieldType) {
191191
converter = ((NumberFieldMapper.NumberFieldType) fieldType)::parsePoint;
192192
} else if (fieldType.getClass() == DateFieldMapper.DateFieldType.class) {
193-
converter = (in) -> LongPoint.decodeDimension(in, 0);
193+
DateFieldMapper.DateFieldType dft = (DateFieldMapper.DateFieldType) fieldType;
194+
/*
195+
* Makes sure that nanoseconds decode to milliseconds, just
196+
* like they do when you run the agg without the optimization.
197+
*/
198+
converter = (in) -> dft.resolution().toInstant(LongPoint.decodeDimension(in, 0)).toEpochMilli();
194199
}
195200
return converter;
196201
}

server/src/test/java/org/elasticsearch/search/aggregations/metrics/MinAggregatorTests.java

+57-49
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,17 @@
4646
import org.apache.lucene.search.TermQuery;
4747
import org.apache.lucene.store.Directory;
4848
import org.apache.lucene.util.BytesRef;
49+
import org.elasticsearch.Version;
50+
import org.elasticsearch.cluster.metadata.IndexMetaData;
4951
import org.elasticsearch.common.CheckedConsumer;
5052
import org.elasticsearch.common.collect.Tuple;
5153
import org.elasticsearch.common.settings.Settings;
5254
import org.elasticsearch.index.IndexSettings;
55+
import org.elasticsearch.index.mapper.ContentPath;
5356
import org.elasticsearch.index.mapper.DateFieldMapper;
5457
import org.elasticsearch.index.mapper.KeywordFieldMapper;
5558
import org.elasticsearch.index.mapper.MappedFieldType;
59+
import org.elasticsearch.index.mapper.Mapper;
5660
import org.elasticsearch.index.mapper.MapperService;
5761
import org.elasticsearch.index.mapper.NumberFieldMapper;
5862
import org.elasticsearch.index.query.QueryShardContext;
@@ -87,17 +91,16 @@
8791
import org.elasticsearch.search.lookup.LeafDocLookup;
8892

8993
import java.io.IOException;
94+
import java.time.Instant;
9095
import java.util.ArrayList;
9196
import java.util.Arrays;
92-
import java.util.Collection;
9397
import java.util.Collections;
9498
import java.util.Comparator;
9599
import java.util.HashMap;
96100
import java.util.List;
97101
import java.util.Map;
98102
import java.util.function.BiFunction;
99103
import java.util.function.Consumer;
100-
import java.util.function.DoubleConsumer;
101104
import java.util.function.Function;
102105
import java.util.function.Supplier;
103106

@@ -740,41 +743,56 @@ public void testShortcutIsApplicable() {
740743
)
741744
);
742745
}
743-
assertNotNull(
746+
for (DateFieldMapper.Resolution resolution : DateFieldMapper.Resolution.values()) {
747+
assertNull(
748+
MinAggregator.getPointReaderOrNull(
749+
mockSearchContext(new MatchAllDocsQuery()),
750+
mockAggregator(),
751+
mockDateValuesSourceConfig("number", true, resolution)
752+
)
753+
);
754+
assertNull(
755+
MinAggregator.getPointReaderOrNull(
756+
mockSearchContext(new TermQuery(new Term("foo", "bar"))),
757+
null,
758+
mockDateValuesSourceConfig("number", true, resolution)
759+
)
760+
);
761+
assertNull(
762+
MinAggregator.getPointReaderOrNull(
763+
mockSearchContext(null),
764+
mockAggregator(),
765+
mockDateValuesSourceConfig("number", true, resolution)
766+
)
767+
);
768+
assertNull(
769+
MinAggregator.getPointReaderOrNull(
770+
mockSearchContext(null),
771+
null,
772+
mockDateValuesSourceConfig("number", false, resolution)
773+
)
774+
);
775+
}
776+
// Check that we decode a dates "just like" the doc values instance.
777+
Instant expected = Instant.from(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.parse("2020-01-01T00:00:00Z"));
778+
byte[] scratch = new byte[8];
779+
LongPoint.encodeDimension(DateFieldMapper.Resolution.MILLISECONDS.convert(expected), scratch, 0);
780+
assertThat(
744781
MinAggregator.getPointReaderOrNull(
745782
mockSearchContext(new MatchAllDocsQuery()),
746783
null,
747-
mockDateValuesSourceConfig("number", true)
748-
)
784+
mockDateValuesSourceConfig("number", true, DateFieldMapper.Resolution.MILLISECONDS)
785+
).apply(scratch), equalTo(expected.toEpochMilli())
749786
);
750-
assertNull(
787+
LongPoint.encodeDimension(DateFieldMapper.Resolution.NANOSECONDS.convert(expected), scratch, 0);
788+
assertThat(
751789
MinAggregator.getPointReaderOrNull(
752790
mockSearchContext(new MatchAllDocsQuery()),
753-
mockAggregator(),
754-
mockDateValuesSourceConfig("number", true)
755-
)
756-
);
757-
assertNull(
758-
MinAggregator.getPointReaderOrNull(
759-
mockSearchContext(new TermQuery(new Term("foo", "bar"))),
760791
null,
761-
mockDateValuesSourceConfig("number", true)
762-
)
763-
);
764-
assertNull(
765-
MinAggregator.getPointReaderOrNull(
766-
mockSearchContext(null),
767-
mockAggregator(),
768-
mockDateValuesSourceConfig("number", true)
769-
)
770-
);
771-
assertNull(
772-
MinAggregator.getPointReaderOrNull(
773-
mockSearchContext(null),
774-
null,
775-
mockDateValuesSourceConfig("number", false)
776-
)
792+
mockDateValuesSourceConfig("number", true, DateFieldMapper.Resolution.NANOSECONDS)
793+
).apply(scratch), equalTo(expected.toEpochMilli())
777794
);
795+
778796
}
779797

780798
public void testMinShortcutRandom() throws Exception {
@@ -799,21 +817,6 @@ public void testMinShortcutRandom() throws Exception {
799817
(v) -> DoublePoint.decodeDimension(v, 0));
800818
}
801819

802-
private void testMinCase(IndexSearcher searcher,
803-
AggregationBuilder aggregationBuilder,
804-
MappedFieldType ft,
805-
DoubleConsumer testResult) throws IOException {
806-
Collection<Query> queries = Arrays.asList(new MatchAllDocsQuery(), new DocValuesFieldExistsQuery(ft.name()));
807-
for (Query query : queries) {
808-
MinAggregator aggregator = createAggregator(query, aggregationBuilder, searcher, createIndexSettings(), ft);
809-
aggregator.preCollection();
810-
searcher.search(new MatchAllDocsQuery(), aggregator);
811-
aggregator.postCollection();
812-
InternalMin result = (InternalMin) aggregator.buildAggregation(0L);
813-
testResult.accept(result.getValue());
814-
}
815-
}
816-
817820
private void testMinShortcutCase(Supplier<Number> randomNumber,
818821
Function<Number, Field> pointFieldFunc,
819822
Function<byte[], Number> pointConvertFunc) throws IOException {
@@ -889,12 +892,17 @@ private ValuesSourceConfig<ValuesSource.Numeric> mockNumericValuesSourceConfig(S
889892
return config;
890893
}
891894

892-
private ValuesSourceConfig<ValuesSource.Numeric> mockDateValuesSourceConfig(String fieldName, boolean indexed) {
895+
private ValuesSourceConfig<ValuesSource.Numeric> mockDateValuesSourceConfig(String fieldName, boolean indexed,
896+
DateFieldMapper.Resolution resolution) {
893897
ValuesSourceConfig<ValuesSource.Numeric> config = mock(ValuesSourceConfig.class);
894-
MappedFieldType ft = new DateFieldMapper.Builder(fieldName).fieldType();
895-
ft.setName(fieldName);
896-
ft.setIndexOptions(indexed ? IndexOptions.DOCS : IndexOptions.NONE);
897-
ft.freeze();
898+
Mapper.BuilderContext builderContext = new Mapper.BuilderContext(
899+
Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build(),
900+
new ContentPath());
901+
MappedFieldType ft = new DateFieldMapper.Builder(fieldName)
902+
.index(indexed)
903+
.withResolution(resolution)
904+
.build(builderContext)
905+
.fieldType();
898906
when(config.fieldContext()).thenReturn(new FieldContext(fieldName, null, ft));
899907
return config;
900908
}

0 commit comments

Comments
 (0)