diff --git a/x-pack/plugins/ml/public/components/influencers_list/styles/main.less b/x-pack/plugins/ml/public/components/influencers_list/styles/main.less index 3fe182b89c7c..d1fd0f14ebb9 100644 --- a/x-pack/plugins/ml/public/components/influencers_list/styles/main.less +++ b/x-pack/plugins/ml/public/components/influencers_list/styles/main.less @@ -34,7 +34,7 @@ text-align: right; line-height: 18px; display: inline-block; - + transition: none; } } diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 10b84f6a53fa..fd4b30b41fea 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -74,7 +74,6 @@ module.controller('MlExplorerController', function ( let resizeTimeout = null; const $mlExplorer = $('.ml-explorer'); - const MAX_INFLUENCER_FIELD_NAMES = 10; const MAX_INFLUENCER_FIELD_VALUES = 10; const VIEW_BY_JOB_LABEL = 'job ID'; @@ -166,20 +165,6 @@ module.controller('MlExplorerController', function ( }); } - $scope.loadAnomaliesTable = function (jobIds, influencers, earliestMs, latestMs) { - mlResultsService.getRecordsForInfluencer( - jobIds, influencers, 0, earliestMs, latestMs, 500 - ) - .then((resp) => { - // Need to use $timeout to ensure the update happens after the child scope is updated with the new data. - $scope.$evalAsync(() => { - // Sort in descending time order before storing in scope. - $scope.anomalyRecords = _.chain(resp.records).sortBy(record => record[$scope.timeFieldName]).reverse().value(); - console.log('Explorer anomalies table data set:', $scope.anomalyRecords); - }); - }); - }; - $scope.setSelectedJobs = function (selectedIds) { let previousSelected = 0; if ($scope.selectedJobs !== null) { @@ -367,7 +352,7 @@ module.controller('MlExplorerController', function ( $scope.cellData = cellData; const args = [jobIds, influencers, timerange.earliestMs, timerange.latestMs]; - $scope.loadAnomaliesTable(...args); + loadAnomalies(...args); $scope.loadAnomaliesForCharts(...args); } }; @@ -424,7 +409,7 @@ module.controller('MlExplorerController', function ( $scope.loadAnomaliesForCharts = function (jobIds, influencers, earliestMs, latestMs) { // Load the top anomalies (by record_score) which will be displayed in the charts. - // TODO - combine this with loadAnomaliesTable() if the table is being retained. + // TODO - combine this with loadAnomalies(). mlResultsService.getRecordsForInfluencer( jobIds, influencers, 0, earliestMs, latestMs, 500 ).then((resp) => { @@ -439,6 +424,70 @@ module.controller('MlExplorerController', function ( }); }; + function loadAnomalies(jobIds, influencers, earliestMs, latestMs) { + // Loads the anomalies for the table, plus the scores for + // the Top Influencers List for the influencers in the anomaly records. + + if (influencers.length === 0) { + getTopInfluencers(jobIds, earliestMs, latestMs); + } + + mlResultsService.getRecordsForInfluencer( + jobIds, influencers, 0, earliestMs, latestMs, 500 + ) + .then((resp) => { + if (influencers.length > 0) { + // Filter the Top Influencers list to show just the influencers from + // the records in the selected time range. + const recordInfluencersByName = {}; + resp.records.forEach((record) => { + const influencersByName = record.influencers || []; + influencersByName.forEach((influencer) => { + const fieldName = influencer.influencer_field_name; + const fieldValues = influencer.influencer_field_values; + if (recordInfluencersByName[fieldName] === undefined) { + recordInfluencersByName[fieldName] = []; + } + recordInfluencersByName[fieldName].push(...fieldValues); + }); + }); + + const uniqValuesByName = {}; + Object.keys(recordInfluencersByName).forEach((fieldName) => { + const fieldValues = recordInfluencersByName[fieldName]; + uniqValuesByName[fieldName] = _.uniq(fieldValues); + }); + + const filterInfluencers = []; + Object.keys(uniqValuesByName).forEach((fieldName) => { + // Find record influencers with the same field name as the clicked on cell(s). + const matchingFieldName = influencers.find((influencer) => { + return influencer.fieldName === fieldName; + }); + + if (matchingFieldName !== undefined) { + // Filter for the value(s) of the clicked on cell(s). + filterInfluencers.push(...influencers); + } else { + // For other field names, add values from all records. + uniqValuesByName[fieldName].forEach((fieldValue) => { + filterInfluencers.push({ fieldName, fieldValue }); + }); + } + }); + + getTopInfluencers(jobIds, earliestMs, latestMs, filterInfluencers); + } + + // Use $evalAsync to ensure the update happens after the child scope is updated with the new data. + $scope.$evalAsync(() => { + // Sort in descending time order before storing in scope. + $scope.anomalyRecords = _.chain(resp.records).sortBy(record => record[$scope.timeFieldName]).reverse().value(); + console.log('Explorer anomalies table data set:', $scope.anomalyRecords); + }); + }); + } + function loadViewBySwimlaneOptions() { // Obtain the list of 'View by' fields per job. $scope.swimlaneViewByFieldName = null; @@ -600,14 +649,14 @@ module.controller('MlExplorerController', function ( } - function getTopInfluencers(selectedJobIds, earliestMs, latestMs) { + function getTopInfluencers(selectedJobIds, earliestMs, latestMs, influencers = []) { if ($scope.noInfluencersConfigured !== true) { mlResultsService.getTopInfluencers( selectedJobIds, earliestMs, latestMs, - MAX_INFLUENCER_FIELD_NAMES, - MAX_INFLUENCER_FIELD_VALUES + MAX_INFLUENCER_FIELD_VALUES, + influencers ).then((resp) => { // TODO - sort the influencers keys so that the partition field(s) are first. $scope.influencers = resp.influencers; @@ -689,7 +738,6 @@ module.controller('MlExplorerController', function ( selectedJobIds, earliestMs, latestMs, - MAX_INFLUENCER_FIELD_NAMES, swimlaneLimit ).then((resp) => { const topFieldValues = []; @@ -725,8 +773,7 @@ module.controller('MlExplorerController', function ( const earliestMs = bounds.min.valueOf(); const latestMs = bounds.max.valueOf(); mlExplorerDashboardService.anomalyDataChange.changed($scope.anomalyChartRecords, earliestMs, latestMs); - getTopInfluencers(jobIds, earliestMs, latestMs); - $scope.loadAnomaliesTable(jobIds, [], earliestMs, latestMs); + loadAnomalies(jobIds, [], earliestMs, latestMs); } function calculateSwimlaneBucketInterval() { diff --git a/x-pack/plugins/ml/public/services/results_service.js b/x-pack/plugins/ml/public/services/results_service.js index bbe3cb715c7d..ff4829fd4a6e 100644 --- a/x-pack/plugins/ml/public/services/results_service.js +++ b/x-pack/plugins/ml/public/services/results_service.js @@ -259,9 +259,16 @@ function getScheduledEventsByBucket( // Obtains the top influencers, by maximum influencer score, for the specified index, time range and job ID(s). // Pass an empty array or ['*'] to search over all job IDs. +// An optional array of influencers may be supplied, with each object in the array having 'fieldName' +// and 'fieldValue' properties, to limit data to the supplied list of influencers. // Returned response contains an influencers property, with a key for each of the influencer field names, // whose value is an array of objects containing influencerFieldValue, maxAnomalyScore and sumAnomalyScore keys. -function getTopInfluencers(jobIds, earliestMs, latestMs, maxFieldNames, maxFieldValues) { +function getTopInfluencers( + jobIds, + earliestMs, + latestMs, + maxFieldValues = 10, + influencers = []) { return new Promise((resolve, reject) => { const obj = { success: true, influencers: {} }; @@ -296,6 +303,25 @@ function getTopInfluencers(jobIds, earliestMs, latestMs, maxFieldNames, maxField }); } + // Add a should query to filter for each of the specified influencers. + if (influencers.length > 0) { + boolCriteria.push({ + bool: { + should: influencers.map((influencer) => { + return { + bool: { + must: [ + { term: { influencer_field_name: influencer.fieldName } }, + { term: { influencer_field_value: influencer.fieldValue } } + ] + } + }; + }), + minimum_should_match: 1, + } + }); + } + ml.esSearch({ index: ML_RESULTS_INDEX_PATTERN, size: 0, @@ -335,7 +361,7 @@ function getTopInfluencers(jobIds, earliestMs, latestMs, maxFieldNames, maxField influencerFieldValues: { terms: { field: 'influencer_field_value', - size: maxFieldValues !== undefined ? maxFieldValues : 10, + size: maxFieldValues, order: { maxAnomalyScore: 'desc' }