diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx index b14abfe2f9895..f72fb8d00f173 100644 --- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/field_stats_flyout_provider.tsx @@ -10,15 +10,36 @@ import type { DataView } from '@kbn/data-plugin/common'; import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats'; import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; import type { FieldStatsProps } from '@kbn/unified-field-list/src/components/field_stats'; -import { MLFieldStatsFlyoutContext } from './use_field_stats_flytout_context'; +import { useEffect } from 'react'; +import { getProcessedFields } from '@kbn/ml-data-grid'; +import { stringHash } from '@kbn/ml-string-hash'; +import { lastValueFrom } from 'rxjs'; +import { useRef } from 'react'; +import { getMergedSampleDocsForPopulatedFieldsQuery } from './populated_fields/get_merged_populated_fields_query'; +import { useMlKibana } from '../../contexts/kibana'; import { FieldStatsFlyout } from './field_stats_flyout'; +import { MLFieldStatsFlyoutContext } from './use_field_stats_flytout_context'; +import { PopulatedFieldsCacheManager } from './populated_fields/populated_fields_cache_manager'; export const FieldStatsFlyoutProvider: FC<{ dataView: DataView; fieldStatsServices: FieldStatsServices; timeRangeMs?: TimeRangeMs; dslQuery?: FieldStatsProps['dslQuery']; -}> = ({ dataView, fieldStatsServices, timeRangeMs, dslQuery, children }) => { + disablePopulatedFields?: boolean; +}> = ({ + dataView, + fieldStatsServices, + timeRangeMs, + dslQuery, + disablePopulatedFields = false, + children, +}) => { + const { + services: { + data: { search }, + }, + } = useMlKibana(); const [isFieldStatsFlyoutVisible, setFieldStatsIsFlyoutVisible] = useState(false); const [fieldName, setFieldName] = useState(); const [fieldValue, setFieldValue] = useState(); @@ -27,6 +48,86 @@ export const FieldStatsFlyoutProvider: FC<{ () => setFieldStatsIsFlyoutVisible(!isFieldStatsFlyoutVisible), [isFieldStatsFlyoutVisible] ); + const [manager] = useState(new PopulatedFieldsCacheManager()); + const [populatedFields, setPopulatedFields] = useState | undefined>(); + const abortController = useRef(new AbortController()); + + useEffect( + function fetchSampleDocsEffect() { + if (disablePopulatedFields) return; + + let unmounted = false; + + if (abortController.current) { + abortController.current.abort(); + abortController.current = new AbortController(); + } + + const queryAndRunTimeMappings = getMergedSampleDocsForPopulatedFieldsQuery({ + searchQuery: dslQuery, + runtimeFields: dataView.getRuntimeMappings(), + datetimeField: dataView.getTimeField()?.name, + timeRange: timeRangeMs, + }); + const indexPattern = dataView.getIndexPattern(); + const esSearchRequestParams = { + index: indexPattern, + body: { + fields: ['*'], + _source: false, + ...queryAndRunTimeMappings, + size: 1000, + }, + }; + const cacheKey = stringHash(JSON.stringify(esSearchRequestParams)).toString(); + + const fetchSampleDocuments = async function () { + try { + const resp = await lastValueFrom( + search.search( + { + params: esSearchRequestParams, + }, + { abortSignal: abortController.current.signal } + ) + ); + + const docs = resp.rawResponse.hits.hits.map((d) => getProcessedFields(d.fields ?? {})); + + // Get all field names for each returned doc and flatten it + // to a list of unique field names used across all docs. + const fieldsWithData = new Set(docs.map(Object.keys).flat(1)); + manager.set(cacheKey, fieldsWithData); + if (!unmounted) { + setPopulatedFields(fieldsWithData); + } + } catch (e) { + if (e.name !== 'AbortError') { + // eslint-disable-next-line no-console + console.error( + `An error occurred fetching sample documents to determine populated field stats. + \nQuery:\n${JSON.stringify(esSearchRequestParams)} + \nError:${e}` + ); + } + } + }; + + const cachedResult = manager.get(cacheKey); + if (cachedResult) { + return cachedResult; + } else { + fetchSampleDocuments(); + } + + return () => { + unmounted = true; + abortController.current.abort(); + }; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [JSON.stringify({ dslQuery, dataViewId: dataView.id, timeRangeMs })] + ); return ( ; export const FieldStatsInfoButton = ({ field, label, - searchValue = '', onButtonClick, + disabled, + isEmpty = false, + hideTrigger = false, }: { field: FieldForStats; label: string; searchValue?: string; + disabled?: boolean; + isEmpty?: boolean; onButtonClick?: (field: FieldForStats) => void; + hideTrigger?: boolean; }) => { + const themeVars = useCurrentThemeVars(); + const emptyFieldMessage = isEmpty + ? ' ' + + i18n.translate('xpack.ml.newJob.wizard.fieldContextPopover.emptyFieldInSampleDocsMsg', { + defaultMessage: '(no data found in 1000 sample records)', + }) + : ''; return ( - - ) => { - if (ev.type === 'click') { - ev.currentTarget.focus(); - } - ev.preventDefault(); - ev.stopPropagation(); + > + ) => { + if (ev.type === 'click') { + ev.currentTarget.focus(); + } + ev.preventDefault(); + ev.stopPropagation(); - if (onButtonClick) { - onButtonClick(field); - } - }} - aria-label={i18n.translate( - 'xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipArialabel', - { - defaultMessage: 'Inspect field statistics', + if (onButtonClick) { + onButtonClick(field); + } + }} + aria-label={ + i18n.translate( + 'xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipAriaLabel', + { + defaultMessage: 'Inspect field statistics', + } + ) + emptyFieldMessage } - )} - /> - + /> + + ) : null} - - + + - {label} + + {label} + ); diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.test.ts b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.test.ts new file mode 100644 index 0000000000000..0ead649ab428a --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getMergedSampleDocsForPopulatedFieldsQuery } from './get_merged_populated_fields_query'; + +describe('getMergedSampleDocsForPopulatedFieldsQuery()', () => { + it('should wrap the original query in function_score', () => { + expect( + getMergedSampleDocsForPopulatedFieldsQuery({ + searchQuery: { match_all: {} }, + runtimeFields: {}, + }) + ).toStrictEqual({ + query: { + function_score: { query: { bool: { must: [{ match_all: {} }] } }, random_score: {} }, + }, + runtime_mappings: {}, + }); + }); + + it('should append the time range to the query if timeRange and datetimeField are provided', () => { + expect( + getMergedSampleDocsForPopulatedFieldsQuery({ + searchQuery: { + bool: { + should: [{ match_phrase: { version: '1' } }], + minimum_should_match: 1, + filter: [ + { + terms: { + cluster_uuid: '', + }, + }, + ], + must_not: [], + }, + }, + runtimeFields: {}, + timeRange: { from: 1613995874349, to: 1614082617000 }, + datetimeField: '@timestamp', + }) + ).toStrictEqual({ + query: { + function_score: { + query: { + bool: { + filter: [ + { terms: { cluster_uuid: '' } }, + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1613995874349, + lte: 1614082617000, + }, + }, + }, + ], + minimum_should_match: 1, + must_not: [], + should: [{ match_phrase: { version: '1' } }], + }, + }, + random_score: {}, + }, + }, + runtime_mappings: {}, + }); + }); + + it('should not append the time range to the query if datetimeField is undefined', () => { + expect( + getMergedSampleDocsForPopulatedFieldsQuery({ + searchQuery: { + bool: { + should: [{ match_phrase: { airline: 'AAL' } }], + minimum_should_match: 1, + filter: [], + must_not: [], + }, + }, + runtimeFields: {}, + timeRange: { from: 1613995874349, to: 1614082617000 }, + }) + ).toStrictEqual({ + query: { + function_score: { + query: { + bool: { + filter: [], + minimum_should_match: 1, + must_not: [], + should: [{ match_phrase: { airline: 'AAL' } }], + }, + }, + random_score: {}, + }, + }, + runtime_mappings: {}, + }); + }); +}); diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.ts b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.ts new file mode 100644 index 0000000000000..7287392849980 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/get_merged_populated_fields_query.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { cloneDeep } from 'lodash'; +import { getDefaultDSLQuery } from '@kbn/ml-query-utils'; + +export const getMergedSampleDocsForPopulatedFieldsQuery = ({ + runtimeFields, + searchQuery, + datetimeField, + timeRange, +}: { + runtimeFields: estypes.MappingRuntimeFields; + searchQuery?: estypes.QueryDslQueryContainer; + datetimeField?: string; + timeRange?: TimeRangeMs; +}): { + query: estypes.QueryDslQueryContainer; + runtime_mappings?: estypes.MappingRuntimeFields; +} => { + let rangeFilter; + + if (timeRange && datetimeField !== undefined) { + if (isPopulatedObject(timeRange, ['from', 'to']) && timeRange.to > timeRange.from) { + rangeFilter = { + range: { + [datetimeField]: { + gte: timeRange.from, + lte: timeRange.to, + format: 'epoch_millis', + }, + }, + }; + } + } + + const query = cloneDeep( + !searchQuery || isPopulatedObject(searchQuery, ['match_all']) + ? getDefaultDSLQuery() + : searchQuery + ); + + if (rangeFilter && isPopulatedObject(query, ['bool'])) { + if (Array.isArray(query.bool.filter)) { + query.bool.filter.push(rangeFilter); + } else { + query.bool.filter = [rangeFilter]; + } + } + + const queryAndRuntimeFields: { + query: estypes.QueryDslQueryContainer; + runtime_mappings?: estypes.MappingRuntimeFields; + } = { + query: { + function_score: { + query, + // @ts-expect-error random_score is valid dsl query + random_score: {}, + }, + }, + }; + if (runtimeFields) { + queryAndRuntimeFields.runtime_mappings = runtimeFields; + } + return queryAndRuntimeFields; +}; diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/index.ts b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/index.ts new file mode 100644 index 0000000000000..339f112beeb9f --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { PopulatedFieldsCacheManager } from './populated_fields_cache_manager'; diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/populated_fields_cache_manager.ts b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/populated_fields_cache_manager.ts new file mode 100644 index 0000000000000..547ad65c1179e --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/populated_fields/populated_fields_cache_manager.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +type StringifiedQueryKey = string; +type UpdatedTimestamp = number; + +const DEFAULT_EXPIRATION_MS = 60000; +export class PopulatedFieldsCacheManager { + private _resultsCache = new Map(); + private readonly _lastUpdatedTimestamps = new Map(); + + constructor(private readonly _expirationDurationMs = DEFAULT_EXPIRATION_MS) {} + + private clearOldCacheIfNeeded() { + if (this._resultsCache.size > 10) { + this._resultsCache.clear(); + this._lastUpdatedTimestamps.clear(); + } + } + + private clearExpiredCache(key: StringifiedQueryKey) { + // If result is available but past the expiration duration, clear cache + const lastUpdatedTs = this._lastUpdatedTimestamps.get(key); + const now = Date.now(); + if (lastUpdatedTs !== undefined && lastUpdatedTs - now > this._expirationDurationMs) { + this._resultsCache.delete(key); + } + } + + public get(key: StringifiedQueryKey) { + return this._resultsCache.get(key); + } + + public set(key: StringifiedQueryKey, value: any) { + this.clearExpiredCache(key); + this.clearOldCacheIfNeeded(); + + this._resultsCache.set(key, Date.now()); + this._resultsCache.set(key, value); + } +} diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_flytout_context.ts b/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_flytout_context.ts index 2de9fda1ded17..8aa7cfcc42de4 100644 --- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_flytout_context.ts +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_flytout_context.ts @@ -6,6 +6,7 @@ */ import { createContext, useContext } from 'react'; +import { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; interface MLJobWizardFieldStatsFlyoutProps { isFlyoutVisible: boolean; setIsFlyoutVisible: (v: boolean) => void; @@ -14,6 +15,8 @@ interface MLJobWizardFieldStatsFlyoutProps { fieldName?: string; setFieldValue: (v: string) => void; fieldValue?: string | number; + timeRangeMs?: TimeRangeMs; + populatedFields?: Set; } export const MLFieldStatsFlyoutContext = createContext({ isFlyoutVisible: false, @@ -21,6 +24,8 @@ export const MLFieldStatsFlyoutContext = createContext {}, setFieldName: () => {}, setFieldValue: () => {}, + timeRangeMs: undefined, + populatedFields: undefined, }); export function useFieldStatsFlyoutContext() { diff --git a/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_trigger.tsx b/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_trigger.tsx index da69ad87c46bc..52011e72a6ddc 100644 --- a/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_trigger.tsx +++ b/x-pack/plugins/ml/public/application/components/field_stats_flyout/use_field_stats_trigger.tsx @@ -6,17 +6,17 @@ */ import React, { ReactNode, useCallback } from 'react'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; +import type { EuiComboBoxOptionOption } from '@elastic/eui'; import type { Field } from '@kbn/ml-anomaly-utils'; import { optionCss } from './eui_combo_box_with_field_stats'; import { useFieldStatsFlyoutContext } from '.'; import { FieldForStats, FieldStatsInfoButton } from './field_stats_info_button'; - interface Option extends EuiComboBoxOptionOption { field: Field; } + export const useFieldStatsTrigger = () => { - const { setIsFlyoutVisible, setFieldName } = useFieldStatsFlyoutContext(); + const { setIsFlyoutVisible, setFieldName, populatedFields } = useFieldStatsFlyoutContext(); const closeFlyout = useCallback(() => setIsFlyoutVisible(false), [setIsFlyoutVisible]); @@ -29,6 +29,7 @@ export const useFieldStatsTrigger = () => { }, [setFieldName, setIsFlyoutVisible] ); + const renderOption = useCallback( (option: EuiComboBoxOptionOption, searchValue: string): ReactNode => { const field = (option as Option).field; @@ -36,13 +37,15 @@ export const useFieldStatsTrigger = () => { option.label ) : ( ); }, - [handleFieldStatsButtonClick] + // eslint-disable-next-line react-hooks/exhaustive-deps + [handleFieldStatsButtonClick, populatedFields?.size] ); return { renderOption, @@ -51,5 +54,6 @@ export const useFieldStatsTrigger = () => { handleFieldStatsButtonClick, closeFlyout, optionCss, + populatedFields, }; }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx index 4c8d2996a318b..824fb52398fd4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx @@ -8,6 +8,7 @@ import React, { FC, useContext, useState, useEffect, useMemo } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui'; import type { Field, Aggregation, AggFieldPair } from '@kbn/ml-anomaly-utils'; +import { EVENT_RATE_FIELD_ID } from '@kbn/ml-anomaly-utils'; import { FieldStatsInfoButton } from '../../../../../../../components/field_stats_flyout/field_stats_info_button'; import { JobCreatorContext } from '../../../job_creator_context'; import { useFieldStatsTrigger } from '../../../../../../../components/field_stats_flyout/use_field_stats_trigger'; @@ -43,7 +44,7 @@ export const AggSelect: FC = ({ fields, changeHandler, selectedOptions, r // create list of labels based on already selected detectors // so they can be removed from the dropdown list const removeLabels = removeOptions.map(createLabel); - const { handleFieldStatsButtonClick } = useFieldStatsTrigger(); + const { handleFieldStatsButtonClick, populatedFields } = useFieldStatsTrigger(); const options: EuiComboBoxOptionOption[] = useMemo( () => @@ -55,6 +56,8 @@ export const AggSelect: FC = ({ fields, changeHandler, selectedOptions, r // for more robust rendering label: ( = ({ fields, changeHandler, selectedOptions, r } return aggOption; }), - [handleFieldStatsButtonClick, fields, removeLabels] + // eslint-disable-next-line react-hooks/exhaustive-deps + [handleFieldStatsButtonClick, fields, removeLabels, populatedFields?.size] ); useEffect(() => { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/pivot_configuration/pivot_configuration.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/pivot_configuration/pivot_configuration.tsx index 214158606d32e..158d26f8ee9d1 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/pivot_configuration/pivot_configuration.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/pivot_configuration/pivot_configuration.tsx @@ -28,7 +28,8 @@ export const PivotConfiguration: FC = memo( const { ml: { useFieldStatsTrigger, FieldStatsInfoButton }, } = useAppDependencies(); - const { handleFieldStatsButtonClick, closeFlyout, renderOption } = useFieldStatsTrigger(); + const { handleFieldStatsButtonClick, closeFlyout, renderOption, populatedFields } = + useFieldStatsTrigger(); const { addAggregation, @@ -52,6 +53,7 @@ export const PivotConfiguration: FC = memo( // for more robust rendering label: ( = memo( }; return aggOption; }), - [aggOptions, FieldStatsInfoButton, handleFieldStatsButtonClick] + [aggOptions, FieldStatsInfoButton, handleFieldStatsButtonClick, populatedFields] ); return ( diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 11f7f76868a51..05b6128ced426 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -24808,7 +24808,7 @@ "xpack.ml.newJob.wizard.fieldContextFlyoutCloseButton": "Fermer", "xpack.ml.newJob.wizard.fieldContextFlyoutTitle": "Statistiques de champ", "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltip": "Inspecter les statistiques de champ", - "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipArialabel": "Inspecter les statistiques de champ", + "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipAriaLabel": "Inspecter les statistiques de champ", "xpack.ml.newJob.wizard.jobCreatorTitle.advanced": "Avancé", "xpack.ml.newJob.wizard.jobCreatorTitle.categorization": "Catégorisation", "xpack.ml.newJob.wizard.jobCreatorTitle.geo": "Données géographiques", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index db52ef8b1fae0..9a50ace7956fd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -24808,7 +24808,7 @@ "xpack.ml.newJob.wizard.fieldContextFlyoutCloseButton": "閉じる", "xpack.ml.newJob.wizard.fieldContextFlyoutTitle": "フィールド統計情報", "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltip": "検査フィールド統計情報", - "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipArialabel": "検査フィールド統計情報", + "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipAriaLabel": "検査フィールド統計情報", "xpack.ml.newJob.wizard.jobCreatorTitle.advanced": "高度な設定", "xpack.ml.newJob.wizard.jobCreatorTitle.categorization": "カテゴリー分け", "xpack.ml.newJob.wizard.jobCreatorTitle.geo": "地理情報", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b15afb493e31d..b4ba29347e08f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -24807,7 +24807,7 @@ "xpack.ml.newJob.wizard.fieldContextFlyoutCloseButton": "关闭", "xpack.ml.newJob.wizard.fieldContextFlyoutTitle": "字段统计信息", "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltip": "检查字段统计信息", - "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipArialabel": "检查字段统计信息", + "xpack.ml.newJob.wizard.fieldContextPopover.inspectFieldStatsTooltipAriaLabel": "检查字段统计信息", "xpack.ml.newJob.wizard.jobCreatorTitle.advanced": "高级", "xpack.ml.newJob.wizard.jobCreatorTitle.categorization": "归类", "xpack.ml.newJob.wizard.jobCreatorTitle.geo": "地理",