Skip to content

Commit

Permalink
[ML] Rare anomaly detection job wizard (#100390)
Browse files Browse the repository at this point in the history
* [ML] Rare anomaly detection job wizard

* fixing fields selection

* small improvements

* adding event rate chart to summary step

* [ML] Changes UI text for rare wizard.

* improving detector summary

* fixing translations

* removing comments

* fixing field selection

* fixing advanced wizard

* updating detector text

* fixing bucketspan estimator

* bug fixes

Co-authored-by: Kibana Machine <[email protected]>
Co-authored-by: István Zoltán Szabó <[email protected]>
  • Loading branch information
3 people authored Jun 29, 2021
1 parent 824463a commit 2e00e9c
Show file tree
Hide file tree
Showing 45 changed files with 1,276 additions and 119 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/ml/common/constants/new_job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ export enum JOB_TYPE {
POPULATION = 'population',
ADVANCED = 'advanced',
CATEGORIZATION = 'categorization',
RARE = 'rare',
}

export enum CREATED_BY_LABEL {
SINGLE_METRIC = 'single-metric-wizard',
MULTI_METRIC = 'multi-metric-wizard',
POPULATION = 'population-wizard',
CATEGORIZATION = 'categorization-wizard',
RARE = 'rare-wizard',
APM_TRANSACTION = 'ml-module-apm-transaction',
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class CategorizationJobCreator extends JobCreator {
eventRate: Field | null
) {
if (count === null || rare === null || eventRate === null) {
return;
throw Error('event_rate field or count or rare aggregations missing');
}

this._createCountDetector = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ export { MultiMetricJobCreator } from './multi_metric_job_creator';
export { PopulationJobCreator } from './population_job_creator';
export { AdvancedJobCreator } from './advanced_job_creator';
export { CategorizationJobCreator } from './categorization_job_creator';
export { RareJobCreator } from './rare_job_creator';
export {
JobCreatorType,
isSingleMetricJobCreator,
isMultiMetricJobCreator,
isPopulationJobCreator,
isAdvancedJobCreator,
isCategorizationJobCreator,
isRareJobCreator,
} from './type_guards';
export { jobCreatorFactory } from './job_creator_factory';
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,9 @@ export class JobCreator {
// change the detector to be a non-zer or non-null count or sum.
// note, the aggregations will always be a standard count or sum and not a non-null or non-zero version
this._detectors.forEach((d, i) => {
if (this._aggs[i] === undefined) {
return;
}
switch (this._aggs[i].id) {
case ML_JOB_AGGREGATION.COUNT:
d.function = this._sparseData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { PopulationJobCreator } from './population_job_creator';
import { AdvancedJobCreator } from './advanced_job_creator';
import { IndexPattern } from '../../../../../../../../../src/plugins/data/public';
import { CategorizationJobCreator } from './categorization_job_creator';
import { RareJobCreator } from './rare_job_creator';

import { JOB_TYPE } from '../../../../../../common/constants/new_job';

Expand All @@ -37,6 +38,9 @@ export const jobCreatorFactory = (jobType: JOB_TYPE) => (
case JOB_TYPE.CATEGORIZATION:
jc = CategorizationJobCreator;
break;
case JOB_TYPE.RARE:
jc = RareJobCreator;
break;
default:
jc = SingleMetricJobCreator;
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { IndexPattern } from '../../../../../../../../../src/plugins/data/public
export class PopulationJobCreator extends JobCreator {
// a population job has one overall over (split) field, which is the same for all detectors
// each detector has an optional by field
private _splitField: SplitField = null;
private _populatonField: SplitField = null;
private _byFields: SplitField[] = [];
protected _type: JOB_TYPE = JOB_TYPE.POPULATION;

Expand Down Expand Up @@ -65,27 +65,27 @@ export class PopulationJobCreator extends JobCreator {
}

// add an over field to all detectors
public setSplitField(field: SplitField) {
this._splitField = field;
public setPopulationField(field: SplitField) {
this._populatonField = field;

if (this._splitField === null) {
this.removeSplitField();
if (this._populatonField === null) {
this.removePopulationField();
} else {
for (let i = 0; i < this._detectors.length; i++) {
this._detectors[i].over_field_name = this._splitField.id;
this._detectors[i].over_field_name = this._populatonField.id;
}
}
}

// remove over field from all detectors
public removeSplitField() {
public removePopulationField() {
this._detectors.forEach((d) => {
delete d.over_field_name;
});
}

public get splitField(): SplitField {
return this._splitField;
public get populationField(): SplitField {
return this._populatonField;
}

public addDetector(agg: Aggregation, field: Field) {
Expand All @@ -112,8 +112,8 @@ export class PopulationJobCreator extends JobCreator {
private _createDetector(agg: Aggregation, field: Field) {
const dtr: Detector = createBasicDetector(agg, field);

if (this._splitField !== null) {
dtr.over_field_name = this._splitField.id;
if (this._populatonField !== null) {
dtr.over_field_name = this._populatonField.id;
}
return dtr;
}
Expand Down Expand Up @@ -143,7 +143,7 @@ export class PopulationJobCreator extends JobCreator {

if (detectors.length) {
if (detectors[0].overField !== null) {
this.setSplitField(detectors[0].overField);
this.setPopulationField(detectors[0].overField);
}
}
detectors.forEach((d, i) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { SavedSearchSavedObject } from '../../../../../../common/types/kibana';
import { JobCreator } from './job_creator';
import { Field, SplitField, Aggregation } from '../../../../../../common/types/fields';
import { Job, Datafeed, Detector } from '../../../../../../common/types/anomaly_detection_jobs';
import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job';
import { getRichDetectors } from './util/general';
import { IndexPattern } from '../../../../../../../../../src/plugins/data/public';
import { isSparseDataJob } from './util/general';
import { ML_JOB_AGGREGATION } from '../../../../../../common/constants/aggregation_types';

export class RareJobCreator extends JobCreator {
private _rareField: Field | null = null;
private _populationField: SplitField = null;
private _splitField: SplitField = null;

protected _type: JOB_TYPE = JOB_TYPE.RARE;
private _rareInPopulation: boolean = false;
private _frequentlyRare: boolean = false;
private _rareAgg: Aggregation;
private _freqRareAgg: Aggregation;

constructor(
indexPattern: IndexPattern,
savedSearch: SavedSearchSavedObject | null,
query: object
) {
super(indexPattern, savedSearch, query);
this.createdBy = CREATED_BY_LABEL.RARE;
this._wizardInitialized$.next(true);
this._rareAgg = {} as Aggregation;
this._freqRareAgg = {} as Aggregation;
}

public setDefaultDetectorProperties(rare: Aggregation | null, freqRare: Aggregation | null) {
if (rare === null || freqRare === null) {
throw Error('rare or freq_rare aggregations missing');
}
this._rareAgg = rare;
this._freqRareAgg = freqRare;
}

public setRareField(field: Field | null) {
this._rareField = field;

if (field === null) {
this.removePopulationField();
this.removeSplitField();
this._removeDetector(0);
this._detectors.length = 0;
this._fields.length = 0;
return;
}

const agg = this._frequentlyRare ? this._freqRareAgg : this._rareAgg;

const dtr: Detector = {
function: agg.id,
};
if (this._detectors.length === 0) {
this._addDetector(dtr, agg, field);
} else {
this._editDetector(dtr, agg, field, 0);
}

this._detectors[0].by_field_name = field.id;
}

public get rareField() {
return this._rareField;
}

public get rareInPopulation() {
return this._rareInPopulation;
}

public set rareInPopulation(bool: boolean) {
this._rareInPopulation = bool;
if (bool === false) {
this.removePopulationField();
}
}

public get frequentlyRare() {
return this._frequentlyRare;
}

public set frequentlyRare(bool: boolean) {
this._frequentlyRare = bool;
if (this._detectors.length) {
const agg = bool ? this._freqRareAgg : this._rareAgg;
this._detectors[0].function = agg.id;
this._aggs[0] = agg;
}
}

// set the population field, applying it to each detector
public setPopulationField(field: SplitField) {
this._populationField = field;

if (this._populationField === null) {
this.removePopulationField();
} else {
for (let i = 0; i < this._detectors.length; i++) {
this._detectors[i].over_field_name = this._populationField.id;
}
}
}

public removePopulationField() {
this._populationField = null;
this._detectors.forEach((d) => {
delete d.over_field_name;
});
}

public get populationField(): SplitField {
return this._populationField;
}

// set the split field, applying it to each detector
public setSplitField(field: SplitField) {
this._splitField = field;

if (this._splitField === null) {
this.removeSplitField();
} else {
for (let i = 0; i < this._detectors.length; i++) {
this._detectors[i].partition_field_name = this._splitField.id;
}
}
}

public removeSplitField() {
this._detectors.forEach((d) => {
delete d.partition_field_name;
});
}

public get splitField(): SplitField {
return this._splitField;
}

public cloneFromExistingJob(job: Job, datafeed: Datafeed) {
this._overrideConfigs(job, datafeed);
this.createdBy = CREATED_BY_LABEL.RARE;
this._sparseData = isSparseDataJob(job, datafeed);
const detectors = getRichDetectors(job, datafeed, this.additionalFields, false);

this.removeSplitField();
this.removePopulationField();
this.removeAllDetectors();

if (detectors.length) {
this.setRareField(detectors[0].byField);
this.frequentlyRare = detectors[0].agg?.id === ML_JOB_AGGREGATION.FREQ_RARE;

if (detectors[0].overField !== null) {
this.setPopulationField(detectors[0].overField);
this.rareInPopulation = true;
}
if (detectors[0].partitionField !== null) {
this.setSplitField(detectors[0].partitionField);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ import { MultiMetricJobCreator } from './multi_metric_job_creator';
import { PopulationJobCreator } from './population_job_creator';
import { AdvancedJobCreator } from './advanced_job_creator';
import { CategorizationJobCreator } from './categorization_job_creator';
import { RareJobCreator } from './rare_job_creator';
import { JOB_TYPE } from '../../../../../../common/constants/new_job';

export type JobCreatorType =
| SingleMetricJobCreator
| MultiMetricJobCreator
| PopulationJobCreator
| AdvancedJobCreator
| CategorizationJobCreator;
| CategorizationJobCreator
| RareJobCreator;

export function isSingleMetricJobCreator(
jobCreator: JobCreatorType
Expand Down Expand Up @@ -46,3 +48,7 @@ export function isCategorizationJobCreator(
): jobCreator is CategorizationJobCreator {
return jobCreator.type === JOB_TYPE.CATEGORIZATION;
}

export function isRareJobCreator(jobCreator: JobCreatorType): jobCreator is RareJobCreator {
return jobCreator.type === JOB_TYPE.RARE;
}
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,10 @@ export function getJobCreatorTitle(jobCreator: JobCreatorType) {
return i18n.translate('xpack.ml.newJob.wizard.jobCreatorTitle.categorization', {
defaultMessage: 'Categorization',
});
case JOB_TYPE.RARE:
return i18n.translate('xpack.ml.newJob.wizard.jobCreatorTitle.rare', {
defaultMessage: 'Rare',
});
default:
return '';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { Fragment, FC, useContext, useState } from 'react';
import React, { Fragment, FC, useContext, useState, useEffect } from 'react';

import { JobCreatorContext } from '../../../job_creator_context';
import { AdvancedJobCreator } from '../../../../../common/job_creator';
Expand Down Expand Up @@ -33,12 +33,16 @@ const emptyRichDetector: RichDetector = {
};

export const AdvancedDetectors: FC<Props> = ({ setIsValid }) => {
const { jobCreator: jc, jobCreatorUpdate } = useContext(JobCreatorContext);
const { jobCreator: jc, jobCreatorUpdate, jobCreatorUpdated } = useContext(JobCreatorContext);
const jobCreator = jc as AdvancedJobCreator;

const { fields, aggs } = newJobCapsService;
const [modalPayload, setModalPayload] = useState<ModalPayload | null>(null);

useEffect(() => {
setIsValid(jobCreator.detectors.length > 0);
}, [jobCreatorUpdated]);

function closeModal() {
setModalPayload(null);
}
Expand Down
Loading

0 comments on commit 2e00e9c

Please sign in to comment.