[ML] Add search time runtime support for index based Data Visualizer (#95252)

* [ML] Add runtime support from index pattern for data viz

* [ML] move runtime mappings outside of aggregatableFields loop

* [ML] Change arg name to runtimeMappings

* [ML] Fix dv full time range broken

* [ML] Fix dv broken with time range

* [ML] Add better error handling/transparency

* [ML] Update to using estypes.RuntimeField

* [ML] Update to use some shared common functions between ml and transform

* Revert "[ML] Update to use some shared common functions between ml and transform"

This reverts commit ce813f01

* [ML] Disable context menu if no charts
This commit is contained in:
Quynh Nguyen 2021-03-29 12:10:08 -05:00 committed by GitHub
parent 3f86bab334
commit 587f83a859
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 177 additions and 47 deletions

View file

@ -0,0 +1,33 @@
/*
* 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 { isPopulatedObject } from './object_utils';
import {
RUNTIME_FIELD_TYPES,
RuntimeType,
} from '../../../../../src/plugins/data/common/index_patterns';
import type { RuntimeField, RuntimeMappings } from '../types/fields';
export function isRuntimeField(arg: unknown): arg is RuntimeField {
return (
isPopulatedObject(arg) &&
((Object.keys(arg).length === 1 && arg.hasOwnProperty('type')) ||
(Object.keys(arg).length === 2 &&
arg.hasOwnProperty('type') &&
arg.hasOwnProperty('script') &&
(typeof arg.script === 'string' ||
(isPopulatedObject(arg.script) &&
Object.keys(arg.script).length === 1 &&
arg.script.hasOwnProperty('source') &&
typeof arg.script.source === 'string')))) &&
RUNTIME_FIELD_TYPES.includes(arg.type as RuntimeType)
);
}
export function isRuntimeMappings(arg: unknown): arg is RuntimeMappings {
return isPopulatedObject(arg) && Object.values(arg).every((d) => isRuntimeField(d));
}

View file

@ -13,6 +13,8 @@ import dateMath from '@elastic/datemath';
import { getTimefilter, getToastNotifications } from '../../util/dependency_cache';
import { ml, GetTimeFieldRangeResponse } from '../../services/ml_api_service';
import { IndexPattern } from '../../../../../../../src/plugins/data/public';
import { isPopulatedObject } from '../../../../common/util/object_utils';
import { RuntimeMappings } from '../../../../common/types/fields';
export interface TimeRange {
from: number;
@ -25,10 +27,12 @@ export async function setFullTimeRange(
): Promise<GetTimeFieldRangeResponse> {
try {
const timefilter = getTimefilter();
const runtimeMappings = indexPattern.getComputedFields().runtimeFields as RuntimeMappings;
const resp = await ml.getTimeFieldRange({
index: indexPattern.title,
timeFieldName: indexPattern.timeFieldName,
query,
...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}),
});
timefilter.setTime({
from: moment(resp.start.epoch).toISOString(),

View file

@ -18,23 +18,30 @@ import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../../common/constants/fiel
import { ml } from '../../../services/ml_api_service';
import { FieldHistogramRequestConfig, FieldRequestConfig } from '../common';
import { RuntimeMappings } from '../../../../../common/types/fields';
import {
ToastNotificationService,
toastNotificationServiceProvider,
} from '../../../services/toast_notification_service';
// Maximum number of examples to obtain for text type fields.
const MAX_EXAMPLES_DEFAULT: number = 10;
export class DataLoader {
private _indexPattern: IndexPattern;
private _runtimeMappings: RuntimeMappings;
private _indexPatternTitle: IndexPatternTitle = '';
private _maxExamples: number = MAX_EXAMPLES_DEFAULT;
private _toastNotifications: CoreSetup['notifications']['toasts'];
private _toastNotificationsService: ToastNotificationService;
constructor(
indexPattern: IndexPattern,
toastNotifications: CoreSetup['notifications']['toasts']
) {
this._indexPattern = indexPattern;
this._runtimeMappings = this._indexPattern.getComputedFields().runtimeFields as RuntimeMappings;
this._indexPatternTitle = indexPattern.title;
this._toastNotifications = toastNotifications;
this._toastNotificationsService = toastNotificationServiceProvider(toastNotifications);
}
async loadOverallData(
@ -70,6 +77,7 @@ export class DataLoader {
latest,
aggregatableFields,
nonAggregatableFields,
runtimeMappings: this._runtimeMappings,
});
return stats;
@ -93,6 +101,7 @@ export class DataLoader {
interval,
fields,
maxExamples: this._maxExamples,
runtimeMappings: this._runtimeMappings,
});
return stats;
@ -108,6 +117,7 @@ export class DataLoader {
query,
fields,
samplerShardSize,
runtimeMappings: this._runtimeMappings,
});
return stats;
@ -115,7 +125,8 @@ export class DataLoader {
displayError(err: any) {
if (err.statusCode === 500) {
this._toastNotifications.addDanger(
this._toastNotificationsService.displayErrorToast(
err,
i18n.translate('xpack.ml.datavisualizer.dataLoader.internalServerErrorMessage', {
defaultMessage:
'Error loading data in index {index}. {message}. ' +
@ -127,7 +138,8 @@ export class DataLoader {
})
);
} else {
this._toastNotifications.addDanger(
this._toastNotificationsService.displayErrorToast(
err,
i18n.translate('xpack.ml.datavisualizer.page.errorLoadingDataMessage', {
defaultMessage: 'Error loading data in index {index}. {message}',
values: {

View file

@ -38,10 +38,9 @@ import { FullTimeRangeSelector } from '../../components/full_time_range_selector
import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service';
import { useMlContext } from '../../contexts/ml';
import { kbnTypeToMLJobType } from '../../util/field_types_utils';
import { useTimefilter } from '../../contexts/kibana';
import { useNotifications, useTimefilter } from '../../contexts/kibana';
import { timeBasedIndexCheck, getQueryFromSavedSearch } from '../../util/index_utils';
import { getTimeBucketsFromCache } from '../../util/time_buckets';
import { getToastNotifications } from '../../util/dependency_cache';
import { usePageUrlState, useUrlState } from '../../util/url_state';
import { ActionsPanel } from './components/actions_panel';
import { SearchPanel } from './components/search_panel';
@ -132,7 +131,8 @@ export const Page: FC = () => {
autoRefreshSelector: true,
});
const dataLoader = useMemo(() => new DataLoader(currentIndexPattern, getToastNotifications()), [
const { toasts } = useNotifications();
const dataLoader = useMemo(() => new DataLoader(currentIndexPattern, toasts), [
currentIndexPattern,
]);

View file

@ -25,7 +25,6 @@ import {
import { MlCapabilitiesResponse } from '../../../../common/types/capabilities';
import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars';
import { BucketSpanEstimatorData } from '../../../../common/types/job_service';
import { RuntimeMappings } from '../../../../common/types/fields';
import {
Job,
JobStats,
@ -42,6 +41,7 @@ import {
} from '../../datavisualizer/index_based/common';
import { DataRecognizerConfigResponse, Module } from '../../../../common/types/modules';
import { getHttp } from '../../util/dependency_cache';
import type { RuntimeMappings } from '../../../../common/types/fields';
export interface MlInfoResponse {
defaults: MlServerDefaults;
@ -474,6 +474,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
interval,
fields,
maxExamples,
runtimeMappings,
}: {
indexPatternTitle: string;
query: any;
@ -484,6 +485,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
interval?: number;
fields?: FieldRequestConfig[];
maxExamples?: number;
runtimeMappings?: RuntimeMappings;
}) {
const body = JSON.stringify({
query,
@ -494,6 +496,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
interval,
fields,
maxExamples,
runtimeMappings,
});
return httpService.http<any>({
@ -508,16 +511,19 @@ export function mlApiServicesProvider(httpService: HttpService) {
query,
fields,
samplerShardSize,
runtimeMappings,
}: {
indexPatternTitle: string;
query: any;
fields: FieldHistogramRequestConfig[];
samplerShardSize?: number;
runtimeMappings?: RuntimeMappings;
}) {
const body = JSON.stringify({
query,
fields,
samplerShardSize,
runtimeMappings,
});
return httpService.http<any>({
@ -536,6 +542,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
samplerShardSize,
aggregatableFields,
nonAggregatableFields,
runtimeMappings,
}: {
indexPatternTitle: string;
query: any;
@ -545,6 +552,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
samplerShardSize?: number;
aggregatableFields: string[];
nonAggregatableFields: string[];
runtimeMappings?: RuntimeMappings;
}) {
const body = JSON.stringify({
query,
@ -554,6 +562,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
samplerShardSize,
aggregatableFields,
nonAggregatableFields,
runtimeMappings,
});
return httpService.http<any>({

View file

@ -211,7 +211,7 @@ const getAggIntervals = async (
query,
aggs: buildSamplerAggregation(minMaxAggs, samplerShardSize),
size: 0,
...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}),
...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}),
},
});
@ -297,7 +297,7 @@ export const getHistogramsForFields = async (
query,
aggs: buildSamplerAggregation(chartDataAggs, samplerShardSize),
size: 0,
...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}),
...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}),
},
});
@ -369,7 +369,8 @@ export class DataVisualizer {
samplerShardSize: number,
timeFieldName: string,
earliestMs: number,
latestMs: number
latestMs: number,
runtimeMappings?: RuntimeMappings
) {
const stats = {
totalCount: 0,
@ -400,7 +401,9 @@ export class DataVisualizer {
samplerShardSize,
timeFieldName,
earliestMs,
latestMs
latestMs,
undefined,
runtimeMappings
);
// Total count will be returned with each batch of fields. Just overwrite.
@ -420,7 +423,8 @@ export class DataVisualizer {
field,
timeFieldName,
earliestMs,
latestMs
latestMs,
runtimeMappings
);
const fieldData: FieldData = {
@ -447,14 +451,16 @@ export class DataVisualizer {
indexPatternTitle: string,
query: any,
fields: HistogramField[],
samplerShardSize: number
samplerShardSize: number,
runtimeMappings?: RuntimeMappings
): Promise<any> {
return await getHistogramsForFields(
this._client,
indexPatternTitle,
query,
fields,
samplerShardSize
samplerShardSize,
runtimeMappings
);
}
@ -470,7 +476,8 @@ export class DataVisualizer {
earliestMs: number,
latestMs: number,
intervalMs: number,
maxExamples: number
maxExamples: number,
runtimeMappings: RuntimeMappings
): Promise<BatchStats[]> {
// Batch up fields by type, getting stats for multiple fields at a time.
const batches: Field[][] = [];
@ -516,7 +523,8 @@ export class DataVisualizer {
samplerShardSize,
timeFieldName,
earliestMs,
latestMs
latestMs,
runtimeMappings
);
} else {
// Will only ever be one document count card,
@ -527,7 +535,8 @@ export class DataVisualizer {
timeFieldName,
earliestMs,
latestMs,
intervalMs
intervalMs,
runtimeMappings
);
batchStats.push(stats);
}
@ -541,7 +550,8 @@ export class DataVisualizer {
samplerShardSize,
timeFieldName,
earliestMs,
latestMs
latestMs,
runtimeMappings
);
break;
case ML_JOB_FIELD_TYPES.DATE:
@ -552,7 +562,8 @@ export class DataVisualizer {
samplerShardSize,
timeFieldName,
earliestMs,
latestMs
latestMs,
runtimeMappings
);
break;
case ML_JOB_FIELD_TYPES.BOOLEAN:
@ -563,7 +574,8 @@ export class DataVisualizer {
samplerShardSize,
timeFieldName,
earliestMs,
latestMs
latestMs,
runtimeMappings
);
break;
case ML_JOB_FIELD_TYPES.TEXT:
@ -579,7 +591,8 @@ export class DataVisualizer {
timeFieldName,
earliestMs,
latestMs,
maxExamples
maxExamples,
runtimeMappings
);
batchStats.push(stats);
})
@ -602,7 +615,8 @@ export class DataVisualizer {
timeFieldName: string,
earliestMs?: number,
latestMs?: number,
datafeedConfig?: Datafeed
datafeedConfig?: Datafeed,
runtimeMappings?: RuntimeMappings
) {
const index = indexPatternTitle;
const size = 0;
@ -612,7 +626,14 @@ export class DataVisualizer {
// Value count aggregation faster way of checking if field exists than using
// filter aggregation with exists query.
const aggs: Aggs = datafeedAggregations !== undefined ? { ...datafeedAggregations } : {};
const runtimeMappings: { runtime_mappings?: RuntimeMappings } = {};
// Combine runtime mappings from the index pattern as well as the datafeed
const combinedRuntimeMappings: RuntimeMappings = {
...(isPopulatedObject(runtimeMappings) ? runtimeMappings : {}),
...(isPopulatedObject(datafeedConfig) && isPopulatedObject(datafeedConfig.runtime_mappings)
? datafeedConfig.runtime_mappings
: {}),
};
aggregatableFields.forEach((field, i) => {
const safeFieldName = getSafeAggregationName(field, i);
@ -629,9 +650,6 @@ export class DataVisualizer {
cardinalityField = {
cardinality: { field },
};
if (datafeedConfig !== undefined && isPopulatedObject(datafeedConfig?.runtime_mappings)) {
runtimeMappings.runtime_mappings = datafeedConfig.runtime_mappings;
}
}
aggs[`${safeFieldName}_cardinality`] = cardinalityField;
});
@ -643,7 +661,9 @@ export class DataVisualizer {
},
},
...(isPopulatedObject(aggs) ? { aggs: buildSamplerAggregation(aggs, samplerShardSize) } : {}),
...runtimeMappings,
...(isPopulatedObject(combinedRuntimeMappings)
? { runtime_mappings: combinedRuntimeMappings }
: {}),
};
const { body } = await this._asCurrentUser.search({
@ -720,7 +740,8 @@ export class DataVisualizer {
field: string,
timeFieldName: string,
earliestMs: number,
latestMs: number
latestMs: number,
runtimeMappings?: RuntimeMappings
) {
const index = indexPatternTitle;
const size = 0;
@ -732,6 +753,7 @@ export class DataVisualizer {
filter: filterCriteria,
},
},
...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}),
};
filterCriteria.push({ exists: { field } });
@ -750,7 +772,8 @@ export class DataVisualizer {
timeFieldName: string,
earliestMs: number,
latestMs: number,
intervalMs: number
intervalMs: number,
runtimeMappings: RuntimeMappings
): Promise<DocumentCountStats> {
const index = indexPatternTitle;
const size = 0;
@ -776,6 +799,7 @@ export class DataVisualizer {
},
},
aggs,
...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}),
};
const { body } = await this._asCurrentUser.search({
@ -810,7 +834,8 @@ export class DataVisualizer {
samplerShardSize: number,
timeFieldName: string,
earliestMs: number,
latestMs: number
latestMs: number,
runtimeMappings?: RuntimeMappings
) {
const index = indexPatternTitle;
const size = 0;
@ -879,6 +904,7 @@ export class DataVisualizer {
},
},
aggs: buildSamplerAggregation(aggs, samplerShardSize),
...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}),
};
const { body } = await this._asCurrentUser.search({
@ -958,7 +984,8 @@ export class DataVisualizer {
samplerShardSize: number,
timeFieldName: string,
earliestMs: number,
latestMs: number
latestMs: number,
runtimeMappings?: RuntimeMappings
) {
const index = indexPatternTitle;
const size = 0;
@ -1000,6 +1027,7 @@ export class DataVisualizer {
},
},
aggs: buildSamplerAggregation(aggs, samplerShardSize),
...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}),
};
const { body } = await this._asCurrentUser.search({
@ -1048,7 +1076,8 @@ export class DataVisualizer {
samplerShardSize: number,
timeFieldName: string,
earliestMs: number,
latestMs: number
latestMs: number,
runtimeMappings?: RuntimeMappings
) {
const index = indexPatternTitle;
const size = 0;
@ -1074,6 +1103,7 @@ export class DataVisualizer {
},
},
aggs: buildSamplerAggregation(aggs, samplerShardSize),
...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}),
};
const { body } = await this._asCurrentUser.search({
@ -1114,7 +1144,8 @@ export class DataVisualizer {
samplerShardSize: number,
timeFieldName: string,
earliestMs: number,
latestMs: number
latestMs: number,
runtimeMappings?: RuntimeMappings
) {
const index = indexPatternTitle;
const size = 0;
@ -1141,6 +1172,7 @@ export class DataVisualizer {
},
},
aggs: buildSamplerAggregation(aggs, samplerShardSize),
...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}),
};
const { body } = await this._asCurrentUser.search({
@ -1182,7 +1214,8 @@ export class DataVisualizer {
timeFieldName: string,
earliestMs: number,
latestMs: number,
maxExamples: number
maxExamples: number,
runtimeMappings?: RuntimeMappings
): Promise<FieldExamples> {
const index = indexPatternTitle;
@ -1204,6 +1237,7 @@ export class DataVisualizer {
filter: filterCriteria,
},
},
...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}),
};
const { body } = await this._asCurrentUser.search({

View file

@ -15,6 +15,7 @@ import { isValidAggregationField } from '../../../common/util/validation_utils';
import { getDatafeedAggregations } from '../../../common/util/datafeed_utils';
import { Datafeed, IndicesOptions } from '../../../common/types/anomaly_detection_jobs';
import { RuntimeMappings } from '../../../common/types/fields';
import { isPopulatedObject } from '../../../common/util/object_utils';
/**
* Service for carrying out queries to obtain data
@ -243,7 +244,7 @@ export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) {
},
},
},
...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}),
...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}),
},
...(indicesOptions ?? {}),
});

View file

@ -16,6 +16,7 @@ import {
indexPatternTitleSchema,
} from './schemas/data_visualizer_schema';
import { RouteInitialization } from '../types';
import { RuntimeMappings } from '../../common/types/fields';
function getOverallStats(
client: IScopedClusterClient,
@ -26,7 +27,8 @@ function getOverallStats(
samplerShardSize: number,
timeFieldName: string,
earliestMs: number,
latestMs: number
latestMs: number,
runtimeMappings: RuntimeMappings
) {
const dv = new DataVisualizer(client);
return dv.getOverallStats(
@ -37,7 +39,8 @@ function getOverallStats(
samplerShardSize,
timeFieldName,
earliestMs,
latestMs
latestMs,
runtimeMappings
);
}
@ -51,7 +54,8 @@ function getStatsForFields(
earliestMs: number,
latestMs: number,
interval: number,
maxExamples: number
maxExamples: number,
runtimeMappings: RuntimeMappings
) {
const dv = new DataVisualizer(client);
return dv.getStatsForFields(
@ -63,7 +67,8 @@ function getStatsForFields(
earliestMs,
latestMs,
interval,
maxExamples
maxExamples,
runtimeMappings
);
}
@ -72,10 +77,17 @@ function getHistogramsForFields(
indexPatternTitle: string,
query: any,
fields: HistogramField[],
samplerShardSize: number
samplerShardSize: number,
runtimeMappings: RuntimeMappings
) {
const dv = new DataVisualizer(client);
return dv.getHistogramsForFields(indexPatternTitle, query, fields, samplerShardSize);
return dv.getHistogramsForFields(
indexPatternTitle,
query,
fields,
samplerShardSize,
runtimeMappings
);
}
/**
@ -109,7 +121,7 @@ export function dataVisualizerRoutes({ router, routeGuard }: RouteInitialization
try {
const {
params: { indexPatternTitle },
body: { query, fields, samplerShardSize },
body: { query, fields, samplerShardSize, runtimeMappings },
} = request;
const results = await getHistogramsForFields(
@ -117,7 +129,8 @@ export function dataVisualizerRoutes({ router, routeGuard }: RouteInitialization
indexPatternTitle,
query,
fields,
samplerShardSize
samplerShardSize,
runtimeMappings
);
return response.ok({
@ -165,9 +178,9 @@ export function dataVisualizerRoutes({ router, routeGuard }: RouteInitialization
latest,
interval,
maxExamples,
runtimeMappings,
},
} = request;
const results = await getStatsForFields(
client,
indexPatternTitle,
@ -178,7 +191,8 @@ export function dataVisualizerRoutes({ router, routeGuard }: RouteInitialization
earliest,
latest,
interval,
maxExamples
maxExamples,
runtimeMappings
);
return response.ok({
@ -229,6 +243,7 @@ export function dataVisualizerRoutes({ router, routeGuard }: RouteInitialization
timeFieldName,
earliest,
latest,
runtimeMappings,
},
} = request;
@ -241,7 +256,8 @@ export function dataVisualizerRoutes({ router, routeGuard }: RouteInitialization
samplerShardSize,
timeFieldName,
earliest,
latest
latest,
runtimeMappings
);
return response.ok({

View file

@ -6,12 +6,27 @@
*/
import { schema } from '@kbn/config-schema';
import { isRuntimeField } from '../../../common/util/runtime_field_utils';
export const indexPatternTitleSchema = schema.object({
/** Title of the index pattern for which to return stats. */
indexPatternTitle: schema.string(),
});
const runtimeMappingsSchema = schema.maybe(
schema.object(
{},
{
unknowns: 'allow',
validate: (v: object) => {
if (Object.values(v).some((o) => !isRuntimeField(o))) {
return 'Invalid runtime field';
}
},
}
)
);
export const dataVisualizerFieldHistogramsSchema = schema.object({
/** Query to match documents in the index. */
query: schema.any(),
@ -19,6 +34,8 @@ export const dataVisualizerFieldHistogramsSchema = schema.object({
fields: schema.arrayOf(schema.any()),
/** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */
samplerShardSize: schema.number(),
/** Optional search time runtime mappings */
runtimeMappings: runtimeMappingsSchema,
});
export const dataVisualizerFieldStatsSchema = schema.object({
@ -37,6 +54,8 @@ export const dataVisualizerFieldStatsSchema = schema.object({
interval: schema.maybe(schema.number()),
/** Maximum number of examples to return for text type fields. */
maxExamples: schema.number(),
/** Optional search time runtime mappings */
runtimeMappings: runtimeMappingsSchema,
});
export const dataVisualizerOverallStatsSchema = schema.object({
@ -54,4 +73,6 @@ export const dataVisualizerOverallStatsSchema = schema.object({
earliest: schema.maybe(schema.number()),
/** Latest timestamp for search, as epoch ms (optional). */
latest: schema.maybe(schema.number()),
/** Optional search time runtime mappings */
runtimeMappings: runtimeMappingsSchema,
});