[ML] Fixes loading of Single Metric Viewer if partition field is text (#37975)

* [ML] Fixes loading of Single Metric Viewer if partition field is text

* [ML] Fix failing test and edit translations for cardinality check

* [ML] Edits to fields_service.js following review
This commit is contained in:
Pete Harverson 2019-06-05 12:02:03 +01:00 committed by GitHub
parent d8b0368c5e
commit 4a3034de49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 109 additions and 48 deletions

View file

@ -128,6 +128,8 @@ function getChartDetails(job, detectorIndex, entityFields, earliestMs, latestMs)
})
.then((results) => {
_.each(blankEntityFields, (field) => {
// results will not contain keys for non-aggregatable fields,
// so store as 0 to indicate over all field values.
obj.results.entityData.entities.push({
fieldName: field.fieldName,
cardinality: _.get(results, field.fieldName, 0)

View file

@ -118,10 +118,11 @@
<span
ng-repeat="countData in chartDetails.entityData.entities"
i18n-id="xpack.ml.timeSeriesExplorer.countDataInChartDetailsDescription"
i18n-default-message="{openBrace}{cardinality} distinct {fieldName} {cardinality, plural, one {} other { values}}{closeBrace}"
i18n-default-message="{openBrace}{cardinalityValue} distinct {fieldName} {cardinality, plural, one {} other { values}}{closeBrace}"
i18n-values="{
openBrace: $first ? '(' : '',
closeBrace: $last ? ')' : ', ',
cardinalityValue: countData.cardinality === 0 ? allValuesLabel : countData.cardinality,
cardinality: countData.cardinality,
fieldName: countData.fieldName
}"

View file

@ -121,6 +121,13 @@ module.controller('MlTimeSeriesExplorerController', function (
$scope.focusAnnotationData = [];
// Used in the template to indicate the chart is being plotted across
// all partition field values, where the cardinality of the field cannot be
// obtained as it is not aggregatable e.g. 'all distinct kpi_indicator values'
$scope.allValuesLabel = i18n.translate('xpack.ml.timeSeriesExplorer.allPartitionValuesLabel', {
defaultMessage: 'all',
});
// Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table.
const tzConfig = config.get('dateFormat:tz');
const dateFormatTz = (tzConfig !== 'Browser') ? tzConfig : moment.tz.guess();

View file

@ -13,6 +13,8 @@ export function fieldsServiceProvider(callWithRequest) {
// Obtains the cardinality of one or more fields.
// Returns an Object whose keys are the names of the fields,
// with values equal to the cardinality of the field.
// Any of the supplied fieldNames which are not aggregatable will
// be omitted from the returned Object.
function getCardinalityOfFields(
index,
fieldNames,
@ -21,63 +23,95 @@ export function fieldsServiceProvider(callWithRequest) {
earliestMs,
latestMs) {
// Build the criteria to use in the bool filter part of the request.
// Add criteria for the time range and the datafeed config query.
const mustCriteria = [
{
range: {
[timeFieldName]: {
gte: earliestMs,
lte: latestMs,
format: 'epoch_millis'
}
}
}
];
if (query) {
mustCriteria.push(query);
}
const aggs = fieldNames.reduce((obj, field) => {
obj[field] = { cardinality: { field } };
return obj;
}, {});
const body = {
query: {
bool: {
must: mustCriteria
}
},
size: 0,
_source: {
excludes: []
},
aggs
};
// First check that each of the supplied fieldNames are aggregatable,
// then obtain the cardinality for each of the aggregatable fields.
return new Promise((resolve, reject) => {
callWithRequest('search', {
callWithRequest('fieldCaps', {
index,
body
fields: fieldNames
})
.then((resp) => {
const aggregations = resp.aggregations;
if (aggregations !== undefined) {
const results = fieldNames.reduce((obj, field) => {
obj[field] = (aggregations[field] || { value: 0 }).value;
.then((fieldCapsResp) => {
const aggregatableFields = [];
fieldNames.forEach((fieldName) => {
const fieldInfo = fieldCapsResp.fields[fieldName];
const typeKeys = Object.keys(fieldInfo);
if (typeKeys.length > 0) {
const fieldType = typeKeys[0];
const isFieldAggregatable = fieldInfo[fieldType].aggregatable;
if (isFieldAggregatable === true) {
aggregatableFields.push(fieldName);
}
}
});
if (aggregatableFields.length > 0) {
// Build the criteria to use in the bool filter part of the request.
// Add criteria for the time range and the datafeed config query.
const mustCriteria = [
{
range: {
[timeFieldName]: {
gte: earliestMs,
lte: latestMs,
format: 'epoch_millis'
}
}
}
];
if (query) {
mustCriteria.push(query);
}
const aggs = aggregatableFields.reduce((obj, field) => {
obj[field] = { cardinality: { field } };
return obj;
}, {});
resolve(results);
const body = {
query: {
bool: {
must: mustCriteria
}
},
size: 0,
_source: {
excludes: []
},
aggs
};
callWithRequest('search', {
index,
body
})
.then((resp) => {
const aggregations = resp.aggregations;
if (aggregations !== undefined) {
const results = aggregatableFields.reduce((obj, field) => {
obj[field] = (aggregations[field] || { value: 0 }).value;
return obj;
}, {});
resolve(results);
} else {
resolve({});
}
})
.catch((resp) => {
reject(resp);
});
} else {
// None of the fields are aggregatable. Return empty Object.
resolve({});
}
})
.catch((resp) => {
reject(resp);
});
});
}
function getTimeFieldRange(

View file

@ -29,6 +29,22 @@ describe('ML - validateModelMemoryLimit', () => {
}
};
// mock field caps response
const fieldCapsResponse = {
indices: [
'cloudwatch'
],
fields: {
instance: {
keyword: {
type: 'keyword',
searchable: true,
aggregatable: true
}
}
}
};
// mock cardinality search response
const cardinalitySearchResponse = {
took: 8,
@ -52,9 +68,10 @@ describe('ML - validateModelMemoryLimit', () => {
};
// mock callWithRequest
// used in two places:
// used in three places:
// - to retrieve the info endpoint
// - to search for cardinality of split field
// - to retrieve field capabilities used in search for split field cardinality
function callWithRequest(call) {
if (typeof call === undefined) {
return Promise.reject();
@ -65,6 +82,8 @@ describe('ML - validateModelMemoryLimit', () => {
response = mlInfoResponse;
} else if(call === 'search') {
response = cardinalitySearchResponse;
} else if (call === 'fieldCaps') {
response = fieldCapsResponse;
}
return Promise.resolve(response);
}

View file

@ -7013,7 +7013,6 @@
"xpack.ml.timeSeriesExplorer.anomaliesTitle": "異常",
"xpack.ml.timeSeriesExplorer.autoSelectingFirstJobText": "、初めのジョブを自動選択します",
"xpack.ml.timeSeriesExplorer.canNotViewRequestedJobsWarningMessage": "リクエストされた‘{invalidIdsCount, plural, one {ジョブ} other {ジョブ}} {invalidIds} をこのダッシュボードで表示できません",
"xpack.ml.timeSeriesExplorer.countDataInChartDetailsDescription": "{openBrace}{cardinality} 特徴的な {fieldName} {cardinality, plural, one {} other { 値}}{closeBrace}",
"xpack.ml.timeSeriesExplorer.createNewSingleMetricJobLinkText": "新規シングルメトリックジョブを作成",
"xpack.ml.timeSeriesExplorer.dataNotChartableDescription": "選択された{entityCount, plural, one {エントリー} other {エントリー}}にはモデルプロットは収取されず、ソースデータはこのディテクター用にプロットできません",
"xpack.ml.timeSeriesExplorer.deleteAnnotationModal.cancelButtonLabel": "キャンセル",

View file

@ -5747,7 +5747,6 @@
"xpack.ml.timeSeriesExplorer.anomaliesTitle": "异常",
"xpack.ml.timeSeriesExplorer.autoSelectingFirstJobText": ",自动选择第一个作业",
"xpack.ml.timeSeriesExplorer.canNotViewRequestedJobsWarningMessage": "您无法在此仪表板中查看请求的 {invalidIdsCount, plural, one {作业} other {作业}} {invalidIds}",
"xpack.ml.timeSeriesExplorer.countDataInChartDetailsDescription": "{openBrace}{cardinality} 个不同 {fieldName} {cardinality, plural, one {} other {值}}{closeBrace}",
"xpack.ml.timeSeriesExplorer.createNewSingleMetricJobLinkText": "创建新的单指标作业",
"xpack.ml.timeSeriesExplorer.deleteAnnotationModal.cancelButtonLabel": "取消",
"xpack.ml.timeSeriesExplorer.deleteAnnotationModal.deleteAnnotationTitle": "删除此注释?",