Skip to content

Commit

Permalink
[ML] Maps integration: adds influencers to tooltip in anomalies layer (
Browse files Browse the repository at this point in the history
…#126007)

* adds influencers to tooltip

* do not add to tooltip if already partition field

* adds unit test for influencer string function

* add unit test for getResultsForJobId
  • Loading branch information
alvarezmelissa87 committed Mar 1, 2022
1 parent 7f364ea commit 058ad18
Show file tree
Hide file tree
Showing 4 changed files with 301 additions and 6 deletions.
6 changes: 6 additions & 0 deletions x-pack/plugins/ml/public/maps/anomaly_source_field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ export const ANOMALY_SOURCE_FIELDS: Record<string, Record<string, string>> = {
}),
type: 'string',
},
influencers: {
label: i18n.translate('xpack.ml.maps.anomalyLayerInfluencersLabel', {
defaultMessage: 'Influencers',
}),
type: 'string',
},
};

export class AnomalySourceTooltipProperty implements ITooltipProperty {
Expand Down
90 changes: 90 additions & 0 deletions x-pack/plugins/ml/public/maps/maps_util.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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 { getInfluencersHtmlString, getResultsForJobId } from './util';
import {
mlResultsServiceMock,
typicalExpected,
actualExpected,
typicalToActualExpected,
} from './results.test.mock';

describe('Maps util', () => {
describe('getInfluencersHtmlString', () => {
const splitField = 'split_field_influencer';
const valueFour = 'value_four';
const influencerFour = 'influencer_four';
const influencers = [
{
influencer_field_name: 'influencer_one',
influencer_field_values: ['value_one', 'value_two', 'value_three', valueFour],
},
{
influencer_field_name: 'influencer_two',
influencer_field_values: ['value_one', 'value_two', 'value_three', valueFour],
},
{
influencer_field_name: splitField,
influencer_field_values: ['value_one', 'value_two'],
},
{
influencer_field_name: 'influencer_three',
influencer_field_values: ['value_one', 'value_two', 'value_three', valueFour],
},
{
influencer_field_name: influencerFour,
influencer_field_values: ['value_one', 'value_two', 'value_three', valueFour],
},
];

test('should create the html string when given an array of influencers', () => {
const expected =
'<ul><li>influencer_one: value_one, value_two, value_three</li><li>influencer_two: value_one, value_two, value_three</li><li>influencer_three: value_one, value_two, value_three</li></ul>';
const actual = getInfluencersHtmlString(influencers, splitField);
expect(actual).toBe(expected);
// Should not include split field
expect(actual.includes(splitField)).toBe(false);
// should limit to the first three influencer values
expect(actual.includes(valueFour)).toBe(false);
// should limit to the first three influencer names
expect(actual.includes(influencerFour)).toBe(false);
});
});

describe('getResultsForJobId', () => {
const jobId = 'jobId';
const searchFilters = {
timeFilters: { from: 'now-2y', to: 'now' },
query: { language: 'kuery', query: '' },
};

test('should get map features from job anomalies results for typical layer', async () => {
const actual = await getResultsForJobId(
mlResultsServiceMock,
jobId,
'typical',
searchFilters
);
expect(actual).toEqual(typicalExpected);
});

test('should get map features from job anomalies results for actual layer', async () => {
const actual = await getResultsForJobId(mlResultsServiceMock, jobId, 'actual', searchFilters);
expect(actual).toEqual(actualExpected);
});

test('should get map features from job anomalies results for "typical to actual" layer', async () => {
const actual = await getResultsForJobId(
mlResultsServiceMock,
jobId,
'typical to actual',
searchFilters
);
expect(actual).toEqual(typicalToActualExpected);
});
});
});
160 changes: 160 additions & 0 deletions x-pack/plugins/ml/public/maps/results.test.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* 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.
*/

const results = {
took: 9,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 19,
relation: 'eq',
},
max_score: 4.4813457,
hits: [
{
_index: '.ml-anomalies-shared',
_id: 'test-tooltip-one_record_1645974000000_900_0_0_0',
_score: 4.4813457,
_source: {
job_id: 'test-tooltip-one',
result_type: 'record',
probability: 0.00042878057629659614,
multi_bucket_impact: -5,
record_score: 77.74620142126848,
initial_record_score: 77.74620142126848,
bucket_span: 900,
detector_index: 0,
is_interim: false,
timestamp: 1645974000000,
function: 'lat_long',
function_description: 'lat_long',
typical: [39.9864616394043, -97.862548828125],
actual: [29.261693651787937, -121.93940273718908],
field_name: 'geo.coordinates',
influencers: [
{
influencer_field_name: 'geo.dest',
influencer_field_values: ['CN', 'DO', 'RU', 'US'],
},
{
influencer_field_name: 'clientip',
influencer_field_values: [
'108.131.25.207',
'192.41.143.247',
'194.12.201.131',
'41.91.106.242',
],
},
{
influencer_field_name: 'agent.keyword',
influencer_field_values: [
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)',
'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24',
'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1',
],
},
],
geo_results: {
typical_point: '39.986461639404,-97.862548828125',
actual_point: '29.261693651788,-121.939402737189',
},
'geo.dest': ['CN', 'DO', 'RU', 'US'],
'agent.keyword': [
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)',
'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24',
'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1',
],
clientip: ['108.131.25.207', '192.41.143.247', '194.12.201.131', '41.91.106.242'],
},
},
],
},
};

