Sfoglia il codice sorgente

More validation for routing_path (#79520)

This adds more validation for fields matching `routing_path`. In
particular, it disallows runtime fields, fields with a `script`, and
fails when you try to use `dynamic:false` to skip mapping them fields
that would match the `routing_path`. This makes sure that the we get
predictable time routing for the time series.
Nik Everett 4 anni fa
parent
commit
cfa92ef5d7

+ 0 - 42
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/10_settings.yml

@@ -168,45 +168,3 @@ routing required:
             mappings:
               _routing:
                 required: true
-
----
-bad routing_path:
-  - skip:
-      version: " - 7.99.99"
-      reason: introduced in 8.0.0
-
-  - do:
-      catch: /All fields that match routing_path must be keyword time_series_dimensions but \[@timestamp\] was \[date\]/
-      indices.create:
-          index: test_index
-          body:
-            settings:
-              index:
-                mode: time_series
-                routing_path: [metricset, k8s.pod.uid, "@timestamp"]
-                number_of_replicas: 0
-                number_of_shards: 2
-            mappings:
-              properties:
-                "@timestamp":
-                  type: date
-                metricset:
-                  type: keyword
-                  time_series_dimension: true
-                k8s:
-                  properties:
-                    pod:
-                      properties:
-                        uid:
-                          type: keyword
-                          time_series_dimension: true
-                        name:
-                          type: keyword
-                        ip:
-                          type: ip
-                        network:
-                          properties:
-                            tx:
-                              type: long
-                            rx:
-                              type: long

+ 146 - 0
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/20_mapping.yml

@@ -104,3 +104,149 @@ top level dim object:
                             latency:
                               type: double
                               time_series_metric: gauge
+
+---
+non keyword matches routing_path:
+  - skip:
+      version: " - 7.99.99"
+      reason: introduced in 8.0.0
+
+  - do:
+      catch: '/All fields that match routing_path must be keywords with \[time_series_dimension: true\] and without the \[script\] parameter. \[@timestamp\] was \[date\]./'
+      indices.create:
+          index: test_index
+          body:
+            settings:
+              index:
+                mode: time_series
+                routing_path: [metricset, k8s.pod.uid, "@timestamp"]
+                number_of_replicas: 0
+                number_of_shards: 2
+            mappings:
+              properties:
+                "@timestamp":
+                  type: date
+                metricset:
+                  type: keyword
+                  time_series_dimension: true
+                k8s:
+                  properties:
+                    pod:
+                      properties:
+                        uid:
+                          type: keyword
+                          time_series_dimension: true
+                        name:
+                          type: keyword
+                        ip:
+                          type: ip
+                        network:
+                          properties:
+                            tx:
+                              type: long
+                            rx:
+                              type: long
+
+---
+runtime field matching routing path:
+  - skip:
+      version: " - 7.99.99"
+      reason: introduced in 8.0.0
+
+  - do:
+      indices.create:
+          index: test
+          body:
+            settings:
+              index:
+                mode: time_series
+                routing_path: [dim.*]
+            mappings:
+              properties:
+                "@timestamp":
+                  type: date
+
+  - do:
+      bulk:
+        refresh: true
+        index: test
+        body:
+          - '{"index": {}}'
+          - '{"@timestamp": "2021-04-28T18:50:04.467Z", "dim": {"foo": "a"}}'
+
+  - do:
+      catch: /runtime fields may not match \[routing_path\] but \[dim.bar\] matched/
+      search:
+        index: test
+        body:
+          runtime_mappings:
+            dim.bar:
+              type: keyword
+          query:
+            match:
+              dim.foo: a
+
+---
+"dynamic: runtime matches routing_path":
+  - skip:
+      version: " - 7.99.99"
+      reason: introduced in 8.0.0
+
+  - do:
+      indices.create:
+          index: test
+          body:
+            settings:
+              index:
+                mode: time_series
+                routing_path: [dim.*]
+            mappings:
+              properties:
+                "@timestamp":
+                  type: date
+                dim:
+                  type: object
+                  dynamic: runtime
+
+  - do:
+      bulk:
+        refresh: true
+        index: test
+        body:
+          - '{"index": {}}'
+          - '{"@timestamp": "2021-04-28T18:50:04.467Z", "dim": {"foo": "a"}}'
+  - match: {items.0.index.error.reason: "All fields that match routing_path must be keywords with [time_series_dimension: true] and without the [script] parameter. [dim.foo] was a runtime [keyword]."}
+
+---
+"dynamic: false matches routing_path":
+  - skip:
+      version: " - 7.99.99"
+      reason: introduced in 8.0.0
+
+  - do:
+      indices.create:
+          index: test
+          body:
+            settings:
+              index:
+                mode: time_series
+                routing_path: [dim.*]
+            mappings:
+              properties:
+                "@timestamp":
+                  type: date
+                dim:
+                  type: object
+                  dynamic: false
+
+  - do:
+      bulk:
+        refresh: true
+        index: test
+        body:
+          - '{"index": {}}'
+          - '{"@timestamp": "2021-04-28T18:50:04.467Z", "dim": {"foo": "a"}}'
+          - '{"index": {}}'
+          - '{"@timestamp": "2021-04-28T18:50:04.467Z", "dim": {"foo": {"bar": "a"}}}'
+  - match: {items.0.index.error.reason: "All fields matching [routing_path] must be mapped but [dim.foo] was declared as [dynamic: false]"}
+  - match: {items.1.index.error.reason: "All fields matching [routing_path] must be mapped but [dim.foo] was declared as [dynamic: false]"}

+ 12 - 0
server/src/main/java/org/elasticsearch/index/mapper/AbstractScriptFieldType.java

@@ -194,6 +194,18 @@ abstract class AbstractScriptFieldType<LeafFactory> extends MappedFieldType {
         return leafFactory(context.lookup().forkAndTrackFieldReferences(name()));
     }
 
+    @Override
+    public void validateMatchedRoutingPath() {
+        throw new IllegalArgumentException(
+            "All fields that match routing_path must be keywords with [time_series_dimension: true] "
+                + "and without the [script] parameter. ["
+                + name()
+                + "] was a runtime ["
+                + typeName()
+                + "]."
+        );
+    }
+
     // Placeholder Script for source-only fields
     // TODO rework things so that we don't need this
     protected static final Script DEFAULT_SCRIPT = new Script("");

+ 15 - 2
server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java

@@ -9,6 +9,7 @@
 package org.elasticsearch.index.mapper;
 
 import org.elasticsearch.common.compress.CompressedXContent;
+import org.elasticsearch.common.regex.Regex;
 import org.elasticsearch.index.IndexSettings;
 
 import java.util.List;
@@ -90,8 +91,20 @@ public class DocumentMapper {
             throw new IllegalArgumentException("cannot have nested fields when index sort is activated");
         }
         List<String> routingPaths = settings.getIndexMetadata().getRoutingPaths();
-        if (false == routingPaths.isEmpty()) {
-            mappingLookup.getMapping().getRoot().validateRoutingPath(routingPaths);
+        for (String path : routingPaths) {
+            for (String match : mappingLookup.getMatchingFieldNames(path)) {
+                mappingLookup.getFieldType(match).validateMatchedRoutingPath();
+            }
+            for (String objectName : mappingLookup.objectMappers().keySet()) {
+                if (Regex.simpleMatch(path, objectName)) {
+                    throw new IllegalArgumentException(
+                        "All fields that match routing_path must be keywords with [time_series_dimension: true] "
+                            + "and without the [script] parameter. ["
+                            + objectName
+                            + "] was [object]."
+                    );
+                }
+            }
         }
         if (checkLimits) {
             this.mappingLookup.checkLimits(settings);

+ 19 - 4
server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java

@@ -15,13 +15,10 @@ import org.apache.lucene.search.Query;
 import org.elasticsearch.Version;
 import org.elasticsearch.common.Explicit;
 import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.regex.Regex;
 import org.elasticsearch.common.time.DateFormatter;
 import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
-import org.elasticsearch.xcontent.NamedXContentRegistry;
-import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentHelper;
-import org.elasticsearch.xcontent.XContentParser;
-import org.elasticsearch.xcontent.XContentType;
 import org.elasticsearch.core.Tuple;
 import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.analysis.IndexAnalyzers;
@@ -29,6 +26,10 @@ import org.elasticsearch.index.fielddata.IndexFieldDataCache;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
 import org.elasticsearch.search.lookup.SearchLookup;
+import org.elasticsearch.xcontent.NamedXContentRegistry;
+import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xcontent.XContentParser;
+import org.elasticsearch.xcontent.XContentType;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -573,6 +574,7 @@ public final class DocumentParser {
             if (dynamic == ObjectMapper.Dynamic.STRICT) {
                 throw new StrictDynamicMappingException(mapper.fullPath(), currentFieldName);
             } else if (dynamic == ObjectMapper.Dynamic.FALSE) {
+                failIfMatchesRoutingPath(context, parentMapper, currentFieldName);
                 // not dynamic, read everything up to end object
                 context.parser().skipChildren();
             } else {
@@ -708,11 +710,24 @@ public final class DocumentParser {
             throw new StrictDynamicMappingException(parentMapper.fullPath(), currentFieldName);
         }
         if (dynamic == ObjectMapper.Dynamic.FALSE) {
+            failIfMatchesRoutingPath(context, parentMapper, currentFieldName);
             return;
         }
         dynamic.getDynamicFieldsBuilder().createDynamicFieldFromValue(context, token, currentFieldName);
     }
 
+    private static void failIfMatchesRoutingPath(DocumentParserContext context, ObjectMapper parentMapper, String currentFieldName) {
+        if (context.indexSettings().getIndexMetadata().getRoutingPaths().isEmpty()) {
+            return;
+        }
+        String path = parentMapper.fullPath().isEmpty() ? currentFieldName : parentMapper.fullPath() + "." + currentFieldName;
+        if (Regex.simpleMatch(context.indexSettings().getIndexMetadata().getRoutingPaths(), path)) {
+            throw new MapperParsingException(
+                "All fields matching [routing_path] must be mapped but [" + path + "] was declared as [dynamic: false]"
+            );
+        }
+    }
+
     /**
      * Creates instances of the fields that the current field should be copied to
      */

+ 21 - 12
server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java

@@ -31,7 +31,6 @@ import org.apache.lucene.util.automaton.MinimizationOperations;
 import org.apache.lucene.util.automaton.Operations;
 import org.elasticsearch.common.lucene.Lucene;
 import org.elasticsearch.common.lucene.search.AutomatonQueries;
-import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.index.analysis.IndexAnalyzers;
 import org.elasticsearch.index.analysis.NamedAnalyzer;
 import org.elasticsearch.index.fielddata.IndexFieldData;
@@ -44,6 +43,7 @@ import org.elasticsearch.script.StringFieldScript;
 import org.elasticsearch.search.aggregations.support.CoreValuesSourceType;
 import org.elasticsearch.search.lookup.FieldValues;
 import org.elasticsearch.search.lookup.SearchLookup;
+import org.elasticsearch.xcontent.XContentParser;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
@@ -436,6 +436,26 @@ public final class KeywordFieldMapper extends FieldMapper {
         public boolean isDimension() {
             return isDimension;
         }
+
+        @Override
+        public void validateMatchedRoutingPath() {
+            if (false == isDimension) {
+                throw new IllegalArgumentException(
+                    "All fields that match routing_path must be keywords with [time_series_dimension: true] "
+                        + "and without the [script] parameter. ["
+                        + name()
+                        + "] was not [time_series_dimension: true]."
+                );
+            }
+            if (scriptValues != null) {
+                throw new IllegalArgumentException(
+                    "All fields that match routing_path must be keywords with [time_series_dimension: true] "
+                        + "and without the [script] parameter. ["
+                        + name()
+                        + "] has a [script] parameter."
+                );
+            }
+        }
     }
 
     /** The maximum keyword length allowed for a dimension field */
@@ -583,15 +603,4 @@ public final class KeywordFieldMapper extends FieldMapper {
     public FieldMapper.Builder getMergeBuilder() {
         return new Builder(simpleName(), indexAnalyzers, scriptCompiler).dimension(dimension).init(this);
     }
-
-    @Override
-    protected void validateMatchedRoutingPath() {
-        if (false == fieldType().isDimension()) {
-            throw new IllegalArgumentException(
-                "All fields that match routing_path must be keyword time_series_dimensions but ["
-                    + name()
-                    + "] was not a time_series_dimension"
-            );
-        }
-    }
 }

+ 15 - 0
server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java

@@ -29,6 +29,7 @@ import org.apache.lucene.queries.spans.SpanMultiTermQueryWrapper;
 import org.apache.lucene.queries.spans.SpanQuery;
 import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.ElasticsearchParseException;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.common.geo.ShapeRelation;
 import org.elasticsearch.common.time.DateMathParser;
@@ -477,4 +478,18 @@ public abstract class MappedFieldType {
         throws IOException {
         return null;
     }
+
+    /**
+     * Validate that this field can be the target of {@link IndexMetadata#INDEX_ROUTING_PATH}.
+     */
+    public void validateMatchedRoutingPath() {
+        throw new IllegalArgumentException(
+            "All fields that match routing_path must be keywords with [time_series_dimension: true] "
+                + "and without the [script] parameter. ["
+                + name()
+                + "] was ["
+                + typeName()
+                + "]."
+        );
+    }
 }

+ 0 - 39
server/src/main/java/org/elasticsearch/index/mapper/Mapper.java

@@ -8,16 +8,10 @@
 
 package org.elasticsearch.index.mapper;
 
-import com.fasterxml.jackson.core.filter.TokenFilter;
-
-import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.xcontent.ToXContentFragment;
-import org.elasticsearch.xcontent.support.filtering.FilterPathBasedFilter;
 
-import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 
 public abstract class Mapper implements ToXContentFragment, Iterable<Mapper> {
 
@@ -71,37 +65,4 @@ public abstract class Mapper implements ToXContentFragment, Iterable<Mapper> {
      * @param mappers a {@link MappingLookup} that can produce references to other mappers
      */
     public abstract void validate(MappingLookup mappers);
-
-    /**
-     * Validate a {@link TokenFilter} made from {@link IndexMetadata#INDEX_ROUTING_PATH}.
-     */
-    public final void validateRoutingPath(List<String> routingPaths) {
-        validateRoutingPath(new FilterPathBasedFilter(Set.copyOf(routingPaths), true));
-    }
-
-    /**
-     * Validate a {@link TokenFilter} made from {@link IndexMetadata#INDEX_ROUTING_PATH}.
-     */
-    private void validateRoutingPath(TokenFilter filter) {
-        if (filter == TokenFilter.INCLUDE_ALL) {
-            validateMatchedRoutingPath();
-        }
-        for (Mapper m : this) {
-            TokenFilter next = filter.includeProperty(m.simpleName());
-            if (next == null) {
-                // null means "do not include"
-                continue;
-            }
-            m.validateRoutingPath(next);
-        }
-    }
-
-    /**
-     * Validate that this field can be the target of {@link IndexMetadata#INDEX_ROUTING_PATH}.
-     */
-    protected void validateMatchedRoutingPath() {
-        throw new IllegalArgumentException(
-            "All fields that match routing_path must be keyword time_series_dimensions but [" + name() + "] was [" + typeName() + "]"
-        );
-    }
 }

+ 7 - 0
server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java

@@ -224,6 +224,13 @@ public class SearchExecutionContext extends QueryRewriteContext {
         this.valuesSourceRegistry = valuesSourceRegistry;
         this.runtimeMappings = runtimeMappings;
         this.allowedFields = allowedFields;
+        if (false == indexSettings.getIndexMetadata().getRoutingPaths().isEmpty()) {
+            for (String r : runtimeMappings.keySet()) {
+                if (Regex.simpleMatch(indexSettings.getIndexMetadata().getRoutingPaths(), r)) {
+                    throw new IllegalArgumentException("runtime fields may not match [routing_path] but [" + r + "] matched");
+                }
+            }
+        }
     }
 
     private void reset() {

+ 69 - 4
server/src/test/java/org/elasticsearch/index/TimeSeriesModeTests.java

@@ -11,8 +11,14 @@ package org.elasticsearch.index;
 import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.index.mapper.MapperServiceTestCase;
+import org.elasticsearch.script.Script;
+import org.elasticsearch.script.ScriptContext;
+import org.elasticsearch.script.StringFieldScript;
+import org.elasticsearch.script.StringFieldScript.LeafFactory;
+import org.elasticsearch.search.lookup.SearchLookup;
 
 import java.io.IOException;
+import java.util.Map;
 
 import static org.hamcrest.Matchers.equalTo;
 
@@ -122,7 +128,10 @@ public class TimeSeriesModeTests extends MapperServiceTestCase {
         })));
         assertThat(
             e.getMessage(),
-            equalTo("All fields that match routing_path must be keyword time_series_dimensions but [dim.o] was [object]")
+            equalTo(
+                "All fields that match routing_path must be keywords with [time_series_dimension: true] "
+                    + "and without the [script] parameter. [dim.o] was [object]."
+            )
         );
     }
 
@@ -140,8 +149,8 @@ public class TimeSeriesModeTests extends MapperServiceTestCase {
         assertThat(
             e.getMessage(),
             equalTo(
-                "All fields that match routing_path must be keyword time_series_dimensions but "
-                    + "[dim.non_dim] was not a time_series_dimension"
+                "All fields that match routing_path must be keywords with [time_series_dimension: true] "
+                    + "and without the [script] parameter. [dim.non_dim] was not [time_series_dimension: true]."
             )
         );
     }
@@ -159,7 +168,49 @@ public class TimeSeriesModeTests extends MapperServiceTestCase {
         })));
         assertThat(
             e.getMessage(),
-            equalTo("All fields that match routing_path must be keyword time_series_dimensions but [dim.non_kwd] was [integer]")
+            equalTo(
+                "All fields that match routing_path must be keywords with [time_series_dimension: true] "
+                    + "and without the [script] parameter. [dim.non_kwd] was [integer]."
+            )
+        );
+    }
+
+    public void testRoutingPathMatchesScriptedKeyword() {
+        Settings s = Settings.builder()
+            .put(IndexSettings.MODE.getKey(), "time_series")
+            .put(IndexMetadata.INDEX_ROUTING_PATH.getKey(), randomBoolean() ? "dim.kwd" : "dim.*")
+            .build();
+        Exception e = expectThrows(IllegalArgumentException.class, () -> createMapperService(s, mapping(b -> {
+            b.startObject("dim.kwd");
+            b.field("type", "keyword");
+            b.field("time_series_dimension", true);
+            b.startObject("script").field("lang", "mock").field("source", "mock").endObject();
+            b.endObject();
+        })));
+        assertThat(
+            e.getMessage(),
+            equalTo(
+                "All fields that match routing_path must be keywords with [time_series_dimension: true] "
+                    + "and without the [script] parameter. [dim.kwd] has a [script] parameter."
+            )
+        );
+    }
+
+    public void testRoutingPathMatchesRuntimeKeyword() {
+        Settings s = Settings.builder()
+            .put(IndexSettings.MODE.getKey(), "time_series")
+            .put(IndexMetadata.INDEX_ROUTING_PATH.getKey(), randomBoolean() ? "dim.kwd" : "dim.*")
+            .build();
+        Exception e = expectThrows(
+            IllegalArgumentException.class,
+            () -> createMapperService(s, runtimeMapping(b -> b.startObject("dim.kwd").field("type", "keyword").endObject()))
+        );
+        assertThat(
+            e.getMessage(),
+            equalTo(
+                "All fields that match routing_path must be keywords with [time_series_dimension: true] "
+                    + "and without the [script] parameter. [dim.kwd] was a runtime [keyword]."
+            )
         );
     }
 
