Skip to content

Commit ad2dc83

Browse files
authored
Add synthetic_source support to aggregate_metric_double fields (#88909)
This PR implements synthetic_source support to the aggregate_metric_double field type Relates to #86603
1 parent 6121a8a commit ad2dc83

File tree

8 files changed

+284
-20
lines changed

8 files changed

+284
-20
lines changed

docs/changelog/88909.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 88909
2+
summary: Add `synthetic_source` support to `aggregate_metric_double` fields
3+
area: Mapping
4+
type: enhancement
5+
issues: []

docs/reference/mapping/fields/synthetic-source.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ space. There are a couple of restrictions to be aware of:
2828
* Synthetic `_source` can be used with indices that contain only these field
2929
types:
3030

31+
** <<aggregate-metric-double-synthetic-source, `aggregate_metric_double`>>
3132
** <<boolean-synthetic-source,`boolean`>>
3233
** <<numeric-synthetic-source,`byte`>>
3334
** <<numeric-synthetic-source,`double`>>

docs/reference/mapping/types/aggregate-metric-double.asciidoc

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,55 @@ The search returns the following hit. The value of the `default_metric` field,
251251
}
252252
----
253253
// TESTRESPONSE[s/\.\.\./"took": $body.took,"timed_out": false,"_shards": $body._shards,/]
254+
255+
ifeval::["{release-state}"=="unreleased"]
256+
[[aggregate-metric-double-synthetic-source]]
257+
==== Synthetic source
258+
`aggregate_metric-double` fields support <<synthetic-source,synthetic `_source`>> in their default
259+
configuration. Synthetic `_source` cannot be used together with <<ignore-malformed,`ignore_malformed`>>.
260+
261+
For example:
262+
[source,console,id=synthetic-source-aggregate-metric-double-example]
263+
----
264+
PUT idx
265+
{
266+
"mappings": {
267+
"_source": { "mode": "synthetic" },
268+
"properties": {
269+
"agg_metric": {
270+
"type": "aggregate_metric_double",
271+
"metrics": [ "min", "max", "sum", "value_count" ],
272+
"default_metric": "max"
273+
}
274+
}
275+
}
276+
}
277+
278+
PUT idx/_doc/1
279+
{
280+
"agg_metric": {
281+
"min": -302.50,
282+
"max": 702.30,
283+
"sum": 200.0,
284+
"value_count": 25
285+
}
286+
}
287+
----
288+
// TEST[s/$/\nGET idx\/_doc\/1?filter_path=_source\n/]
289+
290+
Will become:
291+
292+
[source,console-result]
293+
----
294+
{
295+
"agg_metric": {
296+
"min": -302.50,
297+
"max": 702.30,
298+
"sum": 200.0,
299+
"value_count": 25
300+
}
301+
}
302+
----
303+
// TEST[s/^/{"_source":/ s/\n$/}/]
304+
305+
endif::[]

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,7 +1719,7 @@ protected NumericSyntheticFieldLoader(String name, String simpleName) {
17191719

17201720
@Override
17211721
public Leaf leaf(LeafReader reader, int[] docIdsInLeaf) throws IOException {
1722-
SortedNumericDocValues dv = dv(reader);
1722+
SortedNumericDocValues dv = docValuesOrNull(reader, name);
17231723
if (dv == null) {
17241724
return SourceLoader.SyntheticFieldLoader.NOTHING_LEAF;
17251725
}
@@ -1830,12 +1830,12 @@ public void write(XContentBuilder b) throws IOException {
18301830
* an "empty" implementation if there aren't any doc values. We need to be able to
18311831
* tell if there aren't any and return our empty leaf source loader.
18321832
*/
1833-
private SortedNumericDocValues dv(LeafReader reader) throws IOException {
1834-
SortedNumericDocValues dv = reader.getSortedNumericDocValues(name);
1833+
public static SortedNumericDocValues docValuesOrNull(LeafReader reader, String fieldName) throws IOException {
1834+
SortedNumericDocValues dv = reader.getSortedNumericDocValues(fieldName);
18351835
if (dv != null) {
18361836
return dv;
18371837
}
1838-
NumericDocValues single = reader.getNumericDocValues(name);
1838+
NumericDocValues single = reader.getNumericDocValues(fieldName);
18391839
if (single != null) {
18401840
return DocValues.singleton(single);
18411841
}

test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,13 @@ protected boolean supportsMeta() {
253253
return true;
254254
}
255255

256+
/**
257+
* Override to disable testing {@code copy_to} in fields that don't support it.
258+
*/
259+
protected boolean supportsCopyTo() {
260+
return true;
261+
}
262+
256263
protected void metaMapping(XContentBuilder b) throws IOException {
257264
minimalMapping(b);
258265
}
@@ -893,15 +900,17 @@ public final void testSyntheticEmptyList() throws IOException {
893900

894901
public final void testSyntheticSourceInvalid() throws IOException {
895902
List<SyntheticSourceInvalidExample> examples = new ArrayList<>(syntheticSourceSupport().invalidExample());
896-
examples.add(
897-
new SyntheticSourceInvalidExample(
898-
matchesPattern("field \\[field] of type \\[.+] doesn't support synthetic source because it declares copy_to"),
899-
b -> {
900-
syntheticSourceSupport().example(5).mapping().accept(b);
901-
b.field("copy_to", "bar");
902-
}
903-
)
904-
);
903+
if (supportsCopyTo()) {
904+
examples.add(
905+
new SyntheticSourceInvalidExample(
906+
matchesPattern("field \\[field] of type \\[.+] doesn't support synthetic source because it declares copy_to"),
907+
b -> {
908+
syntheticSourceSupport().example(5).mapping().accept(b);
909+
b.field("copy_to", "bar");
910+
}
911+
)
912+
);
913+
}
905914
for (SyntheticSourceInvalidExample example : examples) {
906915
Exception e = expectThrows(
907916
IllegalArgumentException.class,

x-pack/plugin/mapper-aggregate-metric/src/main/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapper.java

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.apache.lucene.index.DocValues;
1010
import org.apache.lucene.index.IndexReader;
1111
import org.apache.lucene.index.IndexableField;
12+
import org.apache.lucene.index.LeafReader;
1213
import org.apache.lucene.index.LeafReaderContext;
1314
import org.apache.lucene.index.SortedNumericDocValues;
1415
import org.apache.lucene.search.Query;
@@ -32,6 +33,7 @@
3233
import org.elasticsearch.index.mapper.MapperBuilderContext;
3334
import org.elasticsearch.index.mapper.NumberFieldMapper;
3435
import org.elasticsearch.index.mapper.SimpleMappedFieldType;
36+
import org.elasticsearch.index.mapper.SourceLoader;
3537
import org.elasticsearch.index.mapper.SourceValueFetcher;
3638
import org.elasticsearch.index.mapper.TextSearchInfo;
3739
import org.elasticsearch.index.mapper.TimeSeriesParams;
@@ -574,7 +576,6 @@ public Iterator<Mapper> iterator() {
574576

575577
@Override
576578
protected void parseCreateField(DocumentParserContext context) throws IOException {
577-
578579
context.path().add(simpleName());
579580
XContentParser.Token token;
580581
XContentSubParser subParser = null;
@@ -675,4 +676,95 @@ protected void parseCreateField(DocumentParserContext context) throws IOExceptio
675676
public FieldMapper.Builder getMergeBuilder() {
676677
return new Builder(simpleName(), ignoreMalformedByDefault, indexCreatedVersion).metric(metricType).init(this);
677678
}
679+
680+
@Override
681+
public SourceLoader.SyntheticFieldLoader syntheticFieldLoader() {
682+
if (ignoreMalformed) {
683+
throw new IllegalArgumentException(
684+
"field [" + name() + "] of type [" + typeName() + "] doesn't support synthetic source because it ignores malformed numbers"
685+
);
686+
}
687+
return new AggregateMetricSyntheticFieldLoader(name(), simpleName(), metrics);
688+
}
689+
690+
public static class AggregateMetricSyntheticFieldLoader implements SourceLoader.SyntheticFieldLoader {
691+
private final String name;
692+
private final String simpleName;
693+
private final EnumSet<Metric> metrics;
694+
695+
protected AggregateMetricSyntheticFieldLoader(String name, String simpleName, EnumSet<Metric> metrics) {
696+
this.name = name;
697+
this.simpleName = simpleName;
698+
this.metrics = metrics;
699+
}
700+
701+
@Override
702+
public Leaf leaf(LeafReader reader, int[] docIdsInLeaf) throws IOException {
703+
Map<Metric, SortedNumericDocValues> metricDocValues = new EnumMap<>(Metric.class);
704+
for (Metric m : metrics) {
705+
String fieldName = subfieldName(name, m);
706+
SortedNumericDocValues dv = NumberFieldMapper.NumericSyntheticFieldLoader.docValuesOrNull(reader, fieldName);
707+
if (dv != null) {
708+
metricDocValues.put(m, dv);
709+
}
710+
}
711+
712+
if (metricDocValues.isEmpty()) {
713+
return SourceLoader.SyntheticFieldLoader.NOTHING_LEAF;
714+
}
715+
716+
return new AggregateMetricSyntheticFieldLoader.ImmediateLeaf(metricDocValues);
717+
}
718+
719+
private class ImmediateLeaf implements Leaf {
720+
private final Map<Metric, SortedNumericDocValues> metricDocValues;
721+
private final Set<Metric> metricHasValue = EnumSet.noneOf(Metric.class);
722+
723+
ImmediateLeaf(Map<Metric, SortedNumericDocValues> metricDocValues) {
724+
assert metricDocValues.isEmpty() == false : "doc_values for metrics cannot be empty";
725+
this.metricDocValues = metricDocValues;
726+
}
727+
728+
@Override
729+
public boolean empty() {
730+
return false;
731+
}
732+
733+
@Override
734+
public boolean advanceToDoc(int docId) throws IOException {
735+
// It is required that all defined metrics must exist. In this case
736+
// it is enough to check for the first docValue. However, in the future
737+
// we may relax the requirement of all metrics existing. In this case
738+
// we should check the doc value for each metric separately
739+
metricHasValue.clear();
740+
for (Map.Entry<Metric, SortedNumericDocValues> e : metricDocValues.entrySet()) {
741+
if (e.getValue().advanceExact(docId)) {
742+
metricHasValue.add(e.getKey());
743+
}
744+
}
745+
746+
return metricHasValue.isEmpty() == false;
747+
}
748+
749+
@Override
750+
public void write(XContentBuilder b) throws IOException {
751+
if (metricHasValue.isEmpty()) {
752+
return;
753+
}
754+
b.startObject(simpleName);
755+
for (Map.Entry<Metric, SortedNumericDocValues> entry : metricDocValues.entrySet()) {
756+
if (metricHasValue.contains(entry.getKey())) {
757+
String metricName = entry.getKey().name();
758+
long value = entry.getValue().nextValue();
759+
if (entry.getKey() == Metric.value_count) {
760+
b.field(metricName, value);
761+
} else {
762+
b.field(metricName, NumericUtils.sortableLongToDouble(value));
763+
}
764+
}
765+
}
766+
b.endObject();
767+
}
768+
}
769+
}
678770
}

x-pack/plugin/mapper-aggregate-metric/src/test/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapperTests.java

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,24 @@
2020
import org.elasticsearch.xcontent.XContentBuilder;
2121
import org.elasticsearch.xcontent.XContentFactory;
2222
import org.elasticsearch.xpack.aggregatemetric.AggregateMetricMapperPlugin;
23+
import org.elasticsearch.xpack.aggregatemetric.mapper.AggregateDoubleMetricFieldMapper.Metric;
2324
import org.hamcrest.Matchers;
2425
import org.junit.AssumptionViolatedException;
2526

2627
import java.io.IOException;
28+
import java.util.Arrays;
2729
import java.util.Collection;
30+
import java.util.EnumSet;
2831
import java.util.Iterator;
32+
import java.util.LinkedHashMap;
2933
import java.util.List;
3034
import java.util.Map;
3135

3236
import static org.elasticsearch.xpack.aggregatemetric.mapper.AggregateDoubleMetricFieldMapper.Names.IGNORE_MALFORMED;
3337
import static org.elasticsearch.xpack.aggregatemetric.mapper.AggregateDoubleMetricFieldMapper.Names.METRICS;
3438
import static org.hamcrest.Matchers.containsString;
3539
import static org.hamcrest.Matchers.equalTo;
40+
import static org.hamcrest.Matchers.matchesPattern;
3641
import static org.hamcrest.Matchers.notNullValue;
3742
import static org.hamcrest.Matchers.nullValue;
3843
import static org.hamcrest.core.IsInstanceOf.instanceOf;
@@ -393,7 +398,7 @@ public void testExplicitDefaultMetric() throws Exception {
393398

394399
Mapper fieldMapper = mapper.mappers().getMapper("field");
395400
assertThat(fieldMapper, instanceOf(AggregateDoubleMetricFieldMapper.class));
396-
assertEquals(AggregateDoubleMetricFieldMapper.Metric.sum, ((AggregateDoubleMetricFieldMapper) fieldMapper).defaultMetric());
401+
assertEquals(Metric.sum, ((AggregateDoubleMetricFieldMapper) fieldMapper).defaultMetric());
397402
}
398403

399404
/**
@@ -406,7 +411,7 @@ public void testImplicitDefaultMetricSingleMetric() throws Exception {
406411

407412
Mapper fieldMapper = mapper.mappers().getMapper("field");
408413
assertThat(fieldMapper, instanceOf(AggregateDoubleMetricFieldMapper.class));
409-
assertEquals(AggregateDoubleMetricFieldMapper.Metric.value_count, ((AggregateDoubleMetricFieldMapper) fieldMapper).defaultMetric);
414+
assertEquals(Metric.value_count, ((AggregateDoubleMetricFieldMapper) fieldMapper).defaultMetric);
410415
}
411416

412417
/**
@@ -416,7 +421,7 @@ public void testImplicitDefaultMetric() throws Exception {
416421
DocumentMapper mapper = createDocumentMapper(fieldMapping(this::minimalMapping));
417422
Mapper fieldMapper = mapper.mappers().getMapper("field");
418423
assertThat(fieldMapper, instanceOf(AggregateDoubleMetricFieldMapper.class));
419-
assertEquals(AggregateDoubleMetricFieldMapper.Metric.max, ((AggregateDoubleMetricFieldMapper) fieldMapper).defaultMetric);
424+
assertEquals(Metric.max, ((AggregateDoubleMetricFieldMapper) fieldMapper).defaultMetric);
420425
}
421426

422427
/**
@@ -505,8 +510,8 @@ public void testParseNestedValue() throws Exception {
505510
* subfields of aggregate_metric_double should not be searchable or exposed in field_caps
506511
*/
507512
public void testNoSubFieldsIterated() throws IOException {
508-
AggregateDoubleMetricFieldMapper.Metric[] values = AggregateDoubleMetricFieldMapper.Metric.values();
509-
List<AggregateDoubleMetricFieldMapper.Metric> subset = randomSubsetOf(randomIntBetween(1, values.length), values);
513+
Metric[] values = Metric.values();
514+
List<Metric> subset = randomSubsetOf(randomIntBetween(1, values.length), values);
510515
DocumentMapper mapper = createDocumentMapper(
511516
fieldMapping(b -> b.field("type", CONTENT_TYPE).field(METRICS_FIELD, subset).field(DEFAULT_METRIC, subset.get(0)))
512517
);
@@ -589,11 +594,58 @@ public void testMetricType() throws IOException {
589594

590595
@Override
591596
protected SyntheticSourceSupport syntheticSourceSupport() {
592-
throw new AssumptionViolatedException("not supported");
597+
return new AggregateDoubleMetricSyntheticSourceSupport();
593598
}
594599

595600
@Override
596601
protected IngestScriptSupport ingestScriptSupport() {
597602
throw new AssumptionViolatedException("not supported");
598603
}
604+
605+
protected final class AggregateDoubleMetricSyntheticSourceSupport implements SyntheticSourceSupport {
606+
607+
private final EnumSet<Metric> storedMetrics = EnumSet.copyOf(randomNonEmptySubsetOf(Arrays.asList(Metric.values())));
608+
609+
@Override
610+
public SyntheticSourceExample example(int maxVals) {
611+
// aggregate_metric_double field does not support arrays
612+
Map<String, Object> value = randomAggregateMetric();
613+
return new SyntheticSourceExample(value, value, this::mapping);
614+
}
615+
616+
private Map<String, Object> randomAggregateMetric() {
617+
Map<String, Object> value = new LinkedHashMap<>(storedMetrics.size());
618+
for (Metric m : storedMetrics) {
619+
if (Metric.value_count == m) {
620+
value.put(m.name(), randomLongBetween(1, 1_000_000));
621+
} else {
622+
value.put(m.name(), randomDouble());
623+
}
624+
}
625+
return value;
626+
}
627+
628+
private void mapping(XContentBuilder b) throws IOException {
629+
String[] metrics = storedMetrics.stream().map(Metric::toString).toArray(String[]::new);
630+
b.field("type", CONTENT_TYPE).array(METRICS_FIELD, metrics).field(DEFAULT_METRIC, metrics[0]);
631+
}
632+
633+
@Override
634+
public List<SyntheticSourceInvalidExample> invalidExample() throws IOException {
635+
return List.of(
636+
new SyntheticSourceInvalidExample(
637+
matchesPattern("field \\[field] of type \\[.+] doesn't support synthetic source because it ignores malformed numbers"),
638+
b -> {
639+
mapping(b);
640+
b.field("ignore_malformed", true);
641+
}
642+
)
643+
);
644+
}
645+
}
646+
647+
@Override
648+
protected boolean supportsCopyTo() {
649+
return false;
650+
}
599651
}

0 commit comments

Comments
 (0)