export const typicalExpected = {
features: [
{
geometry: { coordinates: [-97.862548828125, 39.986461639404], type: 'Point' },
properties: {
actual: [-121.939402737189, 29.261693651788],
actualDisplay: [-121.94, 29.26],
fieldName: 'geo.coordinates',
functionDescription: 'lat_long',
influencers:
'<ul><li>geo.dest: CN, DO, RU</li><li>clientip: 108.131.25.207, 192.41.143.247, 194.12.201.131</li><li>agent.keyword: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322), Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24, Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1</li></ul>',
record_score: 77,
timestamp: 'February 27th 2022, 10:00:00',
typical: [-97.862548828125, 39.986461639404],
typicalDisplay: [-97.86, 39.99],
},
type: 'Feature',
},
],
type: 'FeatureCollection',
};

export const actualExpected = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [-121.939402737189, 29.261693651788],
},
properties: {
actual: [-121.939402737189, 29.261693651788],
actualDisplay: [-121.94, 29.26],
typical: [-97.862548828125, 39.986461639404],
typicalDisplay: [-97.86, 39.99],
fieldName: 'geo.coordinates',
functionDescription: 'lat_long',
timestamp: 'February 27th 2022, 10:00:00',
record_score: 77,
influencers:
'<ul><li>geo.dest: CN, DO, RU</li><li>clientip: 108.131.25.207, 192.41.143.247, 194.12.201.131</li><li>agent.keyword: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322), Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24, Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1</li></ul>',
},
},
],
};
export const typicalToActualExpected = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: [
[-97.862548828125, 39.986461639404],
[-121.939402737189, 29.261693651788],
],
},
properties: {
actual: [-121.939402737189, 29.261693651788],
actualDisplay: [-121.94, 29.26],
typical: [-97.862548828125, 39.986461639404],
typicalDisplay: [-97.86, 39.99],
fieldName: 'geo.coordinates',
functionDescription: 'lat_long',
timestamp: 'February 27th 2022, 10:00:00',
record_score: 77,
influencers:
'<ul><li>geo.dest: CN, DO, RU</li><li>clientip: 108.131.25.207, 192.41.143.247, 194.12.201.131</li><li>agent.keyword: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322), Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24, Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1</li></ul>',
},
},
],
};

export const mlResultsServiceMock = {
anomalySearch: () => results,
};
51 changes: 45 additions & 6 deletions x-pack/plugins/ml/public/maps/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,34 @@ export const ML_ANOMALY_LAYERS = {
} as const;

export type MlAnomalyLayersType = typeof ML_ANOMALY_LAYERS[keyof typeof ML_ANOMALY_LAYERS];
const INFLUENCER_LIMIT = 3;
const INFLUENCER_MAX_VALUES = 3;

export function getInfluencersHtmlString(
influencers: Array<{ influencer_field_name: string; influencer_field_values: string[] }>,
splitFields: string[]
) {
let htmlString = '<ul>';
let influencerCount = 0;
for (let i = 0; i < influencers.length; i++) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { influencer_field_name, influencer_field_values } = influencers[i];
// Skip if there are no values or it's a partition field
if (!influencer_field_values.length || splitFields.includes(influencer_field_name)) continue;

const fieldValuesString = influencer_field_values.slice(0, INFLUENCER_MAX_VALUES).join(', ');

htmlString += `<li>${influencer_field_name}: ${fieldValuesString}</li>`;
influencerCount += 1;

if (influencerCount === INFLUENCER_LIMIT) {
break;
}
}
htmlString += '</ul>';

return htmlString;
}

// Must reverse coordinates here. Map expects [lon, lat] - anomalies are stored as [lat, lon] for lat_lon jobs
function getCoordinates(actualCoordinateStr: string, round: boolean = false): number[] {
Expand Down Expand Up @@ -136,6 +164,15 @@ export async function getResultsForJobId(
coordinates: [typical, actual],
};
}

const splitFields = {
...(_source.partition_field_name
? { [_source.partition_field_name]: _source.partition_field_value }
: {}),
...(_source.by_field_name ? { [_source.by_field_name]: _source.by_field_value } : {}),
...(_source.over_field_name ? { [_source.over_field_name]: _source.over_field_value } : {}),
};

return {
type: 'Feature',
geometry,
Expand All @@ -148,12 +185,14 @@ export async function getResultsForJobId(
functionDescription: _source.function_description,
timestamp: formatHumanReadableDateTimeSeconds(_source.timestamp),
record_score: Math.floor(_source.record_score),
...(_source.partition_field_name
? { [_source.partition_field_name]: _source.partition_field_value }
: {}),
...(_source.by_field_name ? { [_source.by_field_name]: _source.by_field_value } : {}),
...(_source.over_field_name
? { [_source.over_field_name]: _source.over_field_value }
...(Object.keys(splitFields).length > 0 ? splitFields : {}),
...(_source.influencers?.length
? {
influencers: getInfluencersHtmlString(
_source.influencers,
Object.keys(splitFields)
),
}
: {}),
},
};
Expand Down

0 comments on commit 058ad18

Please sign in to comment.