Add an error margin when comparing floats. (#129721)

We add a margin of error when comparing floats to the DynamicFieldMatcher to account for a small loss of accuracy when loading fields from synthetic source.
This commit is contained in:
Mary Gouseti 2025-06-23 18:46:46 +03:00 committed by GitHub
parent 816caf70fc
commit d859366d4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 37 additions and 26 deletions

View file

@ -529,15 +529,6 @@ tests:
- class: org.elasticsearch.xpack.esql.qa.multi_node.EsqlSpecIT - class: org.elasticsearch.xpack.esql.qa.multi_node.EsqlSpecIT
method: test {knn-function.KnnSearchWithKOption SYNC} method: test {knn-function.KnnSearchWithKOption SYNC}
issue: https://github.com/elastic/elasticsearch/issues/129512 issue: https://github.com/elastic/elasticsearch/issues/129512
- class: org.elasticsearch.xpack.logsdb.qa.StandardVersusStandardReindexedIntoLogsDbChallengeRestIT
method: testMatchAllQuery
issue: https://github.com/elastic/elasticsearch/issues/129527
- class: org.elasticsearch.xpack.logsdb.qa.BulkChallengeRestIT
method: testMatchAllQuery
issue: https://github.com/elastic/elasticsearch/issues/129528
- class: org.elasticsearch.xpack.logsdb.qa.StoredSourceLogsDbVersusReindexedLogsDbChallengeRestIT
method: testMatchAllQuery
issue: https://github.com/elastic/elasticsearch/issues/129529
- class: org.elasticsearch.index.engine.ThreadPoolMergeExecutorServiceTests - class: org.elasticsearch.index.engine.ThreadPoolMergeExecutorServiceTests
method: testIORateIsAdjustedForAllRunningMergeTasks method: testIORateIsAdjustedForAllRunningMergeTasks
issue: https://github.com/elastic/elasticsearch/issues/129531 issue: https://github.com/elastic/elasticsearch/issues/129531

View file

@ -17,14 +17,14 @@ import org.elasticsearch.xcontent.XContentBuilder;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.function.Supplier;
import static org.elasticsearch.datageneration.matchers.Messages.formatErrorMessage; import static org.elasticsearch.datageneration.matchers.Messages.formatErrorMessage;
import static org.elasticsearch.datageneration.matchers.Messages.prettyPrintCollections; import static org.elasticsearch.datageneration.matchers.Messages.prettyPrintCollections;
class DynamicFieldMatcher { class DynamicFieldMatcher {
private static final double FLOAT_ERROR_MARGIN = 1e-8;
private final XContentBuilder actualMappings; private final XContentBuilder actualMappings;
private final Settings.Builder actualSettings; private final Settings.Builder actualSettings;
private final XContentBuilder expectedMappings; private final XContentBuilder expectedMappings;
@ -62,10 +62,7 @@ class DynamicFieldMatcher {
var normalizedActual = normalizeDoubles(actual); var normalizedActual = normalizeDoubles(actual);
var normalizedExpected = normalizeDoubles(expected); var normalizedExpected = normalizeDoubles(expected);
Supplier<MatchResult> noMatchSupplier = () -> MatchResult.noMatch(
return normalizedActual.equals(normalizedExpected)
? MatchResult.match()
: MatchResult.noMatch(
formatErrorMessage( formatErrorMessage(
actualMappings, actualMappings,
actualSettings, actualSettings,
@ -75,18 +72,41 @@ class DynamicFieldMatcher {
+ prettyPrintCollections(normalizedActual, normalizedExpected) + prettyPrintCollections(normalizedActual, normalizedExpected)
) )
); );
if (normalizedActual.size() != normalizedExpected.size()) {
return noMatchSupplier.get();
}
for (int i = 0; i < normalizedActual.size(); i++) {
if (floatEquals(normalizedActual.get(i), normalizedExpected.get(i)) == false) {
return noMatchSupplier.get();
}
}
return MatchResult.match();
} }
return matchWithGenericMatcher(actual, expected); return matchWithGenericMatcher(actual, expected);
} }
private static Set<Float> normalizeDoubles(List<Object> values) { /**
* We make the normalisation of double values stricter than {@link SourceTransforms#normalizeValues} to facilitate the equality of the
* values within a margin of error. Synthetic source does support duplicate values and preserves the order, but it loses some accuracy,
* this is why the margin of error is very important. In the future, we can make {@link SourceTransforms#normalizeValues} also stricter.
*/
private static List<Float> normalizeDoubles(List<Object> values) {
if (values == null) { if (values == null) {
return Set.of(); return List.of();
} }
Function<Object, Float> toFloat = (o) -> o instanceof Number n ? n.floatValue() : Float.parseFloat((String) o); Function<Object, Float> toFloat = (o) -> o instanceof Number n ? n.floatValue() : Float.parseFloat((String) o);
return values.stream().filter(Objects::nonNull).map(toFloat).collect(Collectors.toSet());
// We skip nulls because they trip the pretty print collections.
return values.stream().filter(Objects::nonNull).map(toFloat).toList();
}
private static boolean floatEquals(Float actual, Float expected) {
return Math.abs(actual - expected) < FLOAT_ERROR_MARGIN;
} }
private MatchResult matchWithGenericMatcher(List<Object> actualValues, List<Object> expectedValues) { private MatchResult matchWithGenericMatcher(List<Object> actualValues, List<Object> expectedValues) {