Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] Kibana API endpoint for histogram chart data #70976

Merged
merged 21 commits into from
Jul 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1743e9e
[ML] Introduce ML API server side histograms endpoint.
walterra Jul 7, 2020
b66de70
[ML] Use histogram endpoint for analytics results pages.
walterra Jul 7, 2020
e079df8
[ML] Use histogram endpoint for analytics wizard.
walterra Jul 8, 2020
8296524
[ML] Fix DataLoader dependencies and constructor. Use API endpoint fo…
walterra Jul 8, 2020
b939a87
[ML] Sampler support. Reintroduce KBN_FIELD_TYPES.
walterra Jul 8, 2020
5daa854
[ML] Sample info tooltip.
walterra Jul 8, 2020
3f45214
[ML] Added jest and API integration tests.
walterra Jul 9, 2020
ea91900
[ML] Fix TS.
walterra Jul 9, 2020
4371a65
Merge branch 'master' into ml-api-histograms
walterra Jul 9, 2020
0984587
Merge branch 'master' into ml-api-histograms
walterra Jul 9, 2020
84203df
[ML] Use setDependencyCache to initialize http when using DataLoader …
walterra Jul 9, 2020
45857f9
Merge branch 'master' into ml-api-histograms
elasticmachine Jul 9, 2020
a117e43
[ML] Histogram API endpoint for transforms.
walterra Jul 13, 2020
02f1fc6
Merge branch 'master' into ml-api-histograms
walterra Jul 13, 2020
1b16be3
Merge branch 'ml-api-histograms' of github.com:walterra/kibana into m…
walterra Jul 13, 2020
7a0a599
Merge branch 'master' into ml-api-histograms
walterra Jul 13, 2020
3306ff8
[ML] Fix API endpoint for transform.
walterra Jul 14, 2020
8eedef2
[ML] e2e tests for outlier detection histogram charts.
walterra Jul 14, 2020
b794e97
[ML] schema comments.
walterra Jul 14, 2020
3883477
[ML] Use useMemo() for DataLoader.
walterra Jul 14, 2020
5b2ecdc
[ML] Fix API code structure.
walterra Jul 14, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions x-pack/plugins/ml/common/constants/field_histograms.ts
Original file line number Diff line number Diff line change
@@ -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;
* you may not use this file except in compliance with the Elastic License.
*/

// Default sampler shard size used for field histograms
export const DEFAULT_SAMPLER_SHARD_SIZE = 5000;
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ import {
EuiFlexItem,
EuiSpacer,
EuiTitle,
EuiToolTip,
} from '@elastic/eui';

import { CoreSetup } from 'src/core/public';

import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../common/constants/field_histograms';

import { INDEX_STATUS } from '../../data_frame_analytics/common';

