diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/_index.scss b/x-pack/legacy/plugins/ml/public/data_frame_analytics/_index.scss
index 4c0ecd8f9ce44..c231c405b5369 100644
--- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/_index.scss
+++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/_index.scss
@@ -1,4 +1,5 @@
@import 'pages/analytics_exploration/components/exploration/index';
+@import 'pages/analytics_exploration/components/regression_exploration/index';
@import 'pages/analytics_management/components/analytics_list/index';
@import 'pages/analytics_management/components/create_analytics_form/index';
@import 'pages/analytics_management/components/create_analytics_flyout/index';
diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts
index 04dff6e0b4dc5..b1eedc1378d43 100644
--- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts
+++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts
@@ -9,8 +9,11 @@ import { BehaviorSubject } from 'rxjs';
import { filter, distinctUntilChanged } from 'rxjs/operators';
import { Subscription } from 'rxjs';
import { idx } from '@kbn/elastic-idx';
+import { cloneDeep } from 'lodash';
import { ml } from '../../services/ml_api_service';
+import { Dictionary } from '../../../common/types/common';
import { getErrorMessage } from '../pages/analytics_management/hooks/use_create_analytics_form';
+import { SavedSearchQuery } from '../../contexts/kibana';
export type IndexName = string;
export type IndexPattern = string;
@@ -38,8 +41,8 @@ export enum INDEX_STATUS {
}
export interface Eval {
- meanSquaredError: number | '';
- rSquared: number | '';
+ meanSquaredError: number | string;
+ rSquared: number | string;
error: null | string;
}
@@ -119,6 +122,13 @@ export const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => {
return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION;
};
+export const isRegressionResultsSearchBoolQuery = (
+ arg: any
+): arg is RegressionResultsSearchBoolQuery => {
+ const keys = Object.keys(arg);
+ return keys.length === 1 && keys[0] === 'bool';
+};
+
export interface DataFrameAnalyticsConfig {
id: DataFrameAnalyticsId;
// Description attribute is not supported yet
@@ -212,6 +222,42 @@ export function getValuesFromResponse(response: RegressionEvaluateResponse) {
return { meanSquaredError, rSquared };
}
+interface RegressionResultsSearchBoolQuery {
+ bool: Dictionary;
+}
+interface RegressionResultsSearchTermQuery {
+ term: Dictionary;
+}
+
+export type RegressionResultsSearchQuery =
+ | RegressionResultsSearchBoolQuery
+ | RegressionResultsSearchTermQuery
+ | SavedSearchQuery;
+
+export function getEvalQueryBody({
+ resultsField,
+ isTraining,
+ searchQuery,
+ ignoreDefaultQuery,
+}: {
+ resultsField: string;
+ isTraining: boolean;
+ searchQuery?: RegressionResultsSearchQuery;
+ ignoreDefaultQuery?: boolean;
+}) {
+ let query: RegressionResultsSearchQuery = {
+ term: { [`${resultsField}.is_training`]: { value: isTraining } },
+ };
+
+ if (searchQuery !== undefined && ignoreDefaultQuery === true) {
+ query = searchQuery;
+ } else if (isRegressionResultsSearchBoolQuery(searchQuery)) {
+ const searchQueryClone = cloneDeep(searchQuery);
+ searchQueryClone.bool.must.push(query);
+ query = searchQueryClone;
+ }
+ return query;
+}
export const loadEvalData = async ({
isTraining,
@@ -219,12 +265,16 @@ export const loadEvalData = async ({
dependentVariable,
resultsField,
predictionFieldName,
+ searchQuery,
+ ignoreDefaultQuery,
}: {
isTraining: boolean;
index: string;
dependentVariable: string;
resultsField: string;
predictionFieldName?: string;
+ searchQuery?: RegressionResultsSearchQuery;
+ ignoreDefaultQuery?: boolean;
}) => {
const results: LoadEvaluateResult = { success: false, eval: null, error: null };
const defaultPredictionField = `${dependentVariable}_prediction`;
@@ -232,7 +282,7 @@ export const loadEvalData = async ({
predictionFieldName ? predictionFieldName : defaultPredictionField
}`;
- const query = { term: { [`${resultsField}.is_training`]: { value: isTraining } } };
+ const query = getEvalQueryBody({ resultsField, isTraining, searchQuery, ignoreDefaultQuery });
const config = {
index,
diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_index.scss b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_index.scss
new file mode 100644
index 0000000000000..bb948785d3efa
--- /dev/null
+++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_index.scss
@@ -0,0 +1 @@
+@import 'regression_exploration';
diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_regression_exploration.scss b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_regression_exploration.scss
new file mode 100644
index 0000000000000..edcc9870ff93b
--- /dev/null
+++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/_regression_exploration.scss
@@ -0,0 +1,3 @@
+.mlDataFrameAnalyticsRegression__evaluateStat {
+ padding-top: $euiSizeL;
+}
diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx
index d0633d586063a..9765192f0e446 100644
--- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx
+++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx
@@ -62,6 +62,29 @@ export const ErrorCallout: FC = ({ error }) => {
);
+ } else if (error.includes('userProvidedQueryBuilder')) {
+ // query bar syntax is incorrect
+ errorCallout = (
+
+
+ {i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody',
+ {
+ defaultMessage:
+ 'The query syntax is invalid and returned no results. Please check the query syntax and try again.',
+ }
+ )}
+
+
+ );
}
return {errorCallout};
diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx
index 20ab6678da820..8bb44da74087c 100644
--- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx
+++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx
@@ -6,7 +6,8 @@
import React, { FC, Fragment, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiStat, EuiTitle } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { ErrorCallout } from './error_callout';
import {
getValuesFromResponse,
@@ -16,33 +17,42 @@ import {
Eval,
DataFrameAnalyticsConfig,
} from '../../../../common';
+import { ml } from '../../../../../services/ml_api_service';
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
+import { EvaluateStat } from './evaluate_stat';
+import {
+ getEvalQueryBody,
+ isRegressionResultsSearchBoolQuery,
+ RegressionResultsSearchQuery,
+} from '../../../../common/analytics';
+import { SearchQuery } from './use_explore_data';
interface Props {
jobConfig: DataFrameAnalyticsConfig;
jobStatus: DATA_FRAME_TASK_STATE;
+ searchQuery: RegressionResultsSearchQuery;
+}
+
+interface TrackTotalHitsSearchResponse {
+ hits: {
+ total: {
+ value: number;
+ relation: string;
+ };
+ hits: any[];
+ };
}
-const meanSquaredErrorText = i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText',
- {
- defaultMessage: 'Mean squared error',
- }
-);
-const rSquaredText = i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText',
- {
- defaultMessage: 'R squared',
- }
-);
const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null };
-export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => {
+export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) => {
const [trainingEval, setTrainingEval] = useState(defaultEval);
const [generalizationEval, setGeneralizationEval] = useState(defaultEval);
const [isLoadingTraining, setIsLoadingTraining] = useState(false);
const [isLoadingGeneralization, setIsLoadingGeneralization] = useState(false);
+ const [trainingDocsCount, setTrainingDocsCount] = useState(null);
+ const [generalizationDocsCount, setGeneralizationDocsCount] = useState(null);
const index = jobConfig.dest.index;
const dependentVariable = getDependentVar(jobConfig.analysis);
@@ -50,9 +60,42 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => {
// default is 'ml'
const resultsField = jobConfig.dest.results_field;
- const loadData = async () => {
+ const loadDocsCount = async ({
+ ignoreDefaultQuery = true,
+ isTraining,
+ }: {
+ ignoreDefaultQuery?: boolean;
+ isTraining: boolean;
+ }): Promise<{
+ docsCount: number | null;
+ success: boolean;
+ }> => {
+ const query = getEvalQueryBody({ resultsField, isTraining, ignoreDefaultQuery, searchQuery });
+
+ try {
+ const body: SearchQuery = {
+ track_total_hits: true,
+ query,
+ };
+
+ const resp: TrackTotalHitsSearchResponse = await ml.esSearch({
+ index: jobConfig.dest.index,
+ size: 0,
+ body,
+ });
+
+ const docsCount = resp.hits.total && resp.hits.total.value;
+ return { docsCount, success: true };
+ } catch (e) {
+ return {
+ docsCount: null,
+ success: false,
+ };
+ }
+ };
+
+ const loadGeneralizationData = async (ignoreDefaultQuery: boolean = true) => {
setIsLoadingGeneralization(true);
- setIsLoadingTraining(true);
const genErrorEval = await loadEvalData({
isTraining: false,
@@ -60,6 +103,8 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => {
dependentVariable,
resultsField,
predictionFieldName,
+ searchQuery,
+ ignoreDefaultQuery,
});
if (genErrorEval.success === true && genErrorEval.eval) {
@@ -78,6 +123,10 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => {
error: genErrorEval.error,
});
}
+ };
+
+ const loadTrainingData = async (ignoreDefaultQuery: boolean = true) => {
+ setIsLoadingTraining(true);
const trainingErrorEval = await loadEvalData({
isTraining: true,
@@ -85,6 +134,8 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => {
dependentVariable,
resultsField,
predictionFieldName,
+ searchQuery,
+ ignoreDefaultQuery,
});
if (trainingErrorEval.success === true && trainingErrorEval.eval) {
@@ -100,14 +151,89 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => {
setTrainingEval({
meanSquaredError: '',
rSquared: '',
- error: genErrorEval.error,
+ error: trainingErrorEval.error,
+ });
+ }
+ };
+
+ const loadData = async ({
+ isTrainingClause,
+ }: {
+ isTrainingClause?: { query: string; operator: string };
+ }) => {
+ // searchBar query is filtering for testing data
+ if (isTrainingClause !== undefined && isTrainingClause.query === 'false') {
+ loadGeneralizationData();
+
+ const docsCountResp = await loadDocsCount({ isTraining: false });
+ if (docsCountResp.success === true) {
+ setGeneralizationDocsCount(docsCountResp.docsCount);
+ } else {
+ setGeneralizationDocsCount(null);
+ }
+
+ setTrainingDocsCount(0);
+ setTrainingEval({
+ meanSquaredError: '--',
+ rSquared: '--',
+ error: null,
});
+ } else if (isTrainingClause !== undefined && isTrainingClause.query === 'true') {
+ // searchBar query is filtering for training data
+ loadTrainingData();
+
+ const docsCountResp = await loadDocsCount({ isTraining: true });
+ if (docsCountResp.success === true) {
+ setTrainingDocsCount(docsCountResp.docsCount);
+ } else {
+ setTrainingDocsCount(null);
+ }
+
+ setGeneralizationDocsCount(0);
+ setGeneralizationEval({
+ meanSquaredError: '--',
+ rSquared: '--',
+ error: null,
+ });
+ } else {
+ // No is_training clause/filter from search bar so load both
+ loadGeneralizationData(false);
+ const genDocsCountResp = await loadDocsCount({
+ ignoreDefaultQuery: false,
+ isTraining: false,
+ });
+ if (genDocsCountResp.success === true) {
+ setGeneralizationDocsCount(genDocsCountResp.docsCount);
+ } else {
+ setGeneralizationDocsCount(null);
+ }
+
+ loadTrainingData(false);
+ const trainDocsCountResp = await loadDocsCount({
+ ignoreDefaultQuery: false,
+ isTraining: true,
+ });
+ if (trainDocsCountResp.success === true) {
+ setTrainingDocsCount(trainDocsCountResp.docsCount);
+ } else {
+ setTrainingDocsCount(null);
+ }
}
};
useEffect(() => {
- loadData();
- }, []);
+ const hasIsTrainingClause =
+ isRegressionResultsSearchBoolQuery(searchQuery) &&
+ searchQuery.bool.must.filter(
+ (clause: any) => clause.match && clause.match[`${resultsField}.is_training`] !== undefined
+ );
+ const isTrainingClause =
+ hasIsTrainingClause &&
+ hasIsTrainingClause[0] &&
+ hasIsTrainingClause[0].match[`${resultsField}.is_training`];
+
+ loadData({ isTrainingClause });
+ }, [JSON.stringify(searchQuery)]);
return (
@@ -129,7 +255,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => {
-
+
{i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle',
@@ -139,27 +265,32 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => {
)}
+ {generalizationDocsCount !== null && (
+
+
+
+ )}
{generalizationEval.error !== null && }
{generalizationEval.error === null && (
-
-
@@ -167,7 +298,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => {
-
+
{i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle',
@@ -177,27 +308,32 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus }) => {
)}
+ {trainingDocsCount !== null && (
+
+
+
+ )}
{trainingEval.error !== null && }
{trainingEval.error === null && (
-
-
diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx
new file mode 100644
index 0000000000000..692a2afc729d5
--- /dev/null
+++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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 React, { FC } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+interface Props {
+ isLoading: boolean;
+ title: number | string;
+ isMSE: boolean;
+}
+
+const meanSquaredErrorText = i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText',
+ {
+ defaultMessage: 'Mean squared error',
+ }
+);
+const rSquaredText = i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText',
+ {
+ defaultMessage: 'R squared',
+ }
+);
+const meanSquaredErrorTooltipContent = i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent',
+ {
+ defaultMessage:
+ 'Measures how well the regression analysis model is performing. Mean squared sum of the difference between true and predicted values.',
+ }
+);
+const rSquaredTooltipContent = i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent',
+ {
+ defaultMessage:
+ 'Represents the goodness of fit. Measures how well the observed outcomes are replicated by the model.',
+ }
+);
+
+export const EvaluateStat: FC = ({ isLoading, isMSE, title }) => (
+
+
+
+
+
+
+
+
+);
diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx
index 7beea07f9502d..b188334934ae0 100644
--- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx
+++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx
@@ -12,6 +12,8 @@ import { DataFrameAnalyticsConfig } from '../../../../common';
import { EvaluatePanel } from './evaluate_panel';
import { ResultsTable } from './results_table';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
+import { defaultSearchQuery } from './use_explore_data';
+import { RegressionResultsSearchQuery } from '../../../../common/analytics';
interface GetDataFrameAnalyticsResponse {
count: number;
@@ -44,6 +46,7 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => {
const [jobConfig, setJobConfig] = useState(undefined);
const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false);
const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined);
+ const [searchQuery, setSearchQuery] = useState(defaultSearchQuery);
const loadJobConfig = async () => {
setIsLoadingJobConfig(true);
@@ -98,12 +101,16 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => {
{isLoadingJobConfig === true && jobConfig === undefined && }
{isLoadingJobConfig === false && jobConfig !== undefined && (
-
+
)}
{isLoadingJobConfig === true && jobConfig === undefined && }
{isLoadingJobConfig === false && jobConfig !== undefined && (
-
+
)}
);
diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx
index 5ba3b8ed45939..a1d4261d2cf32 100644
--- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx
+++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx
@@ -62,301 +62,280 @@ const PAGE_SIZE_OPTIONS = [5, 10, 25, 50];
interface Props {
jobConfig: DataFrameAnalyticsConfig;
jobStatus: DATA_FRAME_TASK_STATE;
+ setEvaluateSearchQuery: React.Dispatch>;
}
-export const ResultsTable: FC = React.memo(({ jobConfig, jobStatus }) => {
- const [pageIndex, setPageIndex] = useState(0);
- const [pageSize, setPageSize] = useState(25);
- const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]);
- const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false);
- const [searchQuery, setSearchQuery] = useState(defaultSearchQuery);
- const [searchError, setSearchError] = useState(undefined);
- const [searchString, setSearchString] = useState(undefined);
-
- function toggleColumnsPopover() {
- setColumnsPopoverVisible(!isColumnsPopoverVisible);
- }
+export const ResultsTable: FC = React.memo(
+ ({ jobConfig, jobStatus, setEvaluateSearchQuery }) => {
+ const [pageIndex, setPageIndex] = useState(0);
+ const [pageSize, setPageSize] = useState(25);
+ const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]);
+ const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false);
+ const [searchQuery, setSearchQuery] = useState(defaultSearchQuery);
+ const [searchError, setSearchError] = useState(undefined);
+ const [searchString, setSearchString] = useState(undefined);
+
+ function toggleColumnsPopover() {
+ setColumnsPopoverVisible(!isColumnsPopoverVisible);
+ }
- function closeColumnsPopover() {
- setColumnsPopoverVisible(false);
- }
+ function closeColumnsPopover() {
+ setColumnsPopoverVisible(false);
+ }
- function toggleColumn(column: EsFieldName) {
- if (tableItems.length > 0 && jobConfig !== undefined) {
- // spread to a new array otherwise the component wouldn't re-render
- setSelectedFields([...toggleSelectedField(selectedFields, column)]);
+ function toggleColumn(column: EsFieldName) {
+ if (tableItems.length > 0 && jobConfig !== undefined) {
+ // spread to a new array otherwise the component wouldn't re-render
+ setSelectedFields([...toggleSelectedField(selectedFields, column)]);
+ }
}
- }
- const {
- errorMessage,
- loadExploreData,
- sortField,
- sortDirection,
- status,
- tableItems,
- } = useExploreData(jobConfig, selectedFields, setSelectedFields);
-
- let docFields: EsFieldName[] = [];
- let docFieldsCount = 0;
- if (tableItems.length > 0) {
- docFields = Object.keys(tableItems[0]);
- docFields.sort((a, b) => sortRegressionResultsFields(a, b, jobConfig));
- docFieldsCount = docFields.length;
- }
+ const {
+ errorMessage,
+ loadExploreData,
+ sortField,
+ sortDirection,
+ status,
+ tableItems,
+ } = useExploreData(jobConfig, selectedFields, setSelectedFields);
+
+ let docFields: EsFieldName[] = [];
+ let docFieldsCount = 0;
+ if (tableItems.length > 0) {
+ docFields = Object.keys(tableItems[0]);
+ docFields.sort((a, b) => sortRegressionResultsFields(a, b, jobConfig));
+ docFieldsCount = docFields.length;
+ }
- const columns: ColumnType[] = [];
-
- if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) {
- columns.push(
- ...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => {
- const column: ColumnType = {
- field: k,
- name: k,
- sortable: true,
- truncateText: true,
- };
-
- const render = (d: any, fullItem: EsDoc) => {
- if (Array.isArray(d) && d.every(item => typeof item === 'string')) {
- // If the cells data is an array of strings, return as a comma separated list.
- // The list will get limited to 5 items with `…` at the end if there's more in the original array.
- return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`;
- } else if (Array.isArray(d)) {
- // If the cells data is an array of e.g. objects, display a 'array' badge with a
- // tooltip that explains that this type of field is not supported in this table.
- return (
-
-
- {i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent',
+ const columns: ColumnType[] = [];
+
+ if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) {
+ columns.push(
+ ...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => {
+ const column: ColumnType = {
+ field: k,
+ name: k,
+ sortable: true,
+ truncateText: true,
+ };
+
+ const render = (d: any, fullItem: EsDoc) => {
+ if (Array.isArray(d) && d.every(item => typeof item === 'string')) {
+ // If the cells data is an array of strings, return as a comma separated list.
+ // The list will get limited to 5 items with `…` at the end if there's more in the original array.
+ return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`;
+ } else if (Array.isArray(d)) {
+ // If the cells data is an array of e.g. objects, display a 'array' badge with a
+ // tooltip that explains that this type of field is not supported in this table.
+ return (
+
-
- );
- } else if (typeof d === 'object' && d !== null) {
- // If the cells data is an object, display a 'object' badge with a
- // tooltip that explains that this type of field is not supported in this table.
- return (
-
-
- {i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.indexObjectBadgeContent',
+ >
+
+ {i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.indexArrayBadgeContent',
+ {
+ defaultMessage: 'array',
+ }
+ )}
+
+
+ );
+ } else if (typeof d === 'object' && d !== null) {
+ // If the cells data is an object, display a 'object' badge with a
+ // tooltip that explains that this type of field is not supported in this table.
+ return (
+
-
- );
- }
-
- return d;
- };
+ >
+
+ {i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.indexObjectBadgeContent',
+ {
+ defaultMessage: 'object',
+ }
+ )}
+
+
+ );
+ }
- let columnType;
+ return d;
+ };
- if (tableItems.length > 0) {
- columnType = typeof tableItems[0][k];
- }
+ let columnType;
- if (typeof columnType !== 'undefined') {
- switch (columnType) {
- case 'boolean':
- column.dataType = 'boolean';
- break;
- case 'Date':
- column.align = 'right';
- column.render = (d: any) =>
- formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
- break;
- case 'number':
- column.dataType = 'number';
- column.render = render;
- break;
- default:
- column.render = render;
- break;
+ if (tableItems.length > 0) {
+ columnType = typeof tableItems[0][k];
}
- } else {
- column.render = render;
- }
- return column;
- })
- );
- }
+ if (typeof columnType !== 'undefined') {
+ switch (columnType) {
+ case 'boolean':
+ column.dataType = 'boolean';
+ break;
+ case 'Date':
+ column.align = 'right';
+ column.render = (d: any) =>
+ formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
+ break;
+ case 'number':
+ column.dataType = 'number';
+ column.render = render;
+ break;
+ default:
+ column.render = render;
+ break;
+ }
+ } else {
+ column.render = render;
+ }
- useEffect(() => {
- if (jobConfig !== undefined) {
- const predictedFieldName = getPredictedFieldName(
- jobConfig.dest.results_field,
- jobConfig.analysis
+ return column;
+ })
);
- const predictedFieldSelected = selectedFields.includes(predictedFieldName);
-
- const field = predictedFieldSelected ? predictedFieldName : selectedFields[0];
- const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
- loadExploreData({ field, direction, searchQuery });
}
- }, [JSON.stringify(searchQuery)]);
-
- useEffect(() => {
- // by default set the sorting to descending on the prediction field (`_prediction`).
- // if that's not available sort ascending on the first column.
- // also check if the current sorting field is still available.
- if (jobConfig !== undefined && columns.length > 0 && !selectedFields.includes(sortField)) {
- const predictedFieldName = getPredictedFieldName(
- jobConfig.dest.results_field,
- jobConfig.analysis
- );
- const predictedFieldSelected = selectedFields.includes(predictedFieldName);
- const field = predictedFieldSelected ? predictedFieldName : selectedFields[0];
- const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
- loadExploreData({ field, direction, searchQuery });
+ useEffect(() => {
+ if (jobConfig !== undefined) {
+ const predictedFieldName = getPredictedFieldName(
+ jobConfig.dest.results_field,
+ jobConfig.analysis
+ );
+ const predictedFieldSelected = selectedFields.includes(predictedFieldName);
+
+ const field = predictedFieldSelected ? predictedFieldName : selectedFields[0];
+ const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
+ loadExploreData({ field, direction, searchQuery });
+ }
+ }, [JSON.stringify(searchQuery)]);
+
+ useEffect(() => {
+ // by default set the sorting to descending on the prediction field (`_prediction`).
+ // if that's not available sort ascending on the first column.
+ // also check if the current sorting field is still available.
+ if (jobConfig !== undefined && columns.length > 0 && !selectedFields.includes(sortField)) {
+ const predictedFieldName = getPredictedFieldName(
+ jobConfig.dest.results_field,
+ jobConfig.analysis
+ );
+ const predictedFieldSelected = selectedFields.includes(predictedFieldName);
+
+ const field = predictedFieldSelected ? predictedFieldName : selectedFields[0];
+ const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
+ loadExploreData({ field, direction, searchQuery });
+ }
+ }, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]);
+
+ let sorting: SortingPropType = false;
+ let onTableChange;
+
+ if (columns.length > 0 && sortField !== '' && sortField !== undefined) {
+ sorting = {
+ sort: {
+ field: sortField,
+ direction: sortDirection,
+ },
+ };
+
+ onTableChange = ({
+ page = { index: 0, size: 10 },
+ sort = { field: sortField, direction: sortDirection },
+ }: OnTableChangeArg) => {
+ const { index, size } = page;
+ setPageIndex(index);
+ setPageSize(size);
+
+ if (sort.field !== sortField || sort.direction !== sortDirection) {
+ loadExploreData({ ...sort, searchQuery });
+ }
+ };
}
- }, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]);
- let sorting: SortingPropType = false;
- let onTableChange;
-
- if (columns.length > 0 && sortField !== '' && sortField !== undefined) {
- sorting = {
- sort: {
- field: sortField,
- direction: sortDirection,
- },
+ const pagination = {
+ initialPageIndex: pageIndex,
+ initialPageSize: pageSize,
+ totalItemCount: tableItems.length,
+ pageSizeOptions: PAGE_SIZE_OPTIONS,
+ hidePerPageOptions: false,
};
- onTableChange = ({
- page = { index: 0, size: 10 },
- sort = { field: sortField, direction: sortDirection },
- }: OnTableChangeArg) => {
- const { index, size } = page;
- setPageIndex(index);
- setPageSize(size);
-
- if (sort.field !== sortField || sort.direction !== sortDirection) {
- loadExploreData({ ...sort, searchQuery });
+ const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => {
+ if (error) {
+ setSearchError(error.message);
+ } else {
+ try {
+ const esQueryDsl = Query.toESQuery(query);
+ setSearchQuery(esQueryDsl);
+ setSearchString(query.text);
+ setSearchError(undefined);
+ // set query for use in evaluate panel
+ setEvaluateSearchQuery(esQueryDsl);
+ } catch (e) {
+ setSearchError(e.toString());
+ }
}
};
- }
- const pagination = {
- initialPageIndex: pageIndex,
- initialPageSize: pageSize,
- totalItemCount: tableItems.length,
- pageSizeOptions: PAGE_SIZE_OPTIONS,
- hidePerPageOptions: false,
- };
-
- const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => {
- if (error) {
- setSearchError(error.message);
- } else {
- try {
- const esQueryDsl = Query.toESQuery(query);
- setSearchQuery(esQueryDsl);
- setSearchString(query.text);
- setSearchError(undefined);
- } catch (e) {
- setSearchError(e.toString());
- }
- }
- };
-
- const search = {
- onChange: onQueryChange,
- defaultQuery: searchString,
- box: {
- incremental: false,
- placeholder: i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder',
- {
- defaultMessage: 'E.g. avg>0.5',
- }
- ),
- },
- filters: [
- {
- type: 'field_value_toggle_group',
- field: `${jobConfig.dest.results_field}.is_training`,
- items: [
- {
- value: false,
- name: i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel',
- {
- defaultMessage: 'Testing',
- }
- ),
- },
+ const search = {
+ onChange: onQueryChange,
+ defaultQuery: searchString,
+ box: {
+ incremental: false,
+ placeholder: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder',
{
- value: true,
- name: i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel',
- {
- defaultMessage: 'Training',
- }
- ),
- },
- ],
+ defaultMessage: 'E.g. avg>0.5',
+ }
+ ),
},
- ],
- };
-
- if (jobConfig === undefined) {
- return null;
- }
-
- if (status === INDEX_STATUS.ERROR) {
- return (
-
-
-
-
-
-
- {getTaskStateBadge(jobStatus)}
-
-
-
- {errorMessage}
-
-
- );
- }
+ filters: [
+ {
+ type: 'field_value_toggle_group',
+ field: `${jobConfig.dest.results_field}.is_training`,
+ items: [
+ {
+ value: false,
+ name: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel',
+ {
+ defaultMessage: 'Testing',
+ }
+ ),
+ },
+ {
+ value: true,
+ name: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel',
+ {
+ defaultMessage: 'Training',
+ }
+ ),
+ },
+ ],
+ },
+ ],
+ };
- return (
-
-
-
+ if (jobConfig === undefined) {
+ return null;
+ }
+ // if it's a searchBar syntax error leave the table visible so they can try again
+ if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) {
+ return (
+
@@ -365,103 +344,136 @@ export const ResultsTable: FC = React.memo(({ jobConfig, jobStatus }) =>
{getTaskStateBadge(jobStatus)}
-
-
-
-
- {docFieldsCount > MAX_COLUMNS && (
+
+ {errorMessage}
+
+
+ );
+ }
+
+ const tableError =
+ status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception')
+ ? errorMessage
+ : searchError;
+
+ return (
+
+
+
+
+
+
+
+
+ {getTaskStateBadge(jobStatus)}
+
+
+
+
+
+
+ {docFieldsCount > MAX_COLUMNS && (
+
+ {i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection',
+ {
+ defaultMessage:
+ '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected',
+ values: { selectedFieldsLength: selectedFields.length, docFieldsCount },
+ }
+ )}
+
+ )}
+
+
- {i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection',
- {
- defaultMessage:
- '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected',
- values: { selectedFieldsLength: selectedFields.length, docFieldsCount },
+
}
- )}
-
- )}
-
-
-
-
+
+ {i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle',
{
- defaultMessage: 'Select columns',
+ defaultMessage: 'Select fields',
}
)}
- />
+
+
+ {docFields.map(d => (
+ toggleColumn(d)}
+ disabled={selectedFields.includes(d) && selectedFields.length === 1}
+ />
+ ))}
+
+
+
+
+
+
+
+ {status === INDEX_STATUS.LOADING && }
+ {status !== INDEX_STATUS.LOADING && (
+
+ )}
+ {(columns.length > 0 || searchQuery !== defaultSearchQuery) && (
+
+ {tableItems.length === SEARCH_SIZE && (
+
-
- {i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle',
- {
- defaultMessage: 'Select fields',
- }
- )}
-
-
- {docFields.map(d => (
- toggleColumn(d)}
- disabled={selectedFields.includes(d) && selectedFields.length === 1}
- />
- ))}
-
-
-
-
-
-
-
- {status === INDEX_STATUS.LOADING && }
- {status !== INDEX_STATUS.LOADING && (
-
- )}
- {(columns.length > 0 || searchQuery !== defaultSearchQuery) && (
-
-
+
+
)}
- >
-
-
-
-
-
- )}
-
- );
-});
+
+
+
+ )}
+
+ );
+ }
+);
diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts
index 3e7266eb89474..332451c6e4d7a 100644
--- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts
+++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts
@@ -49,7 +49,8 @@ export interface UseExploreDataReturnType {
tableItems: TableItem[];
}
-interface SearchQuery {
+export interface SearchQuery {
+ track_total_hits?: boolean;
query: SavedSearchQuery;
sort?: any;
}