diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/script/ScriptScoreBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/script/ScriptScoreBenchmark.java index f5d2bacedaa86..a51f9f67b91f9 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/script/ScriptScoreBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/script/ScriptScoreBenchmark.java @@ -83,7 +83,7 @@ public class ScriptScoreBenchmark { private final Map fieldTypes = org.elasticsearch.core.Map.ofEntries( org.elasticsearch.core.Map.entry( "n", - new NumberFieldType("n", NumberType.LONG, false, false, true, true, null, org.elasticsearch.core.Map.of(), null) + new NumberFieldType("n", NumberType.LONG, false, false, true, true, null, org.elasticsearch.core.Map.of(), null, false, null) ) ); private final IndexFieldDataCache fieldDataCache = new IndexFieldDataCache.None(); diff --git a/docs/reference/mapping/types/keyword.asciidoc b/docs/reference/mapping/types/keyword.asciidoc index 0a9e445d1309c..1e27c2af943b9 100644 --- a/docs/reference/mapping/types/keyword.asciidoc +++ b/docs/reference/mapping/types/keyword.asciidoc @@ -171,4 +171,3 @@ Dimension fields have the following constraints: include::constant-keyword.asciidoc[] include::wildcard.asciidoc[] - diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/ScaledFloatFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/ScaledFloatFieldMapper.java index c69eabaa09116..677d65457c39b 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/ScaledFloatFieldMapper.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/ScaledFloatFieldMapper.java @@ -85,6 +85,13 @@ public static class Builder extends FieldMapper.Builder { private final Parameter> meta = Parameter.metaParam(); + /** + * Parameter that marks this field as a time series metric defining its time series metric type. + * For the numeric fields gauge and counter metric types are + * supported + */ + private final Parameter metric; + public Builder(String name, Settings settings) { this(name, IGNORE_MALFORMED_SETTING.get(settings), COERCE_SETTING.get(settings)); } @@ -95,6 +102,18 @@ public Builder(String name, boolean ignoreMalformedByDefault, boolean coerceByDe = Parameter.explicitBoolParam("ignore_malformed", true, m -> toType(m).ignoreMalformed, ignoreMalformedByDefault); this.coerce = Parameter.explicitBoolParam("coerce", true, m -> toType(m).coerce, coerceByDefault); + + this.metric = TimeSeriesParams.metricParam( + m -> toType(m).metricType, + TimeSeriesParams.MetricType.gauge, + TimeSeriesParams.MetricType.counter + ).addValidator(v -> { + if (v != null && hasDocValues.getValue() == false) { + throw new IllegalArgumentException( + "Field [" + TimeSeriesParams.TIME_SERIES_METRIC_PARAM + "] requires that [" + hasDocValues.name + "] is true" + ); + } + }); } Builder scalingFactor(double scalingFactor) { @@ -107,15 +126,28 @@ Builder nullValue(double nullValue) { return this; } + public Builder metric(TimeSeriesParams.MetricType metric) { + this.metric.setValue(metric); + return this; + } + @Override protected List> getParameters() { - return Arrays.asList(indexed, hasDocValues, stored, ignoreMalformed, meta, scalingFactor, coerce, nullValue); + return Arrays.asList(indexed, hasDocValues, stored, ignoreMalformed, meta, scalingFactor, coerce, nullValue, metric); } @Override public ScaledFloatFieldMapper build(MapperBuilderContext context) { - ScaledFloatFieldType type = new ScaledFloatFieldType(context.buildFullName(name), indexed.getValue(), stored.getValue(), - hasDocValues.getValue(), meta.getValue(), scalingFactor.getValue(), nullValue.getValue()); + ScaledFloatFieldType type = new ScaledFloatFieldType( + context.buildFullName(name), + indexed.getValue(), + stored.getValue(), + hasDocValues.getValue(), + meta.getValue(), + scalingFactor.getValue(), + nullValue.getValue(), + metric.getValue() + ); return new ScaledFloatFieldMapper(name, type, multiFieldsBuilder.build(this, context), copyTo.build(), this); } } @@ -126,16 +158,20 @@ public static final class ScaledFloatFieldType extends SimpleMappedFieldType { private final double scalingFactor; private final Double nullValue; + private final TimeSeriesParams.MetricType metricType; + public ScaledFloatFieldType(String name, boolean indexed, boolean stored, boolean hasDocValues, - Map meta, double scalingFactor, Double nullValue) { + Map meta, double scalingFactor, Double nullValue, + TimeSeriesParams.MetricType metricType) { super(name, indexed, stored, hasDocValues, TextSearchInfo.SIMPLE_MATCH_WITHOUT_TERMS, meta); this.scalingFactor = scalingFactor; this.nullValue = nullValue; + this.metricType = metricType; } public ScaledFloatFieldType(String name, double scalingFactor) { - this(name, true, false, true, Collections.emptyMap(), scalingFactor, null); + this(name, true, false, true, Collections.emptyMap(), scalingFactor, null, null); } public double getScalingFactor() { @@ -266,6 +302,14 @@ public DocValueFormat docValueFormat(String format, ZoneId timeZone) { private double scale(Object input) { return new BigDecimal(Double.toString(parse(input))).multiply(BigDecimal.valueOf(scalingFactor)).doubleValue(); } + + /** + * If field is a time series metric field, returns its metric type + * @return the metric type or null + */ + public TimeSeriesParams.MetricType getMetricType() { + return metricType; + } } private final Explicit ignoreMalformed; @@ -278,6 +322,7 @@ private double scale(Object input) { private final boolean ignoreMalformedByDefault; private final boolean coerceByDefault; + private final TimeSeriesParams.MetricType metricType; private ScaledFloatFieldMapper( String simpleName, @@ -295,6 +340,7 @@ private ScaledFloatFieldMapper( this.coerce = builder.coerce.getValue(); this.ignoreMalformedByDefault = builder.ignoreMalformed.getDefaultValue().value(); this.coerceByDefault = builder.coerce.getDefaultValue().value(); + this.metricType = builder.metric.getValue(); } boolean coerce() { @@ -317,12 +363,11 @@ protected String contentType() { @Override public FieldMapper.Builder getMergeBuilder() { - return new Builder(simpleName(), ignoreMalformedByDefault, coerceByDefault).init(this); + return new Builder(simpleName(), ignoreMalformedByDefault, coerceByDefault).metric(metricType).init(this); } @Override protected void parseCreateField(DocumentParserContext context) throws IOException { - XContentParser parser = context.parser(); Object value; Number numericValue = null; diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/TokenCountFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/TokenCountFieldMapper.java index fe3015a8a9962..084df18e7ade3 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/TokenCountFieldMapper.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/TokenCountFieldMapper.java @@ -75,9 +75,27 @@ public TokenCountFieldMapper build(MapperBuilderContext context) { static class TokenCountFieldType extends NumberFieldMapper.NumberFieldType { - TokenCountFieldType(String name, boolean isSearchable, boolean isStored, - boolean hasDocValues, Number nullValue, Map meta) { - super(name, NumberFieldMapper.NumberType.INTEGER, isSearchable, isStored, hasDocValues, false, nullValue, meta, null); + TokenCountFieldType( + String name, + boolean isSearchable, + boolean isStored, + boolean hasDocValues, + Number nullValue, + Map meta + ) { + super( + name, + NumberFieldMapper.NumberType.INTEGER, + isSearchable, + isStored, + hasDocValues, + false, + nullValue, + meta, + null, + false, + null + ); } @Override diff --git a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/ScaledFloatFieldMapperTests.java b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/ScaledFloatFieldMapperTests.java index 18d1b27b1f84d..6474413752924 100644 --- a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/ScaledFloatFieldMapperTests.java +++ b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/ScaledFloatFieldMapperTests.java @@ -272,6 +272,47 @@ public void testRejectIndexOptions() { assertWarnings("Parameter [index_options] has no effect on type [scaled_float] and will be removed in future"); } + public void testMetricType() throws IOException { + // Test default setting + MapperService mapperService = createMapperService(fieldMapping(b -> minimalMapping(b))); + ScaledFloatFieldMapper.ScaledFloatFieldType ft = (ScaledFloatFieldMapper.ScaledFloatFieldType) mapperService.fieldType("field"); + assertNull(ft.getMetricType()); + + assertMetricType("gauge", ScaledFloatFieldMapper.ScaledFloatFieldType::getMetricType); + assertMetricType("counter", ScaledFloatFieldMapper.ScaledFloatFieldType::getMetricType); + + { + // Test invalid metric type for this field type + Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(fieldMapping(b -> { + minimalMapping(b); + b.field("time_series_metric", "histogram"); + }))); + assertThat( + e.getCause().getMessage(), + containsString("Unknown value [histogram] for field [time_series_metric] - accepted values are [gauge, counter]") + ); + } + { + // Test invalid metric type for this field type + Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(fieldMapping(b -> { + minimalMapping(b); + b.field("time_series_metric", "unknown"); + }))); + assertThat( + e.getCause().getMessage(), + containsString("Unknown value [unknown] for field [time_series_metric] - accepted values are [gauge, counter]") + ); + } + } + + public void testMetricAndDocvalues() { + Exception e = expectThrows(MapperParsingException.class, () -> createDocumentMapper(fieldMapping(b -> { + minimalMapping(b); + b.field("time_series_metric", "counter").field("doc_values", false); + }))); + assertThat(e.getCause().getMessage(), containsString("Field [time_series_metric] requires that [doc_values] is true")); + } + @Override protected void randomFetchTestFieldConfig(XContentBuilder b) throws IOException { // Large floats are a terrible idea but the round trip should still work no matter how badly you configure the field diff --git a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/ScaledFloatFieldTypeTests.java b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/ScaledFloatFieldTypeTests.java index 29f93241f073b..da06d36a0b23f 100644 --- a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/ScaledFloatFieldTypeTests.java +++ b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/ScaledFloatFieldTypeTests.java @@ -54,7 +54,15 @@ public void testRangeQuery() throws IOException { // this test checks that searching scaled floats yields the same results as // searching doubles that are rounded to the closest half float ScaledFloatFieldMapper.ScaledFloatFieldType ft = new ScaledFloatFieldMapper.ScaledFloatFieldType( - "scaled_float", true, false, false, Collections.emptyMap(), 0.1 + randomDouble() * 100, null); + "scaled_float", + true, + false, + false, + Collections.emptyMap(), + 0.1 + randomDouble() * 100, + null, + null + ); Directory dir = newDirectory(); IndexWriter w = new IndexWriter(dir, new IndexWriterConfig(null)); final int numDocs = 1000; diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/10_settings.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/10_settings.yml index 11644c3052119..f8c23412cd51f 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/10_settings.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/10_settings.yml @@ -18,12 +18,14 @@ enable: type: date metricset: type: keyword + time_series_dimension: true k8s: properties: pod: properties: uid: type: keyword + time_series_dimension: true name: type: keyword ip: diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/20_mappings.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/20_mappings.yml new file mode 100644 index 0000000000000..e747f068f6f67 --- /dev/null +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/20_mappings.yml @@ -0,0 +1,50 @@ +add time series mappings: + - skip: + version: " - 7.99.99" + reason: introduced in 8.0.0 to be backported to 7.16.0 + + - do: + indices.create: + index: test_index + body: + settings: + index: + mode: time_series + number_of_replicas: 0 + number_of_shards: 2 + mappings: + properties: + "@timestamp": + type: date + metricset: + type: keyword + time_series_dimension: true + k8s: + properties: + pod: + properties: + availability_zone: + type: short + time_series_dimension: true + uid: + type: keyword + time_series_dimension: true + name: + type: keyword + ip: + type: ip + time_series_dimension: true + network: + properties: + tx: + type: long + time_series_metric: counter + rx: + type: integer + time_series_metric: gauge + packets_dropped: + type: long + time_series_metric: gauge + latency: + type: double + time_series_metric: gauge diff --git a/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java b/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java index 09ce795dce6ca..1c9079f101326 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java @@ -149,6 +149,7 @@ public final class IndexScopedSettings extends AbstractScopedSettings { MapperService.INDEX_MAPPING_NESTED_DOCS_LIMIT_SETTING, MapperService.INDEX_MAPPING_TOTAL_FIELDS_LIMIT_SETTING, MapperService.INDEX_MAPPING_DEPTH_LIMIT_SETTING, + MapperService.INDEX_MAPPING_DIMENSION_FIELDS_LIMIT_SETTING, MapperService.INDEX_MAPPING_FIELD_NAME_LENGTH_LIMIT_SETTING, BitsetFilterCache.INDEX_LOAD_RANDOM_ACCESS_FILTERS_EAGERLY_SETTING, IndexModule.INDEX_STORE_TYPE_SETTING, diff --git a/server/src/main/java/org/elasticsearch/index/IndexSettings.java b/server/src/main/java/org/elasticsearch/index/IndexSettings.java index 9570a91ffbe86..ec5f371997753 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexSettings.java +++ b/server/src/main/java/org/elasticsearch/index/IndexSettings.java @@ -35,6 +35,7 @@ import java.util.function.Function; import static org.elasticsearch.index.mapper.MapperService.INDEX_MAPPING_DEPTH_LIMIT_SETTING; +import static org.elasticsearch.index.mapper.MapperService.INDEX_MAPPING_DIMENSION_FIELDS_LIMIT_SETTING; import static org.elasticsearch.index.mapper.MapperService.INDEX_MAPPING_FIELD_NAME_LENGTH_LIMIT_SETTING; import static org.elasticsearch.index.mapper.MapperService.INDEX_MAPPING_NESTED_DOCS_LIMIT_SETTING; import static org.elasticsearch.index.mapper.MapperService.INDEX_MAPPING_NESTED_FIELDS_LIMIT_SETTING; @@ -452,6 +453,7 @@ private void setRetentionLeaseMillis(final TimeValue retentionLease) { private volatile long mappingTotalFieldsLimit; private volatile long mappingDepthLimit; private volatile long mappingFieldNameLengthLimit; + private volatile long mappingDimensionFieldsLimit; /** * The maximum number of refresh listeners allows on this shard. @@ -579,6 +581,7 @@ public IndexSettings(final IndexMetadata indexMetadata, final Settings nodeSetti mappingTotalFieldsLimit = scopedSettings.get(INDEX_MAPPING_TOTAL_FIELDS_LIMIT_SETTING); mappingDepthLimit = scopedSettings.get(INDEX_MAPPING_DEPTH_LIMIT_SETTING); mappingFieldNameLengthLimit = scopedSettings.get(INDEX_MAPPING_FIELD_NAME_LENGTH_LIMIT_SETTING); + mappingDimensionFieldsLimit = scopedSettings.get(INDEX_MAPPING_DIMENSION_FIELDS_LIMIT_SETTING); scopedSettings.addSettingsUpdateConsumer(MergePolicyConfig.INDEX_COMPOUND_FORMAT_SETTING, mergePolicyConfig::setNoCFSRatio); scopedSettings.addSettingsUpdateConsumer(MergePolicyConfig.INDEX_MERGE_POLICY_DELETES_PCT_ALLOWED_SETTING, @@ -637,6 +640,7 @@ public IndexSettings(final IndexMetadata indexMetadata, final Settings nodeSetti scopedSettings.addSettingsUpdateConsumer(INDEX_MAPPING_TOTAL_FIELDS_LIMIT_SETTING, this::setMappingTotalFieldsLimit); scopedSettings.addSettingsUpdateConsumer(INDEX_MAPPING_DEPTH_LIMIT_SETTING, this::setMappingDepthLimit); scopedSettings.addSettingsUpdateConsumer(INDEX_MAPPING_FIELD_NAME_LENGTH_LIMIT_SETTING, this::setMappingFieldNameLengthLimit); + scopedSettings.addSettingsUpdateConsumer(INDEX_MAPPING_DIMENSION_FIELDS_LIMIT_SETTING, this::setMappingDimensionFieldsLimit); } private void setSearchIdleAfter(TimeValue searchIdleAfter) { this.searchIdleAfter = searchIdleAfter; } @@ -1173,4 +1177,12 @@ public long getMappingFieldNameLengthLimit() { private void setMappingFieldNameLengthLimit(long value) { this.mappingFieldNameLengthLimit = value; } + + public long getMappingDimensionFieldsLimit() { + return mappingDimensionFieldsLimit; + } + + private void setMappingDimensionFieldsLimit(long value) { + this.mappingDimensionFieldsLimit = value; + } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java index 4b5811d3fc11b..c3322c38d4ccd 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java @@ -32,6 +32,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -904,6 +905,67 @@ public static Parameter restrictedStringParam(String name, boolean updat }); } + /** + * Defines a parameter that takes any of the values of an enumeration. + * + * @param name the parameter name + * @param updateable whether the parameter can be changed by a mapping update + * @param initializer a function that reads the parameter value from an existing mapper + * @param defaultValue the default value, to be used if the parameter is undefined in a mapping + * @param enumClass the enumeration class the parameter takes values from + */ + public static > Parameter enumParam( + String name, + boolean updateable, + Function initializer, + T defaultValue, + Class enumClass + ) { + Set acceptedValues = EnumSet.allOf(enumClass); + return restrictedEnumParam(name, updateable, initializer, defaultValue, enumClass, acceptedValues); + } + + /** + * Defines a parameter that takes one of a restricted set of values from an enumeration. + * + * @param name the parameter name + * @param updateable whether the parameter can be changed by a mapping update + * @param initializer a function that reads the parameter value from an existing mapper + * @param defaultValue the default value, to be used if the parameter is undefined in a mapping + * @param enumClass the enumeration class the parameter takes values from + * @param values the set of values that the parameter can take + */ + public static > Parameter restrictedEnumParam( + String name, + boolean updateable, + Function initializer, + T defaultValue, + Class enumClass, + Set values + ) { + assert values.size() > 0; + return new Parameter(name, updateable, () -> defaultValue, (n, c, o) -> { + if (o == null) { + return defaultValue; + } + try { + @SuppressWarnings("unchecked") + T enumValue = Enum.valueOf(enumClass, (String) o); + return enumValue; + } catch (IllegalArgumentException e) { + throw new MapperParsingException( + "Unknown value [" + o + "] for field [" + name + "] - accepted values are " + values + ); + } + }, initializer).addValidator(v -> { + if (v != null && values.contains(v) == false) { + throw new MapperParsingException( + "Unknown value [" + v + "] for field [" + name + "] - accepted values are " + values + ); + } + }); + } + /** * Defines a parameter that takes an analyzer name * @param name the parameter name diff --git a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java index 05592d086bcb9..e59bca92f9326 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java @@ -8,6 +8,7 @@ package org.elasticsearch.index.mapper; +import org.apache.lucene.document.Field; import org.apache.lucene.document.InetAddressPoint; import org.apache.lucene.document.SortedSetDocValuesField; import org.apache.lucene.document.StoredField; @@ -74,6 +75,7 @@ public static class Builder extends FieldMapper.Builder { private final Parameter onScriptError = Parameter.onScriptErrorParam(m -> toType(m).onScriptError, script); private final Parameter> meta = Parameter.metaParam(); + private final Parameter dimension; private final boolean ignoreMalformedByDefault; private final Version indexCreatedVersion; @@ -88,6 +90,19 @@ public Builder(String name, ScriptCompiler scriptCompiler, boolean ignoreMalform = Parameter.boolParam("ignore_malformed", true, m -> toType(m).ignoreMalformed, ignoreMalformedByDefault); this.script.precludesParameters(nullValue, ignoreMalformed); addScriptValidation(script, indexed, hasDocValues); + this.dimension = TimeSeriesParams.dimensionParam(m -> toType(m).dimension).addValidator(v -> { + if (v && (indexed.getValue() == false || hasDocValues.getValue() == false)) { + throw new IllegalArgumentException( + "Field [" + + TimeSeriesParams.TIME_SERIES_DIMENSION_PARAM + + "] requires that [" + + indexed.name + + "] and [" + + hasDocValues.name + + "] are true" + ); + } + }); } Builder nullValue(String nullValue) { @@ -95,6 +110,11 @@ Builder nullValue(String nullValue) { return this; } + public Builder dimension(boolean dimension) { + this.dimension.setValue(dimension); + return this; + } + private InetAddress parseNullValue() { String nullValueAsString = nullValue.getValue(); if (nullValueAsString == null) { @@ -122,14 +142,14 @@ private FieldValues scriptValues() { @Override protected List> getParameters() { - return Arrays.asList(indexed, hasDocValues, stored, ignoreMalformed, nullValue, script, onScriptError, meta); + return Arrays.asList(indexed, hasDocValues, stored, ignoreMalformed, nullValue, script, onScriptError, meta, dimension); } @Override public IpFieldMapper build(MapperBuilderContext context) { return new IpFieldMapper(name, new IpFieldType(context.buildFullName(name), indexed.getValue(), stored.getValue(), - hasDocValues.getValue(), parseNullValue(), scriptValues(), meta.getValue()), + hasDocValues.getValue(), parseNullValue(), scriptValues(), meta.getValue(), dimension.getValue()), multiFieldsBuilder.build(this, context), copyTo.build(), this); } @@ -144,16 +164,18 @@ public static final class IpFieldType extends SimpleMappedFieldType { private final InetAddress nullValue; private final FieldValues scriptValues; + private final boolean isDimension; public IpFieldType(String name, boolean indexed, boolean stored, boolean hasDocValues, - InetAddress nullValue, FieldValues scriptValues, Map meta) { + InetAddress nullValue, FieldValues scriptValues, Map meta, boolean isDimension) { super(name, indexed, stored, hasDocValues, TextSearchInfo.SIMPLE_MATCH_WITHOUT_TERMS, meta); this.nullValue = nullValue; this.scriptValues = scriptValues; + this.isDimension = isDimension; } public IpFieldType(String name) { - this(name, true, false, true, null, null, Collections.emptyMap()); + this(name, true, false, true, null, null, Collections.emptyMap(), false); } @Override @@ -362,12 +384,20 @@ public DocValueFormat docValueFormat(@Nullable String format, ZoneId timeZone) { checkNoTimeZone(timeZone); return DocValueFormat.IP; } + + /** + * @return true if field has been marked as a dimension field + */ + public boolean isDimension() { + return isDimension; + } } private final boolean indexed; private final boolean hasDocValues; private final boolean stored; private final boolean ignoreMalformed; + private final boolean dimension; private final InetAddress nullValue; private final String nullValueAsString; @@ -397,6 +427,7 @@ private IpFieldMapper( this.script = builder.script.get(); this.scriptValues = builder.scriptValues(); this.scriptCompiler = builder.scriptCompiler; + this.dimension = builder.dimension.getValue(); } boolean ignoreMalformed() { @@ -441,7 +472,17 @@ private static InetAddress value(XContentParser parser, InetAddress nullValue) t private void indexValue(DocumentParserContext context, InetAddress address) { if (indexed) { - context.doc().add(new InetAddressPoint(fieldType().name(), address)); + Field field = new InetAddressPoint(fieldType().name(), address); + if (dimension) { + // Add dimension field with key so that we ensure it is single-valued. + // Dimension fields are always indexed. + if (context.doc().getByKey(fieldType().name()) != null) { + throw new IllegalArgumentException("Dimension field [" + fieldType().name() + "] cannot be a multi-valued field."); + } + context.doc().addWithKey(fieldType().name(), field); + } else { + context.doc().add(field); + } } if (hasDocValues) { context.doc().add(new SortedSetDocValuesField(fieldType().name(), new BytesRef(InetAddressPoint.encode(address)))); @@ -461,6 +502,6 @@ protected void indexScriptValues(SearchLookup searchLookup, LeafReaderContext re @Override public FieldMapper.Builder getMergeBuilder() { - return new Builder(simpleName(), scriptCompiler, ignoreMalformedByDefault, indexCreatedVersion).init(this); + return new Builder(simpleName(), scriptCompiler, ignoreMalformedByDefault, indexCreatedVersion).dimension(dimension).init(this); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java index 39fa107b6857d..55b6748380a69 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java @@ -114,6 +114,7 @@ public static class Builder extends FieldMapper.Builder { private final Parameter