import { euiDataGridStyle, euiDataGridToolbarSettings } from './common';
Expand Down Expand Up @@ -193,21 +196,31 @@ export const DataGrid: FC<Props> = memo(
...(chartsButtonVisible
? {
additionalControls: (
<EuiButtonEmpty
aria-checked={chartsVisible}
className={`euiDataGrid__controlBtn${
chartsVisible ? ' euiDataGrid__controlBtn--active' : ''
}`}
data-test-subj={`${dataTestSubj}HistogramButton`}
size="xs"
iconType="visBarVertical"
color="text"
onClick={toggleChartVisibility}
>
{i18n.translate('xpack.ml.dataGrid.histogramButtonText', {
defaultMessage: 'Histogram charts',
<EuiToolTip
content={i18n.translate('xpack.ml.dataGrid.histogramButtonToolTipContent', {
defaultMessage:
'Queries run to fetch histogram chart data will use a sample size per shard of {samplerShardSize} documents.',
values: {
samplerShardSize: DEFAULT_SAMPLER_SHARD_SIZE,
},
})}
</EuiButtonEmpty>
>
<EuiButtonEmpty
aria-checked={chartsVisible}
className={`euiDataGrid__controlBtn${
chartsVisible ? ' euiDataGrid__controlBtn--active' : ''
}`}
data-test-subj={`${dataTestSubj}HistogramButton`}
size="xs"
iconType="visBarVertical"
color="text"
onClick={toggleChartVisibility}
>
{i18n.translate('xpack.ml.dataGrid.histogramButtonText', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we use FormattedMessage in templates?

defaultMessage: 'Histogram charts',
})}
</EuiButtonEmpty>
</EuiToolTip>
),
}
: {}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export {
showDataGridColumnChartErrorMessageToast,
useRenderCellValue,
} from './common';
export { fetchChartsData, ChartData } from './use_column_chart';
export { getFieldType, ChartData } from './use_column_chart';
export { useDataGrid } from './use_data_grid';
export { DataGrid } from './data_grid';
export {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { getFieldType } from './use_column_chart';

describe('getFieldType()', () => {
it('should return the Kibana field type for a given EUI data grid schema', () => {
expect(getFieldType('text')).toBe('string');
expect(getFieldType('datetime')).toBe('date');
expect(getFieldType('numeric')).toBe('number');
expect(getFieldType('boolean')).toBe('boolean');
expect(getFieldType('json')).toBe('object');
expect(getFieldType('non-aggregatable')).toBe(undefined);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ import { i18n } from '@kbn/i18n';

import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public';

import { stringHash } from '../../../../common/util/string_utils';

import { NON_AGGREGATABLE } from './common';

export const hoveredRow$ = new BehaviorSubject<any | null>(null);
Expand All @@ -40,7 +38,7 @@ const getXScaleType = (kbnFieldType: KBN_FIELD_TYPES | undefined): XScaleType =>
}
};

const getFieldType = (schema: EuiDataGridColumn['schema']): KBN_FIELD_TYPES | undefined => {
export const getFieldType = (schema: EuiDataGridColumn['schema']): KBN_FIELD_TYPES | undefined => {
if (schema === NON_AGGREGATABLE) {
return undefined;
}
Expand All @@ -67,188 +65,6 @@ const getFieldType = (schema: EuiDataGridColumn['schema']): KBN_FIELD_TYPES | un
return fieldType;
};

interface NumericColumnStats {
interval: number;
min: number;
max: number;
}
type NumericColumnStatsMap = Record<string, NumericColumnStats>;
const getAggIntervals = async (
indexPatternTitle: string,
esSearch: (payload: any) => Promise<any>,
query: any,
columnTypes: EuiDataGridColumn[]
): Promise<NumericColumnStatsMap> => {
const numericColumns = columnTypes.filter((cT) => {
const fieldType = getFieldType(cT.schema);
return fieldType === KBN_FIELD_TYPES.NUMBER || fieldType === KBN_FIELD_TYPES.DATE;
});

if (numericColumns.length === 0) {
return {};
}

const minMaxAggs = numericColumns.reduce((aggs, c) => {
const id = stringHash(c.id);
aggs[id] = {
stats: {
field: c.id,
},
};
return aggs;
}, {} as Record<string, object>);

const respStats = await esSearch({
index: indexPatternTitle,
size: 0,
body: {
query,
aggs: minMaxAggs,
size: 0,
},
});

return Object.keys(respStats.aggregations).reduce((p, aggName) => {
const stats = [respStats.aggregations[aggName].min, respStats.aggregations[aggName].max];
if (!stats.includes(null)) {
const delta = respStats.aggregations[aggName].max - respStats.aggregations[aggName].min;

let aggInterval = 1;

if (delta > MAX_CHART_COLUMNS) {
aggInterval = Math.round(delta / MAX_CHART_COLUMNS);
}

if (delta <= 1) {
aggInterval = delta / MAX_CHART_COLUMNS;
}

p[aggName] = { interval: aggInterval, min: stats[0], max: stats[1] };
}

return p;
}, {} as NumericColumnStatsMap);
};

interface AggHistogram {
histogram: {
field: string;
interval: number;
};
}

interface AggCardinality {
cardinality: {
field: string;
};
}

interface AggTerms {
terms: {
field: string;
size: number;
};
}

type ChartRequestAgg = AggHistogram | AggCardinality | AggTerms;

export const fetchChartsData = async (
indexPatternTitle: string,
esSearch: (payload: any) => Promise<any>,
query: any,
columnTypes: EuiDataGridColumn[]
): Promise<ChartData[]> => {
const aggIntervals = await getAggIntervals(indexPatternTitle, esSearch, query, columnTypes);

const chartDataAggs = columnTypes.reduce((aggs, c) => {
const fieldType = getFieldType(c.schema);
const id = stringHash(c.id);
if (fieldType === KBN_FIELD_TYPES.NUMBER || fieldType === KBN_FIELD_TYPES.DATE) {
if (aggIntervals[id] !== undefined) {
aggs[`${id}_histogram`] = {
histogram: {
field: c.id,
interval: aggIntervals[id].interval !== 0 ? aggIntervals[id].interval : 1,
},
};
}
} else if (fieldType === KBN_FIELD_TYPES.STRING || fieldType === KBN_FIELD_TYPES.BOOLEAN) {
if (fieldType === KBN_FIELD_TYPES.STRING) {
aggs[`${id}_cardinality`] = {
cardinality: {
field: c.id,
},
};
}
aggs[`${id}_terms`] = {
terms: {
field: c.id,
size: MAX_CHART_COLUMNS,
},
};
}
return aggs;
}, {} as Record<string, ChartRequestAgg>);

if (Object.keys(chartDataAggs).length === 0) {
return [];
}

const respChartsData = await esSearch({
index: indexPatternTitle,
size: 0,
body: {
query,
aggs: chartDataAggs,
size: 0,
},
});

const chartsData: ChartData[] = columnTypes.map(
(c): ChartData => {
const fieldType = getFieldType(c.schema);
const id = stringHash(c.id);

if (fieldType === KBN_FIELD_TYPES.NUMBER || fieldType === KBN_FIELD_TYPES.DATE) {
if (aggIntervals[id] === undefined) {
return {
type: 'numeric',
data: [],
interval: 0,
stats: [0, 0],
id: c.id,
};
}

return {
data: respChartsData.aggregations[`${id}_histogram`].buckets,
interval: aggIntervals[id].interval,
stats: [aggIntervals[id].min, aggIntervals[id].max],
type: 'numeric',
id: c.id,
};
} else if (fieldType === KBN_FIELD_TYPES.STRING || fieldType === KBN_FIELD_TYPES.BOOLEAN) {
return {
type: fieldType === KBN_FIELD_TYPES.STRING ? 'ordinal' : 'boolean',
cardinality:
fieldType === KBN_FIELD_TYPES.STRING
? respChartsData.aggregations[`${id}_cardinality`].value
: 2,
data: respChartsData.aggregations[`${id}_terms`].buckets,
id: c.id,
};
}

return {
type: 'unsupported',
id: c.id,
};
}
);

return chartsData;
};

interface NumericDataItem {
key: number;
key_as_string?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';

import { EuiDataGridColumn } from '@elastic/eui';

import { CoreSetup } from 'src/core/public';

import { IndexPattern } from '../../../../../../../../../src/plugins/data/public';

import { DataLoader } from '../../../../datavisualizer/index_based/data_loader';

import {
fetchChartsData,
getFieldType,
getDataGridSchemaFromKibanaFieldType,
getFieldsFromKibanaIndexPattern,
showDataGridColumnChartErrorMessageToast,
Expand Down Expand Up @@ -103,13 +106,20 @@ export const useIndexData = (
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]);

const dataLoader = useMemo(() => new DataLoader(indexPattern, toastNotifications), [
indexPattern,
]);

const fetchColumnChartsData = async function () {
try {
const columnChartsData = await fetchChartsData(
indexPattern.title,
ml.esSearch,
query,
columns.filter((cT) => dataGrid.visibleColumns.includes(cT.id))
const columnChartsData = await dataLoader.loadFieldHistograms(
columns
.filter((cT) => dataGrid.visibleColumns.includes(cT.id))
.map((cT) => ({
fieldName: cT.id,
type: getFieldType(cT.schema),
})),
query
);
dataGrid.setColumnCharts(columnChartsData);
} catch (e) {
Expand Down
Loading