Browse Source

[ML] Alias timestamp to @timestamp in anomaly detection results index (#90812)

The ML anomaly detection results use `timestamp` for the time
field instead of the ECS recommendation of `@timestamp`. It's too
late to change the underlying field name, but as a convenience for
ECS users we can alias `timestamp` as `@timestamp` so that they can
search using either.
David Roberts 3 years ago
parent
commit
6592b79560

+ 5 - 0
docs/changelog/90812.yaml

@@ -0,0 +1,5 @@
+pr: 90812
+summary: Alias timestamp to @timestamp in anomaly detection results index
+area: Machine Learning
+type: enhancement
+issues: []

+ 1 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappings.java

@@ -65,6 +65,7 @@ public class ElasticsearchMappings {
     public static final String WHITESPACE = "whitespace";
     public static final String NESTED = "nested";
     public static final String COPY_TO = "copy_to";
+    public static final String PATH = "path";
     public static final String PROPERTIES = "properties";
     public static final String TYPE = "type";
     public static final String DYNAMIC = "dynamic";

+ 6 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/ReservedFieldNames.java

@@ -23,6 +23,8 @@ import java.util.HashSet;
 import java.util.Set;
 import java.util.regex.Pattern;
 
+import static org.elasticsearch.cluster.metadata.MetadataIndexTemplateService.DEFAULT_TIMESTAMP_FIELD;
+
 /**
  * Defines the field names that we use for our results.
  * Fields from the raw data with these names are not added to any result.  Even
@@ -201,6 +203,10 @@ public final class ReservedFieldNames {
         ExponentialAverageCalculationContext.LATEST_TIMESTAMP.getPreferredName(),
         ExponentialAverageCalculationContext.PREVIOUS_EXPONENTIAL_AVERAGE_MS.getPreferredName(),
 
+        // ML results use "timestamp" as their time field, but we add an alias "@timestamp" to be
+        // slightly more ECS-like as a convenience for users of components that only work with ECS
+        DEFAULT_TIMESTAMP_FIELD,
+
         GetResult._ID,
         GetResult._INDEX };
 

+ 4 - 0
x-pack/plugin/core/src/main/resources/org/elasticsearch/xpack/core/ml/anomalydetection/results_index_mappings.json

@@ -475,6 +475,10 @@
     "timestamp" : {
       "type" : "date"
     },
+    "@timestamp" : {
+      "type" : "alias",
+      "path" : "timestamp"
+    },
     "total_by_field_count" : {
       "type" : "long"
     },

+ 3 - 3
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappingsTests.java

@@ -44,7 +44,6 @@ import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -65,19 +64,20 @@ public class ElasticsearchMappingsTests extends ESTestCase {
 
     // These are not reserved because they're Elasticsearch keywords, not
     // field names
-    private static final List<String> KEYWORDS = Arrays.asList(
+    private static final List<String> KEYWORDS = List.of(
         ElasticsearchMappings.ANALYZER,
         ElasticsearchMappings.COPY_TO,
         ElasticsearchMappings.DYNAMIC,
         ElasticsearchMappings.ENABLED,
         ElasticsearchMappings.NESTED,
+        ElasticsearchMappings.PATH,
         ElasticsearchMappings.PROPERTIES,
         ElasticsearchMappings.TYPE,
         ElasticsearchMappings.WHITESPACE,
         SearchSourceBuilder.RUNTIME_MAPPINGS_FIELD.getPreferredName()
     );
 
-    private static final List<String> INTERNAL_FIELDS = Arrays.asList(GetResult._ID, GetResult._INDEX);
+    private static final List<String> INTERNAL_FIELDS = List.of(GetResult._ID, GetResult._INDEX);
 
     public void testResultsMappingReservedFields() throws Exception {
         Set<String> overridden = new HashSet<>(KEYWORDS);

+ 26 - 3
x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/DatafeedWithAggsIT.java

@@ -11,6 +11,7 @@ import org.elasticsearch.action.bulk.BulkResponse;
 import org.elasticsearch.action.index.IndexRequest;
 import org.elasticsearch.action.support.WriteRequest;
 import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.index.query.QueryBuilders;
 import org.elasticsearch.search.aggregations.AggregationBuilders;
 import org.elasticsearch.search.aggregations.AggregatorFactories;
 import org.elasticsearch.search.aggregations.bucket.composite.DateHistogramValuesSourceBuilder;
@@ -25,6 +26,7 @@ import org.elasticsearch.xpack.core.ml.job.config.AnalysisConfig;
 import org.elasticsearch.xpack.core.ml.job.config.DataDescription;
 import org.elasticsearch.xpack.core.ml.job.config.Detector;
 import org.elasticsearch.xpack.core.ml.job.config.Job;
+import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndex;
 import org.elasticsearch.xpack.core.ml.job.results.Bucket;
 import org.junit.After;
 
@@ -155,9 +157,30 @@ public class DatafeedWithAggsIT extends MlNativeAutodetectIntegTestCase {
         getBucketsRequest.setExcludeInterim(true);
         List<Bucket> buckets = getBuckets(getBucketsRequest);
         for (Bucket bucket : buckets) {
-            if (bucket.getEventCount() != 2) {
-                fail("Bucket [" + bucket.getTimestamp().getTime() + "] has [" + bucket.getEventCount() + "] when 2 were expected");
-            }
+            assertEquals(
+                "Bucket [" + bucket.getTimestamp().getTime() + "] has [" + bucket.getEventCount() + "] when 2 were expected",
+                2L,
+                bucket.getEventCount()
+            );
+            // Confirm that it's possible to search for the same buckets by @timestamp - proves that @timestamp works as a field alias
+            assertThat(
+                client().prepareSearch(AnomalyDetectorsIndex.jobResultsAliasedName(jobId))
+                    .setQuery(
+                        QueryBuilders.boolQuery()
+                            .filter(QueryBuilders.termQuery("job_id", jobId))
+                            .filter(QueryBuilders.termQuery("result_type", "bucket"))
+                            .filter(
+                                QueryBuilders.rangeQuery("@timestamp")
+                                    .gte(bucket.getTimestamp().getTime())
+                                    .lte(bucket.getTimestamp().getTime())
+                            )
+                    )
+                    .setTrackTotalHits(true)
+                    .get()
+                    .getHits()
+                    .getTotalHits().value,
+                equalTo(1L)
+            );
         }
     }
 }