Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent long value saturation within scaled_float mappings #105790

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
import org.apache.lucene.search.Query;
import org.elasticsearch.common.Explicit;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.index.IndexMode;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.index.IndexVersions;
import org.elasticsearch.index.fielddata.FieldData;
import org.elasticsearch.index.fielddata.FieldDataContext;
import org.elasticsearch.index.fielddata.IndexFieldData;
Expand All @@ -34,6 +35,7 @@
import org.elasticsearch.index.mapper.DocumentParserContext;
import org.elasticsearch.index.mapper.FieldMapper;
import org.elasticsearch.index.mapper.MapperBuilderContext;
import org.elasticsearch.index.mapper.MappingParserContext;
import org.elasticsearch.index.mapper.NumberFieldMapper;
import org.elasticsearch.index.mapper.SimpleMappedFieldType;
import org.elasticsearch.index.mapper.SortedNumericDocValuesSyntheticFieldLoader;
Expand Down Expand Up @@ -123,12 +125,25 @@ public static class Builder extends FieldMapper.Builder {
private final Parameter<TimeSeriesParams.MetricType> metric;

private final IndexMode indexMode;

public Builder(String name, Settings settings, IndexMode indexMode) {
this(name, IGNORE_MALFORMED_SETTING.get(settings), COERCE_SETTING.get(settings), indexMode);
private final IndexVersion indexVersion;

public Builder(String name, MappingParserContext parserContext) {
this(
name,
IGNORE_MALFORMED_SETTING.get(parserContext.getSettings()),
COERCE_SETTING.get(parserContext.getSettings()),
parserContext.getIndexSettings().getMode(),
parserContext.indexVersionCreated()
);
}

public Builder(String name, boolean ignoreMalformedByDefault, boolean coerceByDefault, IndexMode indexMode) {
public Builder(
String name,
boolean ignoreMalformedByDefault,
boolean coerceByDefault,
IndexMode indexMode,
IndexVersion indexVersion
) {
super(name);
this.ignoreMalformed = Parameter.explicitBoolParam(
"ignore_malformed",
Expand Down Expand Up @@ -157,6 +172,7 @@ public Builder(String name, boolean ignoreMalformedByDefault, boolean coerceByDe
);
}
});
this.indexVersion = indexVersion;
}

Builder scalingFactor(double scalingFactor) {
Expand Down Expand Up @@ -200,7 +216,7 @@ public ScaledFloatFieldMapper build(MapperBuilderContext context) {
}
}

public static final TypeParser PARSER = new TypeParser((n, c) -> new Builder(n, c.getSettings(), c.getIndexSettings().getMode()));
public static final TypeParser PARSER = new TypeParser((n, c) -> new Builder(n, c));

public static final class ScaledFloatFieldType extends SimpleMappedFieldType {

Expand Down Expand Up @@ -456,6 +472,7 @@ public String toString() {
private final boolean coerceByDefault;
private final TimeSeriesParams.MetricType metricType;
private final IndexMode indexMode;
private final IndexVersion indexVersion;

private ScaledFloatFieldMapper(
String simpleName,
Expand All @@ -476,6 +493,7 @@ private ScaledFloatFieldMapper(
this.coerceByDefault = builder.coerce.getDefaultValue().value();
this.metricType = builder.metric.getValue();
this.indexMode = builder.indexMode;
this.indexVersion = builder.indexVersion;
}

boolean coerce() {
Expand All @@ -499,7 +517,7 @@ protected String contentType() {

@Override
public FieldMapper.Builder getMergeBuilder() {
return new Builder(simpleName(), ignoreMalformedByDefault, coerceByDefault, indexMode).metric(metricType).init(this);
return new Builder(simpleName(), ignoreMalformedByDefault, coerceByDefault, indexMode, indexVersion).metric(metricType).init(this);
}

@Override
Expand Down Expand Up @@ -547,7 +565,11 @@ protected void parseCreateField(DocumentParserContext context) throws IOExceptio
throw new IllegalArgumentException("[scaled_float] only supports finite values, but got [" + doubleValue + "]");
}
}
long scaledValue = encode(doubleValue, scalingFactor);
long scaledValue = encode(
doubleValue,
scalingFactor,
ignoreMalformed.value() == false && indexVersion.onOrAfter(IndexVersions.STRICT_SCALED_FLOAT_PARSING)
);

NumberFieldMapper.NumberType.LONG.addFields(context.doc(), fieldType().name(), scaledValue, indexed, hasDocValues, stored);

Expand All @@ -556,8 +578,22 @@ protected void parseCreateField(DocumentParserContext context) throws IOExceptio
}
}

static long encode(double value, double scalingFactor) {
return Math.round(value * scalingFactor);
static long encode(double value, double scalingFactor, boolean strict) {
double scaled = value * scalingFactor;
if (strict) {
if (scaled > Long.MAX_VALUE || scaled < Long.MIN_VALUE) {
throw new IllegalArgumentException(
"[scaled_float] value ["
+ scaled
+ "] input ["
+ value
+ "] and scaling factor ["
+ scalingFactor
+ "] is too large to be represented as a long"
);
}
}
return Math.round(scaled);
}

private static class ScaledFloatIndexFieldData extends IndexNumericFieldData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ protected Number randomNumber() {

public void testEncodeDecodeExactScalingFactor() {
double v = randomValue();
assertThat(encodeDecode(1 / v, v), equalTo(1 / v));
assertThat(encodeDecode(1 / v, v, false), equalTo(1 / v));
}

/**
Expand All @@ -501,7 +501,7 @@ public void testEncodeDecodeNoSaturation() {
double scalingFactor = randomValue();
double unsaturated = randomDoubleBetween(Long.MIN_VALUE / scalingFactor, Long.MAX_VALUE / scalingFactor, true);
assertEquals(
encodeDecode(unsaturated, scalingFactor),
encodeDecode(unsaturated, scalingFactor, true),
Math.round(unsaturated * scalingFactor) / scalingFactor,
unsaturated * 1e-10
);
Expand All @@ -517,12 +517,12 @@ public void testEncodeDecodeSaturatedLow() {
if (min * scalingFactor != Long.MIN_VALUE) {
min -= Math.ulp(min);
}
assertThat(ScaledFloatFieldMapper.encode(min, scalingFactor), equalTo(Long.MIN_VALUE));
assertThat(encodeDecode(min, scalingFactor), equalTo(min));
assertThat(ScaledFloatFieldMapper.encode(min, scalingFactor, false), equalTo(Long.MIN_VALUE));
assertThat(encodeDecode(min, scalingFactor, false), equalTo(min));

double saturated = randomDoubleBetween(-Double.MAX_VALUE, min, true);
assertThat(ScaledFloatFieldMapper.encode(saturated, scalingFactor), equalTo(Long.MIN_VALUE));
assertThat(encodeDecode(saturated, scalingFactor), equalTo(min));
assertThat(ScaledFloatFieldMapper.encode(saturated, scalingFactor, false), equalTo(Long.MIN_VALUE));
assertThat(encodeDecode(saturated, scalingFactor, false), equalTo(min));
}

/**
Expand All @@ -535,19 +535,21 @@ public void testEncodeDecodeSaturatedHigh() {
if (max * scalingFactor != Long.MAX_VALUE) {
max += Math.ulp(max);
}
assertThat(ScaledFloatFieldMapper.encode(max, scalingFactor), equalTo(Long.MAX_VALUE));
assertThat(encodeDecode(max, scalingFactor), equalTo(max));
assertThat(ScaledFloatFieldMapper.encode(max, scalingFactor, false), equalTo(Long.MAX_VALUE));
assertThat(encodeDecode(max, scalingFactor, false), equalTo(max));

double saturated = randomDoubleBetween(max, Double.MAX_VALUE, true);
assertThat(ScaledFloatFieldMapper.encode(saturated, scalingFactor), equalTo(Long.MAX_VALUE));
assertThat(encodeDecode(saturated, scalingFactor), equalTo(max));
assertThat(ScaledFloatFieldMapper.encode(saturated, scalingFactor, false), equalTo(Long.MAX_VALUE));
assertThat(encodeDecode(saturated, scalingFactor, false), equalTo(max));
}

public void testEncodeDecodeRandom() {
double scalingFactor = randomDoubleBetween(0, Double.MAX_VALUE, false);
double v = randomValue();
double once = encodeDecode(v, scalingFactor);
double twice = encodeDecode(once, scalingFactor);
double scalingFactor = randomDoubleBetween(0, Long.MAX_VALUE / 2.0, false);
double v = randomBoolean()
? randomDoubleBetween((Long.MIN_VALUE + 1) / scalingFactor, (Long.MAX_VALUE - 1) / scalingFactor, true)
: randomFloat();
double once = encodeDecode(v, scalingFactor, true);
double twice = encodeDecode(once, scalingFactor, true);
assertThat(twice, equalTo(once));
}

Expand All @@ -560,8 +562,8 @@ public void testEncodeDecodeRandom() {
public void testEncodeDecodeNeedNudge() {
double scalingFactor = 2.4206374697469164E16;
double v = 0.15527719259262085;
double once = encodeDecode(v, scalingFactor);
double twice = encodeDecode(once, scalingFactor);
double once = encodeDecode(v, scalingFactor, true);
double twice = encodeDecode(once, scalingFactor, true);
assertThat(twice, equalTo(once));
}

Expand All @@ -575,13 +577,13 @@ public void testDecodeEncode() {
double scalingFactor = randomValueOtherThanMany(d -> Double.isInfinite(Long.MAX_VALUE / d), ESTestCase::randomDouble);
long encoded = randomLongBetween(-2 << 53, 2 << 53);
assertThat(
ScaledFloatFieldMapper.encode(ScaledFloatFieldMapper.decodeForSyntheticSource(encoded, scalingFactor), scalingFactor),
ScaledFloatFieldMapper.encode(ScaledFloatFieldMapper.decodeForSyntheticSource(encoded, scalingFactor), scalingFactor, true),
equalTo(encoded)
);
}

private double encodeDecode(double value, double scalingFactor) {
return ScaledFloatFieldMapper.decodeForSyntheticSource(ScaledFloatFieldMapper.encode(value, scalingFactor), scalingFactor);
private double encodeDecode(double value, double scalingFactor, boolean strict) {
return ScaledFloatFieldMapper.decodeForSyntheticSource(ScaledFloatFieldMapper.encode(value, scalingFactor, strict), scalingFactor);
}

private static double randomValue() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.apache.lucene.util.NumericUtils;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.core.IOUtils;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.index.fielddata.FieldDataContext;
import org.elasticsearch.index.fielddata.IndexNumericFieldData;
import org.elasticsearch.index.fielddata.LeafNumericFieldData;
Expand Down Expand Up @@ -219,14 +220,15 @@ public void testFieldData() throws IOException {
}

public void testFetchSourceValue() throws IOException {
MappedFieldType mapper = new ScaledFloatFieldMapper.Builder("field", false, false, null).scalingFactor(100)
MappedFieldType mapper = new ScaledFloatFieldMapper.Builder("field", false, false, null, IndexVersion.current()).scalingFactor(100)
.build(MapperBuilderContext.root(false, false))
.fieldType();
assertEquals(List.of(3.14), fetchSourceValue(mapper, 3.1415926));
assertEquals(List.of(3.14), fetchSourceValue(mapper, "3.1415"));
assertEquals(List.of(), fetchSourceValue(mapper, ""));

MappedFieldType nullValueMapper = new ScaledFloatFieldMapper.Builder("field", false, false, null).scalingFactor(100)
MappedFieldType nullValueMapper = new ScaledFloatFieldMapper.Builder("field", false, false, null, IndexVersion.current())
.scalingFactor(100)
.nullValue(2.71)
.build(MapperBuilderContext.root(false, false))
.fieldType();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,32 @@ setup:
number:
order: asc
- match: { hits.hits.0.fields.number: [-2.1] }

---
"Disabled saturation of long values":
- skip:
version: " - 8.13.99"
reason: disabled saturation of long values is not supported in versions before 8.14.0
- do:
catch: bad_request
index:
index: test
id: "5"
body: { "number" : 9223372036854775807 }
- do:
indices.create:
index: test-saturation
body:
settings:
number_of_replicas: 0
mappings:
"properties":
"number":
"type" : "scaled_float"
"scaling_factor": 100
"ignore_malformed": true
- do:
index:
index: test-saturation
id: "5"
body: { "number" : 9223372036854775807 }
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ private static IndexVersion def(int id, Version luceneVersion) {
public static final IndexVersion NEW_INDEXVERSION_FORMAT = def(8_501_00_0, Version.LUCENE_9_9_1);
public static final IndexVersion UPGRADE_LUCENE_9_9_2 = def(8_502_00_0, Version.LUCENE_9_9_2);
public static final IndexVersion TIME_SERIES_ID_HASHING = def(8_502_00_1, Version.LUCENE_9_9_2);
public static final IndexVersion STRICT_SCALED_FLOAT_PARSING = def(8_502_00_2, Version.LUCENE_9_9_2);

/*
* STOP! READ THIS FIRST! No, really,
Expand Down