diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/AnomalyCause.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/AnomalyCause.java index 4fbe5ac1ff3..1c7cb1fcdb0 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/AnomalyCause.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/AnomalyCause.java @@ -18,7 +18,10 @@ */ package org.elasticsearch.client.ml.job.results; +import org.elasticsearch.client.ml.job.config.DetectorFunction; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -256,6 +259,28 @@ public class AnomalyCause implements ToXContentObject { this.influencers = Collections.unmodifiableList(influencers); } + @Nullable + public GeoPoint getTypicalGeoPoint() { + if (DetectorFunction.LAT_LONG.getFullName().equals(function) == false || typical == null) { + return null; + } + if (typical.size() == 2) { + return new GeoPoint(typical.get(0), typical.get(1)); + } + return null; + } + + @Nullable + public GeoPoint getActualGeoPoint() { + if (DetectorFunction.LAT_LONG.getFullName().equals(function) == false || actual == null) { + return null; + } + if (actual.size() == 2) { + return new GeoPoint(actual.get(0), actual.get(1)); + } + return null; + } + @Override public int hashCode() { return Objects.hash(probability, actual, typical, byFieldName, byFieldValue, correlatedByFieldValue, fieldName, function, diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/AnomalyRecord.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/AnomalyRecord.java index 3c52aad74d0..e7789d22c50 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/AnomalyRecord.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/results/AnomalyRecord.java @@ -19,8 +19,11 @@ package org.elasticsearch.client.ml.job.results; import org.elasticsearch.client.common.TimeUtil; +import org.elasticsearch.client.ml.job.config.DetectorFunction; import org.elasticsearch.client.ml.job.config.Job; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ObjectParser.ValueType; import org.elasticsearch.common.xcontent.ToXContentObject; @@ -388,6 +391,28 @@ public class AnomalyRecord implements ToXContentObject { this.influences = Collections.unmodifiableList(influencers); } + @Nullable + public GeoPoint getTypicalGeoPoint() { + if (DetectorFunction.LAT_LONG.getFullName().equals(function) == false || typical == null) { + return null; + } + if (typical.size() == 2) { + return new GeoPoint(typical.get(0), typical.get(1)); + } + return null; + } + + @Nullable + public GeoPoint getActualGeoPoint() { + if (DetectorFunction.LAT_LONG.getFullName().equals(function) == false || actual == null) { + return null; + } + if (actual.size() == 2) { + return new GeoPoint(actual.get(0), actual.get(1)); + } + return null; + } + @Override public int hashCode() { return Objects.hash(jobId, detectorIndex, bucketSpan, probability, multiBucketImpact, recordScore, diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyCauseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyCauseTests.java index 3ac6a0b6ec4..f6cae338eb2 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyCauseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyCauseTests.java @@ -18,12 +18,20 @@ */ package org.elasticsearch.client.ml.job.results; +import org.elasticsearch.client.ml.job.config.DetectorFunction; +import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; + public class AnomalyCauseTests extends AbstractXContentTestCase { @Override @@ -103,4 +111,40 @@ public class AnomalyCauseTests extends AbstractXContentTestCase { protected boolean supportsUnknownFields() { return true; } + + public void testActualAsGeoPoint() { + AnomalyCause anomalyCause = new AnomalyCause(); + + assertThat(anomalyCause.getActualGeoPoint(), is(nullValue())); + + anomalyCause.setFunction(DetectorFunction.LAT_LONG.getFullName()); + assertThat(anomalyCause.getActualGeoPoint(), is(nullValue())); + + anomalyCause.setActual(Collections.singletonList(80.0)); + assertThat(anomalyCause.getActualGeoPoint(), is(nullValue())); + + anomalyCause.setActual(Arrays.asList(90.0, 80.0)); + assertThat(anomalyCause.getActualGeoPoint(), equalTo(new GeoPoint(90.0, 80.0))); + + anomalyCause.setActual(Arrays.asList(10.0, 100.0, 90.0)); + assertThat(anomalyCause.getActualGeoPoint(), is(nullValue())); + } + + public void testTypicalAsGeoPoint() { + AnomalyCause anomalyCause = new AnomalyCause(); + + assertThat(anomalyCause.getTypicalGeoPoint(), is(nullValue())); + + anomalyCause.setFunction(DetectorFunction.LAT_LONG.getFullName()); + assertThat(anomalyCause.getTypicalGeoPoint(), is(nullValue())); + + anomalyCause.setTypical(Collections.singletonList(80.0)); + assertThat(anomalyCause.getTypicalGeoPoint(), is(nullValue())); + + anomalyCause.setTypical(Arrays.asList(90.0, 80.0)); + assertThat(anomalyCause.getTypicalGeoPoint(), equalTo(new GeoPoint(90.0, 80.0))); + + anomalyCause.setTypical(Arrays.asList(10.0, 100.0, 90.0)); + assertThat(anomalyCause.getTypicalGeoPoint(), is(nullValue())); + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyRecordTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyRecordTests.java index 39bfff3a7e8..923e25aa2b4 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyRecordTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/job/results/AnomalyRecordTests.java @@ -18,14 +18,21 @@ */ package org.elasticsearch.client.ml.job.results; +import org.elasticsearch.client.ml.job.config.DetectorFunction; +import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; + public class AnomalyRecordTests extends AbstractXContentTestCase { @Override @@ -93,4 +100,40 @@ public class AnomalyRecordTests extends AbstractXContentTestCase protected boolean supportsUnknownFields() { return true; } + + public void testActualAsGeoPoint() { + AnomalyRecord anomalyRecord = new AnomalyRecord(randomAlphaOfLength(10), new Date(), randomNonNegativeLong()); + + assertThat(anomalyRecord.getActualGeoPoint(), is(nullValue())); + + anomalyRecord.setFunction(DetectorFunction.LAT_LONG.getFullName()); + assertThat(anomalyRecord.getActualGeoPoint(), is(nullValue())); + + anomalyRecord.setActual(Collections.singletonList(80.0)); + assertThat(anomalyRecord.getActualGeoPoint(), is(nullValue())); + + anomalyRecord.setActual(Arrays.asList(90.0, 80.0)); + assertThat(anomalyRecord.getActualGeoPoint(), equalTo(new GeoPoint(90.0, 80.0))); + + anomalyRecord.setActual(Arrays.asList(10.0, 100.0, 90.0)); + assertThat(anomalyRecord.getActualGeoPoint(), is(nullValue())); + } + + public void testTypicalAsGeoPoint() { + AnomalyRecord anomalyRecord = new AnomalyRecord(randomAlphaOfLength(10), new Date(), randomNonNegativeLong()); + + assertThat(anomalyRecord.getTypicalGeoPoint(), is(nullValue())); + + anomalyRecord.setFunction(DetectorFunction.LAT_LONG.getFullName()); + assertThat(anomalyRecord.getTypicalGeoPoint(), is(nullValue())); + + anomalyRecord.setTypical(Collections.singletonList(80.0)); + assertThat(anomalyRecord.getTypicalGeoPoint(), is(nullValue())); + + anomalyRecord.setTypical(Arrays.asList(90.0, 80.0)); + assertThat(anomalyRecord.getTypicalGeoPoint(), equalTo(new GeoPoint(90.0, 80.0))); + + anomalyRecord.setTypical(Arrays.asList(10.0, 100.0, 90.0)); + assertThat(anomalyRecord.getTypicalGeoPoint(), is(nullValue())); + } } diff --git a/docs/reference/ml/anomaly-detection/apis/resultsresource.asciidoc b/docs/reference/ml/anomaly-detection/apis/resultsresource.asciidoc index ce6a8a90015..b35100c24e6 100644 --- a/docs/reference/ml/anomaly-detection/apis/resultsresource.asciidoc +++ b/docs/reference/ml/anomaly-detection/apis/resultsresource.asciidoc @@ -303,8 +303,9 @@ A record object has the following properties: part of the core analytical modeling, these low-level anomaly records are aggregated for their parent over field record. The causes resource contains similar elements to the record resource, namely `actual`, `typical`, - `*_field_name` and `*_field_value`. Probability and scores are not applicable - to causes. + `geo_results.actual_point`, `geo_results.typical_point`, + `*_field_name` and `*_field_value`. + Probability and scores are not applicable to causes. `detector_index`:: (number) A unique identifier for the detector. @@ -383,6 +384,16 @@ A record object has the following properties: `typical`:: (array) The typical value for the bucket, according to analytical modeling. +`geo_results.actual_point`:: + (string) The actual value for the bucket formatted as a `geo_point`. + If the detector function is `lat_long`, this is a comma delimited string + of the latitude and longitude. + +`geo_results.typical_point`:: + (string) The typical value for the bucket formatted as a `geo_point`. + If the detector function is `lat_long`, this is a comma delimited string + of the latitude and longitude. + NOTE: Additional record properties are added, depending on the fields being analyzed. For example, if it's analyzing `hostname` as a _by field_, then a field `hostname` is added to the result document. This information enables you to diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappings.java index 1eec2a04be4..71b438c40bf 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappings.java @@ -55,6 +55,7 @@ import org.elasticsearch.xpack.core.ml.job.results.BucketInfluencer; import org.elasticsearch.xpack.core.ml.job.results.CategoryDefinition; import org.elasticsearch.xpack.core.ml.job.results.Forecast; import org.elasticsearch.xpack.core.ml.job.results.ForecastRequestStats; +import org.elasticsearch.xpack.core.ml.job.results.GeoResults; import org.elasticsearch.xpack.core.ml.job.results.Influence; import org.elasticsearch.xpack.core.ml.job.results.Influencer; import org.elasticsearch.xpack.core.ml.job.results.ModelPlot; @@ -131,6 +132,7 @@ public class ElasticsearchMappings { public static final String BOOLEAN = "boolean"; public static final String DATE = "date"; public static final String DOUBLE = "double"; + public static final String GEO_POINT = "geo_point"; public static final String INTEGER = "integer"; public static final String KEYWORD = "keyword"; public static final String LONG = "long"; @@ -885,6 +887,16 @@ public class ElasticsearchMappings { .field(TYPE, KEYWORD) .field(COPY_TO, ALL_FIELD_VALUES) .endObject() + .startObject(AnomalyCause.GEO_RESULTS.getPreferredName()) + .startObject(PROPERTIES) + .startObject(GeoResults.ACTUAL_POINT.getPreferredName()) + .field(TYPE, GEO_POINT) + .endObject() + .startObject(GeoResults.TYPICAL_POINT.getPreferredName()) + .field(TYPE, GEO_POINT) + .endObject() + .endObject() + .endObject() .endObject() .endObject() .startObject(AnomalyRecord.INFLUENCERS.getPreferredName()) @@ -899,6 +911,16 @@ public class ElasticsearchMappings { .field(COPY_TO, ALL_FIELD_VALUES) .endObject() .endObject() + .endObject() + .startObject(AnomalyRecord.GEO_RESULTS.getPreferredName()) + .startObject(PROPERTIES) + .startObject(GeoResults.ACTUAL_POINT.getPreferredName()) + .field(TYPE, GEO_POINT) + .endObject() + .startObject(GeoResults.TYPICAL_POINT.getPreferredName()) + .field(TYPE, GEO_POINT) + .endObject() + .endObject() .endObject(); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyCause.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyCause.java index 50efe24ab0f..3a4a902064e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyCause.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyCause.java @@ -5,7 +5,9 @@ */ package org.elasticsearch.xpack.core.ml.job.results; +import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -39,6 +41,7 @@ public class AnomalyCause implements ToXContentObject, Writeable { public static final ParseField TYPICAL = new ParseField("typical"); public static final ParseField ACTUAL = new ParseField("actual"); public static final ParseField INFLUENCERS = new ParseField("influencers"); + public static final ParseField GEO_RESULTS = new ParseField("geo_results"); /** * Metric Results @@ -67,6 +70,9 @@ public class AnomalyCause implements ToXContentObject, Writeable { parser.declareString(AnomalyCause::setOverFieldValue, OVER_FIELD_VALUE); parser.declareObjectArray(AnomalyCause::setInfluencers, ignoreUnknownFields ? Influence.LENIENT_PARSER : Influence.STRICT_PARSER, INFLUENCERS); + parser.declareObject(AnomalyCause::setGeoResults, + ignoreUnknownFields ? GeoResults.LENIENT_PARSER : GeoResults.STRICT_PARSER, + GEO_RESULTS); return parser; } @@ -81,6 +87,7 @@ public class AnomalyCause implements ToXContentObject, Writeable { private String functionDescription; private List typical; private List actual; + private GeoResults geoResults; private String fieldName; @@ -114,6 +121,9 @@ public class AnomalyCause implements ToXContentObject, Writeable { if (in.readBoolean()) { influencers = in.readList(Influence::new); } + if (in.getVersion().onOrAfter(Version.V_7_6_0)) { + geoResults = in.readOptionalWriteable(GeoResults::new); + } } @Override @@ -144,6 +154,9 @@ public class AnomalyCause implements ToXContentObject, Writeable { if (hasInfluencers) { out.writeList(influencers); } + if (out.getVersion().onOrAfter(Version.V_7_6_0)) { + out.writeOptionalWriteable(geoResults); + } } @Override @@ -189,11 +202,13 @@ public class AnomalyCause implements ToXContentObject, Writeable { if (influencers != null) { builder.field(INFLUENCERS.getPreferredName(), influencers); } + if (geoResults != null) { + builder.field(GEO_RESULTS.getPreferredName(), geoResults); + } builder.endObject(); return builder; } - public double getProbability() { return probability; } @@ -307,6 +322,14 @@ public class AnomalyCause implements ToXContentObject, Writeable { this.influencers = influencers; } + public GeoResults getGeoResults() { + return geoResults; + } + + public void setGeoResults(GeoResults geoResults) { + this.geoResults = geoResults; + } + @Override public int hashCode() { return Objects.hash(probability, @@ -322,7 +345,8 @@ public class AnomalyCause implements ToXContentObject, Writeable { overFieldValue, partitionFieldName, partitionFieldValue, - influencers); + influencers, + geoResults); } @Override @@ -350,8 +374,13 @@ public class AnomalyCause implements ToXContentObject, Writeable { Objects.equals(this.partitionFieldValue, that.partitionFieldValue) && Objects.equals(this.overFieldName, that.overFieldName) && Objects.equals(this.overFieldValue, that.overFieldValue) && + Objects.equals(this.geoResults, that.geoResults) && Objects.equals(this.influencers, that.influencers); } + @Override + public String toString() { + return Strings.toString(this, true, true); + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyRecord.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyRecord.java index 6bd9d147b82..16e12b1fcb2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyRecord.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyRecord.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.ml.job.results; +import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; @@ -56,6 +57,7 @@ public class AnomalyRecord implements ToXContentObject, Writeable { public static final ParseField ACTUAL = new ParseField("actual"); public static final ParseField INFLUENCERS = new ParseField("influencers"); public static final ParseField BUCKET_SPAN = new ParseField("bucket_span"); + public static final ParseField GEO_RESULTS = new ParseField("geo_results"); // Used for QueryPage public static final ParseField RESULTS_FIELD = new ParseField("records"); @@ -115,6 +117,9 @@ public class AnomalyRecord implements ToXContentObject, Writeable { CAUSES); parser.declareObjectArray(AnomalyRecord::setInfluencers, ignoreUnknownFields ? Influence.LENIENT_PARSER : Influence.STRICT_PARSER, INFLUENCERS); + parser.declareObject(AnomalyRecord::setGeoResults, + ignoreUnknownFields ? GeoResults.LENIENT_PARSER : GeoResults.STRICT_PARSER, + GEO_RESULTS); return parser; } @@ -133,6 +138,7 @@ public class AnomalyRecord implements ToXContentObject, Writeable { private List typical; private List actual; private boolean isInterim; + private GeoResults geoResults; private String fieldName; @@ -190,6 +196,9 @@ public class AnomalyRecord implements ToXContentObject, Writeable { if (in.readBoolean()) { influences = in.readList(Influence::new); } + if (in.getVersion().onOrAfter(Version.V_7_6_0)) { + geoResults = in.readOptionalWriteable(GeoResults::new); + } } @Override @@ -235,6 +244,9 @@ public class AnomalyRecord implements ToXContentObject, Writeable { if (hasInfluencers) { out.writeList(influences); } + if (out.getVersion().onOrAfter(Version.V_7_6_0)) { + out.writeOptionalWriteable(geoResults); + } } @Override @@ -300,6 +312,9 @@ public class AnomalyRecord implements ToXContentObject, Writeable { if (influences != null) { builder.field(INFLUENCERS.getPreferredName(), influences); } + if (geoResults != null) { + builder.field(GEO_RESULTS.getPreferredName(), geoResults); + } Map> inputFields = inputFieldMap(); for (String fieldName : inputFields.keySet()) { @@ -529,6 +544,13 @@ public class AnomalyRecord implements ToXContentObject, Writeable { this.influences = influencers; } + public GeoResults getGeoResults() { + return geoResults; + } + + public void setGeoResults(GeoResults geoResults) { + this.geoResults = geoResults; + } @Override public int hashCode() { @@ -536,10 +558,9 @@ public class AnomalyRecord implements ToXContentObject, Writeable { initialRecordScore, typical, actual,function, functionDescription, fieldName, byFieldName, byFieldValue, correlatedByFieldValue, partitionFieldName, partitionFieldValue, overFieldName, overFieldValue, timestamp, isInterim, - causes, influences, jobId); + causes, influences, jobId, geoResults); } - @Override public boolean equals(Object other) { if (this == other) { @@ -574,6 +595,12 @@ public class AnomalyRecord implements ToXContentObject, Writeable { && Objects.equals(this.timestamp, that.timestamp) && Objects.equals(this.isInterim, that.isInterim) && Objects.equals(this.causes, that.causes) + && Objects.equals(this.geoResults, that.geoResults) && Objects.equals(this.influences, that.influences); } + + @Override + public String toString() { + return Strings.toString(this, true, true); + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/GeoResults.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/GeoResults.java new file mode 100644 index 00000000000..93168095bb7 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/GeoResults.java @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.job.results; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +public class GeoResults implements ToXContentObject, Writeable { + + public static final ParseField GEO_RESULTS = new ParseField("geo_results"); + + public static final ParseField TYPICAL_POINT = new ParseField("typical_point"); + public static final ParseField ACTUAL_POINT = new ParseField("actual_point"); + + public static final ObjectParser STRICT_PARSER = createParser(false); + public static final ObjectParser LENIENT_PARSER = createParser(true); + + private static ObjectParser createParser(boolean ignoreUnknownFields) { + ObjectParser parser = new ObjectParser<>(GEO_RESULTS.getPreferredName(), ignoreUnknownFields, + GeoResults::new); + parser.declareString(GeoResults::setActualPoint, ACTUAL_POINT); + parser.declareString(GeoResults::setTypicalPoint, TYPICAL_POINT); + return parser; + } + + private String actualPoint; + private String typicalPoint; + + public GeoResults() {} + + public GeoResults(StreamInput in) throws IOException { + this.actualPoint = in.readOptionalString(); + this.typicalPoint = in.readOptionalString(); + } + + public String getActualPoint() { + return actualPoint; + } + + public void setActualPoint(String actualPoint) { + this.actualPoint = actualPoint; + } + + public String getTypicalPoint() { + return typicalPoint; + } + + public void setTypicalPoint(String typicalPoint) { + this.typicalPoint = typicalPoint; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeOptionalString(actualPoint); + out.writeOptionalString(typicalPoint); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (typicalPoint != null) { + builder.field(TYPICAL_POINT.getPreferredName(), typicalPoint); + } + if (actualPoint != null) { + builder.field(ACTUAL_POINT.getPreferredName(), actualPoint); + } + builder.endObject(); + return builder; + } + + @Override + public int hashCode() { + return Objects.hash(typicalPoint, actualPoint); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + GeoResults that = (GeoResults) other; + return Objects.equals(this.typicalPoint, that.typicalPoint) && Objects.equals(this.actualPoint, that.actualPoint); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/ReservedFieldNames.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/ReservedFieldNames.java index c40fa2f026b..225da4b8a29 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/ReservedFieldNames.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/results/ReservedFieldNames.java @@ -74,6 +74,7 @@ public final class ReservedFieldNames { AnomalyCause.FUNCTION_DESCRIPTION.getPreferredName(), AnomalyCause.TYPICAL.getPreferredName(), AnomalyCause.ACTUAL.getPreferredName(), + AnomalyCause.GEO_RESULTS.getPreferredName(), AnomalyCause.INFLUENCERS.getPreferredName(), AnomalyCause.FIELD_NAME.getPreferredName(), @@ -88,6 +89,7 @@ public final class ReservedFieldNames { AnomalyRecord.FUNCTION_DESCRIPTION.getPreferredName(), AnomalyRecord.TYPICAL.getPreferredName(), AnomalyRecord.ACTUAL.getPreferredName(), + AnomalyRecord.GEO_RESULTS.getPreferredName(), AnomalyRecord.INFLUENCERS.getPreferredName(), AnomalyRecord.FIELD_NAME.getPreferredName(), AnomalyRecord.OVER_FIELD_NAME.getPreferredName(), @@ -97,6 +99,9 @@ public final class ReservedFieldNames { AnomalyRecord.INITIAL_RECORD_SCORE.getPreferredName(), AnomalyRecord.BUCKET_SPAN.getPreferredName(), + GeoResults.TYPICAL_POINT.getPreferredName(), + GeoResults.ACTUAL_POINT.getPreferredName(), + Bucket.ANOMALY_SCORE.getPreferredName(), Bucket.BUCKET_INFLUENCERS.getPreferredName(), Bucket.BUCKET_SPAN.getPreferredName(), diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappingsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappingsTests.java index 13ce6f2ab61..168dd24109d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappingsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/persistence/ElasticsearchMappingsTests.java @@ -89,7 +89,7 @@ public class ElasticsearchMappingsTests extends ESTestCase { GetResult._TYPE ); - public void testResultsMapppingReservedFields() throws Exception { + public void testResultsMappingReservedFields() throws Exception { Set overridden = new HashSet<>(KEYWORDS); // These are not reserved because they're data types, not field names @@ -109,7 +109,7 @@ public class ElasticsearchMappingsTests extends ESTestCase { compareFields(expected, ReservedFieldNames.RESERVED_RESULT_FIELD_NAMES); } - public void testConfigMapppingReservedFields() throws Exception { + public void testConfigMappingReservedFields() throws Exception { Set overridden = new HashSet<>(KEYWORDS); // These are not reserved because they're data types, not field names diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyCauseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyCauseTests.java index 033392eddce..dbcad6444dd 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyCauseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyCauseTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.ml.job.results; +import org.elasticsearch.client.ml.job.config.DetectorFunction; import org.elasticsearch.common.io.stream.Writeable.Reader; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; @@ -59,7 +60,8 @@ public class AnomalyCauseTests extends AbstractSerializingTestCase anomalyCause.setPartitionFieldValue(randomAlphaOfLengthBetween(1, 20)); } if (randomBoolean()) { - anomalyCause.setFunction(randomAlphaOfLengthBetween(1, 20)); + anomalyCause.setFunction(DetectorFunction.LAT_LONG.getFullName()); + anomalyCause.setGeoResults(GeoResultsTests.createTestGeoResults()); } if (randomBoolean()) { anomalyCause.setFunctionDescription(randomAlphaOfLengthBetween(1, 20)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyRecordTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyRecordTests.java index 882a46f3cbe..7c94e672143 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyRecordTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/job/results/AnomalyRecordTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.core.ml.job.results; +import org.elasticsearch.client.ml.job.config.DetectorFunction; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentHelper; @@ -58,7 +59,12 @@ public class AnomalyRecordTests extends AbstractSerializingTestCase { + + private boolean lenient; + + @Before + public void setLenient() { + lenient = randomBoolean(); + } + + static GeoResults createTestGeoResults() { + GeoResults geoResults = new GeoResults(); + if (randomBoolean()) { + geoResults.setActualPoint(randomDoubleBetween(-90.0, 90.0, true) + "," + + randomDoubleBetween(-90.0, 90.0, true)); + } + if (randomBoolean()) { + geoResults.setTypicalPoint(randomDoubleBetween(-90.0, 90.0, true) + "," + + randomDoubleBetween(-90.0, 90.0, true)); + } + return geoResults; + } + + @Override + protected GeoResults createTestInstance() { + return createTestGeoResults(); + } + + @Override + protected Reader instanceReader() { + return GeoResults::new; + } + + @Override + protected GeoResults doParseInstance(XContentParser parser) { + return lenient ? GeoResults.LENIENT_PARSER.apply(parser, null) : GeoResults.STRICT_PARSER.apply(parser, null); + } + + public void testStrictParser() throws IOException { + String json = "{\"foo\":\"bar\"}"; + try (XContentParser parser = createParser(JsonXContent.jsonXContent, json)) { + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> GeoResults.STRICT_PARSER.apply(parser, null)); + + assertThat(e.getMessage(), containsString("unknown field [foo]")); + } + } + + public void testLenientParser() throws IOException { + String json = "{\"foo\":\"bar\"}"; + try (XContentParser parser = createParser(JsonXContent.jsonXContent, json)) { + GeoResults.LENIENT_PARSER.apply(parser, null); + } + } +}