@@ -177,4 +228,18 @@ public class TimeSeriesModeTests extends MapperServiceTestCase {
             b.endObject().endObject();
         })); // doesn't throw
     }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    protected <T> T compileScript(Script script, ScriptContext<T> context) {
+        if (context.equals(StringFieldScript.CONTEXT) && script.getLang().equals("mock")) {
+            return (T) new StringFieldScript.Factory() {
+                @Override
+                public LeafFactory newFactory(String fieldName, Map<String, Object> params, SearchLookup searchLookup) {
+                    throw new UnsupportedOperationException("error should be thrown before getting here");
+                }
+            };
+        }
+        return super.compileScript(script, context);
+    }
 }

+ 39 - 0
server/src/test/java/org/elasticsearch/index/mapper/DocumentParserTests.java

@@ -14,9 +14,12 @@ import org.apache.lucene.document.LatLonPoint;
 import org.apache.lucene.document.StringField;
 import org.apache.lucene.index.IndexableField;
 import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.bytes.BytesArray;
 import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentFactory;
 import org.elasticsearch.xcontent.XContentParser;
@@ -1960,6 +1963,42 @@ public class DocumentParserTests extends MapperServiceTestCase {
         assertNotNull(doc.dynamicMappingsUpdate());
     }
 
+    public void testDynamicFalseMatchesRoutingPath() throws IOException {
+        DocumentMapper mapper = createMapperService(
+            Settings.builder()
+                .put(getIndexSettings())
+                .put(IndexSettings.MODE.getKey(), "time_series")
+                .put(IndexMetadata.INDEX_ROUTING_PATH.getKey(), "dim.*")
+                .build(),
+            mapping(b -> {
+                b.startObject("dim");
+                b.field("type", "object");
+                b.field("dynamic", false);
+                b.endObject();
+            })
+        ).documentMapper();
+
+        Exception e = expectThrows(MapperParsingException.class, () -> mapper.parse(source(b -> {
+            b.startObject("dim");
+            b.field("foo", "bar");
+            b.endObject();
+        })));
+        assertThat(
+            e.getMessage(),
+            equalTo("All fields matching [routing_path] must be mapped but [dim.foo] was declared as [dynamic: false]")
+        );
+
+        e = expectThrows(MapperParsingException.class, () -> mapper.parse(source(b -> {
+            b.startObject("dim");
+            b.startObject("foo").field("bar", "baz").endObject();
+            b.endObject();
+        })));
+        assertThat(
+            e.getMessage(),
+            equalTo("All fields matching [routing_path] must be mapped but [dim.foo] was declared as [dynamic: false]")
+        );
+    }
+
     /**
      * Mapper plugin providing a mock metadata field mapper implementation that supports setting its value
      */

