Skip to content

Commit

Permalink
[ML] Single Metric Viewer: Enable cross-filtering for 'by', 'over' an…
Browse files Browse the repository at this point in the history
…d 'partition' field values (elastic#193255)

## Summary

Enables cross-filtering for 'by', 'over' and 'partition' field values in
the Single Metric Viewer.

Fixes [elastic#171932](elastic#171932)

Before:


https://github.com/user-attachments/assets/9a279375-7d0b-4422-b9eb-644ae3c0d291

After:


https://github.com/user-attachments/assets/d86d0688-dc69-43f0-aa24-130ff38935e6
  • Loading branch information
rbrtj committed Sep 27, 2024
1 parent e41b38b commit 07290bf
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 25 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/ml/common/types/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type PartitionFieldConfig =
by: 'anomaly_score' | 'name';
order: 'asc' | 'desc';
};
value: string;
}
| undefined;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export class EntityControl extends Component<EntityControlProps, EntityControlSt
selectedOptions={selectedOptions}
onChange={this.onChange}
onSearchChange={this.onSearchChange}
isClearable={false}
isClearable={true}
renderOption={this.renderOption}
data-test-subj={`mlSingleMetricViewerEntitySelection ${entity.fieldName}`}
prepend={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type {
} from '../../../../../common/types/anomaly_detection_jobs';
import { useMlKibana } from '../../../contexts/kibana';
import { APP_STATE_ACTION } from '../../timeseriesexplorer_constants';
import type { ComboBoxOption, EntityControlProps } from '../entity_control/entity_control';
import type { ComboBoxOption, Entity, EntityControlProps } from '../entity_control/entity_control';
import { EMPTY_FIELD_VALUE_LABEL } from '../entity_control/entity_control';
import { getControlsForDetector } from '../../get_controls_for_detector';
import {
Expand Down Expand Up @@ -57,15 +57,16 @@ export type UiPartitionFieldConfig = Exclude<PartitionFieldConfig, undefined>;
* Provides default fields configuration.
*/
const getDefaultFieldConfig = (
fieldTypes: MlEntityFieldType[],
entities: Entity[],
isAnomalousOnly: boolean,
applyTimeRange: boolean
): UiPartitionFieldsConfig => {
return fieldTypes.reduce((acc, f) => {
acc[f] = {
return entities.reduce((acc, f) => {
acc[f.fieldType] = {
applyTimeRange,
anomalousOnly: isAnomalousOnly,
sort: { by: 'anomaly_score', order: 'desc' },
...(f.fieldValue && { value: f.fieldValue }),
};
return acc;
}, {} as UiPartitionFieldsConfig);
Expand Down Expand Up @@ -141,18 +142,28 @@ export const SeriesControls: FC<PropsWithChildren<SeriesControlsProps>> = ({

// Merge the default config with the one from the local storage
const resultFieldsConfig = useMemo(() => {
return {
...getDefaultFieldConfig(
entityControls.map((v) => v.fieldType),
!storageFieldsConfig
? true
: Object.values(storageFieldsConfig).some((v) => !!v?.anomalousOnly),
!storageFieldsConfig
? true
: Object.values(storageFieldsConfig).some((v) => !!v?.applyTimeRange)
),
...(!storageFieldsConfig ? {} : storageFieldsConfig),
};
const resultFieldConfig = getDefaultFieldConfig(
entityControls,
!storageFieldsConfig
? true
: Object.values(storageFieldsConfig).some((v) => !!v?.anomalousOnly),
!storageFieldsConfig
? true
: Object.values(storageFieldsConfig).some((v) => !!v?.applyTimeRange)
);

// Early return to prevent unnecessary looping through the default config
if (!storageFieldsConfig) return resultFieldConfig;

// Override only the fields properties stored in the local storage
for (const key of Object.keys(resultFieldConfig) as MlEntityFieldType[]) {
resultFieldConfig[key] = {
...resultFieldConfig[key],
...storageFieldsConfig[key],
} as UiPartitionFieldConfig;
}

return resultFieldConfig;
}, [entityControls, storageFieldsConfig]);

/**
Expand Down Expand Up @@ -286,9 +297,20 @@ export const SeriesControls: FC<PropsWithChildren<SeriesControlsProps>> = ({
}
}

// Remove the value from the field config to avoid storing it in the local storage
const { value, ...updatedFieldConfigWithoutValue } = updatedFieldConfig;

// Remove the value from the result config to avoid storing it in the local storage
const updatedResultConfigWithoutValues = Object.fromEntries(
Object.entries(updatedResultConfig).map(([key, fieldValue]) => {
const { value: _, ...rest } = fieldValue;
return [key, rest];
})
);

setStorageFieldsConfig({
...updatedResultConfig,
[fieldType]: updatedFieldConfig,
...updatedResultConfigWithoutValues,
[fieldType]: updatedFieldConfigWithoutValue,
});
},
[resultFieldsConfig, setStorageFieldsConfig]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,24 @@ function getFieldAgg(
fieldType: MlPartitionFieldsType,
isModelPlotSearch: boolean,
query?: string,
fieldConfig?: FieldConfig
fieldsConfig?: FieldsConfig
) {
const AGG_SIZE = 100;

const fieldConfig = fieldsConfig?.[fieldType];
const fieldNameKey = `${fieldType}_name`;
const fieldValueKey = `${fieldType}_value`;

const sortByField =
fieldConfig?.sort?.by === 'name' || isModelPlotSearch ? '_key' : 'maxRecordScore';

const splitFieldFilterValues = Object.entries(fieldsConfig ?? {})
.filter(([key, field]) => key !== fieldType && field.value)
.map(([key, field]) => ({
fieldValueKey: `${key}_value`,
fieldValue: field.value,
}));

return {
[fieldNameKey]: {
terms: {
Expand Down Expand Up @@ -77,6 +85,11 @@ function getFieldAgg(
},
]
: []),
...splitFieldFilterValues.map((filterValue) => ({
term: {
[filterValue.fieldValueKey]: filterValue.fieldValue,
},
})),
],
},
},
Expand Down Expand Up @@ -233,7 +246,7 @@ export const getPartitionFieldsValuesFactory = (mlClient: MlClient) =>
...ML_PARTITION_FIELDS.reduce((acc, key) => {
return Object.assign(
acc,
getFieldAgg(key, isModelPlotSearch, searchTerm[key], fieldsConfig[key])
getFieldAgg(key, isModelPlotSearch, searchTerm[key], fieldsConfig)
);
}, {}),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const fieldConfig = schema.maybe(
by: schema.string(),
order: schema.maybe(schema.string()),
}),
value: schema.maybe(schema.string()),
})
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,38 @@ export default ({ getService }: FtrProviderContext) => {
} as Job;
}

function getDatafeedConfig(jobId: string) {
function getJobConfigWithByField(jobId: string) {
return {
job_id: jobId,
description:
'count by geoip.city_name partition=day_of_week on ecommerce dataset with 1h bucket span',
analysis_config: {
bucket_span: '1h',
influencers: ['geoip.city_name', 'day_of_week'],
detectors: [
{
function: 'count',
by_field_name: 'geoip.city_name',
partition_field_name: 'day_of_week',
},
],
},
data_description: {
time_field: 'order_date',
time_format: 'epoch_ms',
},
analysis_limits: {
model_memory_limit: '11mb',
categorization_examples_limit: 4,
},
model_plot_config: { enabled: false },
} as Job;
}

function getDatafeedConfig(jobId: string, indices: string[]) {
return {
datafeed_id: `datafeed-${jobId}`,
indices: ['ft_farequote'],
indices,
job_id: jobId,
query: { bool: { must: [{ match_all: {} }] } },
} as Datafeed;
Expand All @@ -50,12 +78,17 @@ export default ({ getService }: FtrProviderContext) => {
async function createMockJobs() {
await ml.api.createAndRunAnomalyDetectionLookbackJob(
getJobConfig('fq_multi_1_ae'),
getDatafeedConfig('fq_multi_1_ae')
getDatafeedConfig('fq_multi_1_ae', ['ft_farequote'])
);

await ml.api.createAndRunAnomalyDetectionLookbackJob(
getJobConfig('fq_multi_2_ae', false),
getDatafeedConfig('fq_multi_2_ae')
getDatafeedConfig('fq_multi_2_ae', ['ft_farequote'])
);

await ml.api.createAndRunAnomalyDetectionLookbackJob(
getJobConfigWithByField('ecommerce_advanced_1'),
getDatafeedConfig('ecommerce_advanced_1', ['ft_ecommerce'])
);
}

Expand All @@ -72,6 +105,7 @@ export default ({ getService }: FtrProviderContext) => {
describe('PartitionFieldsValues', function () {
before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote');
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce');
await ml.testResources.setKibanaTimeZoneToUTC();
await createMockJobs();
});
Expand Down Expand Up @@ -229,5 +263,63 @@ export default ({ getService }: FtrProviderContext) => {
expect(body.partition_field.values.length).to.eql(19);
});
});

describe('cross filtering', () => {
it('should return filtered values for by_field when partition_field is set', async () => {
const requestBody = {
jobId: 'ecommerce_advanced_1',
criteriaFields: [{ fieldName: 'detector_index', fieldValue: 0 }],
earliestMs: 1687968000000, // June 28, 2023 16:00:00 GMT
latestMs: 1688140800000, // June 30, 2023 16:00:00 GMT
searchTerm: {},
fieldsConfig: {
by_field: {
anomalousOnly: true,
applyTimeRange: true,
sort: { order: 'desc', by: 'anomaly_score' },
},
partition_field: {
anomalousOnly: true,
applyTimeRange: true,
sort: { order: 'desc', by: 'anomaly_score' },
value: 'Saturday',
},
},
};
const body = await runRequest(requestBody);

expect(body.by_field.values.length).to.eql(1);
expect(body.by_field.values[0].value).to.eql('Abu Dhabi');
});

it('should return filtered values for partition_field when by_field is set', async () => {
const requestBody = {
jobId: 'ecommerce_advanced_1',
criteriaFields: [{ fieldName: 'detector_index', fieldValue: 0 }],
earliestMs: 1687968000000, // June 28, 2023 16:00:00 GMT
latestMs: 1688140800000, // June 30, 2023 16:00:00 GMT
searchTerm: {},
fieldsConfig: {
by_field: {
anomalousOnly: true,
applyTimeRange: true,
sort: { order: 'desc', by: 'anomaly_score' },
value: 'Abu Dhabi',
},
partition_field: {
anomalousOnly: true,
applyTimeRange: true,
sort: { order: 'desc', by: 'anomaly_score' },
},
},
};

const body = await runRequest(requestBody);

expect(body.partition_field.values.length).to.eql(2);
expect(body.partition_field.values[0].value).to.eql('Saturday');
expect(body.partition_field.values[1].value).to.eql('Monday');
});
});
});
};

0 comments on commit 07290bf

Please sign in to comment.