Răsfoiți Sursa

Handle infinity during synthetic source construction for scaled float field (#107494)

For really large values, rounding error is enough to push the
reconstructed value for synthetic source into infinity. Existing code
didn't take it into account. This PR adds a check to detect infinity and
just proceed with returning it as is in synthetic source.

Closes #107101.
Oleksandr Kolomiiets 1 an în urmă
părinte
comite
f28529d237

+ 6 - 0
docs/changelog/107494.yaml

@@ -0,0 +1,6 @@
+pr: 107494
+summary: Handle infinity during synthetic source construction for scaled float field
+area: Mapping
+type: bug
+issues:
+ - 107101

+ 14 - 0
modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/ScaledFloatFieldMapper.java

@@ -749,6 +749,20 @@ public class ScaledFloatFieldMapper extends FieldMapper {
      */
     static double decodeForSyntheticSource(long scaledValue, double scalingFactor) {
         double v = scaledValue / scalingFactor;
+
+        // If original double value is close to MAX_VALUE
+        // and rounding is performed in the direction of the same infinity
+        // it is possible to "overshoot" infinity during reconstruction.
+        // E.g. for a value close to Double.MAX_VALUE "true" scaled value is 10.5
+        // and with rounding it becomes 11.
+        // Now, because of that rounding difference, 11 divided by scaling factor goes into infinity.
+        // There is nothing we can do about it so we'll return the closest finite value to infinity
+        // which is MAX_VALUE.
+        if (Double.isInfinite(v)) {
+            var sign = v == Double.POSITIVE_INFINITY ? 1 : -1;
+            return sign * Double.MAX_VALUE;
+        }
+
         long reenc = Math.round(v * scalingFactor);
         if (reenc != scaledValue) {
             if (reenc > scaledValue) {

+ 34 - 1
modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/ScaledFloatFieldMapperTests.java

@@ -395,6 +395,12 @@ public class ScaledFloatFieldMapperTests extends NumberFieldMapperTests {
         private double round(double d) {
             long encoded = Math.round(d * scalingFactor);
             double decoded = encoded / scalingFactor;
+            // Special case due to rounding, see implementation.
+            if (Double.isInfinite(decoded)) {
+                var sign = decoded == Double.POSITIVE_INFINITY ? 1 : -1;
+                return sign * Double.MAX_VALUE;
+            }
+
             long reencoded = Math.round(decoded * scalingFactor);
             if (encoded != reencoded) {
                 if (encoded > reencoded) {
@@ -406,6 +412,11 @@ public class ScaledFloatFieldMapperTests extends NumberFieldMapperTests {
         }
 
         private double roundDocValues(double d) {
+            // Special case due to rounding, see implementation.
+            if (Math.abs(d) == Double.MAX_VALUE) {
+                return d;
+            }
+
             long encoded = Math.round(d * scalingFactor);
             return encoded * (1 / scalingFactor);
         }
@@ -526,7 +537,7 @@ public class ScaledFloatFieldMapperTests extends NumberFieldMapperTests {
     }
 
     /**
-     * Tests that numbers whose encoded value is {@code Long.MIN_VALUE} can be round
+     * Tests that numbers whose encoded value is {@code Long.MAX_VALUE} can be round
      * tripped through synthetic source.
      */
     public void testEncodeDecodeSaturatedHigh() {
@@ -580,6 +591,28 @@ public class ScaledFloatFieldMapperTests extends NumberFieldMapperTests {
         );
     }
 
+    /**
+     * Tests the case when decoded value is infinite due to rounding.
+     */
+    public void testDecodeHandlingInfinity() {
+        for (var sign : new long[] { 1, -1 }) {
+            long encoded = 101;
+            double encodedNoRounding = 100.5;
+            assertEquals(encoded, Math.round(encodedNoRounding));
+
+            var signedMax = sign * Double.MAX_VALUE;
+            // We need a scaling factor that will
+            // 1. make encoded long small resulting in significant loss of precision due to rounding
+            // 2. result in long value being rounded in correct direction.
+            //
+            // So we take a scaling factor that would put us right at MAX_VALUE
+            // without rounding and hence go beyond MAX_VALUE with rounding.
+            double scalingFactor = (encodedNoRounding / signedMax);
+
+            assertThat(ScaledFloatFieldMapper.decodeForSyntheticSource(encoded, scalingFactor), equalTo(signedMax));
+        }
+    }
+
     private double encodeDecode(double value, double scalingFactor) {
         return ScaledFloatFieldMapper.decodeForSyntheticSource(ScaledFloatFieldMapper.encode(value, scalingFactor), scalingFactor);
     }