+ 4 - 1
server/src/test/java/org/elasticsearch/index/mapper/KeywordFieldMapperTests.java

@@ -585,7 +585,10 @@ public class KeywordFieldMapperTests extends MapperTestCase {
 
     @Override
     protected String minimalIsInvalidRoutingPathErrorMessage(Mapper mapper) {
-        return "All fields that match routing_path must be keyword time_series_dimensions but [field] was not a time_series_dimension";
+        return "All fields that match routing_path must be keywords with [time_series_dimension: true] "
+            + "and without the [script] parameter. ["
+            + mapper.name()
+            + "] was not [time_series_dimension: true].";
     }
 
     public void testDimensionInRoutingPath() throws IOException {

+ 6 - 1
test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java

@@ -772,6 +772,11 @@ public abstract class MapperTestCase extends MapperServiceTestCase {
     }
 
     protected String minimalIsInvalidRoutingPathErrorMessage(Mapper mapper) {
-        return "All fields that match routing_path must be keyword time_series_dimensions but [field] was [" + mapper.typeName() + "]";
+        return "All fields that match routing_path must be keywords with [time_series_dimension: true] "
+            + "and without the [script] parameter. ["
+            + mapper.name()
+            + "] was ["
+            + mapper.typeName()
+            + "].";
     }
 }