From e3c556582d7609b772559689a43c1c3202fb187c Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 21 Oct 2020 07:57:16 +0200 Subject: [PATCH] [ML] Data Frame Analytics: Fix race condition and support for feature influence legacy format. (#81123) - Fixes a race condition where searches for data grid results with different parameters would return in different order with the wrong results on display. Fix uses a pattern to cancel useEffect callback for getIndexData(). - Fixes identifying pre 7.10 feature influence format for outlier detection and will display a callout on the results page with information for a workaround. - To fix identifying the legacy format, some cleanup of other code relating to the old format had to be done. The ml results object field is no longer treated as a "special" field for outlier detection and is treated and retrieved in the same way as other fields. - Adds an error callout if no Kibana index pattern is available for source/dest index. --- .../ml/common/types/data_frame_analytics.ts | 4 +- .../plugins/ml/common/util/analytics_utils.ts | 3 + .../components/data_grid/common.ts | 11 +-- .../data_frame_analytics/common/fields.ts | 20 +---- .../common/get_index_data.ts | 18 ++--- .../common/use_results_view_config.ts | 22 +++++- .../exploration_page_wrapper.tsx | 19 ++++- .../use_exploration_results.ts | 9 ++- .../components/outlier_exploration/common.ts | 7 +- .../outlier_exploration.tsx | 76 ++++++++++++++++--- .../outlier_exploration/use_outlier_data.ts | 36 ++++----- 11 files changed, 151 insertions(+), 74 deletions(-) diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index 5d0ecf96fb6b5..cbf2acd152476 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -19,7 +19,9 @@ export type DataFrameAnalyticsId = string; export interface OutlierAnalysis { [key: string]: {}; - outlier_detection: {}; + outlier_detection: { + compute_feature_influence?: boolean; + }; } interface Regression { diff --git a/x-pack/plugins/ml/common/util/analytics_utils.ts b/x-pack/plugins/ml/common/util/analytics_utils.ts index d231ed4344389..94797efdfcfad 100644 --- a/x-pack/plugins/ml/common/util/analytics_utils.ts +++ b/x-pack/plugins/ml/common/util/analytics_utils.ts @@ -13,16 +13,19 @@ import { import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics'; export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => { + if (typeof arg !== 'object' || arg === null) return false; const keys = Object.keys(arg); return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION; }; export const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => { + if (typeof arg !== 'object' || arg === null) return false; const keys = Object.keys(arg); return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION; }; export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysis => { + if (typeof arg !== 'object' || arg === null) return false; const keys = Object.keys(arg); return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; }; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index f88694a1952b2..642d0ae564b85 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -33,7 +33,6 @@ import { import { FEATURE_IMPORTANCE, - FEATURE_INFLUENCE, OUTLIER_SCORE, TOP_CLASSES, } from '../../data_frame_analytics/common/constants'; @@ -112,10 +111,7 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results schema = NON_AGGREGATABLE; } - if ( - field === `${resultsField}.${OUTLIER_SCORE}` || - field.includes(`${resultsField}.${FEATURE_INFLUENCE}`) - ) { + if (field === `${resultsField}.${OUTLIER_SCORE}`) { schema = 'numeric'; } @@ -203,11 +199,6 @@ export const useRenderCellValue = ( } function getCellValue(cId: string) { - if (cId.includes(`.${FEATURE_INFLUENCE}.`) && resultsField !== undefined) { - const results = getNestedProperty(tableItems[adjustedRowIndex], resultsField, null); - return results[cId.replace(`${resultsField}.`, '')]; - } - if (tableItems.hasOwnProperty(adjustedRowIndex)) { const item = tableItems[adjustedRowIndex]; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index e4581f0a87bdd..c606cbd1cc11a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -17,7 +17,7 @@ import { import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; import { newJobCapsService } from '../../services/new_job_capabilities_service'; -import { FEATURE_IMPORTANCE, FEATURE_INFLUENCE, OUTLIER_SCORE, TOP_CLASSES } from './constants'; +import { FEATURE_IMPORTANCE, OUTLIER_SCORE, TOP_CLASSES } from './constants'; import { DataFrameAnalyticsConfig } from '../../../../common/types/data_frame_analytics'; export type EsId = string; @@ -179,7 +179,6 @@ export const getDefaultFieldsFromJobCaps = ( const resultsField = jobConfig.dest.results_field; const featureImportanceFields = []; - const featureInfluenceFields = []; const topClassesFields = []; const allFields: any = []; let type: ES_FIELD_TYPES | undefined; @@ -193,16 +192,6 @@ export const getDefaultFieldsFromJobCaps = ( name: `${resultsField}.${OUTLIER_SCORE}`, type: KBN_FIELD_TYPES.NUMBER, }); - - featureInfluenceFields.push( - ...fields - .filter((d) => !jobConfig.analyzed_fields.excludes.includes(d.id)) - .map((d) => ({ - id: `${resultsField}.${FEATURE_INFLUENCE}.${d.id}`, - name: `${resultsField}.${FEATURE_INFLUENCE}.${d.name}`, - type: KBN_FIELD_TYPES.NUMBER, - })) - ); } } @@ -247,12 +236,7 @@ export const getDefaultFieldsFromJobCaps = ( } } - allFields.push( - ...fields, - ...featureImportanceFields, - ...featureInfluenceFields, - ...topClassesFields - ); + allFields.push(...fields, ...featureImportanceFields, ...topClassesFields); allFields.sort(({ name: a }: { name: string }, { name: b }: { name: string }) => sortExplorationResultsFields(a, b, jobConfig) ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts index 667dea27de96e..8e50aab0914db 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts @@ -19,7 +19,8 @@ import { DataFrameAnalyticsConfig } from '../../../../common/types/data_frame_an export const getIndexData = async ( jobConfig: DataFrameAnalyticsConfig | undefined, dataGrid: UseDataGridReturnType, - searchQuery: SavedSearchQuery + searchQuery: SavedSearchQuery, + options: { didCancel: boolean } ) => { if (jobConfig !== undefined) { const { @@ -52,7 +53,7 @@ export const getIndexData = async ( index: jobConfig.dest.index, body: { fields: ['*'], - _source: jobConfig.dest.results_field, + _source: [], query: searchQuery, from: pageIndex * pageSize, size: pageSize, @@ -60,14 +61,11 @@ export const getIndexData = async ( }, }); - setRowCount(resp.hits.total.value); - const docs = resp.hits.hits.map((d) => ({ - ...getProcessedFields(d.fields), - [jobConfig.dest.results_field]: d._source[jobConfig.dest.results_field], - })); - - setTableItems(docs); - setStatus(INDEX_STATUS.LOADED); + if (!options.didCancel) { + setRowCount(resp.hits.total.value); + setTableItems(resp.hits.hits.map((d) => getProcessedFields(d.fields))); + setStatus(INDEX_STATUS.LOADED); + } } catch (e) { setErrorMessage(extractErrorMessage(e)); setStatus(INDEX_STATUS.ERROR); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts index 7d2ca86a38083..81c2e246120c0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts @@ -6,6 +6,8 @@ import { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; + import { IndexPattern } from '../../../../../../../src/plugins/data/public'; import { extractErrorMessage } from '../../../../common/util/errors'; @@ -32,6 +34,9 @@ export const useResultsViewConfig = (jobId: string) => { const trainedModelsApiService = useTrainedModelsApiService(); const [indexPattern, setIndexPattern] = useState(undefined); + const [indexPatternErrorMessage, setIndexPatternErrorMessage] = useState( + undefined + ); const [isInitialized, setIsInitialized] = useState(false); const [needsDestIndexPattern, setNeedsDestIndexPattern] = useState(false); const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); @@ -105,7 +110,11 @@ export const useResultsViewConfig = (jobId: string) => { setNeedsDestIndexPattern(true); const sourceIndex = jobConfigUpdate.source.index[0]; const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); + try { + indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); + } catch (e) { + indexP = undefined; + } } if (indexP !== undefined) { @@ -114,6 +123,16 @@ export const useResultsViewConfig = (jobId: string) => { setIndexPattern(indexP); setIsInitialized(true); setIsLoadingJobConfig(false); + } else { + setIndexPatternErrorMessage( + i18n.translate( + 'xpack.ml.dataframe.analytics.results.indexPatternsMissingErrorMessage', + { + defaultMessage: + 'To view this page, a Kibana index pattern is necessary for either the destination or source index of this analytics job.', + } + ) + ); } } catch (e) { setJobCapsServiceErrorMessage(extractErrorMessage(e)); @@ -129,6 +148,7 @@ export const useResultsViewConfig = (jobId: string) => { return { indexPattern, + indexPatternErrorMessage, isInitialized, isLoadingJobConfig, jobCapsServiceErrorMessage, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx index de4d1a97f248f..cdecead21d4de 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -6,7 +6,7 @@ import React, { FC, useEffect, useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useUrlState } from '../../../../../util/url_state'; @@ -70,6 +70,7 @@ export const ExplorationPageWrapper: FC = ({ }) => { const { indexPattern, + indexPatternErrorMessage, isInitialized, isLoadingJobConfig, jobCapsServiceErrorMessage, @@ -99,6 +100,22 @@ export const ExplorationPageWrapper: FC = ({ } }, [jobConfig?.dest.results_field]); + if (indexPatternErrorMessage !== undefined) { + return ( + + +

{indexPatternErrorMessage}

+
+
+ ); + } + if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { return ( { - getIndexData(jobConfig, dataGrid, searchQuery); + const options = { didCancel: false }; + getIndexData(jobConfig, dataGrid, searchQuery, options); + return () => { + options.didCancel = true; + }; // custom comparison }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts index d1889a8acb990..1ce3b3528e44b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts @@ -19,11 +19,8 @@ export const getFeatureCount = (resultsField: string, tableItems: DataGridItem[] const fullItem = tableItems[0]; - if ( - fullItem[resultsField] !== undefined && - Array.isArray(fullItem[resultsField][FEATURE_INFLUENCE]) - ) { - return fullItem[resultsField][FEATURE_INFLUENCE].length; + if (Array.isArray(fullItem[`${resultsField}.${FEATURE_INFLUENCE}.feature_name`])) { + return fullItem[`${resultsField}.${FEATURE_INFLUENCE}.feature_name`].length; } return 0; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index 8fc2486599755..9e30ed3cdfe95 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -6,7 +6,9 @@ import React, { useState, FC } from 'react'; -import { EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiCallOut, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; import { useColorRange, @@ -15,7 +17,8 @@ import { } from '../../../../../components/color_range_legend'; import { SavedSearchQuery } from '../../../../../contexts/ml'; -import { defaultSearchQuery, useResultsViewConfig } from '../../../../common'; +import { defaultSearchQuery, isOutlierAnalysis, useResultsViewConfig } from '../../../../common'; +import { FEATURE_INFLUENCE } from '../../../../common/constants'; import { ExpandableSectionAnalytics, ExpandableSectionResults } from '../expandable_section'; import { ExplorationQueryBar } from '../exploration_query_bar'; @@ -30,17 +33,54 @@ interface ExplorationProps { } export const OutlierExploration: FC = React.memo(({ jobId }) => { - const { indexPattern, jobConfig, needsDestIndexPattern } = useResultsViewConfig(jobId); + const { + indexPattern, + indexPatternErrorMessage, + jobConfig, + needsDestIndexPattern, + } = useResultsViewConfig(jobId); const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery); const { columnsWithCharts, tableItems } = outlierData; - const colorRange = useColorRange( - COLOR_RANGE.BLUE, - COLOR_RANGE_SCALE.INFLUENCER, - jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 - ); + const featureCount = getFeatureCount(jobConfig?.dest?.results_field || '', tableItems); + const colorRange = useColorRange(COLOR_RANGE.BLUE, COLOR_RANGE_SCALE.INFLUENCER, featureCount); + + // Show the color range only if feature influence is enabled and there's more than 0 features. + const showColorRange = + featureCount > 0 && + isOutlierAnalysis(jobConfig?.analysis) && + jobConfig?.analysis.outlier_detection.compute_feature_influence === true; + + const resultsField = jobConfig?.dest.results_field ?? ''; + + // Identify if the results index has a legacy feature influence format. + // If feature influence was enabled for the legacy job we'll show a callout + // with some additional information for a workaround. + const showLegacyFeatureInfluenceFormatCallout = + !needsDestIndexPattern && + isOutlierAnalysis(jobConfig?.analysis) && + jobConfig?.analysis.outlier_detection.compute_feature_influence === true && + columnsWithCharts.findIndex( + (d) => d.id === `${resultsField}.${FEATURE_INFLUENCE}.feature_name` + ) === -1; + + if (indexPatternErrorMessage !== undefined) { + return ( + + +

{indexPatternErrorMessage}

+
+
+ ); + } return ( <> @@ -58,8 +98,26 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = )} {typeof jobConfig?.id === 'string' && } + {showLegacyFeatureInfluenceFormatCallout && ( + <> + + + + )} { - getIndexData(jobConfig, dataGrid, searchQuery); + const options = { didCancel: false }; + getIndexData(jobConfig, dataGrid, searchQuery, options); + return () => { + options.didCancel = true; + }; // custom comparison }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); @@ -151,19 +153,17 @@ export const useOutlierData = ( const split = columnId.split('.'); let backgroundColor; + const featureNames = fullItem[`${resultsField}.${FEATURE_INFLUENCE}.feature_name`]; + // column with feature values get color coded by its corresponding influencer value - if ( - fullItem[resultsField] !== undefined && - fullItem[resultsField][FEATURE_INFLUENCE] !== undefined && - fullItem[resultsField][FEATURE_INFLUENCE].find( - (d: FeatureInfluence) => d.feature_name === columnId - ) !== undefined - ) { - backgroundColor = colorRange( - fullItem[resultsField][FEATURE_INFLUENCE].find( - (d: FeatureInfluence) => d.feature_name === columnId - ).influence - ); + if (Array.isArray(featureNames)) { + const featureIndex = featureNames.indexOf(columnId); + + if (featureIndex > -1) { + backgroundColor = colorRange( + fullItem[`${resultsField}.${FEATURE_INFLUENCE}.influence`][featureIndex] + ); + } } // column with influencer values get color coded by its own value