diff --git a/server/src/main/java/org/opensearch/index/mapper/NumberFieldMapper.java b/server/src/main/java/org/opensearch/index/mapper/NumberFieldMapper.java index b18927aebe265..910c687c42949 100644 --- a/server/src/main/java/org/opensearch/index/mapper/NumberFieldMapper.java +++ b/server/src/main/java/org/opensearch/index/mapper/NumberFieldMapper.java @@ -1050,7 +1050,7 @@ Number valueForSearch(String value) { UNSIGNED_LONG("unsigned_long", NumericType.UNSIGNED_LONG) { @Override public BigInteger parse(Object value, boolean coerce) { - return objectToUnsignedLong(value, coerce); + return objectToUnsignedLong(value, coerce, false); } @Override @@ -1060,7 +1060,7 @@ public Number parsePoint(byte[] value) { @Override public void encodePoint(Number value, byte[] point) { - BigIntegerPoint.encodeDimension(objectToUnsignedLong(value, false), point, 0); + BigIntegerPoint.encodeDimension(objectToUnsignedLong(value, false, true), point, 0); } @Override @@ -1300,15 +1300,25 @@ public static long objectToLong(Object value, boolean coerce) { } /** - * Converts and Object to a {@code long} by checking it against known + * Converts an Object to a {@code BigInteger} by checking it against known * types and checking its range. + * + * @param lenientBound if true, use MIN or MAX if the value is out of bound */ - public static BigInteger objectToUnsignedLong(Object value, boolean coerce) { + public static BigInteger objectToUnsignedLong(Object value, boolean coerce, boolean lenientBound) { if (value instanceof Long) { return Numbers.toUnsignedBigInteger(((Long) value).longValue()); } double doubleValue = objectToDouble(value); + if (lenientBound) { + if (doubleValue < Numbers.MIN_UNSIGNED_LONG_VALUE.doubleValue()) { + return Numbers.MIN_UNSIGNED_LONG_VALUE; + } + if (doubleValue > Numbers.MAX_UNSIGNED_LONG_VALUE.doubleValue()) { + return Numbers.MAX_UNSIGNED_LONG_VALUE; + } + } if (doubleValue < Numbers.MIN_UNSIGNED_LONG_VALUE.doubleValue() || doubleValue > Numbers.MAX_UNSIGNED_LONG_VALUE.doubleValue()) { throw new IllegalArgumentException("Value [" + value + "] is out of range for an unsigned long"); @@ -1400,7 +1410,7 @@ public static Query unsignedLongRangeQuery( BigInteger l = Numbers.MIN_UNSIGNED_LONG_VALUE; BigInteger u = Numbers.MAX_UNSIGNED_LONG_VALUE; if (lowerTerm != null) { - l = objectToUnsignedLong(lowerTerm, true); + l = objectToUnsignedLong(lowerTerm, true, false); // if the lower bound is decimal: // - if the bound is positive then we increment it: // if lowerTerm=1.5 then the (inclusive) bound becomes 2 @@ -1415,7 +1425,7 @@ public static Query unsignedLongRangeQuery( } } if (upperTerm != null) { - u = objectToUnsignedLong(upperTerm, true); + u = objectToUnsignedLong(upperTerm, true, false); boolean upperTermHasDecimalPart = hasDecimalPart(upperTerm); if ((upperTermHasDecimalPart == false && includeUpper == false) || (upperTermHasDecimalPart && signum(upperTerm) < 0)) { if (u.compareTo(Numbers.MAX_UNSIGNED_LONG_VALUE) == 0) { diff --git a/server/src/test/java/org/opensearch/search/aggregations/bucket/range/RangeAggregatorTests.java b/server/src/test/java/org/opensearch/search/aggregations/bucket/range/RangeAggregatorTests.java index 342691c14b9b0..fab1c830f6fd4 100644 --- a/server/src/test/java/org/opensearch/search/aggregations/bucket/range/RangeAggregatorTests.java +++ b/server/src/test/java/org/opensearch/search/aggregations/bucket/range/RangeAggregatorTests.java @@ -66,7 +66,10 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -338,6 +341,65 @@ public void testOverlappingRanges() throws IOException { ); } + /** + * @return Map [lower, upper) -> data points + */ + private Map buildRandomRanges(double[][] possibleRanges) { + Map dataSet = new LinkedHashMap<>(); + for (double[] range : possibleRanges) { + double lower = randomDoubleBetween(range[0], range[1], true); + double upper = randomDoubleBetween(range[0], range[1], true); + if (lower > upper) { + double d = lower; + lower = upper; + upper = d; + } + + int dataNumber = randomInt(200); + double[] data = new double[dataNumber]; + for (int i = 0; i < dataNumber; i++) { + data[i] = randomDoubleBetween(lower, upper, true); + } + dataSet.put(new double[] { lower, upper }, data); + } + + return dataSet; + } + + public void testRandomRanges() throws IOException { + Map dataSet = buildRandomRanges(new double[][] { { 0, 100 }, { 200, 1000 }, { 1000, 3000 } }); + + int size = dataSet.size(); + double[][] ranges = new double[size][]; + int[] expected = new int[size]; + List dataPoints = new LinkedList<>(); + + int i = 0; + for (Map.Entry entry : dataSet.entrySet()) { + ranges[i] = entry.getKey(); + expected[i] = entry.getValue().length; + for (double dataPoint : entry.getValue()) { + dataPoints.add(dataPoint); + } + i++; + } + + testRewriteOptimizationCase( + new NumberFieldType(NumberType.DOUBLE.typeName(), NumberType.DOUBLE), + ranges, + new MatchAllDocsQuery(), + dataPoints.toArray(new Number[0]), + range -> { + List rangeBuckets = range.getBuckets(); + assertEquals(size, rangeBuckets.size()); + for (int j = 0; j < rangeBuckets.size(); j++) { + assertEquals(expected[j], rangeBuckets.get(j).getDocCount()); + } + }, + true + ); + } + public void testDoubleType() throws IOException { testRewriteOptimizationCase( new NumberFieldType(NumberType.DOUBLE.typeName(), NumberType.DOUBLE), @@ -412,6 +474,23 @@ public void testUnsignedLongType() throws IOException { }, true ); + + testRewriteOptimizationCase( + new NumberFieldType(NumberType.UNSIGNED_LONG.typeName(), NumberType.UNSIGNED_LONG), + new double[][] { { Double.NEGATIVE_INFINITY, 1 }, { 2, Double.POSITIVE_INFINITY } }, + new MatchAllDocsQuery(), + new Number[] { 0, 1, 2 }, + range -> { + List ranges = range.getBuckets(); + assertEquals(2, ranges.size()); + assertEquals("*-1.0", ranges.get(0).getKeyAsString()); + assertEquals(1, ranges.get(0).getDocCount()); + assertEquals("2.0-*", ranges.get(1).getKeyAsString()); + assertEquals(1, ranges.get(1).getDocCount()); + assertTrue(AggregationInspectionHelper.hasValue(range)); + }, + true + ); } private void testCase(