From 4a3034de49c97facd089277d20ecd0cd0f558b4f Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Wed, 5 Jun 2019 12:02:03 +0100 Subject: [PATCH] [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 --- .../timeseries_search_service.js | 2 + .../timeseriesexplorer.html | 3 +- .../timeseriesexplorer_controller.js | 7 + .../models/fields_service/fields_service.js | 122 +++++++++++------- .../__tests__/validate_model_memory_limit.js | 21 ++- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 7 files changed, 109 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_search_service.js b/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_search_service.js index 50bb1f48086a..d92a0143b767 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_search_service.js +++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseries_search_service.js @@ -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) diff --git a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html index 440c1231b83d..c9822a06d5cf 100644 --- a/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html +++ b/x-pack/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.html @@ -118,10 +118,11 @@ { - 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( diff --git a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js index 2184445481eb..41edfc763923 100644 --- a/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js +++ b/x-pack/plugins/ml/server/models/job_validation/__tests__/validate_model_memory_limit.js @@ -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); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d0949115cd1d..7a25264f2a01 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -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": "キャンセル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 76bf94aac898..225b84b12e11 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -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": "删除此注释?",