[ML] Filter top influencer list based on swimlane selection (#18946)

* [ML] Filter top influencer list based on swimlane selection

* [ML] Edits to Influencer List after review and remove animation
This commit is contained in:
Pete Harverson 2018-05-09 16:56:35 +01:00 committed by GitHub
parent c8b8027866
commit 875a0eb3bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 99 additions and 26 deletions

View file

@ -34,7 +34,7 @@
text-align: right;
line-height: 18px;
display: inline-block;
transition: none;
}
}

View file

@ -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() {

View file

@ -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'
}