[ML] model_debug_config: add enabled and remove bounds_percentile (elastic/x-pack-elasticsearch#627)

This allows it to be disabled/enabled via job updates.
It also simplifies it by removing bounds_percentile as it currently
rarely makes sense to set the bounds to another value than 95.0.

Original commit: elastic/x-pack-elasticsearch@c27fce2d86
This commit is contained in:
Dimitris Athanasiou 2017-02-23 14:11:23 +00:00 committed by GitHub
parent 33b10970ee
commit 412d5e35b1
14 changed files with 82 additions and 115 deletions

View File

@ -11,57 +11,56 @@ 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.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.xpack.ml.job.messages.Messages;
import java.io.IOException;
import java.util.Locale;
import java.util.Objects;
public class ModelDebugConfig extends ToXContentToBytes implements Writeable {
private static final double MAX_PERCENTILE = 100.0;
private static final ParseField TYPE_FIELD = new ParseField("model_debug_config");
public static final ParseField BOUNDS_PERCENTILE_FIELD = new ParseField("bounds_percentile");
private static final ParseField ENABLED_FIELD = new ParseField("enabled");
public static final ParseField TERMS_FIELD = new ParseField("terms");
public static final ConstructingObjectParser<ModelDebugConfig, Void> PARSER = new ConstructingObjectParser<>(
TYPE_FIELD.getPreferredName(), a -> new ModelDebugConfig((Double) a[0], (String) a[1]));
TYPE_FIELD.getPreferredName(), a -> new ModelDebugConfig((boolean) a[0], (String) a[1]));
static {
PARSER.declareDouble(ConstructingObjectParser.constructorArg(), BOUNDS_PERCENTILE_FIELD);
PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), ENABLED_FIELD);
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), TERMS_FIELD);
}
private final double boundsPercentile;
private final boolean enabled;
private final String terms;
public ModelDebugConfig(double boundsPercentile, String terms) {
if (boundsPercentile < 0.0 || boundsPercentile > MAX_PERCENTILE) {
String msg = Messages.getMessage(Messages.JOB_CONFIG_MODEL_DEBUG_CONFIG_INVALID_BOUNDS_PERCENTILE);
throw new IllegalArgumentException(msg);
}
this.boundsPercentile = boundsPercentile;
public ModelDebugConfig() {
this(true, null);
}
public ModelDebugConfig(boolean enabled) {
this(false, null);
}
public ModelDebugConfig(boolean enabled, String terms) {
this.enabled = enabled;
this.terms = terms;
}
public ModelDebugConfig(StreamInput in) throws IOException {
boundsPercentile = in.readDouble();
enabled = in.readBoolean();
terms = in.readOptionalString();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeDouble(boundsPercentile);
out.writeBoolean(enabled);
out.writeOptionalString(terms);
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.field(BOUNDS_PERCENTILE_FIELD.getPreferredName(), boundsPercentile);
builder.field(ENABLED_FIELD.getPreferredName(), enabled);
if (terms != null) {
builder.field(TERMS_FIELD.getPreferredName(), terms);
}
@ -69,8 +68,8 @@ public class ModelDebugConfig extends ToXContentToBytes implements Writeable {
return builder;
}
public double getBoundsPercentile() {
return this.boundsPercentile;
public boolean isEnabled() {
return enabled;
}
public String getTerms() {
@ -88,11 +87,11 @@ public class ModelDebugConfig extends ToXContentToBytes implements Writeable {
}
ModelDebugConfig that = (ModelDebugConfig) other;
return Objects.equals(this.boundsPercentile, that.boundsPercentile) && Objects.equals(this.terms, that.terms);
return this.enabled == that.enabled && Objects.equals(this.terms, that.terms);
}
@Override
public int hashCode() {
return Objects.hash(boundsPercentile, terms);
return Objects.hash(enabled, terms);
}
}

View File

@ -105,8 +105,6 @@ public final class Messages {
public static final String JOB_CONFIG_INVALID_TIMEFORMAT = "job.config.invalid.timeformat";
public static final String JOB_CONFIG_FUNCTION_INCOMPATIBLE_PRESUMMARIZED = "job.config.function.incompatible.presummarized";
public static final String JOB_CONFIG_MISSING_ANALYSISCONFIG = "job.config.missing.analysisconfig";
public static final String JOB_CONFIG_MODEL_DEBUG_CONFIG_INVALID_BOUNDS_PERCENTILE = "job.config.model.debug.config.invalid.bounds."
+ "percentile";
public static final String JOB_CONFIG_FIELD_VALUE_TOO_LOW = "job.config.field.value.too.low";
public static final String JOB_CONFIG_NO_ANALYSIS_FIELD = "job.config.no.analysis.field";
public static final String JOB_CONFIG_NO_ANALYSIS_FIELD_NOT_COUNT = "job.config.no.analysis.field.not.count";

View File

@ -16,6 +16,9 @@ import static org.elasticsearch.xpack.ml.job.process.autodetect.writer.WriterCon
public class ModelDebugConfigWriter {
private static final double BOUNDS_PERCENTILE_DEFAULT = 95.0;
private static final double BOUNDS_PERCENTILE_DISABLE_VALUE = -1.0;
private final ModelDebugConfig modelDebugConfig;
private final Writer writer;
@ -29,7 +32,7 @@ public class ModelDebugConfigWriter {
contents.append("boundspercentile")
.append(EQUALS)
.append(modelDebugConfig.getBoundsPercentile())
.append(modelDebugConfig.isEnabled() ? BOUNDS_PERCENTILE_DEFAULT : BOUNDS_PERCENTILE_DISABLE_VALUE)
.append(NEW_LINE);
String terms = modelDebugConfig.getTerms();

View File

@ -76,7 +76,6 @@ job.config.id.too.long = The job id cannot contain more than {0,number,integer}
job.config.invalid.fieldname.chars = Invalid field name ''{0}''. Field names including over, by and partition fields cannot contain any of these characters: {1}
job.config.invalid.timeformat = Invalid Time format string ''{0}''
job.config.missing.analysisconfig = An analysis_config must be set
job.config.model.debug.config.invalid.bounds.percentile = Invalid model_debug_config: bounds_percentile must be in the range [0, 100]
job.config.field.value.too.low = {0} cannot be less than {1,number}. Value = {2,number}
job.config.no.analysis.field = One of function, field_name, by_field_name or over_field_name must be set
job.config.no.analysis.field.not.count = Unless the function is 'count' one of field_name, by_field_name or over_field_name must be set

View File

@ -7,19 +7,12 @@ package org.elasticsearch.xpack.ml.action;
import org.elasticsearch.xpack.ml.action.GetJobsAction.Response;
import org.elasticsearch.xpack.ml.action.util.QueryPage;
import org.elasticsearch.xpack.ml.job.config.AnalysisConfig;
import org.elasticsearch.xpack.ml.job.config.AnalysisLimits;
import org.elasticsearch.xpack.ml.job.config.DataDescription;
import org.elasticsearch.xpack.ml.job.config.Detector;
import org.elasticsearch.xpack.ml.job.config.Job;
import org.elasticsearch.xpack.ml.job.config.ModelDebugConfig;
import org.elasticsearch.xpack.ml.job.config.JobTests;
import org.elasticsearch.xpack.ml.support.AbstractStreamableTestCase;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
public class GetJobsActionResponseTests extends AbstractStreamableTestCase<GetJobsAction.Response> {
@ -30,46 +23,7 @@ public class GetJobsActionResponseTests extends AbstractStreamableTestCase<GetJo
int listSize = randomInt(10);
List<Job> jobList = new ArrayList<>(listSize);
for (int j = 0; j < listSize; j++) {
String jobId = "job" + j;
String description = randomBoolean() ? randomAsciiOfLength(100) : null;
Date createTime = new Date(randomNonNegativeLong());
Date finishedTime = randomBoolean() ? new Date(randomNonNegativeLong()) : null;
Date lastDataTime = randomBoolean() ? new Date(randomNonNegativeLong()) : null;
long timeout = randomNonNegativeLong();
AnalysisConfig.Builder analysisConfig = new AnalysisConfig.Builder(
Collections.singletonList(new Detector.Builder("metric", "some_field").build()));
AnalysisLimits analysisLimits = new AnalysisLimits(randomNonNegativeLong(), randomNonNegativeLong());
DataDescription.Builder dataDescription = new DataDescription.Builder();
ModelDebugConfig modelDebugConfig = randomBoolean() ? new ModelDebugConfig(randomDouble(), randomAsciiOfLength(10)) : null;
Long normalizationWindowDays = randomBoolean() ? Long.valueOf(randomIntBetween(0, 365)) : null;
Long backgroundPersistInterval = randomBoolean() ? Long.valueOf(randomIntBetween(3600, 86400)) : null;
Long modelSnapshotRetentionDays = randomBoolean() ? Long.valueOf(randomIntBetween(0, 365)) : null;
Long resultsRetentionDays = randomBoolean() ? Long.valueOf(randomIntBetween(0, 365)) : null;
Map<String, Object> customConfig = randomBoolean() ? Collections.singletonMap(randomAsciiOfLength(10), randomAsciiOfLength(10))
: null;
String modelSnapshotId = randomBoolean() ? randomAsciiOfLength(10) : null;
String indexName = "index" + j;
Job.Builder builder = new Job.Builder();
builder.setId(jobId);
builder.setDescription(description);
builder.setCreateTime(createTime);
builder.setFinishedTime(finishedTime);
builder.setLastDataTime(lastDataTime);
builder.setAnalysisConfig(analysisConfig);
builder.setAnalysisLimits(analysisLimits);
builder.setDataDescription(dataDescription);
builder.setModelDebugConfig(modelDebugConfig);
builder.setRenormalizationWindowDays(normalizationWindowDays);
builder.setBackgroundPersistInterval(backgroundPersistInterval);
builder.setModelSnapshotRetentionDays(modelSnapshotRetentionDays);
builder.setResultsRetentionDays(resultsRetentionDays);
builder.setCustomSettings(customConfig);
builder.setModelSnapshotId(modelSnapshotId);
builder.setResultsIndexName(indexName);
builder.setDeleted(randomBoolean());
Job job = builder.build();
jobList.add(job);
jobList.add(JobTests.createRandomizedJob());
}
result = new Response(new QueryPage<>(jobList, jobList.size(), Job.RESULTS_FIELD));

View File

@ -5,11 +5,9 @@
*/
package org.elasticsearch.xpack.ml.action;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.xpack.ml.job.config.JobUpdate;
import org.elasticsearch.xpack.ml.job.config.ModelDebugConfig;
import org.elasticsearch.xpack.ml.support.AbstractStreamableTestCase;
import org.elasticsearch.xpack.ml.support.AbstractStreamableXContentTestCase;
import java.util.List;
@ -20,11 +18,14 @@ public class UpdateProcessActionRequestTests extends AbstractStreamableTestCase<
protected UpdateProcessAction.Request createTestInstance() {
ModelDebugConfig config = null;
if (randomBoolean()) {
config = new ModelDebugConfig(5.0, "debug,config");
config = new ModelDebugConfig(randomBoolean(), randomAsciiOfLength(10));
}
List<JobUpdate.DetectorUpdate> updates = null;
if (randomBoolean()) {
int detectorUpdateCount = randomIntBetween(0, 5);
for (int i = 0; i < detectorUpdateCount; i++) {
new JobUpdate.DetectorUpdate(randomInt(), randomAsciiOfLength(10), null);
}
}
return new UpdateProcessAction.Request(randomAsciiOfLength(10), config, updates);
}

View File

@ -397,7 +397,7 @@ public class JobTests extends AbstractSerializingTestCase<Job> {
outputs = new String[] {ac.getDetectors().get(0).getFieldName()};
}
if (randomBoolean()) {
builder.setModelDebugConfig(new ModelDebugConfig(randomDouble(), randomAsciiOfLength(10)));
builder.setModelDebugConfig(new ModelDebugConfig(randomBoolean(), randomAsciiOfLength(10)));
}
if (randomBoolean()) {
builder.setRenormalizationWindowDays(randomNonNegativeLong());

View File

@ -48,7 +48,7 @@ public class JobUpdateTests extends AbstractSerializingTestCase<JobUpdate> {
update.setDetectorUpdates(detectorUpdates);
}
if (randomBoolean()) {
update.setModelDebugConfig(new ModelDebugConfig(randomDouble(), randomAsciiOfLength(10)));
update.setModelDebugConfig(new ModelDebugConfig(randomBoolean(), randomAsciiOfLength(10)));
}
if (randomBoolean()) {
update.setAnalysisLimits(new AnalysisLimits(randomNonNegativeLong(), randomNonNegativeLong()));
@ -99,7 +99,7 @@ public class JobUpdateTests extends AbstractSerializingTestCase<JobUpdate> {
new RuleCondition(RuleConditionType.NUMERICAL_ACTUAL, null, null, new Condition(Operator.GT, "5"), null))));
detectorUpdates.add(new JobUpdate.DetectorUpdate(1, "description-2", detectionRules2));
ModelDebugConfig modelDebugConfig = new ModelDebugConfig(randomDouble(), randomAsciiOfLength(10));
ModelDebugConfig modelDebugConfig = new ModelDebugConfig(randomBoolean(), randomAsciiOfLength(10));
AnalysisLimits analysisLimits = new AnalysisLimits(randomNonNegativeLong(), randomNonNegativeLong());
List<String> categorizationFilters = Arrays.asList(generateRandomStringArray(10, 10, false));
Map<String, Object> customSettings = Collections.singletonMap(randomAsciiOfLength(10), randomAsciiOfLength(10));
@ -153,7 +153,7 @@ public class JobUpdateTests extends AbstractSerializingTestCase<JobUpdate> {
public void testIsAutodetectProcessUpdate() {
JobUpdate update = new JobUpdate.Builder().build();
assertFalse(update.isAutodetectProcessUpdate());
update = new JobUpdate.Builder().setModelDebugConfig(new ModelDebugConfig(1.0, "ff")).build();
update = new JobUpdate.Builder().setModelDebugConfig(new ModelDebugConfig(true, "ff")).build();
assertTrue(update.isAutodetectProcessUpdate());
update = new JobUpdate.Builder().setDetectorUpdates(Arrays.asList(mock(JobUpdate.DetectorUpdate.class))).build();
assertTrue(update.isAutodetectProcessUpdate());

View File

@ -7,30 +7,21 @@ package org.elasticsearch.xpack.ml.job.config;
import org.elasticsearch.common.io.stream.Writeable.Reader;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.ml.job.messages.Messages;
import org.elasticsearch.xpack.ml.support.AbstractSerializingTestCase;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
public class ModelDebugConfigTests extends AbstractSerializingTestCase<ModelDebugConfig> {
public void testVerify_GivenBoundPercentileLessThanZero() {
IllegalArgumentException e = ESTestCase.expectThrows(IllegalArgumentException.class, () -> new ModelDebugConfig(-1.0, ""));
assertEquals(Messages.getMessage(Messages.JOB_CONFIG_MODEL_DEBUG_CONFIG_INVALID_BOUNDS_PERCENTILE, ""), e.getMessage());
}
public void testVerify_GivenBoundPercentileGreaterThan100() {
IllegalArgumentException e = ESTestCase.expectThrows(IllegalArgumentException.class, () -> new ModelDebugConfig(100.1, ""));
assertEquals(Messages.getMessage(Messages.JOB_CONFIG_MODEL_DEBUG_CONFIG_INVALID_BOUNDS_PERCENTILE, ""), e.getMessage());
}
public void testVerify_GivenValid() {
new ModelDebugConfig(93.0, "");
new ModelDebugConfig(93.0, "foo,bar");
public void testConstructorDefaults() {
assertThat(new ModelDebugConfig().isEnabled(), is(true));
assertThat(new ModelDebugConfig().getTerms(), is(nullValue()));
}
@Override
protected ModelDebugConfig createTestInstance() {
return new ModelDebugConfig(randomDouble(), randomAsciiOfLengthBetween(1, 30));
return new ModelDebugConfig(randomBoolean(), randomAsciiOfLengthBetween(1, 30));
}
@Override

View File

@ -56,7 +56,7 @@ public class AutodetectCommunicatorTests extends ESTestCase {
public void tesWriteUpdateModelDebugMessage() throws IOException {
AutodetectProcess process = mockAutodetectProcessWithOutputStream();
try (AutodetectCommunicator communicator = createAutodetectCommunicator(process, mock(AutoDetectResultProcessor.class))) {
ModelDebugConfig config = new ModelDebugConfig(10.0, "apple,peach");
ModelDebugConfig config = new ModelDebugConfig();
communicator.writeUpdateModelDebugMessage(config);
Mockito.verify(process).writeUpdateModelDebugMessage(config);
}

View File

@ -145,7 +145,7 @@ public class NativeAutodetectProcessTests extends ESTestCase {
new AutodetectResultsParser(Settings.EMPTY))) {
process.start(executorService, mock(StateProcessor.class), mock(InputStream.class));
process.writeUpdateModelDebugMessage(new ModelDebugConfig(1.0, "term,s"));
process.writeUpdateModelDebugMessage(new ModelDebugConfig());
process.flushStream();
String message = new String(bos.toByteArray(), StandardCharsets.UTF_8);

View File

@ -151,12 +151,12 @@ public class ControlMsgToProcessWriterTests extends ESTestCase {
public void testWriteUpdateModelDebugMessage() throws IOException {
ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 2);
writer.writeUpdateModelDebugMessage(new ModelDebugConfig(10.0, "foo,bar"));
writer.writeUpdateModelDebugMessage(new ModelDebugConfig(true, "foo,bar"));
InOrder inOrder = inOrder(lengthEncodedWriter);
inOrder.verify(lengthEncodedWriter).writeNumFields(4);
inOrder.verify(lengthEncodedWriter, times(3)).writeField("");
inOrder.verify(lengthEncodedWriter).writeField("u[modelDebugConfig]\nboundspercentile = 10.0\nterms = foo,bar\n");
inOrder.verify(lengthEncodedWriter).writeField("u[modelDebugConfig]\nboundspercentile = 95.0\nterms = foo,bar\n");
verifyNoMoreInteractions(lengthEncodedWriter);
}

View File

@ -5,17 +5,17 @@
*/
package org.elasticsearch.xpack.ml.job.process.autodetect.writer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.ml.job.config.ModelDebugConfig;
import org.junit.After;
import org.junit.Before;
import org.mockito.Mockito;
import java.io.IOException;
import java.io.OutputStreamWriter;
import org.elasticsearch.test.ESTestCase;
import org.junit.After;
import org.junit.Before;
import org.mockito.Mockito;
import org.elasticsearch.xpack.ml.job.config.ModelDebugConfig;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
public class ModelDebugConfigWriterTests extends ESTestCase {
private OutputStreamWriter writer;
@ -29,12 +29,31 @@ public class ModelDebugConfigWriterTests extends ESTestCase {
public void verifyNoMoreWriterInteractions() {
verifyNoMoreInteractions(writer);
}
public void testWrite_GivenFullConfig() throws IOException {
ModelDebugConfig modelDebugConfig = new ModelDebugConfig(65.0, "foo,bar");
public void testWrite_GivenEnabledConfigWithoutTerms() throws IOException {
ModelDebugConfig modelDebugConfig = new ModelDebugConfig();
ModelDebugConfigWriter writer = new ModelDebugConfigWriter(modelDebugConfig, this.writer);
writer.write();
verify(this.writer).write("boundspercentile = 65.0\nterms = foo,bar\n");
verify(this.writer).write("boundspercentile = 95.0\nterms = \n");
}
public void testWrite_GivenEnabledConfigWithTerms() throws IOException {
ModelDebugConfig modelDebugConfig = new ModelDebugConfig(true, "foo,bar");
ModelDebugConfigWriter writer = new ModelDebugConfigWriter(modelDebugConfig, this.writer);
writer.write();
verify(this.writer).write("boundspercentile = 95.0\nterms = foo,bar\n");
}
public void testWrite_GivenDisabledConfigWithTerms() throws IOException {
ModelDebugConfig modelDebugConfig = new ModelDebugConfig(false, "foo,bar");
ModelDebugConfigWriter writer = new ModelDebugConfigWriter(modelDebugConfig, this.writer);
writer.write();
verify(this.writer).write("boundspercentile = -1.0\nterms = foo,bar\n");
}
}

View File

@ -198,7 +198,8 @@
"field_delimiter":","
},
"model_debug_config": {
"bounds_percentile": 95.0
"enabled": true,
"terms": "foo,bar"
},
"analysis_limits": {
"model_memory_limit": 10
@ -229,7 +230,8 @@
"condition": {"operator": "gt", "value": "10" } } ] } },
{"index": 1, "description": "updated description"}],
"model_debug_config": {
"bounds_percentile": 99.0
"enabled": false,
"terms": "foobar"
},
"analysis_limits": {
"model_memory_limit": 20
@ -245,7 +247,8 @@
}
- match: { job_id: "to-update" }
- match: { description: "Post update description" }
- match: { model_debug_config.bounds_percentile: 99.0 }
- match: { model_debug_config.enabled: false }
- match: { model_debug_config.terms: "foobar" }
- match: { analysis_limits.model_memory_limit: 20 }
- match: { analysis_config.categorization_filters: ["cat3.*"] }
- match: { analysis_config.detectors.0.detector_rules.0.target_field_name: "airline" }