Skip to content

Commit fb6bcce

Browse files
authored
ESQL: ROUND_TO function (#128278) (#128397)
Creates a `ROUND_TO` function that rounds it's input to one of the provided values. Like so: ``` ROUND_TO(v, 0, 5000, 10000, 20000, 40000, 100000) v | ROUND_TO 0 | 0 100 | 0 6000 | 5000 45001 | 40000 999999 | 100000 ``` For some sequences of numbers you could do this with the `/` operator - but for arbitrary sequences of numbers you needed `CASE` which is quite slow. And hard to read! Rewriting the example above would look like: ``` CASE ( v < 5000, 0, v < 10000, 5000, v < 20000, 10000, v < 40000, 20000, v < 100000, 40000, 100000 ) ``` Even better, this is *fast*: ``` (operation) Mode Cnt Score Error Units round_to_4_via_case avgt 7 138.124 ± 0.738 ns/op round_to_4 avgt 7 0.805 ± 0.011 ns/op round_to_3 avgt 7 0.739 ± 0.011 ns/op round_to_2 avgt 7 0.651 ± 0.009 ns/op date_trunc avgt 7 2.425 ± 0.018 ns/op ``` I've included a comparison to `DATE_TRUNC` above because we should be able to rewrite `DATE_TRUNC` into `ROUND_TO` when we know the date range of the index. This doesn't do it now, but it should be possible.
1 parent b94cd56 commit fb6bcce

File tree

48 files changed

+4114
-6
lines changed

Some content is hidden

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

48 files changed

+4114
-6
lines changed

benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/EvalBenchmark.java

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.apache.lucene.util.BytesRef;
1313
import org.elasticsearch.common.breaker.NoopCircuitBreaker;
1414
import org.elasticsearch.common.settings.Settings;
15+
import org.elasticsearch.common.unit.ByteSizeUnit;
1516
import org.elasticsearch.common.util.BigArrays;
1617
import org.elasticsearch.compute.data.Block;
1718
import org.elasticsearch.compute.data.BlockFactory;
@@ -41,13 +42,15 @@
4142
import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Case;
4243
import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc;
4344
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Abs;
45+
import org.elasticsearch.xpack.esql.expression.function.scalar.math.RoundTo;
4446
import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMin;
4547
import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce;
4648
import org.elasticsearch.xpack.esql.expression.function.scalar.string.RLike;
4749
import org.elasticsearch.xpack.esql.expression.function.scalar.string.ToLower;
4850
import org.elasticsearch.xpack.esql.expression.function.scalar.string.ToUpper;
4951
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Add;
5052
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals;
53+
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.LessThan;
5154
import org.elasticsearch.xpack.esql.planner.Layout;
5255
import org.elasticsearch.xpack.esql.plugin.EsqlPlugin;
5356
import org.elasticsearch.xpack.esql.session.Configuration;
@@ -116,6 +119,10 @@ public class EvalBenchmark {
116119
"long_equal_to_int",
117120
"mv_min",
118121
"mv_min_ascending",
122+
"round_to_4_via_case",
123+
"round_to_2",
124+
"round_to_3",
125+
"round_to_4",
119126
"rlike",
120127
"to_lower",
121128
"to_lower_ords",
@@ -228,6 +235,65 @@ private static EvalOperator.ExpressionEvaluator evaluator(String operation) {
228235
RLike rlike = new RLike(Source.EMPTY, keywordField, new RLikePattern(".ar"));
229236
yield EvalMapper.toEvaluator(FOLD_CONTEXT, rlike, layout(keywordField)).get(driverContext);
230237
}
238+
case "round_to_4_via_case" -> {
239+
FieldAttribute f = longField();
240+
241+
Expression ltkb = new LessThan(Source.EMPTY, f, kb());
242+
Expression ltmb = new LessThan(Source.EMPTY, f, mb());
243+
Expression ltgb = new LessThan(Source.EMPTY, f, gb());
244+
EvalOperator.ExpressionEvaluator evaluator = EvalMapper.toEvaluator(
245+
FOLD_CONTEXT,
246+
new Case(Source.EMPTY, ltkb, List.of(b(), ltmb, kb(), ltgb, mb(), gb())),
247+
layout(f)
248+
).get(driverContext);
249+
String desc = "CaseLazyEvaluator";
250+
if (evaluator.toString().contains(desc) == false) {
251+
throw new IllegalArgumentException("Evaluator was [" + evaluator + "] but expected one containing [" + desc + "]");
252+
}
253+
yield evaluator;
254+
}
255+
case "round_to_2" -> {
256+
FieldAttribute f = longField();
257+
258+
EvalOperator.ExpressionEvaluator evaluator = EvalMapper.toEvaluator(
259+
FOLD_CONTEXT,
260+
new RoundTo(Source.EMPTY, f, List.of(b(), kb())),
261+
layout(f)
262+
).get(driverContext);
263+
String desc = "RoundToLong2";
264+
if (evaluator.toString().contains(desc) == false) {
265+
throw new IllegalArgumentException("Evaluator was [" + evaluator + "] but expected one containing [" + desc + "]");
266+
}
267+
yield evaluator;
268+
}
269+
case "round_to_3" -> {
270+
FieldAttribute f = longField();
271+
272+
EvalOperator.ExpressionEvaluator evaluator = EvalMapper.toEvaluator(
273+
FOLD_CONTEXT,
274+
new RoundTo(Source.EMPTY, f, List.of(b(), kb(), mb())),
275+
layout(f)
276+
).get(driverContext);
277+
String desc = "RoundToLong3";
278+
if (evaluator.toString().contains(desc) == false) {
279+
throw new IllegalArgumentException("Evaluator was [" + evaluator + "] but expected one containing [" + desc + "]");
280+
}
281+
yield evaluator;
282+
}
283+
case "round_to_4" -> {
284+
FieldAttribute f = longField();
285+
286+
EvalOperator.ExpressionEvaluator evaluator = EvalMapper.toEvaluator(
287+
FOLD_CONTEXT,
288+
new RoundTo(Source.EMPTY, f, List.of(b(), kb(), mb(), gb())),
289+
layout(f)
290+
).get(driverContext);
291+
String desc = "RoundToLong4";
292+
if (evaluator.toString().contains(desc) == false) {
293+
throw new IllegalArgumentException("Evaluator was [" + evaluator + "] but expected one containing [" + desc + "]");
294+
}
295+
yield evaluator;
296+
}
231297
case "to_lower", "to_lower_ords" -> {
232298
FieldAttribute keywordField = keywordField();
233299
ToLower toLower = new ToLower(Source.EMPTY, keywordField, configuration());
@@ -390,6 +456,69 @@ private static void checkExpected(String operation, Page actual) {
390456
}
391457
}
392458
}
459+
case "round_to_4_via_case", "round_to_4" -> {
460+
long b = 1;
461+
long kb = ByteSizeUnit.KB.toBytes(1);
462+
long mb = ByteSizeUnit.MB.toBytes(1);
463+
long gb = ByteSizeUnit.GB.toBytes(1);
464+
465+
LongVector f = actual.<LongBlock>getBlock(0).asVector();
466+
LongVector result = actual.<LongBlock>getBlock(1).asVector();
467+
for (int i = 0; i < BLOCK_LENGTH; i++) {
468+
long expected = f.getLong(i);
469+
if (expected < kb) {
470+
expected = b;
471+
} else if (expected < mb) {
472+
expected = kb;
473+
} else if (expected < gb) {
474+
expected = mb;
475+
} else {
476+
expected = gb;
477+
}
478+
if (result.getLong(i) != expected) {
479+
throw new AssertionError("[" + operation + "] expected [" + expected + "] but was [" + result.getLong(i) + "]");
480+
}
481+
}
482+
}
483+
case "round_to_3" -> {
484+
long b = 1;
485+
long kb = ByteSizeUnit.KB.toBytes(1);
486+
long mb = ByteSizeUnit.MB.toBytes(1);
487+
488+
LongVector f = actual.<LongBlock>getBlock(0).asVector();
489+
LongVector result = actual.<LongBlock>getBlock(1).asVector();
490+
for (int i = 0; i < BLOCK_LENGTH; i++) {
491+
long expected = f.getLong(i);
492+
if (expected < kb) {
493+
expected = b;
494+
} else if (expected < mb) {
495+
expected = kb;
496+
} else {
497+
expected = mb;
498+
}
499+
if (result.getLong(i) != expected) {
500+
throw new AssertionError("[" + operation + "] expected [" + expected + "] but was [" + result.getLong(i) + "]");
501+
}
502+
}
503+
}
504+
case "round_to_2" -> {
505+
long b = 1;
506+
long kb = ByteSizeUnit.KB.toBytes(1);
507+
508+
LongVector f = actual.<LongBlock>getBlock(0).asVector();
509+
LongVector result = actual.<LongBlock>getBlock(1).asVector();
510+
for (int i = 0; i < BLOCK_LENGTH; i++) {
511+
long expected = f.getLong(i);
512+
if (expected < kb) {
513+
expected = b;
514+
} else {
515+
expected = kb;
516+
}
517+
if (result.getLong(i) != expected) {
518+
throw new AssertionError("[" + operation + "] expected [" + expected + "] but was [" + result.getLong(i) + "]");
519+
}
520+
}
521+
}
393522
case "to_lower" -> checkBytes(operation, actual, false, new BytesRef[] { new BytesRef("foo"), new BytesRef("bar") });
394523
case "to_lower_ords" -> checkBytes(operation, actual, true, new BytesRef[] { new BytesRef("foo"), new BytesRef("bar") });
395524
case "to_upper" -> checkBytes(operation, actual, false, new BytesRef[] { new BytesRef("FOO"), new BytesRef("BAR") });
@@ -421,7 +550,7 @@ private static void checkBytes(String operation, Page actual, boolean expectOrds
421550

422551
private static Page page(String operation) {
423552
return switch (operation) {
424-
case "abs", "add", "date_trunc", "equal_to_const" -> {
553+
case "abs", "add", "date_trunc", "equal_to_const", "round_to_4_via_case", "round_to_2", "round_to_3", "round_to_4" -> {
425554
var builder = blockFactory.newLongBlockBuilder(BLOCK_LENGTH);
426555
for (int i = 0; i < BLOCK_LENGTH; i++) {
427556
builder.appendLong(i * 100_000);
@@ -511,6 +640,26 @@ private static Page page(String operation) {
511640
};
512641
}
513642

643+
private static Literal b() {
644+
return lit(1L);
645+
}
646+
647+
private static Literal kb() {
648+
return lit(ByteSizeUnit.KB.toBytes(1));
649+
}
650+
651+
private static Literal mb() {
652+
return lit(ByteSizeUnit.MB.toBytes(1));
653+
}
654+
655+
private static Literal gb() {
656+
return lit(ByteSizeUnit.GB.toBytes(1));
657+
}
658+
659+
private static Literal lit(long v) {
660+
return new Literal(Source.EMPTY, v, DataType.LONG);
661+
}
662+
514663
@Benchmark
515664
@OperationsPerInvocation(1024 * BLOCK_LENGTH)
516665
public void run() {

docs/changelog/128278.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 128278
2+
summary: ROUND_TO function
3+
area: ES|QL
4+
type: enhancement
5+
issues: []

docs/changelog/128397.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 128397
2+
summary: ROUND_TO function
3+
area: ES|QL
4+
type: enhancement
5+
issues: []

docs/reference/esql/functions/description/round_to.asciidoc

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/reference/esql/functions/examples/round_to.asciidoc

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)