diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 28a7d6981a4cb..5749cfe46272e 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -271,6 +271,11 @@
+
+
+
+
+
diff --git a/modules/data-streams/build.gradle b/modules/data-streams/build.gradle
index 51bb04185cfde..32b3e44866a6d 100644
--- a/modules/data-streams/build.gradle
+++ b/modules/data-streams/build.gradle
@@ -52,6 +52,10 @@ tasks.named("yamlRestCompatTestTransform").configure({ task ->
task.skipTest("data_stream/30_auto_create_data_stream/Don't initialize failure store during data stream auto-creation on successful index", "Configuring the failure store via data stream templates is not supported anymore.")
task.skipTest("data_stream/150_tsdb/TSDB failures go to failure store", "Configuring the failure store via data stream templates is not supported anymore.")
+ // TODO remove these after removing exact _tsid assertions in 8.x
+ task.skipTest("data_stream/150_tsdb/dynamic templates", "The _tsid has changed in a new index version. This tests verifies the exact _tsid value with is too brittle for compatibility testing.")
+ task.skipTest("data_stream/150_tsdb/dynamic templates - conflicting aliases", "The _tsid has changed in a new index version. This tests verifies the exact _tsid value with is too brittle for compatibility testing.")
+ task.skipTest("data_stream/150_tsdb/dynamic templates with nesting", "The _tsid has changed in a new index version. This tests verifies the exact _tsid value with is too brittle for compatibility testing.")
task.skipTest("data_stream/170_modify_data_stream/Modify a data stream's failure store", "Configuring the failure store via data stream templates is not supported anymore.")
diff --git a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/TSDBIndexingIT.java b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/TSDBIndexingIT.java
index eaef99d86a86e..50e612903248c 100644
--- a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/TSDBIndexingIT.java
+++ b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/TSDBIndexingIT.java
@@ -14,6 +14,8 @@
import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeRequest;
import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
import org.elasticsearch.action.admin.indices.get.GetIndexResponse;
+import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
+import org.elasticsearch.action.admin.indices.mapping.put.TransportPutMappingAction;
import org.elasticsearch.action.admin.indices.refresh.RefreshRequest;
import org.elasticsearch.action.admin.indices.rollover.RolloverRequest;
import org.elasticsearch.action.admin.indices.segments.IndicesSegmentsRequest;
@@ -24,6 +26,7 @@
import org.elasticsearch.action.bulk.BulkRequestBuilder;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.bulk.IndexDocFailureStoreStatus;
+import org.elasticsearch.action.datastreams.CreateDataStreamAction;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
@@ -34,6 +37,7 @@
import org.elasticsearch.cluster.metadata.Template;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.compress.CompressedXContent;
+import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.time.DateFormatter;
import org.elasticsearch.common.time.FormatNames;
@@ -58,12 +62,14 @@
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.CountDownLatch;
import static org.elasticsearch.test.MapMatcher.assertMap;
import static org.elasticsearch.test.MapMatcher.matchesMap;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertResponse;
+import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
@@ -320,16 +326,12 @@ public void testTsdbTemplatesNoKeywordFieldType() throws Exception {
ComposableIndexTemplate.builder()
.indexPatterns(List.of("k8s*"))
.template(
- new Template(
- Settings.builder().put("index.mode", "time_series").put("index.routing_path", "metricset").build(),
- new CompressedXContent(mappingTemplate),
- null
- )
+ new Template(Settings.builder().put("index.mode", "time_series").build(), new CompressedXContent(mappingTemplate), null)
)
.dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate(false, false))
.build()
);
- client().execute(TransportPutComposableIndexTemplateAction.TYPE, request).actionGet();
+ assertResponse(client().execute(TransportPutComposableIndexTemplateAction.TYPE, request), ElasticsearchAssertions::assertAcked);
}
public void testInvalidTsdbTemplatesMissingSettings() throws Exception {
@@ -621,6 +623,192 @@ public void testReindexing() throws Exception {
);
}
+ public void testAddDimensionToMapping() throws Exception {
+ String dataStreamName = "my-ds";
+ var putTemplateRequest = new TransportPutComposableIndexTemplateAction.Request("id");
+ putTemplateRequest.indexTemplate(
+ ComposableIndexTemplate.builder()
+ .indexPatterns(List.of(dataStreamName))
+ .template(
+ new Template(
+ Settings.builder().put("index.mode", "time_series").build(),
+ new CompressedXContent(MAPPING_TEMPLATE),
+ null
+ )
+ )
+ .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate(false, false))
+ .build()
+ );
+ assertAcked(client().execute(TransportPutComposableIndexTemplateAction.TYPE, putTemplateRequest));
+
+ // create data stream
+ CreateDataStreamAction.Request createDsRequest = new CreateDataStreamAction.Request(
+ TEST_REQUEST_TIMEOUT,
+ TEST_REQUEST_TIMEOUT,
+ "my-ds"
+ );
+ assertAcked(client().execute(CreateDataStreamAction.INSTANCE, createDsRequest));
+
+ assertThat(getSetting(dataStreamName, IndexMetadata.INDEX_DIMENSIONS), equalTo(List.of("metricset")));
+
+ // put mapping with k8s.pod.uid as another time series dimension
+ var putMappingRequest = new PutMappingRequest(dataStreamName).source("""
+ {
+ "properties": {
+ "k8s.pod.name": {
+ "type": "keyword",
+ "time_series_dimension": true
+ }
+ }
+ }
+ """, XContentType.JSON);
+ assertAcked(client().execute(TransportPutMappingAction.TYPE, putMappingRequest).actionGet());
+
+ assertThat(getSetting(dataStreamName, IndexMetadata.INDEX_DIMENSIONS), containsInAnyOrder("metricset", "k8s.pod.name"));
+
+ indexWithPodNames(dataStreamName, Instant.now(), Map.of(), "dog", "cat");
+ }
+
+ public void testDynamicStringDimensions() throws Exception {
+ String dataStreamName = "my-ds";
+ var putTemplateRequest = new TransportPutComposableIndexTemplateAction.Request("id");
+ putTemplateRequest.indexTemplate(
+ ComposableIndexTemplate.builder()
+ .indexPatterns(List.of(dataStreamName))
+ .template(new Template(Settings.builder().put("index.mode", "time_series").build(), new CompressedXContent("""
+
+ {
+ "_doc": {
+ "dynamic_templates": [
+ {
+ "labels": {
+ "match_mapping_type": "string",
+ "mapping": {
+ "type": "keyword",
+ "time_series_dimension": true
+ }
+ }
+ }
+ ],
+ "properties": {
+ "@timestamp": {
+ "type": "date"
+ },
+ "metricset": {
+ "type": "keyword",
+ "time_series_dimension": true
+ }
+ }
+ }
+ }"""), null))
+ .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate(false, false))
+ .build()
+ );
+ assertAcked(client().execute(TransportPutComposableIndexTemplateAction.TYPE, putTemplateRequest));
+
+ CreateDataStreamAction.Request createDsRequest = new CreateDataStreamAction.Request(
+ TEST_REQUEST_TIMEOUT,
+ TEST_REQUEST_TIMEOUT,
+ "my-ds"
+ );
+ assertAcked(client().execute(CreateDataStreamAction.INSTANCE, createDsRequest));
+
+ // doesn't populate index.dimensions because the "labels" dynamic template doesn't have a path_math
+ assertThat(getSetting(dataStreamName, IndexMetadata.INDEX_ROUTING_PATH), equalTo(List.of("metricset")));
+
+ // index doc
+ BulkResponse bulkResponse = client().prepareBulk()
+ .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE)
+ .add(
+ client().prepareIndex(dataStreamName)
+ .setOpType(DocWriteRequest.OpType.CREATE)
+ .setSource(DOC.replace("$time", formatInstant(Instant.now())), XContentType.JSON)
+ )
+ .get();
+ assertThat(bulkResponse.hasFailures(), is(false));
+
+ assertThat(getSetting(dataStreamName, IndexMetadata.INDEX_ROUTING_PATH), equalTo(List.of("metricset")));
+ }
+
+ public void testDynamicDimensions() throws Exception {
+ String dataStreamName = "my-ds";
+ var putTemplateRequest = new TransportPutComposableIndexTemplateAction.Request("id");
+ putTemplateRequest.indexTemplate(
+ ComposableIndexTemplate.builder()
+ .indexPatterns(List.of(dataStreamName))
+ .template(new Template(Settings.builder().put("index.mode", "time_series").build(), new CompressedXContent("""
+
+ {
+ "_doc": {
+ "dynamic_templates": [
+ {
+ "label": {
+ "mapping": {
+ "type": "keyword",
+ "time_series_dimension": true
+ }
+ }
+ }
+ ],
+ "properties": {
+ "@timestamp": {
+ "type": "date"
+ },
+ "metricset": {
+ "type": "keyword",
+ "time_series_dimension": true
+ }
+ }
+ }
+ }"""), null))
+ .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate(false, false))
+ .build()
+ );
+ assertAcked(client().execute(TransportPutComposableIndexTemplateAction.TYPE, putTemplateRequest));
+
+ CreateDataStreamAction.Request createDsRequest = new CreateDataStreamAction.Request(
+ TEST_REQUEST_TIMEOUT,
+ TEST_REQUEST_TIMEOUT,
+ "my-ds"
+ );
+ assertAcked(client().execute(CreateDataStreamAction.INSTANCE, createDsRequest));
+
+ // doesn't populate index.dimensions because the "label" dynamic template doesn't have a path_math
+ assertThat(getSetting(dataStreamName, IndexMetadata.INDEX_ROUTING_PATH), equalTo(List.of("metricset")));
+
+ // index doc
+ indexWithPodNames(dataStreamName, Instant.now(), Map.of("k8s.pod.name", "label"), "dog", "cat");
+
+ assertThat(getSetting(dataStreamName, IndexMetadata.INDEX_ROUTING_PATH), equalTo(List.of("metricset")));
+ }
+
+ private void indexWithPodNames(String dataStreamName, Instant timestamp, Map dynamicTemplates, String... podNames) {
+ // index doc
+ BulkRequestBuilder bulkRequestBuilder = client().prepareBulk();
+ bulkRequestBuilder.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);
+ for (String podName : podNames) {
+ bulkRequestBuilder.add(
+ client().prepareIndex(dataStreamName)
+ .setOpType(DocWriteRequest.OpType.CREATE)
+ .setSource(DOC.replace("$time", formatInstant(timestamp)).replace("dog", podName), XContentType.JSON)
+ .request()
+ .setDynamicTemplates(dynamicTemplates)
+ );
+ }
+
+ BulkResponse bulkResponse = bulkRequestBuilder.get();
+ assertThat(bulkResponse.hasFailures(), is(false));
+ }
+
+ private T getSetting(String dataStreamName, Setting setting) {
+ GetIndexResponse getIndexResponse = safeGet(
+ indicesAdmin().getIndex(new GetIndexRequest(TEST_REQUEST_TIMEOUT).indices(dataStreamName))
+ );
+ assertThat(getIndexResponse.getIndices().length, equalTo(1));
+ Settings settings = getIndexResponse.getSettings().get(getIndexResponse.getIndices()[0]);
+ return setting.get(settings);
+ }
+
static String formatInstant(Instant instant) {
return DateFormatter.forPattern(FormatNames.STRICT_DATE_OPTIONAL_TIME.getName()).format(instant);
}
diff --git a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/TSDBPassthroughIndexingIT.java b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/TSDBPassthroughIndexingIT.java
index a76dac5db4540..c32eb05ddda65 100644
--- a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/TSDBPassthroughIndexingIT.java
+++ b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/TSDBPassthroughIndexingIT.java
@@ -184,7 +184,7 @@ public void testIndexingGettingAndSearching() throws Exception {
// validate index:
var getIndexResponse = client().admin().indices().getIndex(new GetIndexRequest(TEST_REQUEST_TIMEOUT).indices(index)).actionGet();
- assertThat(getIndexResponse.getSettings().get(index).get("index.routing_path"), equalTo("[attributes.*]"));
+ assertThat(getIndexResponse.getSettings().get(index).get("index.dimensions"), equalTo("[attributes.*]"));
// validate mapping
var mapping = getIndexResponse.mappings().get(index).getSourceAsMap();
assertMap(
diff --git a/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/TsdbDataStreamRestIT.java b/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/TsdbDataStreamRestIT.java
index 9be0c18d18213..a7fc4b102866f 100644
--- a/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/TsdbDataStreamRestIT.java
+++ b/modules/data-streams/src/javaRestTest/java/org/elasticsearch/datastreams/TsdbDataStreamRestIT.java
@@ -433,9 +433,10 @@ public void testSimulateTsdbDataStreamTemplate() throws Exception {
assertThat(ObjectPath.evaluate(responseBody, "template.settings.index.time_series.start_time"), notNullValue());
assertThat(ObjectPath.evaluate(responseBody, "template.settings.index.time_series.end_time"), notNullValue());
assertThat(
- ObjectPath.evaluate(responseBody, "template.settings.index.routing_path"),
+ ObjectPath.evaluate(responseBody, "template.settings.index.dimensions"),
containsInAnyOrder("metricset", "k8s.pod.uid", "pod.labels.*")
);
+ assertThat(ObjectPath.evaluate(responseBody, "template.settings.index.routing_path"), nullValue());
assertThat(ObjectPath.evaluate(responseBody, "overlapping"), empty());
}
diff --git a/modules/data-streams/src/main/java/org/elasticsearch/datastreams/DataStreamIndexSettingsProvider.java b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/DataStreamIndexSettingsProvider.java
index ac3987dfa2771..8c10266907c57 100644
--- a/modules/data-streams/src/main/java/org/elasticsearch/datastreams/DataStreamIndexSettingsProvider.java
+++ b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/DataStreamIndexSettingsProvider.java
@@ -13,6 +13,7 @@
import org.elasticsearch.cluster.metadata.ProjectMetadata;
import org.elasticsearch.common.UUIDs;
import org.elasticsearch.common.compress.CompressedXContent;
+import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.time.DateFormatter;
import org.elasticsearch.core.CheckedFunction;
@@ -23,21 +24,20 @@
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.index.mapper.DateFieldMapper;
-import org.elasticsearch.index.mapper.KeywordFieldMapper;
+import org.elasticsearch.index.mapper.DocumentMapper;
+import org.elasticsearch.index.mapper.FieldMapper;
import org.elasticsearch.index.mapper.Mapper;
-import org.elasticsearch.index.mapper.MapperBuilderContext;
import org.elasticsearch.index.mapper.MapperService;
-import org.elasticsearch.index.mapper.MappingParserContext;
import org.elasticsearch.index.mapper.PassThroughObjectMapper;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.time.Instant;
import java.util.ArrayList;
-import java.util.Iterator;
import java.util.List;
import java.util.Locale;
+import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_DIMENSIONS;
import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_PATH;
/**
@@ -119,13 +119,17 @@ public Settings getAdditionalIndexSettings(
if (indexTemplateAndCreateRequestSettings.hasValue(IndexMetadata.INDEX_ROUTING_PATH.getKey()) == false
&& combinedTemplateMappings.isEmpty() == false) {
- List routingPaths = findRoutingPaths(
+ List dimensions = new ArrayList<>();
+ boolean matchesAllDimensions = findDimensionFields(
indexName,
indexTemplateAndCreateRequestSettings,
- combinedTemplateMappings
+ combinedTemplateMappings,
+ dimensions
);
- if (routingPaths.isEmpty() == false) {
- builder.putList(INDEX_ROUTING_PATH.getKey(), routingPaths);
+ if (dimensions.isEmpty() == false) {
+ // TODO handle the case when adding a dimension field to the mappings of an existing index
+ // at the moment, the index.dimensions setting is only set when an index is created
+ builder.putList(matchesAllDimensions ? INDEX_DIMENSIONS.getKey() : INDEX_ROUTING_PATH.getKey(), dimensions);
}
}
return builder.build();
@@ -137,15 +141,46 @@ public Settings getAdditionalIndexSettings(
}
/**
- * Find fields in mapping that are of type keyword and time_series_dimension enabled.
+ * This is called when mappings are updated, so that the {@link IndexMetadata#INDEX_DIMENSIONS}
+ * and {@link IndexMetadata#INDEX_ROUTING_PATH} settings are updated to match the new mappings.
+ * Updates {@link IndexMetadata#INDEX_DIMENSIONS} if a new dimension field is added to the mappings,
+ * or sets {@link IndexMetadata#INDEX_ROUTING_PATH} if a new dimension field is added that doesn't allow for matching all
+ * dimension fields via a wildcard pattern.
+ */
+ @Override
+ public Settings onUpdateMappings(IndexMetadata indexMetadata, DocumentMapper documentMapper) {
+ List indexDimensions = INDEX_DIMENSIONS.get(indexMetadata.getSettings());
+ if (indexDimensions.isEmpty()) {
+ return Settings.EMPTY;
+ }
+ assert indexMetadata.getIndexMode() == IndexMode.TIME_SERIES;
+ List newIndexDimensions = new ArrayList<>(indexDimensions.size());
+ boolean matchesAllDimensions = findDimensionFields(newIndexDimensions, documentMapper);
+ if (indexDimensions.equals(newIndexDimensions)) {
+ return Settings.EMPTY;
+ }
+ if (matchesAllDimensions) {
+ return Settings.builder().putList(INDEX_DIMENSIONS.getKey(), newIndexDimensions).build();
+ } else {
+ return Settings.builder().putList(INDEX_ROUTING_PATH.getKey(), newIndexDimensions).build();
+ }
+ }
+
+ /**
+ * Find fields in mapping that are time_series_dimension enabled.
* Using MapperService here has an overhead, but allows the mappings from template to
* be merged correctly and fetching the fields without manually parsing the mappings.
- *
+ *
* Alternatively this method can instead parse mappings into map of maps and merge that and
* iterate over all values to find the field that can serve as routing value. But this requires
* mapping specific logic to exist here.
*/
- private List findRoutingPaths(String indexName, Settings allSettings, List combinedTemplateMappings) {
+ private boolean findDimensionFields(
+ String indexName,
+ Settings allSettings,
+ List combinedTemplateMappings,
+ List dimensions
+ ) {
var tmpIndexMetadata = IndexMetadata.builder(indexName);
int dummyPartitionSize = IndexMetadata.INDEX_ROUTING_PARTITION_SIZE_SETTING.get(allSettings);
@@ -169,57 +204,52 @@ private List findRoutingPaths(String indexName, Settings allSettings, Li
// Create MapperService just to extract keyword dimension fields:
try (var mapperService = mapperServiceFactory.apply(tmpIndexMetadata.build())) {
mapperService.merge(MapperService.SINGLE_MAPPING_NAME, combinedTemplateMappings, MapperService.MergeReason.INDEX_TEMPLATE);
- List routingPaths = new ArrayList<>();
- for (var fieldMapper : mapperService.documentMapper().mappers().fieldMappers()) {
- extractPath(routingPaths, fieldMapper);
- }
- for (var objectMapper : mapperService.documentMapper().mappers().objectMappers().values()) {
- if (objectMapper instanceof PassThroughObjectMapper passThroughObjectMapper) {
- if (passThroughObjectMapper.containsDimensions()) {
- routingPaths.add(passThroughObjectMapper.fullPath() + ".*");
- }
- }
- }
- for (var template : mapperService.getAllDynamicTemplates()) {
- if (template.pathMatch().isEmpty()) {
- continue;
- }
-
- var templateName = "__dynamic__" + template.name();
- var mappingSnippet = template.mappingForName(templateName, KeywordFieldMapper.CONTENT_TYPE);
- String mappingSnippetType = (String) mappingSnippet.get("type");
- if (mappingSnippetType == null) {
- continue;
- }
+ DocumentMapper documentMapper = mapperService.documentMapper();
+ return findDimensionFields(dimensions, documentMapper);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
- MappingParserContext parserContext = mapperService.parserContext();
- for (Iterator iterator = template.pathMatch().iterator(); iterator.hasNext();) {
- var mapper = parserContext.typeParser(mappingSnippetType)
- .parse(iterator.next(), mappingSnippet, parserContext)
- .build(MapperBuilderContext.root(false, false));
- extractPath(routingPaths, mapper);
- if (iterator.hasNext()) {
- // Since FieldMapper.parse modifies the Map passed in (removing entries for "type"), that means
- // that only the first pathMatch passed in gets recognized as a time_series_dimension.
- // To avoid this, each parsing call uses a new mapping snippet.
- // Note that a shallow copy of the mappingSnippet map is not enough if there are multi-fields.
- mappingSnippet = template.mappingForName(templateName, KeywordFieldMapper.CONTENT_TYPE);
- }
+ private static boolean findDimensionFields(List dimensions, DocumentMapper documentMapper) {
+ for (var objectMapper : documentMapper.mappers().objectMappers().values()) {
+ if (objectMapper instanceof PassThroughObjectMapper passThroughObjectMapper) {
+ if (passThroughObjectMapper.containsDimensions()) {
+ dimensions.add(passThroughObjectMapper.fullPath() + ".*");
}
}
- return routingPaths;
- } catch (IOException e) {
- throw new UncheckedIOException(e);
}
+ boolean matchesAllDimensions = true;
+ for (var template : documentMapper.mapping().getRoot().dynamicTemplates()) {
+ if (template.isTimeSeriesDimension() == false) {
+ continue;
+ }
+ if (template.isSimplePathMath() == false) {
+ matchesAllDimensions = false;
+ }
+ if (template.pathMatch().isEmpty() == false) {
+ dimensions.addAll(template.pathMatch());
+ }
+ }
+
+ for (var fieldMapper : documentMapper.mappers().fieldMappers()) {
+ extractPath(dimensions, fieldMapper);
+ }
+ return matchesAllDimensions;
}
/**
- * Helper method that adds the name of the mapper to the provided list if it is a keyword dimension field.
+ * Helper method that adds the name of the mapper to the provided list.
*/
- private static void extractPath(List routingPaths, Mapper mapper) {
- if (mapper instanceof KeywordFieldMapper keywordFieldMapper) {
- if (keywordFieldMapper.fieldType().isDimension()) {
- routingPaths.add(mapper.fullPath());
+ private static void extractPath(List dimensions, Mapper mapper) {
+ if (mapper instanceof FieldMapper fieldMapper) {
+ if (fieldMapper.fieldType().isDimension()) {
+ String path = mapper.fullPath();
+ // don't add if the path already matches via a wildcard pattern in the list
+ // e.g. if "path.*" is already added, "path.foo" should not be added
+ if (Regex.simpleMatch(dimensions, path) == false) {
+ dimensions.add(path);
+ }
}
}
}
diff --git a/modules/data-streams/src/test/java/org/elasticsearch/datastreams/DataStreamIndexSettingsProviderTests.java b/modules/data-streams/src/test/java/org/elasticsearch/datastreams/DataStreamIndexSettingsProviderTests.java
index b85b3f6e7ae39..bdd94de1f92c6 100644
--- a/modules/data-streams/src/test/java/org/elasticsearch/datastreams/DataStreamIndexSettingsProviderTests.java
+++ b/modules/data-streams/src/test/java/org/elasticsearch/datastreams/DataStreamIndexSettingsProviderTests.java
@@ -33,7 +33,6 @@
import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.newInstance;
import static org.elasticsearch.common.settings.Settings.builder;
import static org.elasticsearch.datastreams.DataStreamIndexSettingsProvider.FORMATTER;
-import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
@@ -70,6 +69,18 @@ public void testGetAdditionalIndexSettings() throws Exception {
"field3": {
"type": "keyword",
"time_series_dimension": true
+ },
+ "field4": {
+ "type": "long",
+ "time_series_dimension": true
+ },
+ "field5": {
+ "type": "ip",
+ "time_series_dimension": true
+ },
+ "field6": {
+ "type": "boolean",
+ "time_series_dimension": true
}
}
}
@@ -91,7 +102,7 @@ public void testGetAdditionalIndexSettings() throws Exception {
assertThat(IndexSettings.MODE.get(result), equalTo(IndexMode.TIME_SERIES));
assertThat(IndexSettings.TIME_SERIES_START_TIME.get(result), equalTo(now.minusMillis(DEFAULT_LOOK_BACK_TIME.getMillis())));
assertThat(IndexSettings.TIME_SERIES_END_TIME.get(result), equalTo(now.plusMillis(DEFAULT_LOOK_AHEAD_TIME.getMillis())));
- assertThat(IndexMetadata.INDEX_ROUTING_PATH.get(result), contains("field3"));
+ assertThat(IndexMetadata.INDEX_DIMENSIONS.get(result), containsInAnyOrder("field3", "field4", "field5", "field6"));
}
public void testGetAdditionalIndexSettingsIndexRoutingPathAlreadyDefined() throws Exception {
@@ -105,7 +116,7 @@ public void testGetAdditionalIndexSettingsIndexRoutingPathAlreadyDefined() throw
"_doc": {
"properties": {
"field1": {
- "type": "keyword"
+ "type": "keyword",
"time_series_dimension": true
},
"field2": {
@@ -206,7 +217,7 @@ public void testGetAdditionalIndexSettingsMappingsMerging() throws Exception {
assertThat(IndexSettings.MODE.get(result), equalTo(IndexMode.TIME_SERIES));
assertThat(IndexSettings.TIME_SERIES_START_TIME.get(result), equalTo(now.minusMillis(DEFAULT_LOOK_BACK_TIME.getMillis())));
assertThat(IndexSettings.TIME_SERIES_END_TIME.get(result), equalTo(now.plusMillis(DEFAULT_LOOK_AHEAD_TIME.getMillis())));
- assertThat(IndexMetadata.INDEX_ROUTING_PATH.get(result), containsInAnyOrder("field1", "field3"));
+ assertThat(IndexMetadata.INDEX_DIMENSIONS.get(result), containsInAnyOrder("field1", "field3"));
}
public void testGetAdditionalIndexSettingsNoMappings() {
@@ -461,7 +472,7 @@ public void testGenerateRoutingPathFromDynamicTemplate() throws Exception {
assertThat(IndexSettings.MODE.get(result), equalTo(IndexMode.TIME_SERIES));
assertThat(IndexSettings.TIME_SERIES_START_TIME.get(result), equalTo(now.minusMillis(DEFAULT_LOOK_BACK_TIME.getMillis())));
assertThat(IndexSettings.TIME_SERIES_END_TIME.get(result), equalTo(now.plusMillis(DEFAULT_LOOK_AHEAD_TIME.getMillis())));
- assertThat(IndexMetadata.INDEX_ROUTING_PATH.get(result), containsInAnyOrder("host.id", "prometheus.labels.*"));
+ assertThat(IndexMetadata.INDEX_DIMENSIONS.get(result), containsInAnyOrder("host.id", "prometheus.labels.*"));
}
public void testGenerateRoutingPathFromDynamicTemplateWithMultiplePathMatchEntries() throws Exception {
@@ -502,10 +513,10 @@ public void testGenerateRoutingPathFromDynamicTemplateWithMultiplePathMatchEntri
assertThat(IndexSettings.TIME_SERIES_START_TIME.get(result), equalTo(now.minusMillis(DEFAULT_LOOK_BACK_TIME.getMillis())));
assertThat(IndexSettings.TIME_SERIES_END_TIME.get(result), equalTo(now.plusMillis(DEFAULT_LOOK_AHEAD_TIME.getMillis())));
assertThat(
- IndexMetadata.INDEX_ROUTING_PATH.get(result),
+ IndexMetadata.INDEX_DIMENSIONS.get(result),
containsInAnyOrder("host.id", "xprometheus.labels.*", "yprometheus.labels.*")
);
- List routingPathList = IndexMetadata.INDEX_ROUTING_PATH.get(result);
+ List routingPathList = IndexMetadata.INDEX_DIMENSIONS.get(result);
assertEquals(3, routingPathList.size());
}
@@ -552,10 +563,10 @@ public void testGenerateRoutingPathFromDynamicTemplateWithMultiplePathMatchEntri
assertThat(IndexSettings.TIME_SERIES_START_TIME.get(result), equalTo(now.minusMillis(DEFAULT_LOOK_BACK_TIME.getMillis())));
assertThat(IndexSettings.TIME_SERIES_END_TIME.get(result), equalTo(now.plusMillis(DEFAULT_LOOK_AHEAD_TIME.getMillis())));
assertThat(
- IndexMetadata.INDEX_ROUTING_PATH.get(result),
+ IndexMetadata.INDEX_DIMENSIONS.get(result),
containsInAnyOrder("host.id", "xprometheus.labels.*", "yprometheus.labels.*")
);
- List routingPathList = IndexMetadata.INDEX_ROUTING_PATH.get(result);
+ List routingPathList = IndexMetadata.INDEX_DIMENSIONS.get(result);
assertEquals(3, routingPathList.size());
}
@@ -605,7 +616,7 @@ public void testGenerateRoutingPathFromDynamicTemplate_templateWithNoPathMatch()
assertThat(IndexSettings.MODE.get(result), equalTo(IndexMode.TIME_SERIES));
assertThat(IndexSettings.TIME_SERIES_START_TIME.get(result), equalTo(now.minusMillis(DEFAULT_LOOK_BACK_TIME.getMillis())));
assertThat(IndexSettings.TIME_SERIES_END_TIME.get(result), equalTo(now.plusMillis(DEFAULT_LOOK_AHEAD_TIME.getMillis())));
- assertThat(IndexMetadata.INDEX_ROUTING_PATH.get(result), containsInAnyOrder("host.id", "prometheus.labels.*"));
+ assertThat(IndexMetadata.INDEX_DIMENSIONS.get(result), containsInAnyOrder("host.id", "prometheus.labels.*"));
}
public void testGenerateRoutingPathFromDynamicTemplate_nonKeywordTemplate() throws Exception {
@@ -652,8 +663,8 @@ public void testGenerateRoutingPathFromDynamicTemplate_nonKeywordTemplate() thro
Settings result = generateTsdbSettings(mapping, now);
assertThat(IndexSettings.TIME_SERIES_START_TIME.get(result), equalTo(now.minusMillis(DEFAULT_LOOK_BACK_TIME.getMillis())));
assertThat(IndexSettings.TIME_SERIES_END_TIME.get(result), equalTo(now.plusMillis(DEFAULT_LOOK_AHEAD_TIME.getMillis())));
- assertThat(IndexMetadata.INDEX_ROUTING_PATH.get(result), containsInAnyOrder("host.id", "prometheus.labels.*"));
- assertEquals(2, IndexMetadata.INDEX_ROUTING_PATH.get(result).size());
+ assertThat(IndexMetadata.INDEX_DIMENSIONS.get(result), containsInAnyOrder("host.id", "prometheus.labels.*"));
+ assertEquals(2, IndexMetadata.INDEX_DIMENSIONS.get(result).size());
}
public void testGenerateRoutingPathFromPassThroughObject() throws Exception {
@@ -665,7 +676,12 @@ public void testGenerateRoutingPathFromPassThroughObject() throws Exception {
"labels": {
"type": "passthrough",
"time_series_dimension": true,
- "priority": 2
+ "priority": 2,
+ "properties": {
+ "label1": {
+ "type": "keyword"
+ }
+ }
},
"metrics": {
"type": "passthrough",
@@ -683,7 +699,7 @@ public void testGenerateRoutingPathFromPassThroughObject() throws Exception {
assertThat(IndexSettings.MODE.get(result), equalTo(IndexMode.TIME_SERIES));
assertThat(IndexSettings.TIME_SERIES_START_TIME.get(result), equalTo(now.minusMillis(DEFAULT_LOOK_BACK_TIME.getMillis())));
assertThat(IndexSettings.TIME_SERIES_END_TIME.get(result), equalTo(now.plusMillis(DEFAULT_LOOK_AHEAD_TIME.getMillis())));
- assertThat(IndexMetadata.INDEX_ROUTING_PATH.get(result), containsInAnyOrder("labels.*"));
+ assertThat(IndexMetadata.INDEX_DIMENSIONS.get(result), containsInAnyOrder("labels.*"));
}
private Settings generateTsdbSettings(String mapping, Instant now) throws IOException {
diff --git a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/150_tsdb.yml b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/150_tsdb.yml
index 598bc90217574..f393fafb5067e 100644
--- a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/150_tsdb.yml
+++ b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/150_tsdb.yml
@@ -148,13 +148,12 @@ fetch the tsid:
query: '+@timestamp:"2021-04-28T18:51:04.467Z" +k8s.pod.name:cat'
- match: {hits.total.value: 1}
- - match: {hits.hits.0.fields._tsid: [ "KCjEJ9R_BgO8TRX2QOd6dpR12oDh--qoyNZRQPy43y34Qdy2dpsyG0o" ]}
---
"aggregate the tsid":
- requires:
cluster_features: ["gte_v8.13.0"]
- reason: _tsid hahing introduced in 8.13
+ reason: _tsid hashing introduced in 8.13
- do:
search:
@@ -169,9 +168,8 @@ fetch the tsid:
_key: asc
- match: {hits.total.value: 8}
- - match: {aggregations.tsids.buckets.0.key: "KCjEJ9R_BgO8TRX2QOd6dpQ5ihHD--qoyLTiOy0pmP6_RAIE-e0-dKQ"}
+ - length: {aggregations.tsids.buckets: 2}
- match: {aggregations.tsids.buckets.0.doc_count: 4}
- - match: {aggregations.tsids.buckets.1.key: "KCjEJ9R_BgO8TRX2QOd6dpR12oDh--qoyNZRQPy43y34Qdy2dpsyG0o"}
- match: {aggregations.tsids.buckets.1.doc_count: 4}
---
@@ -368,7 +366,6 @@ dynamic templates:
field: _tsid
- length: { aggregations.filterA.tsids.buckets: 1 }
- - match: { aggregations.filterA.tsids.buckets.0.key: "NOHjOAVWLTVWZM4CXLoraZYgYpiKqVppKnpcfycX2dfFiw707uoshWIGVb-ie-ZDQ7hwqiw" }
- match: { aggregations.filterA.tsids.buckets.0.doc_count: 2 }
- do:
@@ -387,7 +384,6 @@ dynamic templates:
field: _tsid
- length: { aggregations.filterA.tsids.buckets: 1 }
- - match: { aggregations.filterA.tsids.buckets.0.key: "NOHjOAVWLTVWZM4CXLoraZYgYpiKqVppKnpcfycX2dfFiw707uoshWIGVb-ie-ZDQ7hwqiw" }
- match: { aggregations.filterA.tsids.buckets.0.doc_count: 2 }
- do:
@@ -406,7 +402,6 @@ dynamic templates:
field: _tsid
- length: { aggregations.filterA.tsids.buckets: 1 }
- - match: { aggregations.filterA.tsids.buckets.0.key: "NOHjOAVWLTVWZM4CXLoraZYgYpiKqVppKnpcfycX2dfFiw707uoshWIGVb-ie-ZDQ7hwqiw" }
- match: { aggregations.filterA.tsids.buckets.0.doc_count: 2 }
- do:
@@ -425,7 +420,6 @@ dynamic templates:
field: _tsid
- length: { aggregations.filterA.tsids.buckets: 1 }
- - match: { aggregations.filterA.tsids.buckets.0.key: "NOHjOAVWLTVWZM4CXLoraZYgYpiKqVppKnpcfycX2dfFiw707uoshWIGVb-ie-ZDQ7hwqiw" }
- match: { aggregations.filterA.tsids.buckets.0.doc_count: 2 }
- do:
search:
@@ -443,7 +437,6 @@ dynamic templates:
field: _tsid
- length: { aggregations.filterA.tsids.buckets: 1 }
- - match: { aggregations.filterA.tsids.buckets.0.key: "NOHjOAVWLTVWZM4CXLoraZYgYpiKqVppKnpcfycX2dfFiw707uoshWIGVb-ie-ZDQ7hwqiw" }
- match: { aggregations.filterA.tsids.buckets.0.doc_count: 2 }
---
@@ -520,7 +513,6 @@ dynamic templates - conflicting aliases:
field: _tsid
- length: { aggregations.filterA.tsids.buckets: 1 }
- - match: { aggregations.filterA.tsids.buckets.0.key: "KGejYryCnrIkXYZdIF_Q8F8X2dfFIGKYisFh7t1RGGWOWgWU7C0RiFE" }
- match: { aggregations.filterA.tsids.buckets.0.doc_count: 2 }
- do:
@@ -539,7 +531,6 @@ dynamic templates - conflicting aliases:
field: _tsid
- length: { aggregations.filterA.tsids.buckets: 1 }
- - match: { aggregations.filterA.tsids.buckets.0.key: "KGejYryCnrIkXYZdIF_Q8F8X2dfFIGKYisFh7t1RGGWOWgWU7C0RiFE" }
- match: { aggregations.filterA.tsids.buckets.0.doc_count: 2 }
---
@@ -699,7 +690,6 @@ dynamic templates with nesting:
field: _tsid
- length: { aggregations.filterA.tsids.buckets: 1 }
- - match: { aggregations.filterA.tsids.buckets.0.key: "OFP9EtCzqs8Sp7Rn2I9NahMBkssYqVppKnpcfycgYpiKiw707hfZ18UMdd8dUGmp6bH35LX6Gni-" }
- match: { aggregations.filterA.tsids.buckets.0.doc_count: 2 }
- do:
@@ -718,7 +708,6 @@ dynamic templates with nesting:
field: _tsid
- length: { aggregations.filterA.tsids.buckets: 1 }
- - match: { aggregations.filterA.tsids.buckets.0.key: "OFP9EtCzqs8Sp7Rn2I9NahMBkssYqVppKnpcfycgYpiKiw707hfZ18UMdd8dUGmp6bH35LX6Gni-" }
- match: { aggregations.filterA.tsids.buckets.0.doc_count: 2 }
- do:
@@ -737,7 +726,6 @@ dynamic templates with nesting:
field: _tsid
- length: { aggregations.filterA.tsids.buckets: 1 }
- - match: { aggregations.filterA.tsids.buckets.0.key: "OFP9EtCzqs8Sp7Rn2I9NahMBkssYqVppKnpcfycgYpiKiw707hfZ18UMdd8dUGmp6bH35LX6Gni-" }
- match: { aggregations.filterA.tsids.buckets.0.doc_count: 2 }
- do:
@@ -756,7 +744,6 @@ dynamic templates with nesting:
field: _tsid
- length: { aggregations.filterA.tsids.buckets: 1 }
- - match: { aggregations.filterA.tsids.buckets.0.key: "OFP9EtCzqs8Sp7Rn2I9NahMBkssYqVppKnpcfycgYpiKiw707hfZ18UMdd8dUGmp6bH35LX6Gni-" }
- match: { aggregations.filterA.tsids.buckets.0.doc_count: 2 }
- do:
@@ -775,7 +762,6 @@ dynamic templates with nesting:
field: _tsid
- length: { aggregations.filterA.tsids.buckets: 1 }
- - match: { aggregations.filterA.tsids.buckets.0.key: "OFP9EtCzqs8Sp7Rn2I9NahMBkssYqVppKnpcfycgYpiKiw707hfZ18UMdd8dUGmp6bH35LX6Gni-" }
- match: { aggregations.filterA.tsids.buckets.0.doc_count: 2 }
- do:
@@ -794,7 +780,6 @@ dynamic templates with nesting:
field: _tsid
- length: { aggregations.filterA.tsids.buckets: 1 }
- - match: { aggregations.filterA.tsids.buckets.0.key: "OFP9EtCzqs8Sp7Rn2I9NahMBkssYqVppKnpcfycgYpiKiw707hfZ18UMdd8dUGmp6bH35LX6Gni-" }
- match: { aggregations.filterA.tsids.buckets.0.doc_count: 2 }
---
diff --git a/server/build.gradle b/server/build.gradle
index 59615e5f42ac5..2c2c9771adcb5 100644
--- a/server/build.gradle
+++ b/server/build.gradle
@@ -59,6 +59,7 @@ dependencies {
// utilities
api project(":libs:cli")
implementation 'com.carrotsearch:hppc:0.8.1'
+ api 'com.dynatrace.hash4j:hash4j:0.25.0'
// precentil ranks aggregation
api 'org.hdrhistogram:HdrHistogram:2.1.9'
diff --git a/server/licenses/hash4j-LICENSE.txt b/server/licenses/hash4j-LICENSE.txt
new file mode 100644
index 0000000000000..261eeb9e9f8b2
--- /dev/null
+++ b/server/licenses/hash4j-LICENSE.txt
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/server/licenses/hash4j-NOTICE.txt b/server/licenses/hash4j-NOTICE.txt
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java
index 90cd3c669a52c..393d180be05f9 100644
--- a/server/src/main/java/module-info.java
+++ b/server/src/main/java/module-info.java
@@ -54,6 +54,7 @@
requires org.apache.lucene.queryparser;
requires org.apache.lucene.sandbox;
requires org.apache.lucene.suggest;
+ requires hash4j;
exports org.elasticsearch;
exports org.elasticsearch.action;
diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java
index f1f892f11ecf1..d863922f81d73 100644
--- a/server/src/main/java/org/elasticsearch/TransportVersions.java
+++ b/server/src/main/java/org/elasticsearch/TransportVersions.java
@@ -360,6 +360,7 @@ static TransportVersion def(int id) {
public static final TransportVersion INDEX_TEMPLATE_TRACKING_INFO = def(9_136_0_00);
public static final TransportVersion EXTENDED_SNAPSHOT_STATS_IN_NODE_INFO = def(9_137_0_00);
public static final TransportVersion SIMULATE_INGEST_MAPPING_MERGE_TYPE = def(9_138_0_00);
+ public static final TransportVersion INGEST_REQUEST_INCLUDE_TSID = def(9_139_0_00);
/*
* STOP! READ THIS FIRST! No, really,
diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexClusterStateUpdateRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexClusterStateUpdateRequest.java
index e8bd38b01414f..2bc51b252386c 100644
--- a/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexClusterStateUpdateRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/admin/indices/create/CreateIndexClusterStateUpdateRequest.java
@@ -52,6 +52,8 @@ public class CreateIndexClusterStateUpdateRequest {
private ComposableIndexTemplate matchingTemplate;
+ private boolean settingsSystemProvided = false;
+
/**
* @deprecated project id ought always be specified
*/
@@ -223,6 +225,19 @@ public CreateIndexClusterStateUpdateRequest setMatchingTemplate(ComposableIndexT
return this;
}
+ /**
+ * Indicates whether the {@link #settings} of this request are system provided.
+ * If this is true, private settings will be allowed to be set in the request.
+ */
+ public CreateIndexClusterStateUpdateRequest settingsSystemProvided(boolean settingsSystemProvided) {
+ this.settingsSystemProvided = settingsSystemProvided;
+ return this;
+ }
+
+ public boolean settingsSystemProvided() {
+ return settingsSystemProvided;
+ }
+
@Override
public String toString() {
return "CreateIndexClusterStateUpdateRequest{"
diff --git a/server/src/main/java/org/elasticsearch/action/bulk/TransportShardBulkAction.java b/server/src/main/java/org/elasticsearch/action/bulk/TransportShardBulkAction.java
index 01968596db932..357504fe87d1b 100644
--- a/server/src/main/java/org/elasticsearch/action/bulk/TransportShardBulkAction.java
+++ b/server/src/main/java/org/elasticsearch/action/bulk/TransportShardBulkAction.java
@@ -415,7 +415,8 @@ static boolean executeBulkItemRequest(
request.routing(),
request.getDynamicTemplates(),
request.getIncludeSourceOnError(),
- meteringParserDecorator
+ meteringParserDecorator,
+ request.tsid()
);
result = primary.applyIndexOperationOnPrimary(
version,
@@ -743,7 +744,11 @@ private static Engine.Result performOpOnReplica(
indexRequest.id(),
indexRequest.source(),
indexRequest.getContentType(),
- indexRequest.routing()
+ indexRequest.routing(),
+ Map.of(),
+ true,
+ XContentMeteringParserDecorator.NOOP,
+ indexRequest.tsid()
);
result = replica.applyIndexOperationOnReplica(
primaryResponse.getSeqNo(),
diff --git a/server/src/main/java/org/elasticsearch/action/bulk/TransportSimulateBulkAction.java b/server/src/main/java/org/elasticsearch/action/bulk/TransportSimulateBulkAction.java
index 7da12e05925af..47c435ada645a 100644
--- a/server/src/main/java/org/elasticsearch/action/bulk/TransportSimulateBulkAction.java
+++ b/server/src/main/java/org/elasticsearch/action/bulk/TransportSimulateBulkAction.java
@@ -204,7 +204,8 @@ private Tuple, Exception> validateMappings(
request.routing(),
request.getDynamicTemplates(),
request.getIncludeSourceOnError(),
- XContentMeteringParserDecorator.NOOP
+ XContentMeteringParserDecorator.NOOP,
+ request.tsid()
);
ProjectMetadata project = projectResolver.getProjectMetadata(clusterService.state());
diff --git a/server/src/main/java/org/elasticsearch/action/index/IndexRequest.java b/server/src/main/java/org/elasticsearch/action/index/IndexRequest.java
index 707ea0919a91f..a3210bcbb4983 100644
--- a/server/src/main/java/org/elasticsearch/action/index/IndexRequest.java
+++ b/server/src/main/java/org/elasticsearch/action/index/IndexRequest.java
@@ -9,6 +9,7 @@
package org.elasticsearch.action.index;
+import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.RamUsageEstimator;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchGenerationException;
@@ -152,6 +153,7 @@ public class IndexRequest extends ReplicatedWriteRequest implement
* rawTimestamp field is used on the coordinate node, it doesn't need to be serialised.
*/
private Object rawTimestamp;
+ private BytesRef tsid;
public IndexRequest(StreamInput in) throws IOException {
this(null, in);
@@ -216,6 +218,13 @@ public IndexRequest(@Nullable ShardId shardId, StreamInput in) throws IOExceptio
if (in.getTransportVersion().onOrAfter(TransportVersions.INGEST_REQUEST_INCLUDE_SOURCE_ON_ERROR)) {
includeSourceOnError = in.readBoolean();
} // else default value is true
+
+ if (in.getTransportVersion().onOrAfter(TransportVersions.INGEST_REQUEST_INCLUDE_TSID)) {
+ tsid = in.readBytesRef();
+ if (tsid.length == 0) {
+ tsid = null; // no tsid set
+ }
+ }
}
public IndexRequest() {
@@ -353,6 +362,22 @@ public String routing() {
return this.routing;
}
+ /**
+ * When {@link org.elasticsearch.cluster.metadata.IndexMetadata#INDEX_DIMENSIONS} is populated,
+ * the coordinating node will calculate _tsid during routing and set it on the request.
+ * For time series indices where the setting is not populated, the _tsid will be created in the data node during document parsing.
+ *
+ * The _tsid can not be directly set by a user, it is set by the coordinating node.
+ */
+ public IndexRequest tsid(BytesRef tsid) {
+ this.tsid = tsid;
+ return this;
+ }
+
+ public BytesRef tsid() {
+ return this.tsid;
+ }
+
/**
* Sets the ingest pipeline to be executed before indexing the document
*/
@@ -815,6 +840,9 @@ private void writeBody(StreamOutput out) throws IOException {
if (out.getTransportVersion().onOrAfter(TransportVersions.INGEST_REQUEST_INCLUDE_SOURCE_ON_ERROR)) {
out.writeBoolean(includeSourceOnError);
}
+ if (out.getTransportVersion().onOrAfter(TransportVersions.INGEST_REQUEST_INCLUDE_TSID)) {
+ out.writeBytesRef(tsid);
+ }
}
@Override
@@ -917,7 +945,7 @@ public Index getConcreteWriteIndex(IndexAbstraction ia, ProjectMetadata project)
@Override
public int route(IndexRouting indexRouting) {
- return indexRouting.indexShard(id, routing, contentType, source);
+ return indexRouting.indexShard(id, routing, tsid, contentType, source);
}
public IndexRequest setRequireAlias(boolean requireAlias) {
diff --git a/server/src/main/java/org/elasticsearch/action/index/IndexRequestBuilder.java b/server/src/main/java/org/elasticsearch/action/index/IndexRequestBuilder.java
index 3041e6bf5e274..620cd90cbebfc 100644
--- a/server/src/main/java/org/elasticsearch/action/index/IndexRequestBuilder.java
+++ b/server/src/main/java/org/elasticsearch/action/index/IndexRequestBuilder.java
@@ -9,6 +9,7 @@
package org.elasticsearch.action.index;
+import org.apache.lucene.util.BytesRef;
import org.elasticsearch.ElasticsearchGenerationException;
import org.elasticsearch.action.DocWriteRequest;
import org.elasticsearch.action.DocWriteResponse;
@@ -50,6 +51,7 @@ public class IndexRequestBuilder extends ReplicationRequestBuilder> settings() {
Property.ServerlessPublic
);
+ /**
+ * Populated when an index that belongs to a time_series data stream is created or its mappings are updated.
+ * This setting is used so that the coordinating node knows which fields are time series dimensions
+ * as it doesn't have access to mappings.
+ * When this setting is populated, an optimization kicks in that allows the coordinating node to create the tsid and the routing hash
+ * in one go.
+ * Otherwise, the coordinating node only creates the routing hash based on {@link #INDEX_ROUTING_PATH} and the tsid is created
+ * during document parsing, effectively requiring two passes over the document.
+ *
+ * The condition for this optimization to kick in is that all possible dimension fields can be identified
+ * via a list of wildcard patterns.
+ * If that's not the case (for example when certain types of dynamic templates are used),
+ * the {@link #INDEX_ROUTING_PATH} is populated instead.
+ */
+ public static final Setting> INDEX_DIMENSIONS = Setting.stringListSetting(
+ "index.dimensions",
+ Setting.Property.IndexScope,
+ Property.Dynamic,
+ Property.PrivateIndex
+ );
+
/**
* Legacy index setting, kept for 7.x BWC compatibility. This setting has no effect in 8.x. Do not use.
* TODO: Remove in 9.0
@@ -576,6 +597,7 @@ public Iterator> settings() {
private final int routingFactor;
private final int routingPartitionSize;
private final List routingPaths;
+ private final List dimensions;
private final int numberOfShards;
private final int numberOfReplicas;
@@ -689,6 +711,7 @@ private IndexMetadata(
final int routingNumShards,
final int routingPartitionSize,
final List routingPaths,
+ final List dimensions,
final ActiveShardCount waitForActiveShards,
final ImmutableOpenMap rolloverInfos,
final boolean isSystem,
@@ -744,6 +767,7 @@ private IndexMetadata(
this.routingFactor = routingNumShards / numberOfShards;
this.routingPartitionSize = routingPartitionSize;
this.routingPaths = routingPaths;
+ this.dimensions = dimensions;
this.waitForActiveShards = waitForActiveShards;
this.rolloverInfos = rolloverInfos;
this.isSystem = isSystem;
@@ -803,6 +827,7 @@ IndexMetadata withMappingMetadata(MappingMetadata mapping) {
this.routingNumShards,
this.routingPartitionSize,
this.routingPaths,
+ this.dimensions,
this.waitForActiveShards,
this.rolloverInfos,
this.isSystem,
@@ -865,6 +890,7 @@ public IndexMetadata withInSyncAllocationIds(int shardId, Set inSyncSet)
this.routingNumShards,
this.routingPartitionSize,
this.routingPaths,
+ this.dimensions,
this.waitForActiveShards,
this.rolloverInfos,
this.isSystem,
@@ -935,6 +961,7 @@ public IndexMetadata withSetPrimaryTerm(int shardId, long primaryTerm) {
this.routingNumShards,
this.routingPartitionSize,
this.routingPaths,
+ this.dimensions,
this.waitForActiveShards,
this.rolloverInfos,
this.isSystem,
@@ -996,6 +1023,7 @@ public IndexMetadata withTimestampRanges(IndexLongFieldRange timestampRange, Ind
this.routingNumShards,
this.routingPartitionSize,
this.routingPaths,
+ this.dimensions,
this.waitForActiveShards,
this.rolloverInfos,
this.isSystem,
@@ -1052,6 +1080,7 @@ public IndexMetadata withIncrementedVersion() {
this.routingNumShards,
this.routingPartitionSize,
this.routingPaths,
+ this.dimensions,
this.waitForActiveShards,
this.rolloverInfos,
this.isSystem,
@@ -1166,6 +1195,10 @@ public List getRoutingPaths() {
return routingPaths;
}
+ public List getDimensions() {
+ return dimensions;
+ }
+
public int getTotalNumberOfShards() {
return totalNumberOfShards;
}
@@ -2378,6 +2411,7 @@ IndexMetadata build(boolean repair) {
}
final List routingPaths = INDEX_ROUTING_PATH.get(settings);
+ final List dimensions = INDEX_DIMENSIONS.get(settings);
final String uuid = settings.get(SETTING_INDEX_UUID, INDEX_UUID_NA_VALUE);
@@ -2457,6 +2491,7 @@ IndexMetadata build(boolean repair) {
getRoutingNumShards(),
routingPartitionSize,
routingPaths,
+ dimensions,
waitForActiveShards,
rolloverInfos.build(),
isSystem,
diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java
index ca612eb20747a..4f4263dcaff04 100644
--- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java
+++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataCreateIndexService.java
@@ -1561,12 +1561,12 @@ private static void validateActiveShardCount(ActiveShardCount waitForActiveShard
private void validate(CreateIndexClusterStateUpdateRequest request, ProjectMetadata projectMetadata, RoutingTable routingTable) {
validateIndexName(request.index(), projectMetadata, routingTable);
- validateIndexSettings(request.index(), request.settings(), forbidPrivateIndexSettings);
+ validateIndexSettings(request.index(), request.settings(), forbidPrivateIndexSettings && request.settingsSystemProvided() == false);
}
public void validateIndexSettings(String indexName, final Settings settings, final boolean forbidPrivateIndexSettings)
throws IndexCreationException {
- List validationErrors = getIndexSettingsValidationErrors(settings, forbidPrivateIndexSettings);
+ List validationErrors = getIndexSettingsValidationErrors(settings, null, forbidPrivateIndexSettings);
if (validationErrors.isEmpty() == false) {
ValidationException validationException = new ValidationException();
@@ -1575,21 +1575,30 @@ public void validateIndexSettings(String indexName, final Settings settings, fin
}
}
- List getIndexSettingsValidationErrors(final Settings settings, final boolean forbidPrivateIndexSettings) {
+ List getIndexSettingsValidationErrors(
+ final Settings settings,
+ @Nullable Settings systemProvided,
+ final boolean forbidPrivateIndexSettings
+ ) {
List validationErrors = validateIndexCustomPath(settings, env.sharedDataDir());
if (forbidPrivateIndexSettings) {
- validationErrors.addAll(validatePrivateSettingsNotExplicitlySet(settings, indexScopedSettings));
+ validationErrors.addAll(validatePrivateSettingsNotExplicitlySet(settings, systemProvided, indexScopedSettings));
}
return validationErrors;
}
- private static List validatePrivateSettingsNotExplicitlySet(Settings settings, IndexScopedSettings indexScopedSettings) {
+ private static List validatePrivateSettingsNotExplicitlySet(
+ Settings settings,
+ @Nullable Settings systemProvided,
+ IndexScopedSettings indexScopedSettings
+ ) {
List validationErrors = new ArrayList<>();
for (final String key : settings.keySet()) {
final Setting> setting = indexScopedSettings.get(key);
if (setting == null) {
assert indexScopedSettings.isPrivateSetting(key) : "expected [" + key + "] to be private but it was not";
- } else if (setting.isPrivateIndex()) {
+ } else if (setting.isPrivateIndex() && (systemProvided == null || settings.get(key).equals(systemProvided.get(key)) == false)) {
+ // if the setting is system provided, they're not set by the user
validationErrors.add("private index setting [" + key + "] can not be set explicitly");
}
}
diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
index d490ba527ad65..bafda76b8cf12 100644
--- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
+++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
@@ -774,6 +774,7 @@ void validateIndexTemplateV2(ProjectMetadata projectMetadata, String name, Compo
final var combinedSettings = resolveSettings(indexTemplate, projectMetadata.componentTemplates());
// First apply settings sourced from index setting providers:
var finalSettings = Settings.builder();
+ var additionalSettingsBuilder = Settings.builder();
for (var provider : indexSettingProviders) {
var newAdditionalSettings = provider.getAdditionalIndexSettings(
VALIDATE_INDEX_NAME,
@@ -784,9 +785,11 @@ void validateIndexTemplateV2(ProjectMetadata projectMetadata, String name, Compo
combinedSettings,
combinedMappings
);
- MetadataCreateIndexService.validateAdditionalSettings(provider, newAdditionalSettings, finalSettings);
- finalSettings.put(newAdditionalSettings);
+ MetadataCreateIndexService.validateAdditionalSettings(provider, newAdditionalSettings, additionalSettingsBuilder);
+ additionalSettingsBuilder.put(newAdditionalSettings);
}
+ Settings additionalSettings = additionalSettingsBuilder.build();
+ finalSettings.put(additionalSettings);
// Then apply setting from component templates:
finalSettings.put(combinedSettings);
// Then finally apply settings resolved from index template:
@@ -796,7 +799,7 @@ void validateIndexTemplateV2(ProjectMetadata projectMetadata, String name, Compo
var templateToValidate = indexTemplate.toBuilder().template(Template.builder(finalTemplate).settings(finalSettings)).build();
- validate(name, templateToValidate);
+ validate(name, templateToValidate, additionalSettings);
validateDataStreamsStillReferenced(projectMetadata, name, templateToValidate);
validateLifecycle(projectMetadata, name, templateToValidate, globalRetentionSettings.get(false));
validateDataStreamOptions(projectMetadata, name, templateToValidate, globalRetentionSettings.get(true));
@@ -2025,18 +2028,19 @@ static void validateTemplate(Settings validateSettings, CompressedXContent mappi
}
private void validate(String name, ComponentTemplate template) {
- validate(name, template.template(), Collections.emptyList());
+ validate(name, template.template(), Collections.emptyList(), null);
}
- private void validate(String name, ComposableIndexTemplate template) {
- validate(name, template.template(), template.indexPatterns());
+ private void validate(String name, ComposableIndexTemplate template, Settings additionalSettings) {
+ validate(name, template.template(), template.indexPatterns(), additionalSettings);
}
- private void validate(String name, Template template, List indexPatterns) {
+ private void validate(String name, Template template, List indexPatterns, @Nullable Settings systemProvided) {
Optional maybeTemplate = Optional.ofNullable(template);
validate(
name,
maybeTemplate.map(Template::settings).orElse(Settings.EMPTY),
+ systemProvided,
indexPatterns,
maybeTemplate.map(Template::aliases).orElse(emptyMap()).values().stream().map(MetadataIndexTemplateService::toAlias).toList()
);
@@ -2055,10 +2059,16 @@ private static Alias toAlias(AliasMetadata aliasMeta) {
}
private void validate(PutRequest putRequest) {
- validate(putRequest.name, putRequest.settings, putRequest.indexPatterns, putRequest.aliases);
+ validate(putRequest.name, putRequest.settings, null, putRequest.indexPatterns, putRequest.aliases);
}
- private void validate(String name, @Nullable Settings settings, List indexPatterns, List aliases) {
+ private void validate(
+ String name,
+ @Nullable Settings settings,
+ @Nullable Settings systemProvided,
+ List indexPatterns,
+ List aliases
+ ) {
List validationErrors = new ArrayList<>();
if (name.contains(" ")) {
validationErrors.add("name must not contain a space");
@@ -2111,7 +2121,11 @@ private void validate(String name, @Nullable Settings settings, List ind
validationErrors.add(t.getMessage());
}
}
- List indexSettingsValidation = metadataCreateIndexService.getIndexSettingsValidationErrors(settings, true);
+ List indexSettingsValidation = metadataCreateIndexService.getIndexSettingsValidationErrors(
+ settings,
+ systemProvided,
+ true
+ );
validationErrors.addAll(indexSettingsValidation);
}
diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataMappingService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataMappingService.java
index 6680b804dc25b..4094c58f499ee 100644
--- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataMappingService.java
+++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataMappingService.java
@@ -24,9 +24,12 @@
import org.elasticsearch.common.Priority;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.compress.CompressedXContent;
+import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.core.IOUtils;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.index.Index;
+import org.elasticsearch.index.IndexSettingProvider;
+import org.elasticsearch.index.IndexSettingProviders;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.index.mapper.DocumentMapper;
import org.elasticsearch.index.mapper.MapperService;
@@ -39,6 +42,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* Service responsible for submitting mapping changes
@@ -53,10 +57,14 @@ public class MetadataMappingService {
private final MasterServiceTaskQueue taskQueue;
@Inject
- public MetadataMappingService(ClusterService clusterService, IndicesService indicesService) {
+ public MetadataMappingService(
+ ClusterService clusterService,
+ IndicesService indicesService,
+ IndexSettingProviders indexSettingProviders
+ ) {
this.clusterService = clusterService;
this.indicesService = indicesService;
- this.taskQueue = clusterService.createTaskQueue("put-mapping", Priority.HIGH, new PutMappingExecutor());
+ this.taskQueue = clusterService.createTaskQueue("put-mapping", Priority.HIGH, new PutMappingExecutor(indexSettingProviders));
}
record PutMappingClusterStateUpdateTask(PutMappingClusterStateUpdateRequest request, ActionListener listener)
@@ -96,6 +104,16 @@ public TimeValue ackTimeout() {
}
class PutMappingExecutor implements ClusterStateTaskExecutor {
+ private final IndexSettingProviders indexSettingProviders;
+
+ PutMappingExecutor() {
+ this(new IndexSettingProviders(Set.of()));
+ }
+
+ PutMappingExecutor(IndexSettingProviders indexSettingProviders) {
+ this.indexSettingProviders = indexSettingProviders;
+ }
+
@Override
public ClusterState execute(BatchExecutionContext batchExecutionContext) throws Exception {
Map indexMapperServices = new HashMap<>();
@@ -126,7 +144,7 @@ public ClusterState execute(BatchExecutionContext indexMapperServices
@@ -200,9 +218,26 @@ private static ClusterState applyRequest(
indexMetadataBuilder.putMapping(new MappingMetadata(docMapper));
indexMetadataBuilder.putInferenceFields(docMapper.mappers().inferenceFields());
}
+ boolean updatedSettings = false;
+ final Settings.Builder additionalIndexSettings = Settings.builder();
if (updatedMapping) {
indexMetadataBuilder.mappingVersion(1 + indexMetadataBuilder.mappingVersion())
.mappingsUpdatedVersion(IndexVersion.current());
+ for (IndexSettingProvider provider : indexSettingProviders.getIndexSettingProviders()) {
+ Settings newAdditionalSettings = provider.onUpdateMappings(indexMetadata, docMapper);
+ if (newAdditionalSettings != null && newAdditionalSettings.isEmpty() == false) {
+ MetadataCreateIndexService.validateAdditionalSettings(provider, newAdditionalSettings, additionalIndexSettings);
+ additionalIndexSettings.put(newAdditionalSettings);
+ updatedSettings = true;
+ }
+ }
+ }
+ if (updatedSettings) {
+ final Settings.Builder indexSettingsBuilder = Settings.builder();
+ indexSettingsBuilder.put(indexMetadata.getSettings());
+ indexSettingsBuilder.put(additionalIndexSettings.build());
+ indexMetadataBuilder.settings(indexSettingsBuilder.build());
+ indexMetadataBuilder.settingsVersion(1 + indexMetadata.getSettingsVersion());
}
/*
* This implicitly increments the index metadata version and builds the index metadata. This means that we need to have
diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/IndexRouting.java b/server/src/main/java/org/elasticsearch/cluster/routing/IndexRouting.java
index 050181802af8d..4d752b7597dad 100644
--- a/server/src/main/java/org/elasticsearch/cluster/routing/IndexRouting.java
+++ b/server/src/main/java/org/elasticsearch/cluster/routing/IndexRouting.java
@@ -24,6 +24,7 @@
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.common.util.ByteUtils;
import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.core.CheckedConsumer;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.features.NodeFeature;
import org.elasticsearch.index.IndexMode;
@@ -42,7 +43,6 @@
import java.util.Base64;
import java.util.Collections;
import java.util.List;
-import java.util.Map;
import java.util.OptionalInt;
import java.util.Set;
import java.util.function.IntConsumer;
@@ -63,7 +63,7 @@ public abstract class IndexRouting {
* Build the routing from {@link IndexMetadata}.
*/
public static IndexRouting fromIndexMetadata(IndexMetadata metadata) {
- if (false == metadata.getRoutingPaths().isEmpty()) {
+ if (metadata.getRoutingPaths().isEmpty() == false || metadata.getDimensions().isEmpty() == false) {
return new ExtractFromSource(metadata);
}
if (metadata.isRoutingPartitionedIndex()) {
@@ -98,7 +98,13 @@ public void postProcess(IndexRequest indexRequest) {}
* Called when indexing a document to generate the shard id that should contain
* a document with the provided parameters.
*/
- public abstract int indexShard(String id, @Nullable String routing, XContentType sourceType, BytesReference source);
+ public abstract int indexShard(
+ String id,
+ @Nullable String routing,
+ @Nullable BytesRef tsid,
+ XContentType sourceType,
+ BytesReference source
+ );
/**
* Called when updating a document to generate the shard id that should contain
@@ -211,7 +217,13 @@ private static boolean isNewIndexVersion(final IndexVersion creationVersion) {
}
@Override
- public int indexShard(String id, @Nullable String routing, XContentType sourceType, BytesReference source) {
+ public int indexShard(
+ String id,
+ @Nullable String routing,
+ @Nullable BytesRef tsid,
+ XContentType sourceType,
+ BytesReference source
+ ) {
if (id == null) {
throw new IllegalStateException("id is required and should have been set by process");
}
@@ -300,8 +312,11 @@ public static class ExtractFromSource extends IndexRouting {
private final XContentParserConfiguration parserConfig;
private final IndexMode indexMode;
private final boolean trackTimeSeriesRoutingHash;
+ private final boolean createTsidDuringRouting;
private final boolean addIdWithRoutingHash;
private int hash = Integer.MAX_VALUE;
+ @Nullable
+ private BytesRef tsid;
ExtractFromSource(IndexMetadata metadata) {
super(metadata);
@@ -309,12 +324,27 @@ public static class ExtractFromSource extends IndexRouting {
throw new IllegalArgumentException("routing_partition_size is incompatible with routing_path");
}
indexMode = metadata.getIndexMode();
- trackTimeSeriesRoutingHash = indexMode == IndexMode.TIME_SERIES
- && metadata.getCreationVersion().onOrAfter(IndexVersions.TIME_SERIES_ROUTING_HASH_IN_ID);
+ var createTsidDuringRouting = false;
+ var trackTimeSeriesRoutingHash = false;
+ List includePaths;
+ includePaths = metadata.getRoutingPaths();
+ if (indexMode == IndexMode.TIME_SERIES) {
+ if (metadata.getDimensions().isEmpty() == false && metadata.getRoutingPaths().isEmpty()) {
+ // This optimization is only available for new indices where
+ // the dimensions index setting is automatically populated from the mappings.
+ // If users manually set the routing paths, the optimization is not applied.
+ createTsidDuringRouting = true;
+ includePaths = metadata.getDimensions();
+ }
+ if (metadata.getCreationVersion().onOrAfter(IndexVersions.TIME_SERIES_ROUTING_HASH_IN_ID)) {
+ trackTimeSeriesRoutingHash = true;
+ }
+ }
+ this.createTsidDuringRouting = createTsidDuringRouting;
+ this.trackTimeSeriesRoutingHash = trackTimeSeriesRoutingHash;
addIdWithRoutingHash = indexMode == IndexMode.LOGSDB;
- List routingPaths = metadata.getRoutingPaths();
- isRoutingPath = Regex.simpleMatcher(routingPaths.toArray(String[]::new));
- this.parserConfig = XContentParserConfiguration.EMPTY.withFiltering(null, Set.copyOf(routingPaths), null, true);
+ isRoutingPath = Regex.simpleMatcher(includePaths.toArray(String[]::new));
+ this.parserConfig = XContentParserConfiguration.EMPTY.withFiltering(null, Set.copyOf(includePaths), null, true);
}
public boolean matchesField(String fieldName) {
@@ -323,8 +353,12 @@ public boolean matchesField(String fieldName) {
@Override
public void postProcess(IndexRequest indexRequest) {
- // Update the request with the routing hash, if needed.
+ // Update the request with the routing hash and the tsid, if needed.
// This needs to happen in post-processing, after the routing hash is calculated.
+ if (createTsidDuringRouting) {
+ assert tsid != null;
+ indexRequest.tsid(tsid);
+ }
if (trackTimeSeriesRoutingHash) {
indexRequest.routing(TimeSeriesRoutingHashFieldMapper.encode(hash));
} else if (addIdWithRoutingHash) {
@@ -334,59 +368,79 @@ public void postProcess(IndexRequest indexRequest) {
}
@Override
- public int indexShard(String id, @Nullable String routing, XContentType sourceType, BytesReference source) {
+ public int indexShard(
+ String id,
+ @Nullable String routing,
+ @Nullable BytesRef tsid,
+ XContentType sourceType,
+ BytesReference source
+ ) {
assert Transports.assertNotTransportThread("parsing the _source can get slow");
checkNoRouting(routing);
- hash = hashSource(sourceType, source).buildHash(IndexRouting.ExtractFromSource::defaultOnEmpty);
+ if (createTsidDuringRouting) {
+ if (tsid == null) {
+ this.tsid = buildTsid(sourceType, source);
+ } else {
+ this.tsid = tsid;
+ }
+ hash = hash(this.tsid);
+ } else {
+ hash = hashRoutingFields(sourceType, source).buildHash(IndexRouting.ExtractFromSource::defaultOnEmpty);
+ }
int shardId = hashToShardId(hash);
return (rerouteIfResharding(shardId));
}
public String createId(XContentType sourceType, BytesReference source, byte[] suffix) {
- return hashSource(sourceType, source).createId(suffix, IndexRouting.ExtractFromSource::defaultOnEmpty);
- }
-
- public String createId(Map flat, byte[] suffix) {
- Builder b = builder();
- for (Map.Entry e : flat.entrySet()) {
- if (isRoutingPath.test(e.getKey())) {
- if (e.getValue() instanceof List> listValue) {
- for (Object v : listValue) {
- b.addHash(e.getKey(), new BytesRef(v.toString()));
- }
- } else {
- b.addHash(e.getKey(), new BytesRef(e.getValue().toString()));
- }
- }
- }
- return b.createId(suffix, IndexRouting.ExtractFromSource::defaultOnEmpty);
+ return hashRoutingFields(sourceType, source).createId(suffix, IndexRouting.ExtractFromSource::defaultOnEmpty);
}
private static int defaultOnEmpty() {
throw new IllegalArgumentException("Error extracting routing: source didn't contain any routing fields");
}
- public Builder builder() {
- return new Builder();
+ public RoutingHashBuilder builder() {
+ return new RoutingHashBuilder();
+ }
+
+ private RoutingHashBuilder hashRoutingFields(XContentType sourceType, BytesReference source) {
+ RoutingHashBuilder b = builder();
+ withFilteredParser(sourceType, source, parser -> b.extractObject(null, parser));
+ return b;
}
- private Builder hashSource(XContentType sourceType, BytesReference source) {
- Builder b = builder();
+ private BytesRef buildTsid(XContentType sourceType, BytesReference source) {
+ TsidBuilder b = new TsidBuilder();
+ withFilteredParser(sourceType, source, parser -> b.add(parser, XContentParserTsidFunnel.get()));
+ return b.buildTsid();
+ }
+
+ private void withFilteredParser(
+ XContentType sourceType,
+ BytesReference source,
+ CheckedConsumer parserConsumer
+ ) {
try (XContentParser parser = XContentHelper.createParserNotCompressed(parserConfig, source, sourceType)) {
parser.nextToken(); // Move to first token
if (parser.currentToken() == null) {
throw new IllegalArgumentException("Error extracting routing: source didn't contain any routing fields");
}
parser.nextToken();
- b.extractObject(null, parser);
+ parserConsumer.accept(parser);
ensureExpectedToken(null, parser.nextToken(), parser);
} catch (IOException | ParsingException e) {
throw new IllegalArgumentException("Error extracting routing: " + e.getMessage(), e);
}
- return b;
}
- public class Builder {
+ /**
+ * Visible for testing
+ */
+ boolean isCreateTsidDuringRouting() {
+ return createTsidDuringRouting;
+ }
+
+ public class RoutingHashBuilder {
private final List hashes = new ArrayList<>();
public void addMatching(String fieldName, BytesRef string) {
@@ -395,6 +449,11 @@ public void addMatching(String fieldName, BytesRef string) {
}
}
+ /**
+ * Only expected to be called for old indices created before
+ * {@link IndexVersions#TIME_SERIES_ROUTING_HASH_IN_ID} while creating (during ingestion)
+ * or synthesizing (at query time) the _id field.
+ */
public String createId(byte[] suffix, IntSupplier onEmpty) {
byte[] idBytes = new byte[4 + suffix.length];
ByteUtils.writeIntLE(buildHash(onEmpty), idBytes, 0);
@@ -465,6 +524,16 @@ private int buildHash(IntSupplier onEmpty) {
}
return hash;
}
+
+ private record NameAndHash(BytesRef name, int hash, int order) implements Comparable {
+ @Override
+ public int compareTo(NameAndHash o) {
+ int i = name.compareTo(o.name);
+ if (i != 0) return i;
+ // ensures array values are in the order as they appear in the source
+ return Integer.compare(order, o.order);
+ }
+ }
}
private static int hash(BytesRef ref) {
@@ -525,15 +594,78 @@ public void collectSearchShards(String routing, IntConsumer consumer) {
private String error(String operation) {
return operation + " is not supported because the destination index [" + indexName + "] is in " + indexMode.getName() + " mode";
}
- }
- private record NameAndHash(BytesRef name, int hash, int order) implements Comparable {
- @Override
- public int compareTo(NameAndHash o) {
- int i = name.compareTo(o.name);
- if (i != 0) return i;
- // ensures array values are in the order as they appear in the source
- return Integer.compare(order, o.order);
+ static class XContentParserTsidFunnel implements TsidBuilder.ThrowingTsidFunnel {
+
+ private static final XContentParserTsidFunnel INSTANCE = new XContentParserTsidFunnel();
+
+ static XContentParserTsidFunnel get() {
+ return INSTANCE;
+ }
+
+ @Override
+ public void add(XContentParser value, TsidBuilder tsidBuilder) throws IOException {
+ extractObject(tsidBuilder, null, value);
+ }
+
+ private void extractObject(TsidBuilder tsidBuilder, @Nullable String path, XContentParser source) throws IOException {
+ while (source.currentToken() != Token.END_OBJECT) {
+ ensureExpectedToken(Token.FIELD_NAME, source.currentToken(), source);
+ String fieldName = source.currentName();
+ String subPath = path == null ? fieldName : path + "." + fieldName;
+ source.nextToken();
+ extractItem(tsidBuilder, subPath, source);
+ }
+ }
+
+ private void extractArray(TsidBuilder tsidBuilder, @Nullable String path, XContentParser source) throws IOException {
+ while (source.currentToken() != Token.END_ARRAY) {
+ expectValueToken(source.currentToken(), source);
+ extractItem(tsidBuilder, path, source);
+ }
+ }
+
+ private void extractItem(TsidBuilder tsidBuilder, String path, XContentParser source) throws IOException {
+ switch (source.currentToken()) {
+ case START_OBJECT:
+ source.nextToken();
+ extractObject(tsidBuilder, path, source);
+ source.nextToken();
+ break;
+ case VALUE_NUMBER:
+ switch (source.numberType()) {
+ case INT -> tsidBuilder.addIntDimension(path, source.intValue());
+ case LONG -> tsidBuilder.addLongDimension(path, source.longValue());
+ case FLOAT -> tsidBuilder.addDoubleDimension(path, source.floatValue());
+ case DOUBLE -> tsidBuilder.addDoubleDimension(path, source.doubleValue());
+ case BIG_DECIMAL, BIG_INTEGER -> tsidBuilder.addStringDimension(path, source.optimizedText().bytes());
+ }
+ source.nextToken();
+ break;
+ case VALUE_BOOLEAN:
+ tsidBuilder.addBooleanDimension(path, source.booleanValue());
+ source.nextToken();
+ break;
+ case VALUE_STRING:
+ tsidBuilder.addStringDimension(path, source.optimizedText().bytes());
+ source.nextToken();
+ break;
+ case START_ARRAY:
+ source.nextToken();
+ extractArray(tsidBuilder, path, source);
+ source.nextToken();
+ break;
+ case VALUE_NULL:
+ source.nextToken();
+ break;
+ default:
+ throw new ParsingException(
+ source.getTokenLocation(),
+ "Cannot extract dimension due to unexpected token [{}]",
+ source.currentToken()
+ );
+ }
+ }
}
}
}
diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/TsidBuilder.java b/server/src/main/java/org/elasticsearch/cluster/routing/TsidBuilder.java
new file mode 100644
index 0000000000000..38bff99917635
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/cluster/routing/TsidBuilder.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.cluster.routing;
+
+import com.dynatrace.hash4j.hashing.HashStream128;
+import com.dynatrace.hash4j.hashing.HashStream32;
+import com.dynatrace.hash4j.hashing.HashValue128;
+import com.dynatrace.hash4j.hashing.Hasher128;
+import com.dynatrace.hash4j.hashing.Hasher32;
+import com.dynatrace.hash4j.hashing.Hashing;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.util.ByteUtils;
+import org.elasticsearch.index.mapper.RoutingPathFields;
+import org.elasticsearch.xcontent.XContentString;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class TsidBuilder {
+
+ private static final int MAX_TSID_VALUE_FIELDS = 16;
+ private static final Hasher128 HASHER_128 = Hashing.murmur3_128();
+ private static final Hasher32 HASHER_32 = Hashing.murmur3_32();
+ private final HashStream128 hashStream = HASHER_128.hashStream();
+
+ private final List dimensions = new ArrayList<>();
+
+ public void addIntDimension(String path, int value) {
+ addDimension(path, new HashValue128(1, value));
+ }
+
+ public void addLongDimension(String path, long value) {
+ addDimension(path, new HashValue128(2, value));
+ }
+
+ public void addDoubleDimension(String path, double value) {
+ addDimension(path, new HashValue128(2, Double.doubleToLongBits(value)));
+ }
+
+ public void addBooleanDimension(String path, boolean value) {
+ addDimension(path, new HashValue128(4, value ? 1 : 0));
+ }
+
+ public void addStringDimension(String path, String value) {
+ addStringDimension(path, new BytesRef(value));
+ }
+
+ private void addStringDimension(String path, BytesRef value) {
+ addStringDimension(path, value.bytes, value.offset, value.length);
+ }
+
+ public void addStringDimension(String path, XContentString.UTF8Bytes value) {
+ addStringDimension(path, value.bytes(), value.offset(), value.length());
+ }
+
+ public void addStringDimension(String path, byte[] value) {
+ addStringDimension(path, value, 0, value.length);
+ }
+
+ private void addStringDimension(String path, byte[] bytes, int offset, int length) {
+ hashStream.reset();
+ hashStream.putBytes(bytes, offset, length);
+ HashValue128 valueHash = hashStream.get();
+ addDimension(path, valueHash);
+ }
+
+ public void addBytesDimension(String path, byte[] value) {
+ hashStream.reset();
+ hashStream.putBytes(value);
+ HashValue128 valueHash = hashStream.get();
+ addDimension(path, valueHash);
+ }
+
+ public void add(T value, TsidFunnel funnel) {
+ funnel.add(value, this);
+ }
+
+ public void add(T value, ThrowingTsidFunnel funnel) throws E {
+ funnel.add(value, this);
+ }
+
+ private void addDimension(String path, HashValue128 valueHash) {
+ hashStream.reset();
+ hashStream.putString(path);
+ HashValue128 pathHash = hashStream.get();
+ dimensions.add(new Dimension(path, pathHash, valueHash, dimensions.size()));
+ }
+
+ public void addAll(TsidBuilder other) {
+ if (other == null || other.dimensions.isEmpty()) {
+ return;
+ }
+ dimensions.addAll(other.dimensions);
+ }
+
+ public HashValue128 hash() {
+ if (dimensions.isEmpty()) {
+ throw new IllegalArgumentException("Error extracting routing: source didn't contain any routing fields");
+ }
+ Collections.sort(dimensions);
+ HashStream128 hashStream = HASHER_128.hashStream();
+ for (Dimension dim : dimensions) {
+ hashStream.putLong(dim.pathHash.getMostSignificantBits());
+ hashStream.putLong(dim.pathHash.getLeastSignificantBits());
+ hashStream.putLong(dim.value.getMostSignificantBits());
+ hashStream.putLong(dim.value.getLeastSignificantBits());
+ }
+ return hashStream.get();
+ }
+
+ /**
+ * Builds a time series identifier (TSID) based on the dimensions added to this builder.
+ * This is a slight adaptation of {@link RoutingPathFields#buildHash()} but creates shorter tsids.
+ * The TSID is a hash that includes:
+ *
+ *
+ * A hash of the dimension field names (4 bytes).
+ * This is to cluster time series that are using the same dimensions together, which makes the encodings more effective.
+ *
+ *
+ * A hash of the dimension field values (1 byte each, up to a maximum of 16 fields).
+ * This is to cluster time series with similar values together, also helping with making encodings more effective.
+ *
+ *
+ * A hash of all names and values combined (16 bytes).
+ * This is to avoid hash collisions.
+ *
+ *
+ *
+ * @return a BytesRef containing the TSID
+ * @throws IllegalArgumentException if no dimensions have been added
+ */
+ public BytesRef buildTsid() {
+ if (dimensions.isEmpty()) {
+ throw new IllegalArgumentException("Error extracting dimensions: source didn't contain any routing fields");
+ }
+
+ int numberOfValues = Math.min(MAX_TSID_VALUE_FIELDS, dimensions.size());
+ byte[] hash = new byte[4 + numberOfValues + 16];
+ int index = 0;
+
+ Collections.sort(dimensions);
+ HashStream32 fieldNameHash = HASHER_32.hashStream();
+ HashStream128 dimensionsHash = HASHER_128.hashStream();
+ for (int i = 0; i < dimensions.size(); i++) {
+ Dimension dim = dimensions.get(i);
+ fieldNameHash.putLong(dim.pathHash.getMostSignificantBits());
+ fieldNameHash.putLong(dim.pathHash.getLeastSignificantBits());
+ dimensionsHash.putLong(dim.pathHash.getMostSignificantBits());
+ dimensionsHash.putLong(dim.pathHash.getLeastSignificantBits());
+ dimensionsHash.putLong(dim.value.getMostSignificantBits());
+ dimensionsHash.putLong(dim.value.getLeastSignificantBits());
+ }
+ ByteUtils.writeIntLE(fieldNameHash.getAsInt(), hash, index);
+ index += 4;
+
+ // similarity hash for values
+ String previousPath = null;
+ for (int i = 0; i < numberOfValues; i++) {
+ Dimension dim = dimensions.get(i);
+ String path = dim.path();
+ if (path == previousPath) {
+ // only add the first value for array fields
+ continue;
+ }
+ hash[index++] = (byte) dim.value().getAsInt();
+ previousPath = path;
+ }
+
+ index = writeHash128(dimensionsHash.get(), hash, index);
+ return new BytesRef(hash, 0, index);
+ }
+
+ private static int writeHash128(HashValue128 hash128, byte[] buffer, int index) {
+ ByteUtils.writeLongLE(hash128.getLeastSignificantBits(), buffer, index);
+ index += 8;
+ ByteUtils.writeLongLE(hash128.getMostSignificantBits(), buffer, index);
+ index += 8;
+ return index;
+ }
+
+ public interface TsidFunnel {
+ void add(T value, TsidBuilder tsidBuilder);
+ }
+
+ public interface ThrowingTsidFunnel {
+ void add(T value, TsidBuilder tsidBuilder) throws E;
+ }
+
+ private record Dimension(String path, HashValue128 pathHash, HashValue128 value, int order) implements Comparable {
+ @Override
+ public int compareTo(Dimension o) {
+ int i = path.compareTo(o.path);
+ if (i != 0) return i;
+ // ensures array values are in the order as they appear in the source
+ return Integer.compare(order, o.order);
+ }
+ }
+}
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 9f4c5b80ccf23..4010a8025100a 100644
--- a/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java
+++ b/server/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java
@@ -225,6 +225,7 @@ public final class IndexScopedSettings extends AbstractScopedSettings {
// TSDB index settings
IndexSettings.MODE,
IndexMetadata.INDEX_ROUTING_PATH,
+ IndexMetadata.INDEX_DIMENSIONS,
IndexSettings.TIME_SERIES_START_TIME,
IndexSettings.TIME_SERIES_END_TIME,
IndexSettings.SEQ_NO_INDEX_OPTIONS_SETTING,
diff --git a/server/src/main/java/org/elasticsearch/index/IndexMode.java b/server/src/main/java/org/elasticsearch/index/IndexMode.java
index 06b10bef8514d..d440d919adaef 100644
--- a/server/src/main/java/org/elasticsearch/index/IndexMode.java
+++ b/server/src/main/java/org/elasticsearch/index/IndexMode.java
@@ -35,6 +35,7 @@
import org.elasticsearch.index.mapper.RoutingFields;
import org.elasticsearch.index.mapper.RoutingPathFields;
import org.elasticsearch.index.mapper.SourceFieldMapper;
+import org.elasticsearch.index.mapper.SourceToParse;
import org.elasticsearch.index.mapper.TimeSeriesIdFieldMapper;
import org.elasticsearch.index.mapper.TimeSeriesRoutingHashFieldMapper;
import org.elasticsearch.index.mapper.TsidExtractingIdFieldMapper;
@@ -113,7 +114,7 @@ public IdFieldMapper buildIdFieldMapper(BooleanSupplier fieldDataEnabled) {
}
@Override
- public RoutingFields buildRoutingFields(IndexSettings settings) {
+ public RoutingFields buildRoutingFields(IndexSettings settings, SourceToParse source) {
return RoutingFields.Noop.INSTANCE;
}
@@ -146,13 +147,17 @@ void validateWithOtherSettings(Map, Object> settings) {
throw new IllegalArgumentException(error(unsupported));
}
}
- checkSetting(settings, IndexMetadata.INDEX_ROUTING_PATH);
+ Setting> routingPath = IndexMetadata.INDEX_ROUTING_PATH;
+ if (isEmpty(settings, routingPath) && isEmpty(settings, IndexMetadata.INDEX_DIMENSIONS)) {
+ // index.dimensions is a private setting that only gets populated for data streams.
+ // We don't include it in the error message to not confuse users that are manually creating time series indices
+ // which is the only case where this error can occur.
+ throw new IllegalArgumentException(tsdbMode() + " requires a non-empty [" + routingPath.getKey() + "]");
+ }
}
- private static void checkSetting(Map, Object> settings, Setting> setting) {
- if (Objects.equals(setting.getDefault(Settings.EMPTY), settings.get(setting))) {
- throw new IllegalArgumentException(tsdbMode() + " requires a non-empty [" + setting.getKey() + "]");
- }
+ private static boolean isEmpty(Map, Object> settings, Setting> setting) {
+ return Objects.equals(setting.getDefault(Settings.EMPTY), settings.get(setting));
}
private static String error(Setting> unsupported) {
@@ -213,7 +218,11 @@ public IdFieldMapper buildIdFieldMapper(BooleanSupplier fieldDataEnabled) {
}
@Override
- public RoutingFields buildRoutingFields(IndexSettings settings) {
+ public RoutingFields buildRoutingFields(IndexSettings settings, SourceToParse source) {
+ if (source.tsid() != null) {
+ // If the source already has a _tsid field, we don't need to extract routing from the source.
+ return RoutingFields.Noop.INSTANCE;
+ }
IndexRouting.ExtractFromSource routing = (IndexRouting.ExtractFromSource) settings.getIndexRouting();
return new RoutingPathFields(routing.builder());
}
@@ -295,7 +304,7 @@ public MetadataFieldMapper timeSeriesRoutingHashFieldMapper() {
}
@Override
- public RoutingFields buildRoutingFields(IndexSettings settings) {
+ public RoutingFields buildRoutingFields(IndexSettings settings, SourceToParse source) {
return RoutingFields.Noop.INSTANCE;
}
@@ -376,7 +385,7 @@ public IdFieldMapper buildIdFieldMapper(BooleanSupplier fieldDataEnabled) {
}
@Override
- public RoutingFields buildRoutingFields(IndexSettings settings) {
+ public RoutingFields buildRoutingFields(IndexSettings settings, SourceToParse source) {
return RoutingFields.Noop.INSTANCE;
}
@@ -460,6 +469,7 @@ private static CompressedXContent createDefaultMapping(boolean includeHostName)
IndexMetadata.INDEX_NUMBER_OF_SHARDS_SETTING,
IndexMetadata.INDEX_ROUTING_PARTITION_SIZE_SETTING,
IndexMetadata.INDEX_ROUTING_PATH,
+ IndexMetadata.INDEX_DIMENSIONS,
IndexSettings.LOGSDB_ROUTE_ON_SORT_FIELDS,
IndexSettings.TIME_SERIES_START_TIME,
IndexSettings.TIME_SERIES_END_TIME
@@ -536,7 +546,7 @@ public String getName() {
/**
* How {@code time_series_dimension} fields are handled by indices in this mode.
*/
- public abstract RoutingFields buildRoutingFields(IndexSettings settings);
+ public abstract RoutingFields buildRoutingFields(IndexSettings settings, SourceToParse source);
/**
* @return Whether timestamps should be validated for being withing the time range of an index.
diff --git a/server/src/main/java/org/elasticsearch/index/IndexSettingProvider.java b/server/src/main/java/org/elasticsearch/index/IndexSettingProvider.java
index ee2657a16393a..1bf9a351e11f8 100644
--- a/server/src/main/java/org/elasticsearch/index/IndexSettingProvider.java
+++ b/server/src/main/java/org/elasticsearch/index/IndexSettingProvider.java
@@ -16,6 +16,7 @@
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.core.CheckedFunction;
import org.elasticsearch.core.Nullable;
+import org.elasticsearch.index.mapper.DocumentMapper;
import org.elasticsearch.index.mapper.MapperService;
import java.io.IOException;
@@ -52,6 +53,10 @@ Settings getAdditionalIndexSettings(
List combinedTemplateMappings
);
+ default Settings onUpdateMappings(IndexMetadata indexMetadata, DocumentMapper indexMetadataBuilder) {
+ return Settings.EMPTY;
+ }
+
/**
* Infrastructure class that holds services that can be used by {@link IndexSettingProvider} instances.
*/
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java
index 15e7ff88350b6..d5eaa5c3136d1 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java
@@ -12,6 +12,7 @@
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.Query;
+import org.apache.lucene.util.BytesRef;
import org.elasticsearch.common.Explicit;
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.common.xcontent.XContentHelper;
@@ -93,7 +94,7 @@ public ParsedDocument parseDocument(SourceToParse source, MappingLookup mappingL
)
)
) {
- context = new RootDocumentParserContext(mappingLookup, mappingParserContext, source, parser);
+ context = new RootDocumentParserContext(mappingLookup, mappingParserContext, source, parser, source.tsid());
validateStart(context.parser());
MetadataFieldMapper[] metadataFieldsMappers = mappingLookup.getMapping().getSortedMetadataMappers();
internalParseDocument(metadataFieldsMappers, context);
@@ -1078,12 +1079,14 @@ private static class RootDocumentParserContext extends DocumentParserContext {
private final long maxAllowedNumNestedDocs;
private long numNestedDocs;
private boolean docsReversed = false;
+ private final BytesRef tsid;
RootDocumentParserContext(
MappingLookup mappingLookup,
MappingParserContext mappingParserContext,
SourceToParse source,
- XContentParser parser
+ XContentParser parser,
+ BytesRef tsid
) throws IOException {
super(
mappingLookup,
@@ -1092,6 +1095,7 @@ private static class RootDocumentParserContext extends DocumentParserContext {
mappingLookup.getMapping().getRoot(),
ObjectMapper.Dynamic.getRootDynamic(mappingLookup)
);
+ this.tsid = tsid;
if (mappingLookup.getMapping().getRoot().subobjects() == ObjectMapper.Subobjects.ENABLED) {
this.parser = DotExpandingXContentParser.expandDots(parser, this.path);
} else {
@@ -1149,6 +1153,11 @@ protected void addDoc(LuceneDocument doc) {
this.documents.add(doc);
}
+ @Override
+ public BytesRef getTsid() {
+ return this.tsid;
+ }
+
@Override
public Iterable nonRootDocuments() {
if (docsReversed) {
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java
index b77c0426c23d4..8c7f44df3f70d 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java
@@ -14,6 +14,7 @@
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.common.time.DateFormatter;
+import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.index.IndexMode;
import org.elasticsearch.index.IndexSettings;
@@ -116,6 +117,11 @@ public XContentParser.Token getImmediateXContentParent() {
public boolean isImmediateParentAnArray() {
return in.isImmediateParentAnArray();
}
+
+ @Override
+ public BytesRef getTsid() {
+ return in.getTsid();
+ }
}
/**
@@ -265,7 +271,7 @@ protected DocumentParserContext(
null,
null,
SeqNoFieldMapper.SequenceIDFields.emptySeqID(mappingParserContext.getIndexSettings().seqNoIndexOptions()),
- RoutingFields.fromIndexSettings(mappingParserContext.getIndexSettings()),
+ RoutingFields.fromIndexSettings(mappingParserContext.getIndexSettings(), source),
parent,
dynamic,
new HashSet<>(),
@@ -871,6 +877,9 @@ public final MapperBuilderContext createDynamicMapperBuilderContext() {
protected abstract void addDoc(LuceneDocument doc);
+ @Nullable
+ public abstract BytesRef getTsid();
+
/**
* Find a dynamic mapping template for the given field and its matching type
*
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DynamicTemplate.java b/server/src/main/java/org/elasticsearch/index/mapper/DynamicTemplate.java
index 7c58e58390503..3f8a79d27986c 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/DynamicTemplate.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/DynamicTemplate.java
@@ -23,6 +23,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Predicate;
@@ -31,6 +32,8 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import static org.elasticsearch.index.mapper.TimeSeriesParams.TIME_SERIES_DIMENSION_PARAM;
+
public class DynamicTemplate implements ToXContentObject {
public enum MatchType {
@@ -447,6 +450,24 @@ public List match() {
return match;
}
+ public boolean isTimeSeriesDimension() {
+ return Optional.of(mapping)
+ .flatMap(m -> Optional.ofNullable(m.get(TIME_SERIES_DIMENSION_PARAM)))
+ .filter(Boolean.class::isInstance)
+ .map(Boolean.class::cast)
+ .orElse(false);
+ }
+
+ public boolean isSimplePathMath() {
+ return pathMatch.isEmpty() == false
+ && pathUnmatch.isEmpty()
+ && match.isEmpty()
+ && unmatch.isEmpty()
+ && matchMappingType.isEmpty()
+ && unmatchMappingType.isEmpty()
+ && matchType != MatchType.REGEX;
+ }
+
public boolean match(String templateName, String path, String fieldName, XContentFieldType xcontentFieldType) {
// If the template name parameter is specified, then we will check only the name of the template and ignore other matches.
if (templateName != null) {
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/IdLoader.java b/server/src/main/java/org/elasticsearch/index/mapper/IdLoader.java
index 741252f98473b..12228760faf35 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/IdLoader.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/IdLoader.java
@@ -67,9 +67,10 @@ final class TsIdLoader implements IdLoader {
}
public IdLoader.Leaf leaf(LeafStoredFieldLoader loader, LeafReader reader, int[] docIdsInLeaf) throws IOException {
- IndexRouting.ExtractFromSource.Builder[] builders = null;
+ IndexRouting.ExtractFromSource.RoutingHashBuilder[] builders = null;
if (indexRouting != null) {
- builders = new IndexRouting.ExtractFromSource.Builder[docIdsInLeaf.length];
+ // this branch is for legacy indices before IndexVersions.TIME_SERIES_ROUTING_HASH_IN_ID
+ builders = new IndexRouting.ExtractFromSource.RoutingHashBuilder[docIdsInLeaf.length];
for (int i = 0; i < builders.length; i++) {
builders[i] = indexRouting.builder();
}
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RoutingFields.java b/server/src/main/java/org/elasticsearch/index/mapper/RoutingFields.java
index 4d8d8fdcbd296..0ad4f4e0b49aa 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/RoutingFields.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/RoutingFields.java
@@ -22,8 +22,8 @@ public interface RoutingFields {
/**
* Collect routing fields from index settings
*/
- static RoutingFields fromIndexSettings(IndexSettings indexSettings) {
- return indexSettings.getMode().buildRoutingFields(indexSettings);
+ static RoutingFields fromIndexSettings(IndexSettings indexSettings, SourceToParse source) {
+ return indexSettings.getMode().buildRoutingFields(indexSettings, source);
}
/**
@@ -45,6 +45,8 @@ default RoutingFields addString(String fieldName, String value) {
RoutingFields addBoolean(String fieldName, boolean value);
+ boolean isNoop();
+
/**
* Noop implementation that doesn't perform validations on routing fields
*/
@@ -81,5 +83,10 @@ public RoutingFields addUnsignedLong(String fieldName, long value) {
public RoutingFields addBoolean(String fieldName, boolean value) {
return this;
}
+
+ @Override
+ public boolean isNoop() {
+ return true;
+ }
}
}
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RoutingPathFields.java b/server/src/main/java/org/elasticsearch/index/mapper/RoutingPathFields.java
index 73baca1bf3fdb..361163ea1be66 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/RoutingPathFields.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/RoutingPathFields.java
@@ -59,9 +59,9 @@ public final class RoutingPathFields implements RoutingFields {
* Builds the routing. Used for building {@code _id}. If null then skipped.
*/
@Nullable
- private final IndexRouting.ExtractFromSource.Builder routingBuilder;
+ private final IndexRouting.ExtractFromSource.RoutingHashBuilder routingBuilder;
- public RoutingPathFields(@Nullable IndexRouting.ExtractFromSource.Builder routingBuilder) {
+ public RoutingPathFields(@Nullable IndexRouting.ExtractFromSource.RoutingHashBuilder routingBuilder) {
this.routingBuilder = routingBuilder;
}
@@ -69,7 +69,7 @@ SortedMap> routingValues() {
return Collections.unmodifiableSortedMap(routingValues);
}
- IndexRouting.ExtractFromSource.Builder routingBuilder() {
+ IndexRouting.ExtractFromSource.RoutingHashBuilder routingBuilder() {
return routingBuilder;
}
@@ -207,6 +207,11 @@ public RoutingFields addBoolean(String fieldName, boolean value) {
return this;
}
+ @Override
+ public boolean isNoop() {
+ return false;
+ }
+
private void add(String fieldName, BytesReference encoded) throws IOException {
BytesRef name = new BytesRef(fieldName);
List values = routingValues.get(name);
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/SourceToParse.java b/server/src/main/java/org/elasticsearch/index/mapper/SourceToParse.java
index 5396fdef0f041..8a2ecb126c6cb 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/SourceToParse.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/SourceToParse.java
@@ -9,6 +9,7 @@
package org.elasticsearch.index.mapper;
+import org.apache.lucene.util.BytesRef;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.core.Nullable;
@@ -24,6 +25,8 @@ public class SourceToParse {
private final String id;
+ private final @Nullable BytesRef tsid;
+
private final @Nullable String routing;
private final XContentType xContentType;
@@ -41,7 +44,8 @@ public SourceToParse(
@Nullable String routing,
Map dynamicTemplates,
boolean includeSourceOnError,
- XContentMeteringParserDecorator meteringParserDecorator
+ XContentMeteringParserDecorator meteringParserDecorator,
+ @Nullable BytesRef tsid
) {
this.id = id;
// we always convert back to byte array, since we store it and Field only supports bytes..
@@ -52,14 +56,15 @@ public SourceToParse(
this.dynamicTemplates = Objects.requireNonNull(dynamicTemplates);
this.includeSourceOnError = includeSourceOnError;
this.meteringParserDecorator = meteringParserDecorator;
+ this.tsid = tsid;
}
public SourceToParse(String id, BytesReference source, XContentType xContentType) {
- this(id, source, xContentType, null, Map.of(), true, XContentMeteringParserDecorator.NOOP);
+ this(id, source, xContentType, null, Map.of(), true, XContentMeteringParserDecorator.NOOP, null);
}
public SourceToParse(String id, BytesReference source, XContentType xContentType, String routing) {
- this(id, source, xContentType, routing, Map.of(), true, XContentMeteringParserDecorator.NOOP);
+ this(id, source, xContentType, routing, Map.of(), true, XContentMeteringParserDecorator.NOOP, null);
}
public SourceToParse(
@@ -67,9 +72,10 @@ public SourceToParse(
BytesReference source,
XContentType xContentType,
String routing,
- Map dynamicTemplates
+ Map dynamicTemplates,
+ BytesRef tsid
) {
- this(id, source, xContentType, routing, dynamicTemplates, true, XContentMeteringParserDecorator.NOOP);
+ this(id, source, xContentType, routing, dynamicTemplates, true, XContentMeteringParserDecorator.NOOP, tsid);
}
public BytesReference source() {
@@ -113,4 +119,8 @@ public XContentMeteringParserDecorator getMeteringParserDecorator() {
public boolean getIncludeSourceOnError() {
return includeSourceOnError;
}
+
+ public BytesRef tsid() {
+ return tsid;
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TimeSeriesIdFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/TimeSeriesIdFieldMapper.java
index b94fa64b42428..0f3776f1db9bb 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/TimeSeriesIdFieldMapper.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/TimeSeriesIdFieldMapper.java
@@ -14,6 +14,7 @@
import org.apache.lucene.document.StringField;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.cluster.routing.IndexRouting;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
@@ -150,16 +151,21 @@ private TimeSeriesIdFieldMapper(boolean useDocValuesSkipper) {
public void postParse(DocumentParserContext context) throws IOException {
assert fieldType().isIndexed() == false;
- final RoutingPathFields routingPathFields = (RoutingPathFields) context.getRoutingFields();
final BytesRef timeSeriesId;
+ final RoutingPathFields routingPathFields;
if (getIndexVersionCreated(context).before(IndexVersions.TIME_SERIES_ID_HASHING)) {
+ routingPathFields = (RoutingPathFields) context.getRoutingFields();
long limit = context.indexSettings().getValue(MapperService.INDEX_MAPPING_DIMENSION_FIELDS_LIMIT_SETTING);
int size = routingPathFields.routingValues().size();
if (size > limit) {
throw new MapperException("Too many dimension fields [" + size + "], max [" + limit + "] dimension fields allowed");
}
timeSeriesId = buildLegacyTsid(routingPathFields).toBytesRef();
+ } else if (context.getTsid() != null) {
+ routingPathFields = null;
+ timeSeriesId = context.getTsid();
} else {
+ routingPathFields = (RoutingPathFields) context.getRoutingFields();
timeSeriesId = routingPathFields.buildHash().toBytesRef();
}
@@ -169,13 +175,15 @@ public void postParse(DocumentParserContext context) throws IOException {
context.doc().add(new SortedDocValuesField(fieldType().name(), timeSeriesId));
}
- BytesRef uidEncoded = TsidExtractingIdFieldMapper.createField(
- context,
- getIndexVersionCreated(context).before(IndexVersions.TIME_SERIES_ROUTING_HASH_IN_ID)
- ? routingPathFields.routingBuilder()
- : null,
- timeSeriesId
- );
+ IndexRouting.ExtractFromSource.RoutingHashBuilder routingBuilder;
+ if (getIndexVersionCreated(context).before(IndexVersions.TIME_SERIES_ROUTING_HASH_IN_ID) && routingPathFields != null) {
+ // For legacy indices, we need to create the routing hash from the routing path fields.
+ routingBuilder = routingPathFields.routingBuilder();
+ } else {
+ // For newer indices, the routing hash is stored in SourceToParse#routing, so we can use that directly.
+ routingBuilder = null;
+ }
+ BytesRef uidEncoded = TsidExtractingIdFieldMapper.createField(context, routingBuilder, timeSeriesId);
// We need to add the uid or id to nested Lucene documents so that when a document gets deleted, the nested documents are
// also deleted. Usually this happens when the nested document is created (in DocumentParserContext#createNestedContext), but
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TsidExtractingIdFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/TsidExtractingIdFieldMapper.java
index 8aca004949209..b48b22520ea36 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/TsidExtractingIdFieldMapper.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/TsidExtractingIdFieldMapper.java
@@ -48,7 +48,7 @@ public IndexFieldData.Builder fielddataBuilder(FieldDataContext fieldDataContext
public static BytesRef createField(
DocumentParserContext context,
- IndexRouting.ExtractFromSource.Builder routingBuilder,
+ IndexRouting.ExtractFromSource.RoutingHashBuilder routingBuilder,
BytesRef tsid
) {
final long timestamp = DataStreamTimestampFieldMapper.extractTimestampValue(context.doc());
@@ -115,7 +115,7 @@ public static String createId(int routingHash, BytesRef tsid, long timestamp) {
public static String createId(
boolean dynamicMappersExists,
- IndexRouting.ExtractFromSource.Builder routingBuilder,
+ IndexRouting.ExtractFromSource.RoutingHashBuilder routingBuilder,
BytesRef tsid,
long timestamp,
byte[] suffix
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldParser.java b/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldParser.java
index 93ef04ddd159a..953ac8f7a6941 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldParser.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldParser.java
@@ -177,7 +177,7 @@ private void addField(Context context, ContentPath path, String currentName, Str
fields.add(new SortedSetDocValuesField(rootFieldFullPath, bytesValue));
fields.add(new SortedSetDocValuesField(keyedFieldFullPath, bytesKeyedValue));
- if (fieldType.isDimension() == false) {
+ if (fieldType.isDimension() == false || context.documentParserContext().getRoutingFields().isNoop()) {
return;
}
diff --git a/server/src/test/java/org/elasticsearch/action/index/IndexRequestTests.java b/server/src/test/java/org/elasticsearch/action/index/IndexRequestTests.java
index 9f5c9db1452d9..68900c612f513 100644
--- a/server/src/test/java/org/elasticsearch/action/index/IndexRequestTests.java
+++ b/server/src/test/java/org/elasticsearch/action/index/IndexRequestTests.java
@@ -8,6 +8,7 @@
*/
package org.elasticsearch.action.index;
+import org.apache.lucene.util.BytesRef;
import org.elasticsearch.TransportVersion;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.DocWriteRequest;
@@ -480,6 +481,7 @@ public void testSerialization() throws IOException {
assertThat(copy.getFinalPipeline(), equalTo(indexRequest.getFinalPipeline()));
assertThat(copy.ifPrimaryTerm(), equalTo(indexRequest.ifPrimaryTerm()));
assertThat(copy.isRequireDataStream(), equalTo(indexRequest.isRequireDataStream()));
+ assertThat(copy.tsid(), equalTo(indexRequest.tsid()));
}
private IndexRequest createTestInstance() {
@@ -495,6 +497,7 @@ private IndexRequest createTestInstance() {
for (int i = 0; i < randomIntBetween(0, 20); i++) {
indexRequest.addPipeline(randomAlphaOfLength(20));
}
+ indexRequest.tsid(randomFrom((BytesRef) null, new BytesRef(randomAlphaOfLength(20))));
return indexRequest;
}
}
diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/IndexRoutingTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/IndexRoutingTests.java
index 1db6192eee80e..0bb950c0c9d12 100644
--- a/server/src/test/java/org/elasticsearch/cluster/routing/IndexRoutingTests.java
+++ b/server/src/test/java/org/elasticsearch/cluster/routing/IndexRoutingTests.java
@@ -19,10 +19,10 @@
import org.elasticsearch.index.IndexMode;
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.IndexVersion;
+import org.elasticsearch.index.IndexVersions;
import org.elasticsearch.index.mapper.IdFieldMapper;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.test.ESTestCase;
-import org.elasticsearch.test.index.IndexVersionUtils;
import org.elasticsearch.xcontent.DeprecationHandler;
import org.elasticsearch.xcontent.NamedXContentRegistry;
import org.elasticsearch.xcontent.XContentType;
@@ -460,7 +460,7 @@ public void testRequiredRouting() {
*/
private int shardIdFromSimple(IndexRouting indexRouting, String id, @Nullable String routing) {
return switch (between(0, 3)) {
- case 0 -> indexRouting.indexShard(id, routing, null, null);
+ case 0 -> indexRouting.indexShard(id, routing, null, null, null);
case 1 -> indexRouting.updateShard(id, routing);
case 2 -> indexRouting.deleteShard(id, routing);
case 3 -> indexRouting.getShard(id, routing);
@@ -493,7 +493,7 @@ public void testRoutingPathEmptySource() throws IOException {
IndexRouting routing = indexRoutingForPath(between(1, 5), randomAlphaOfLength(5));
Exception e = expectThrows(
IllegalArgumentException.class,
- () -> routing.indexShard(randomAlphaOfLength(5), null, XContentType.JSON, source(Map.of()))
+ () -> routing.indexShard(randomAlphaOfLength(5), null, null, XContentType.JSON, source(Map.of()))
);
assertThat(e.getMessage(), equalTo("Error extracting routing: source didn't contain any routing fields"));
}
@@ -502,7 +502,7 @@ public void testRoutingPathMismatchSource() throws IOException {
IndexRouting routing = indexRoutingForPath(between(1, 5), "foo");
Exception e = expectThrows(
IllegalArgumentException.class,
- () -> routing.indexShard(randomAlphaOfLength(5), null, XContentType.JSON, source(Map.of("bar", "dog")))
+ () -> routing.indexShard(randomAlphaOfLength(5), null, null, XContentType.JSON, source(Map.of("bar", "dog")))
);
assertThat(e.getMessage(), equalTo("Error extracting routing: source didn't contain any routing fields"));
}
@@ -523,7 +523,7 @@ public void testRoutingIndexWithRouting() throws IOException {
String docRouting = randomAlphaOfLength(5);
Exception e = expectThrows(
IllegalArgumentException.class,
- () -> indexRouting.indexShard(randomAlphaOfLength(5), docRouting, XContentType.JSON, source)
+ () -> indexRouting.indexShard(randomAlphaOfLength(5), docRouting, null, XContentType.JSON, source)
);
assertThat(
e.getMessage(),
@@ -543,7 +543,7 @@ public void testRoutingPathCollectSearchWithRouting() throws IOException {
public void testRoutingPathOneTopLevel() throws IOException {
int shards = between(2, 1000);
IndexRouting routing = indexRoutingForPath(shards, "foo");
- assertIndexShard(routing, Map.of("foo", "cat", "bar", "dog"), Math.floorMod(hash(List.of("foo", "cat")), shards));
+ assertIndexShard(routing, Map.of("foo", "cat", "bar", "dog"), List.of("foo", "cat"), shards);
}
public void testRoutingPathManyTopLevel() throws IOException {
@@ -552,18 +552,15 @@ public void testRoutingPathManyTopLevel() throws IOException {
assertIndexShard(
routing,
Map.of("foo", "cat", "bar", "dog", "foa", "a", "fob", "b"),
- Math.floorMod(hash(List.of("foa", "a", "fob", "b", "foo", "cat")), shards) // Note that the fields are sorted
+ List.of("foa", "a", "fob", "b", "foo", "cat"),
+ shards // Note that the fields are sorted
);
}
public void testRoutingPathOneSub() throws IOException {
int shards = between(2, 1000);
IndexRouting routing = indexRoutingForPath(shards, "foo.*");
- assertIndexShard(
- routing,
- Map.of("foo", Map.of("bar", "cat"), "baz", "dog"),
- Math.floorMod(hash(List.of("foo.bar", "cat")), shards)
- );
+ assertIndexShard(routing, Map.of("foo", Map.of("bar", "cat"), "baz", "dog"), List.of("foo.bar", "cat"), shards);
}
public void testRoutingPathManySubs() throws IOException {
@@ -572,31 +569,32 @@ public void testRoutingPathManySubs() throws IOException {
assertIndexShard(
routing,
Map.of("foo", Map.of("a", "cat"), "bar", Map.of("thing", "yay", "this", "too")),
- Math.floorMod(hash(List.of("bar.thing", "yay", "bar.this", "too", "foo.a", "cat")), shards)
+ List.of("bar.thing", "yay", "bar.this", "too", "foo.a", "cat"),
+ shards
);
}
public void testRoutingPathDotInName() throws IOException {
int shards = between(2, 1000);
IndexRouting routing = indexRoutingForPath(shards, "foo.bar");
- assertIndexShard(routing, Map.of("foo.bar", "cat", "baz", "dog"), Math.floorMod(hash(List.of("foo.bar", "cat")), shards));
+ assertIndexShard(routing, Map.of("foo.bar", "cat", "baz", "dog"), List.of("foo.bar", "cat"), shards);
}
public void testRoutingPathNumbersInSource() throws IOException {
int shards = between(2, 1000);
- IndexRouting routing = indexRoutingForPath(shards, "foo");
+ IndexRouting routing = indexRoutingForPath(IndexVersions.MATCH_ONLY_TEXT_STORED_AS_BYTES, shards, "foo");
long randomLong = randomLong();
- assertIndexShard(routing, Map.of("foo", randomLong), Math.floorMod(hash(List.of("foo", Long.toString(randomLong))), shards));
+ assertIndexShard(routing, Map.of("foo", randomLong), List.of("foo", randomLong), shards);
double randomDouble = randomDouble();
- assertIndexShard(routing, Map.of("foo", randomDouble), Math.floorMod(hash(List.of("foo", Double.toString(randomDouble))), shards));
- assertIndexShard(routing, Map.of("foo", 123), Math.floorMod(hash(List.of("foo", "123")), shards));
+ assertIndexShard(routing, Map.of("foo", randomDouble), List.of("foo", randomDouble), shards);
+ assertIndexShard(routing, Map.of("foo", 123), List.of("foo", 123), shards);
}
public void testRoutingPathBooleansInSource() throws IOException {
int shards = between(2, 1000);
IndexRouting routing = indexRoutingForPath(shards, "foo");
- assertIndexShard(routing, Map.of("foo", true), Math.floorMod(hash(List.of("foo", "true")), shards));
- assertIndexShard(routing, Map.of("foo", false), Math.floorMod(hash(List.of("foo", "false")), shards));
+ assertIndexShard(routing, Map.of("foo", true), List.of("foo", true), shards);
+ assertIndexShard(routing, Map.of("foo", false), List.of("foo", false), shards);
}
public void testRoutingPathArraysInSource() throws IOException {
@@ -606,7 +604,8 @@ public void testRoutingPathArraysInSource() throws IOException {
routing,
Map.of("c", List.of(true), "d", List.of(), "a", List.of("foo", "bar", "foo"), "b", List.of(21, 42)),
// Note that the fields are sorted
- Math.floorMod(hash(List.of("a", "foo", "a", "bar", "a", "foo", "b", "21", "b", "42", "c", "true")), shards)
+ List.of("a", "foo", "a", "bar", "a", "foo", "b", 21, "b", 42, "c", true),
+ shards
);
}
@@ -617,7 +616,7 @@ public void testRoutingPathObjectArraysInSource() throws IOException {
BytesReference source = source(Map.of("a", List.of("foo", Map.of("foo", "bar"))));
Exception e = expectThrows(
IllegalArgumentException.class,
- () -> routing.indexShard(randomAlphaOfLength(5), null, XContentType.JSON, source)
+ () -> routing.indexShard(randomAlphaOfLength(5), null, null, XContentType.JSON, source)
);
assertThat(
e.getMessage(),
@@ -626,10 +625,9 @@ public void testRoutingPathObjectArraysInSource() throws IOException {
}
public void testRoutingPathBwc() throws IOException {
- IndexVersion version = IndexVersionUtils.randomCompatibleVersion(random());
- IndexRouting routing = indexRoutingForPath(version, 8, "dim.*,other.*,top");
+ IndexRouting routing = indexRoutingForRoutingPath(IndexVersion.current(), 8, "dim.*,other.*,top");
/*
- * These when we first added routing_path. If these values change
+ * These are the expected shards when we first added routing_path. If these values change
* time series will be routed to unexpected shards. You may modify
* them with a new index created version, but when you do you must
* copy this test and patch the versions at the top. Because newer
@@ -645,6 +643,29 @@ public void testRoutingPathBwc() throws IOException {
assertIndexShard(routing, Map.of("dim.a", "a"), 4);
}
+ public void testRoutingPathBwcAfterTsidBasedRouting() throws IOException {
+ IndexRouting routing = indexRoutingForDimensions(IndexVersion.current(), 8, "dim.*,other.*,top");
+ /*
+ * These are the expected shards after tsid based routing. If these values change
+ * time series will be routed to unexpected shards. You may modify
+ * them with a new index created version, but when you do you must
+ * copy this test and patch the versions at the top. Because newer
+ * versions of Elasticsearch must continue to route based on the
+ * version on the index.
+ */
+ assertIndexShard(routing, Map.of("dim", Map.of("a", "a")), 6);
+ assertIndexShard(routing, Map.of("dim", Map.of("a", "b")), 0);
+ assertIndexShard(routing, Map.of("dim", Map.of("c", "d")), 5);
+ assertIndexShard(routing, Map.of("other", Map.of("a", "a")), 2);
+ assertIndexShard(routing, Map.of("top", "a"), 0);
+ assertIndexShard(routing, Map.of("dim", Map.of("c", "d"), "top", "b"), 3);
+ assertIndexShard(routing, Map.of("dim.a", "a"), 6);
+ assertIndexShard(routing, Map.of("dim.a", 1), 7);
+ assertIndexShard(routing, Map.of("dim.a", "1"), 5);
+ assertIndexShard(routing, Map.of("dim.a", true), 3);
+ assertIndexShard(routing, Map.of("dim.a", "true"), 6);
+ }
+
public void testRoutingPathReadWithInvalidString() throws IOException {
int shards = between(2, 1000);
IndexRouting indexRouting = indexRoutingForPath(shards, "foo");
@@ -678,9 +699,9 @@ public void testRoutingPathLogsdb() throws IOException {
assertNull(req.id());
// Verify that routing uses the field name and value in the routing path.
- int expectedShard = Math.floorMod(hash(List.of("foo", "A")), shards);
+ int expectedShard = expectedShard(routing, List.of("foo", "A"), shards);
BytesReference sourceBytes = source(Map.of("foo", "A", "bar", "B"));
- assertEquals(expectedShard, routing.indexShard(null, null, XContentType.JSON, sourceBytes));
+ assertEquals(expectedShard, routing.indexShard(null, null, null, XContentType.JSON, sourceBytes));
// Verify that the request id gets updated to contain the routing hash.
routing.postProcess(req);
@@ -700,7 +721,15 @@ private IndexRouting indexRoutingForPath(int shards, String path) {
return indexRoutingForPath(IndexVersion.current(), shards, path);
}
- private IndexRouting indexRoutingForPath(IndexVersion createdVersion, int shards, String path) {
+ private IndexRouting indexRoutingForPath(IndexVersion indexVersion, int shards, String path) {
+ return randomBoolean()
+ // old way of routing paths created during routing
+ ? indexRoutingForRoutingPath(indexVersion, shards, path)
+ // current way of routing paths created during routing via tsid
+ : indexRoutingForDimensions(indexVersion, shards, path);
+ }
+
+ private IndexRouting indexRoutingForRoutingPath(IndexVersion createdVersion, int shards, String path) {
return IndexRouting.fromIndexMetadata(
IndexMetadata.builder("test")
.settings(
@@ -714,18 +743,41 @@ private IndexRouting indexRoutingForPath(IndexVersion createdVersion, int shards
);
}
+ private IndexRouting indexRoutingForDimensions(IndexVersion createdVersion, int shards, String path) {
+ return IndexRouting.fromIndexMetadata(
+ IndexMetadata.builder("test")
+ .settings(
+ settings(createdVersion).put(IndexMetadata.INDEX_DIMENSIONS.getKey(), path)
+ .put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES)
+ .build()
+ )
+ .numberOfShards(shards)
+ .numberOfReplicas(1)
+ .build()
+ );
+ }
+
+ private void assertIndexShard(IndexRouting routing, Map source, List