Skip to content

Commit ec0f171

Browse files
authored
ES|QL block type for exponential histograms (#133393)
1 parent 48f38e5 commit ec0f171

File tree

46 files changed

+1271
-37
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1271
-37
lines changed

libs/exponential-histogram/src/main/java/org/elasticsearch/exponentialhistogram/ExponentialHistogramXContent.java

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,16 @@
2121

2222
package org.elasticsearch.exponentialhistogram;
2323

24+
import org.elasticsearch.core.Nullable;
25+
import org.elasticsearch.core.Types;
2426
import org.elasticsearch.xcontent.XContentBuilder;
27+
import org.elasticsearch.xcontent.XContentParser;
2528

2629
import java.io.IOException;
30+
import java.util.Collections;
31+
import java.util.List;
32+
import java.util.Map;
33+
import java.util.function.BiConsumer;
2734

2835
/**
2936
* Handles the serialization of an {@link ExponentialHistogram} to XContent.
@@ -48,7 +55,11 @@ public class ExponentialHistogramXContent {
4855
* @param histogram the ExponentialHistogram to serialize
4956
* @throws IOException if the XContentBuilder throws an IOException
5057
*/
51-
public static void serialize(XContentBuilder builder, ExponentialHistogram histogram) throws IOException {
58+
public static void serialize(XContentBuilder builder, @Nullable ExponentialHistogram histogram) throws IOException {
59+
if (histogram == null) {
60+
builder.nullValue();
61+
return;
62+
}
5263
builder.startObject();
5364

5465
builder.field(SCALE_FIELD, histogram.scale());
@@ -101,4 +112,63 @@ private static void writeBuckets(XContentBuilder b, String fieldName, Exponentia
101112
b.endObject();
102113
}
103114

115+
/**
116+
* Parses an {@link ExponentialHistogram} from the provided {@link XContentParser}.
117+
* This method is neither optimized, nor does it do any validation of the parsed content.
118+
* No estimation for missing sum/min/max is done.
119+
* Therefore only intended for testing!
120+
*
121+
* @param xContent the serialized histogram to read
122+
* @return the deserialized histogram
123+
* @throws IOException if the XContentParser throws an IOException
124+
*/
125+
public static ExponentialHistogram parseForTesting(XContentParser xContent) throws IOException {
126+
if (xContent.currentToken() == null) {
127+
xContent.nextToken();
128+
}
129+
if (xContent.currentToken() == XContentParser.Token.VALUE_NULL) {
130+
return null;
131+
}
132+
return parseForTesting(xContent.map());
133+
}
134+
135+
/**
136+
* Parses an {@link ExponentialHistogram} from a {@link Map}.
137+
* This method is neither optimized, nor does it do any validation of the parsed content.
138+
* No estimation for missing sum/min/max is done.
139+
* Therefore only intended for testing!
140+
*
141+
* @param xContent the serialized histogram as a map
142+
* @return the deserialized histogram
143+
*/
144+
public static ExponentialHistogram parseForTesting(@Nullable Map<String, Object> xContent) {
145+
if (xContent == null) {
146+
return null;
147+
}
148+
int scale = ((Number) xContent.get(SCALE_FIELD)).intValue();
149+
ExponentialHistogramBuilder builder = ExponentialHistogram.builder(scale, ExponentialHistogramCircuitBreaker.noop());
150+
151+
Map<String, Number> zero = Types.forciblyCast(xContent.getOrDefault(ZERO_FIELD, Collections.emptyMap()));
152+
double zeroThreshold = zero.getOrDefault(ZERO_THRESHOLD_FIELD, 0).doubleValue();
153+
long zeroCount = zero.getOrDefault(ZERO_COUNT_FIELD, 0).longValue();
154+
builder.zeroBucket(ZeroBucket.create(zeroThreshold, zeroCount));
155+
156+
builder.sum(((Number) xContent.getOrDefault(SUM_FIELD, 0)).doubleValue());
157+
builder.min(((Number) xContent.getOrDefault(MIN_FIELD, Double.NaN)).doubleValue());
158+
builder.max(((Number) xContent.getOrDefault(MAX_FIELD, Double.NaN)).doubleValue());
159+
160+
parseBuckets(Types.forciblyCast(xContent.getOrDefault(NEGATIVE_FIELD, Collections.emptyMap())), builder::setNegativeBucket);
161+
parseBuckets(Types.forciblyCast(xContent.getOrDefault(POSITIVE_FIELD, Collections.emptyMap())), builder::setPositiveBucket);
162+
163+
return builder.build();
164+
}
165+
166+
private static void parseBuckets(Map<String, List<Number>> serializedBuckets, BiConsumer<Long, Long> bucketSetter) {
167+
List<Number> indices = serializedBuckets.getOrDefault(BUCKET_INDICES_FIELD, Collections.emptyList());
168+
List<Number> counts = serializedBuckets.getOrDefault(BUCKET_COUNTS_FIELD, Collections.emptyList());
169+
assert indices.size() == counts.size();
170+
for (int i = 0; i < indices.size(); i++) {
171+
bucketSetter.accept(indices.get(i).longValue(), counts.get(i).longValue());
172+
}
173+
}
104174
}

libs/exponential-histogram/src/test/java/org/elasticsearch/exponentialhistogram/ExponentialHistogramXContentTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
import org.elasticsearch.common.Strings;
2525
import org.elasticsearch.xcontent.XContentBuilder;
26+
import org.elasticsearch.xcontent.XContentParser;
27+
import org.elasticsearch.xcontent.XContentParserConfiguration;
2628
import org.elasticsearch.xcontent.json.JsonXContent;
2729

2830
import java.io.IOException;
@@ -31,6 +33,11 @@
3133

3234
public class ExponentialHistogramXContentTests extends ExponentialHistogramTestCase {
3335

36+
public void testNullHistogram() {
37+
assertThat(toJson(null), equalTo("null"));
38+
checkRoundTrip(null);
39+
}
40+
3441
public void testEmptyHistogram() {
3542
ExponentialHistogram emptyHistogram = ExponentialHistogram.empty();
3643
assertThat(toJson(emptyHistogram), equalTo("{\"scale\":" + emptyHistogram.scale() + ",\"sum\":0.0}"));
@@ -62,18 +69,21 @@ public void testFullHistogram() {
6269
+ "}"
6370
)
6471
);
72+
checkRoundTrip(histo);
6573
}
6674

6775
public void testOnlyZeroThreshold() {
6876
ExponentialHistogram histo = createAutoReleasedHistogram(b -> b.scale(3).sum(1.1).zeroBucket(ZeroBucket.create(5.0, 0)));
6977
assertThat(toJson(histo), equalTo("{\"scale\":3,\"sum\":1.1,\"zero\":{\"threshold\":5.0}}"));
78+
checkRoundTrip(histo);
7079
}
7180

7281
public void testOnlyZeroCount() {
7382
ExponentialHistogram histo = createAutoReleasedHistogram(
7483
b -> b.zeroBucket(ZeroBucket.create(0.0, 7)).scale(2).sum(1.1).min(0).max(0)
7584
);
7685
assertThat(toJson(histo), equalTo("{\"scale\":2,\"sum\":1.1,\"min\":0.0,\"max\":0.0,\"zero\":{\"count\":7}}"));
86+
checkRoundTrip(histo);
7787
}
7888

7989
public void testOnlyPositiveBuckets() {
@@ -84,6 +94,7 @@ public void testOnlyPositiveBuckets() {
8494
toJson(histo),
8595
equalTo("{\"scale\":4,\"sum\":1.1,\"min\":0.5,\"max\":2.5,\"positive\":{\"indices\":[-1,2],\"counts\":[3,5]}}")
8696
);
97+
checkRoundTrip(histo);
8798
}
8899

89100
public void testOnlyNegativeBuckets() {
@@ -94,6 +105,7 @@ public void testOnlyNegativeBuckets() {
94105
toJson(histo),
95106
equalTo("{\"scale\":5,\"sum\":1.1,\"min\":-0.5,\"max\":-0.25,\"negative\":{\"indices\":[-1,2],\"counts\":[4,6]}}")
96107
);
108+
checkRoundTrip(histo);
97109
}
98110

99111
private static String toJson(ExponentialHistogram histo) {
@@ -105,4 +117,17 @@ private static String toJson(ExponentialHistogram histo) {
105117
}
106118
}
107119

120+
private static void checkRoundTrip(ExponentialHistogram histo) {
121+
try (XContentBuilder builder = JsonXContent.contentBuilder()) {
122+
ExponentialHistogramXContent.serialize(builder, histo);
123+
String json = Strings.toString(builder);
124+
try (XContentParser parser = JsonXContent.jsonXContent.createParser(XContentParserConfiguration.EMPTY, json)) {
125+
ExponentialHistogram parsed = ExponentialHistogramXContent.parseForTesting(parser);
126+
assertThat(parsed, equalTo(histo));
127+
}
128+
} catch (IOException e) {
129+
throw new RuntimeException(e);
130+
}
131+
}
132+
108133
}

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/plugin/EsqlCorePlugin.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77

88
package org.elasticsearch.xpack.esql.core.plugin;
99

10+
import org.elasticsearch.common.util.FeatureFlag;
1011
import org.elasticsearch.plugins.ExtensiblePlugin;
1112
import org.elasticsearch.plugins.Plugin;
1213

1314
public class EsqlCorePlugin extends Plugin implements ExtensiblePlugin {
1415

16+
public static final FeatureFlag EXPONENTIAL_HISTOGRAM_FEATURE_FLAG = new FeatureFlag("esql_exponential_histogram");
1517
}

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/type/DataType.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,14 @@ public enum DataType implements Writeable {
340340
.estimatedSize(Double.BYTES * 3 + Integer.BYTES)
341341
.supportedSince(DataTypesTransportVersions.ESQL_AGGREGATE_METRIC_DOUBLE_CREATED_VERSION)
342342
),
343+
344+
EXPONENTIAL_HISTOGRAM(
345+
builder().esType("exponential_histogram")
346+
.estimatedSize(16 * 160)// guess 160 buckets (OTEL default for positive values only histograms) with 16 bytes per bucket
347+
.docValues()
348+
.underConstruction()
349+
),
350+
343351
/**
344352
* Fields with this type are dense vectors, represented as an array of float values.
345353
*/

x-pack/plugin/esql/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dependencies {
4242
implementation project('compute:ann')
4343
implementation project(':libs:dissect')
4444
implementation project(':libs:grok')
45+
implementation project(':libs:exponential-histogram')
4546
api "org.apache.lucene:lucene-spatial3d:${versions.lucene}"
4647
api project(":libs:h3")
4748
implementation project('arrow')

x-pack/plugin/esql/compute/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies {
1616
compileOnly project(xpackModule('ml'))
1717
annotationProcessor project('gen')
1818
implementation 'com.carrotsearch:hppc:0.8.1'
19+
api project(':libs:exponential-histogram')
1920

2021
testImplementation(project(':modules:analysis-common'))
2122
testImplementation(project(':test:framework'))

x-pack/plugin/esql/compute/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
requires org.elasticsearch.geo;
2222
requires org.elasticsearch.xcore;
2323
requires hppc;
24+
requires org.elasticsearch.exponentialhistogram;
2425

2526
exports org.elasticsearch.compute;
2627
exports org.elasticsearch.compute.aggregation;

x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/BlockFactory.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.elasticsearch.common.util.BigArrays;
1515
import org.elasticsearch.common.util.BytesRefArray;
1616
import org.elasticsearch.compute.data.Block.MvOrdering;
17+
import org.elasticsearch.exponentialhistogram.ExponentialHistogram;
1718

1819
import java.util.BitSet;
1920

@@ -467,6 +468,19 @@ public final AggregateMetricDoubleBlock newConstantAggregateMetricDoubleBlock(
467468
}
468469
}
469470

471+
public ExponentialHistogramBlockBuilder newExponentialHistogramBlockBuilder(int estimatedSize) {
472+
return new ExponentialHistogramBlockBuilder(estimatedSize, this);
473+
}
474+
475+
public final ExponentialHistogramBlock newConstantExponentialHistogramBlock(ExponentialHistogram value, int positionCount) {
476+
try (ExponentialHistogramBlockBuilder builder = newExponentialHistogramBlockBuilder(positionCount)) {
477+
for (int i = 0; i < positionCount; i++) {
478+
builder.append(value);
479+
}
480+
return builder.build();
481+
}
482+
}
483+
470484
public final AggregateMetricDoubleBlock newAggregateMetricDoubleBlock(
471485
double[] minValues,
472486
double[] maxValues,

x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/BlockUtils.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import org.elasticsearch.compute.data.AggregateMetricDoubleBlockBuilder.AggregateMetricDoubleLiteral;
1414
import org.elasticsearch.core.Releasable;
1515
import org.elasticsearch.core.Releasables;
16+
import org.elasticsearch.exponentialhistogram.ExponentialHistogram;
17+
import org.elasticsearch.exponentialhistogram.ExponentialHistogramCircuitBreaker;
1618

1719
import java.util.ArrayList;
1820
import java.util.Arrays;
@@ -224,6 +226,7 @@ public static void appendValue(Block.Builder builder, Object val, ElementType ty
224226
case DOUBLE -> ((DoubleBlock.Builder) builder).appendDouble((Double) val);
225227
case BOOLEAN -> ((BooleanBlock.Builder) builder).appendBoolean((Boolean) val);
226228
case AGGREGATE_METRIC_DOUBLE -> ((AggregateMetricDoubleBlockBuilder) builder).appendLiteral((AggregateMetricDoubleLiteral) val);
229+
case EXPONENTIAL_HISTOGRAM -> ((ExponentialHistogramBlockBuilder) builder).append((ExponentialHistogram) val);
227230
default -> throw new UnsupportedOperationException("unsupported element type [" + type + "]");
228231
}
229232
}
@@ -253,6 +256,7 @@ private static Block constantBlock(BlockFactory blockFactory, ElementType type,
253256
case BOOLEAN -> blockFactory.newConstantBooleanBlockWith((boolean) val, size);
254257
case AGGREGATE_METRIC_DOUBLE -> blockFactory.newConstantAggregateMetricDoubleBlock((AggregateMetricDoubleLiteral) val, size);
255258
case FLOAT -> blockFactory.newConstantFloatBlockWith((float) val, size);
259+
case EXPONENTIAL_HISTOGRAM -> blockFactory.newConstantExponentialHistogramBlock((ExponentialHistogram) val, size);
256260
default -> throw new UnsupportedOperationException("unsupported element type [" + type + "]");
257261
};
258262
}
@@ -306,6 +310,12 @@ yield new AggregateMetricDoubleLiteral(
306310
aggBlock.countBlock().getInt(offset)
307311
);
308312
}
313+
case EXPONENTIAL_HISTOGRAM -> {
314+
ExponentialHistogramBlock histoBlock = (ExponentialHistogramBlock) block;
315+
ExponentialHistogram histogram = new ExponentialHistogramBlockAccessor(histoBlock).get(offset);
316+
// return a copy so that the returned value is not bound to the lifetime of the block
317+
yield ExponentialHistogram.builder(histogram, ExponentialHistogramCircuitBreaker.noop()).build();
318+
}
309319
case UNKNOWN -> throw new IllegalArgumentException("can't read values from [" + block + "]");
310320
};
311321
}

x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/data/ConstantNullBlock.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ public final class ConstantNullBlock extends AbstractNonThreadSafeRefCounted
2626
FloatBlock,
2727
DoubleBlock,
2828
BytesRefBlock,
29-
AggregateMetricDoubleBlock {
29+
AggregateMetricDoubleBlock,
30+
ExponentialHistogramBlock {
3031

3132
private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(ConstantNullBlock.class);
3233
private final int positionCount;

0 commit comments

Comments
 (0)