Skip to content

Commit eb4fe68

Browse files
authored
[ML] Data Frame Analytics: Fix race condition and support for feature influence legacy format. (#81123) (#81276)
- 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.
1 parent 81e194b commit eb4fe68

11 files changed

Lines changed: 151 additions & 74 deletions

File tree

x-pack/plugins/ml/common/types/data_frame_analytics.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export type DataFrameAnalyticsId = string;
1919
export interface OutlierAnalysis {
2020
[key: string]: {};
2121

22-
outlier_detection: {};
22+
outlier_detection: {
23+
compute_feature_influence?: boolean;
24+
};
2325
}
2426

2527
interface Regression {

x-pack/plugins/ml/common/util/analytics_utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@ import {
1313
import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics';
1414

1515
export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => {
16+
if (typeof arg !== 'object' || arg === null) return false;
1617
const keys = Object.keys(arg);
1718
return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION;
1819
};
1920

2021
export const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => {
22+
if (typeof arg !== 'object' || arg === null) return false;
2123
const keys = Object.keys(arg);
2224
return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION;
2325
};
2426

2527
export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysis => {
28+
if (typeof arg !== 'object' || arg === null) return false;
2629
const keys = Object.keys(arg);
2730
return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION;
2831
};

x-pack/plugins/ml/public/application/components/data_grid/common.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import {
3333

3434
import {
3535
FEATURE_IMPORTANCE,
36-
FEATURE_INFLUENCE,
3736
OUTLIER_SCORE,
3837
TOP_CLASSES,
3938
} from '../../data_frame_analytics/common/constants';
@@ -112,10 +111,7 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results
112111
schema = NON_AGGREGATABLE;
113112
}
114113

115-
if (
116-
field === `${resultsField}.${OUTLIER_SCORE}` ||
117-
field.includes(`${resultsField}.${FEATURE_INFLUENCE}`)
118-
) {
114+
if (field === `${resultsField}.${OUTLIER_SCORE}`) {
119115
schema = 'numeric';
120116
}
121117

@@ -203,11 +199,6 @@ export const useRenderCellValue = (
203199
}
204200

205201
function getCellValue(cId: string) {
206-
if (cId.includes(`.${FEATURE_INFLUENCE}.`) && resultsField !== undefined) {
207-
const results = getNestedProperty(tableItems[adjustedRowIndex], resultsField, null);
208-
return results[cId.replace(`${resultsField}.`, '')];
209-
}
210-
211202
if (tableItems.hasOwnProperty(adjustedRowIndex)) {
212203
const item = tableItems[adjustedRowIndex];
213204

x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public';
1818
import { newJobCapsService } from '../../services/new_job_capabilities_service';
1919

20-
import { FEATURE_IMPORTANCE, FEATURE_INFLUENCE, OUTLIER_SCORE, TOP_CLASSES } from './constants';
20+
import { FEATURE_IMPORTANCE, OUTLIER_SCORE, TOP_CLASSES } from './constants';
2121
import { DataFrameAnalyticsConfig } from '../../../../common/types/data_frame_analytics';
2222

2323
export type EsId = string;
@@ -179,7 +179,6 @@ export const getDefaultFieldsFromJobCaps = (
179179
const resultsField = jobConfig.dest.results_field;
180180

181181
const featureImportanceFields = [];
182-
const featureInfluenceFields = [];
183182
const topClassesFields = [];
184183
const allFields: any = [];
185184
let type: ES_FIELD_TYPES | undefined;
@@ -193,16 +192,6 @@ export const getDefaultFieldsFromJobCaps = (
193192
name: `${resultsField}.${OUTLIER_SCORE}`,
194193
type: KBN_FIELD_TYPES.NUMBER,
195194
});
196-
197-
featureInfluenceFields.push(
198-
...fields
199-
.filter((d) => !jobConfig.analyzed_fields.excludes.includes(d.id))
200-
.map((d) => ({
201-
id: `${resultsField}.${FEATURE_INFLUENCE}.${d.id}`,
202-
name: `${resultsField}.${FEATURE_INFLUENCE}.${d.name}`,
203-
type: KBN_FIELD_TYPES.NUMBER,
204-
}))
205-
);
206195
}
207196
}
208197

@@ -247,12 +236,7 @@ export const getDefaultFieldsFromJobCaps = (
247236
}
248237
}
249238

250-
allFields.push(
251-
...fields,
252-
...featureImportanceFields,
253-
...featureInfluenceFields,
254-
...topClassesFields
255-
);
239+
allFields.push(...fields, ...featureImportanceFields, ...topClassesFields);
256240
allFields.sort(({ name: a }: { name: string }, { name: b }: { name: string }) =>
257241
sortExplorationResultsFields(a, b, jobConfig)
258242
);

x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import { DataFrameAnalyticsConfig } from '../../../../common/types/data_frame_an
1919
export const getIndexData = async (
2020
jobConfig: DataFrameAnalyticsConfig | undefined,
2121
dataGrid: UseDataGridReturnType,
22-
searchQuery: SavedSearchQuery
22+
searchQuery: SavedSearchQuery,
23+
options: { didCancel: boolean }
2324
) => {
2425
if (jobConfig !== undefined) {
2526
const {
@@ -52,22 +53,19 @@ export const getIndexData = async (
5253
index: jobConfig.dest.index,
5354
body: {
5455
fields: ['*'],
55-
_source: jobConfig.dest.results_field,
56+
_source: [],
5657
query: searchQuery,
5758
from: pageIndex * pageSize,
5859
size: pageSize,
5960
...(Object.keys(sort).length > 0 ? { sort } : {}),
6061
},
6162
});
6263

63-
setRowCount(resp.hits.total.value);
64-
const docs = resp.hits.hits.map((d) => ({
65-
...getProcessedFields(d.fields),
66-
[jobConfig.dest.results_field]: d._source[jobConfig.dest.results_field],
67-
}));
68-
69-
setTableItems(docs);
70-
setStatus(INDEX_STATUS.LOADED);
64+
if (!options.didCancel) {
65+
setRowCount(resp.hits.total.value);
66+
setTableItems(resp.hits.hits.map((d) => getProcessedFields(d.fields)));
67+
setStatus(INDEX_STATUS.LOADED);
68+
}
7169
} catch (e) {
7270
setErrorMessage(extractErrorMessage(e));
7371
setStatus(INDEX_STATUS.ERROR);

x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import { useEffect, useState } from 'react';
88

9+
import { i18n } from '@kbn/i18n';
10+
911
import { IndexPattern } from '../../../../../../../src/plugins/data/public';
1012

1113
import { extractErrorMessage } from '../../../../common/util/errors';
@@ -32,6 +34,9 @@ export const useResultsViewConfig = (jobId: string) => {
3234
const trainedModelsApiService = useTrainedModelsApiService();
3335

3436
const [indexPattern, setIndexPattern] = useState<IndexPattern | undefined>(undefined);
37+
const [indexPatternErrorMessage, setIndexPatternErrorMessage] = useState<undefined | string>(
38+
undefined
39+
);
3540
const [isInitialized, setIsInitialized] = useState<boolean>(false);
3641
const [needsDestIndexPattern, setNeedsDestIndexPattern] = useState<boolean>(false);
3742
const [isLoadingJobConfig, setIsLoadingJobConfig] = useState<boolean>(false);
@@ -105,7 +110,11 @@ export const useResultsViewConfig = (jobId: string) => {
105110
setNeedsDestIndexPattern(true);
106111
const sourceIndex = jobConfigUpdate.source.index[0];
107112
const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex;
108-
indexP = await mlContext.indexPatterns.get(sourceIndexPatternId);
113+
try {
114+
indexP = await mlContext.indexPatterns.get(sourceIndexPatternId);
115+
} catch (e) {
116+
indexP = undefined;
117+
}
109118
}
110119

111120
if (indexP !== undefined) {
@@ -114,6 +123,16 @@ export const useResultsViewConfig = (jobId: string) => {
114123
setIndexPattern(indexP);
115124
setIsInitialized(true);
116125
setIsLoadingJobConfig(false);
126+
} else {
127+
setIndexPatternErrorMessage(
128+
i18n.translate(
129+
'xpack.ml.dataframe.analytics.results.indexPatternsMissingErrorMessage',
130+
{
131+
defaultMessage:
132+
'To view this page, a Kibana index pattern is necessary for either the destination or source index of this analytics job.',
133+
}
134+
)
135+
);
117136
}
118137
} catch (e) {
119138
setJobCapsServiceErrorMessage(extractErrorMessage(e));
@@ -129,6 +148,7 @@ export const useResultsViewConfig = (jobId: string) => {
129148

130149
return {
131150
indexPattern,
151+
indexPatternErrorMessage,
132152
isInitialized,
133153
isLoadingJobConfig,
134154
jobCapsServiceErrorMessage,

x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import React, { FC, useEffect, useState } from 'react';
88

9-
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
9+
import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
1010
import { i18n } from '@kbn/i18n';
1111

1212
import { useUrlState } from '../../../../../util/url_state';
@@ -70,6 +70,7 @@ export const ExplorationPageWrapper: FC<Props> = ({
7070
}) => {
7171
const {
7272
indexPattern,
73+
indexPatternErrorMessage,
7374
isInitialized,
7475
isLoadingJobConfig,
7576
jobCapsServiceErrorMessage,
@@ -99,6 +100,22 @@ export const ExplorationPageWrapper: FC<Props> = ({
99100
}
100101
}, [jobConfig?.dest.results_field]);
101102

103+
if (indexPatternErrorMessage !== undefined) {
104+
return (
105+
<EuiPanel grow={false}>
106+
<EuiCallOut
107+
title={i18n.translate('xpack.ml.dataframe.analytics.exploration.indexError', {
108+
defaultMessage: 'An error occurred loading the index data.',
109+
})}
110+
color="danger"
111+
iconType="cross"
112+
>
113+
<p>{indexPatternErrorMessage}</p>
114+
</EuiCallOut>
115+
</EuiPanel>
116+
);
117+
}
118+
102119
if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) {
103120
return (
104121
<JobConfigErrorCallout

x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,15 @@ export const useExplorationResults = (
7373
dataGrid.resetPagination();
7474
}, [JSON.stringify(searchQuery)]);
7575

76+
// The pattern using `didCancel` allows us to abort out of date remote request.
77+
// We wrap `didCancel` in a object so we can mutate the value as it's being
78+
// passed on to `getIndexData`.
7679
useEffect(() => {
77-
getIndexData(jobConfig, dataGrid, searchQuery);
80+
const options = { didCancel: false };
81+
getIndexData(jobConfig, dataGrid, searchQuery, options);
82+
return () => {
83+
options.didCancel = true;
84+
};
7885
// custom comparison
7986
}, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]);
8087

x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,8 @@ export const getFeatureCount = (resultsField: string, tableItems: DataGridItem[]
1919

2020
const fullItem = tableItems[0];
2121

22-
if (
23-
fullItem[resultsField] !== undefined &&
24-
Array.isArray(fullItem[resultsField][FEATURE_INFLUENCE])
25-
) {
26-
return fullItem[resultsField][FEATURE_INFLUENCE].length;
22+
if (Array.isArray(fullItem[`${resultsField}.${FEATURE_INFLUENCE}.feature_name`])) {
23+
return fullItem[`${resultsField}.${FEATURE_INFLUENCE}.feature_name`].length;
2724
}
2825

2926
return 0;

x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
import React, { useState, FC } from 'react';
88

9-
import { EuiSpacer, EuiText } from '@elastic/eui';
9+
import { EuiCallOut, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
10+
11+
import { i18n } from '@kbn/i18n';
1012

1113
import {
1214
useColorRange,
@@ -15,7 +17,8 @@ import {
1517
} from '../../../../../components/color_range_legend';
1618
import { SavedSearchQuery } from '../../../../../contexts/ml';
1719

18-
import { defaultSearchQuery, useResultsViewConfig } from '../../../../common';
20+
import { defaultSearchQuery, isOutlierAnalysis, useResultsViewConfig } from '../../../../common';
21+
import { FEATURE_INFLUENCE } from '../../../../common/constants';
1922

2023
import { ExpandableSectionAnalytics, ExpandableSectionResults } from '../expandable_section';
2124
import { ExplorationQueryBar } from '../exploration_query_bar';
@@ -30,17 +33,54 @@ interface ExplorationProps {
3033
}
3134

3235
export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) => {
33-
const { indexPattern, jobConfig, needsDestIndexPattern } = useResultsViewConfig(jobId);
36+
const {
37+
indexPattern,
38+
indexPatternErrorMessage,
39+
jobConfig,
40+
needsDestIndexPattern,
41+
} = useResultsViewConfig(jobId);
3442
const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery);
3543
const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery);
3644

3745
const { columnsWithCharts, tableItems } = outlierData;
3846

39-
const colorRange = useColorRange(
40-
COLOR_RANGE.BLUE,
41-
COLOR_RANGE_SCALE.INFLUENCER,
42-
jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1
43-
);
47+
const featureCount = getFeatureCount(jobConfig?.dest?.results_field || '', tableItems);
48+
const colorRange = useColorRange(COLOR_RANGE.BLUE, COLOR_RANGE_SCALE.INFLUENCER, featureCount);
49+
50+
// Show the color range only if feature influence is enabled and there's more than 0 features.
51+
const showColorRange =
52+
featureCount > 0 &&
53+
isOutlierAnalysis(jobConfig?.analysis) &&
54+
jobConfig?.analysis.outlier_detection.compute_feature_influence === true;
55+
56+
const resultsField = jobConfig?.dest.results_field ?? '';
57+
58+
// Identify if the results index has a legacy feature influence format.
59+
// If feature influence was enabled for the legacy job we'll show a callout
60+
// with some additional information for a workaround.
61+
const showLegacyFeatureInfluenceFormatCallout =
62+
!needsDestIndexPattern &&
63+
isOutlierAnalysis(jobConfig?.analysis) &&
64+
jobConfig?.analysis.outlier_detection.compute_feature_influence === true &&
65+
columnsWithCharts.findIndex(
66+
(d) => d.id === `${resultsField}.${FEATURE_INFLUENCE}.feature_name`
67+
) === -1;
68+
69+
if (indexPatternErrorMessage !== undefined) {
70+
return (
71+
<EuiPanel grow={false}>
72+
<EuiCallOut
73+
title={i18n.translate('xpack.ml.dataframe.analytics.exploration.indexError', {
74+
defaultMessage: 'An error occurred loading the index data.',
75+
})}
76+
color="danger"
77+
iconType="cross"
78+
>
79+
<p>{indexPatternErrorMessage}</p>
80+
</EuiCallOut>
81+
</EuiPanel>
82+
);
83+
}
4484

4585
return (
4686
<>
@@ -58,8 +98,26 @@ export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) =
5898
</>
5999
)}
60100
{typeof jobConfig?.id === 'string' && <ExpandableSectionAnalytics jobId={jobConfig?.id} />}
101+
{showLegacyFeatureInfluenceFormatCallout && (
102+
<>
103+
<EuiCallOut
104+
size="s"
105+
title={i18n.translate(
106+
'xpack.ml.dataframe.analytics.outlierExploration.legacyFeatureInfluenceFormatCalloutTitle',
107+
{
108+
defaultMessage:
109+
'Color coded table cells based on feature influence are not available because the results index uses an unsupported legacy format. Please clone and rerun the job.',
110+
}
111+
)}
112+
iconType="pin"
113+
/>
114+
<EuiSpacer size="m" />
115+
</>
116+
)}
61117
<ExpandableSectionResults
62-
colorRange={colorRange}
118+
colorRange={
119+
showColorRange && !showLegacyFeatureInfluenceFormatCallout ? colorRange : undefined
120+
}
63121
indexData={outlierData}
64122
indexPattern={indexPattern}
65123
jobConfig={jobConfig}

0 commit comments

Comments
 (0)