[ML] DF Analytics Classification exploration: replace table with data grid (#63757)

* update classification result to use datagrid

* consider isTraining in docCount fetch

* fix translations
This commit is contained in:
Melissa Alvarez 2020-04-20 11:51:53 -04:00 committed by GitHub
parent 32c6fd777f
commit 784b8beb2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 421 additions and 514 deletions

View file

@ -52,7 +52,7 @@ export interface ClassificationAnalysis {
classification: Classification;
}
export interface LoadRegressionExploreDataArg {
export interface LoadExploreDataArg {
filterByIsTraining?: boolean;
searchQuery: SavedSearchQuery;
}
@ -409,11 +409,11 @@ export function getEvalQueryBody({
ignoreDefaultQuery,
}: {
resultsField: string;
isTraining: boolean;
isTraining?: boolean;
searchQuery?: ResultsSearchQuery;
ignoreDefaultQuery?: boolean;
}) {
let query;
let query: any;
const trainingQuery: ResultsSearchQuery = {
term: { [`${resultsField}.is_training`]: { value: isTraining } },
@ -426,19 +426,25 @@ export function getEvalQueryBody({
searchQueryClone.bool.must = [];
}
searchQueryClone.bool.must.push(trainingQuery);
if (isTraining !== undefined) {
searchQueryClone.bool.must.push(trainingQuery);
}
query = searchQueryClone;
} else if (isQueryStringQuery(searchQueryClone)) {
query = {
bool: {
must: [searchQueryClone, trainingQuery],
must: [searchQueryClone],
},
};
if (isTraining !== undefined) {
query.bool.must.push(trainingQuery);
}
} else {
// Not a bool or string query so we need to create it so can add the trainingQuery
query = {
bool: {
must: [trainingQuery],
must: isTraining !== undefined ? [trainingQuery] : [],
},
};
}
@ -456,7 +462,7 @@ interface EvaluateMetrics {
}
interface LoadEvalDataConfig {
isTraining: boolean;
isTraining?: boolean;
index: string;
dependentVariable: string;
resultsField: string;
@ -535,7 +541,7 @@ interface TrackTotalHitsSearchResponse {
interface LoadDocsCountConfig {
ignoreDefaultQuery?: boolean;
isTraining: boolean;
isTraining?: boolean;
searchQuery: SavedSearchQuery;
resultsField: string;
destIndex: string;

View file

@ -0,0 +1,135 @@
/*
* 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, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui';
import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common';
import { mlFieldFormatService } from '../../../../../services/field_format_service';
import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public';
const PAGE_SIZE_OPTIONS = [5, 10, 25, 50];
type Pagination = Pick<EuiDataGridPaginationProps, 'pageIndex' | 'pageSize'>;
type TableItem = Record<string, any>;
interface ExplorationDataGridProps {
colorRange?: (d: number) => string;
columns: any[];
indexPattern: IndexPattern;
pagination: Pagination;
resultsField: string;
rowCount: number;
selectedFields: string[];
setPagination: Dispatch<SetStateAction<Pagination>>;
setSelectedFields: Dispatch<SetStateAction<string[]>>;
setSortingColumns: Dispatch<SetStateAction<EuiDataGridSorting['columns']>>;
sortingColumns: EuiDataGridSorting['columns'];
tableItems: TableItem[];
}
export const ClassificationExplorationDataGrid: FC<ExplorationDataGridProps> = ({
columns,
indexPattern,
pagination,
resultsField,
rowCount,
selectedFields,
setPagination,
setSelectedFields,
setSortingColumns,
sortingColumns,
tableItems,
}) => {
const renderCellValue = useMemo(() => {
return ({ rowIndex, columnId }: { rowIndex: number; columnId: string; setCellProps: any }) => {
const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize;
const fullItem = tableItems[adjustedRowIndex];
if (fullItem === undefined) {
return null;
}
let format: any;
if (indexPattern !== undefined) {
format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, '');
}
const cellValue =
fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined
? fullItem[columnId]
: null;
if (format !== undefined) {
return format.convert(cellValue, 'text');
}
if (typeof cellValue === 'string' || cellValue === null) {
return cellValue;
}
if (typeof cellValue === 'boolean') {
return cellValue ? 'true' : 'false';
}
if (typeof cellValue === 'object' && cellValue !== null) {
return JSON.stringify(cellValue);
}
return cellValue;
};
}, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]);
const onChangeItemsPerPage = useCallback(
pageSize => {
setPagination(p => {
const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize);
return { pageIndex, pageSize };
});
},
[setPagination]
);
const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [
setPagination,
]);
const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]);
return (
<EuiDataGrid
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.classificationExploration.dataGridAriaLabel',
{
defaultMessage: 'Classification results table',
}
)}
columns={columns}
columnVisibility={{
visibleColumns: selectedFields,
setVisibleColumns: setSelectedFields,
}}
gridStyle={euiDataGridStyle}
rowCount={rowCount}
renderCellValue={renderCellValue}
sorting={{ columns: sortingColumns, onSort }}
toolbarVisibility={euiDataGridToolbarSettings}
pagination={{
...pagination,
pageSizeOptions: PAGE_SIZE_OPTIONS,
onChangeItemsPerPage,
onChangePage,
}}
/>
);
};

View file

@ -117,13 +117,7 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
const resultsField = jobConfig.dest.results_field;
let requiresKeyword = false;
const loadData = async ({
isTrainingClause,
ignoreDefaultQuery = true,
}: {
isTrainingClause: { query: string; operator: string };
ignoreDefaultQuery?: boolean;
}) => {
const loadData = async ({ isTraining }: { isTraining: boolean | undefined }) => {
setIsLoading(true);
try {
@ -134,19 +128,18 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
}
const evalData = await loadEvalData({
isTraining: false,
isTraining,
index,
dependentVariable,
resultsField,
predictionFieldName,
searchQuery,
ignoreDefaultQuery,
jobType: ANALYSIS_CONFIG_TYPE.CLASSIFICATION,
requiresKeyword,
});
const docsCountResp = await loadDocsCount({
isTraining: false,
isTraining,
searchQuery,
resultsField,
destIndex: jobConfig.dest.index,
@ -225,29 +218,46 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
}, [confusionMatrixData]);
useEffect(() => {
const hasIsTrainingClause =
isResultsSearchBoolQuery(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`];
let isTraining: boolean | undefined;
const query =
isResultsSearchBoolQuery(searchQuery) && (searchQuery.bool.should || searchQuery.bool.filter);
const noTrainingQuery = isTrainingClause === false || isTrainingClause === undefined;
if (query !== undefined && query !== false) {
for (let i = 0; i < query.length; i++) {
const clause = query[i];
if (noTrainingQuery) {
if (clause.match && clause.match[`${resultsField}.is_training`] !== undefined) {
isTraining = clause.match[`${resultsField}.is_training`];
break;
} else if (
clause.bool &&
(clause.bool.should !== undefined || clause.bool.filter !== undefined)
) {
const innerQuery = clause.bool.should || clause.bool.filter;
if (innerQuery !== undefined) {
for (let j = 0; j < innerQuery.length; j++) {
const innerClause = innerQuery[j];
if (
innerClause.match &&
innerClause.match[`${resultsField}.is_training`] !== undefined
) {
isTraining = innerClause.match[`${resultsField}.is_training`];
break;
}
}
}
}
}
}
if (isTraining === undefined) {
setDataSubsetTitle(SUBSET_TITLE.ENTIRE);
} else {
setDataSubsetTitle(
isTrainingClause && isTrainingClause.query === 'true'
? SUBSET_TITLE.TRAINING
: SUBSET_TITLE.TESTING
isTraining && isTraining === true ? SUBSET_TITLE.TRAINING : SUBSET_TITLE.TESTING
);
}
loadData({ isTrainingClause });
loadData({ isTraining });
}, [JSON.stringify(searchQuery)]);
const renderCellValue = ({

View file

@ -4,71 +4,39 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, FC, useEffect, useState } from 'react';
import moment from 'moment-timezone';
import React, { Fragment, FC, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiBadge,
EuiButtonIcon,
EuiCallOut,
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiPanel,
EuiPopover,
EuiPopoverTitle,
EuiProgress,
EuiSpacer,
EuiText,
EuiToolTip,
Query,
} from '@elastic/eui';
import { Query as QueryType } from '../../../analytics_management/components/analytics_list/common';
import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public';
import { mlFieldFormatService } from '../../../../../services/field_format_service';
import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public';
import {
ColumnType,
mlInMemoryTableBasicFactory,
OnTableChangeArg,
SortingPropType,
SORT_DIRECTION,
} from '../../../../../components/ml_in_memory_table';
import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils';
import { Field } from '../../../../../../../common/types/fields';
import { SavedSearchQuery } from '../../../../../contexts/ml';
import {
BASIC_NUMERICAL_TYPES,
EXTENDED_NUMERICAL_TYPES,
isKeywordAndTextType,
sortRegressionResultsFields,
} from '../../../../common/fields';
import {
toggleSelectedField,
EsDoc,
DataFrameAnalyticsConfig,
EsFieldName,
MAX_COLUMNS,
getPredictedFieldName,
INDEX_STATUS,
SEARCH_SIZE,
defaultSearchQuery,
getDependentVar,
} from '../../../../common';
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
import { useExploreData, TableItem } from './use_explore_data';
import { useExploreData } from './use_explore_data'; // TableItem
import { ExplorationTitle } from './classification_exploration';
const PAGE_SIZE_OPTIONS = [5, 10, 25, 50];
const MlInMemoryTableBasic = mlInMemoryTableBasicFactory<TableItem>();
import { ClassificationExplorationDataGrid } from './classification_exploration_data_grid';
import { ExplorationQueryBar } from '../exploration_query_bar';
const showingDocs = i18n.translate(
'xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText',
@ -94,307 +62,65 @@ interface Props {
export const ResultsTable: FC<Props> = React.memo(
({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(25);
const [selectedFields, setSelectedFields] = useState([] as Field[]);
const [docFields, setDocFields] = useState([] as Field[]);
const [depVarType, setDepVarType] = useState<ES_FIELD_TYPES | undefined>(undefined);
const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false);
const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery);
const [searchError, setSearchError] = useState<any>(undefined);
const [searchString, setSearchString] = useState<string | undefined>(undefined);
const predictedFieldName = getPredictedFieldName(
jobConfig.dest.results_field,
jobConfig.analysis
);
const dependentVariable = getDependentVar(jobConfig.analysis);
function toggleColumnsPopover() {
setColumnsPopoverVisible(!isColumnsPopoverVisible);
}
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, jobConfig.dest.results_field, depVarType),
]);
}
}
const needsDestIndexFields = indexPattern && indexPattern.title === jobConfig.source.index[0];
const resultsField = jobConfig.dest.results_field;
const {
errorMessage,
loadExploreData,
sortField,
sortDirection,
status,
tableItems,
} = useExploreData(
jobConfig,
needsDestIndexFields,
fieldTypes,
pagination,
searchQuery,
selectedFields,
rowCount,
setPagination,
setSearchQuery,
setSelectedFields,
setDocFields,
setDepVarType
);
setSortingColumns,
sortingColumns,
status,
tableFields,
tableItems,
} = useExploreData(jobConfig, needsDestIndexFields);
const columns: Array<ColumnType<TableItem>> = selectedFields
.sort(({ name: a }, { name: b }) => sortRegressionResultsFields(a, b, jobConfig))
.map(field => {
const { type } = field;
let format: any;
useEffect(() => {
setEvaluateSearchQuery(searchQuery);
}, [JSON.stringify(searchQuery)]);
if (indexPattern !== undefined) {
format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, field.id, '');
}
const columns = tableFields
.sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig))
.map((field: any) => {
// Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json']
// To fall back to the default string schema it needs to be undefined.
let schema;
let isSortable = true;
const type = fieldTypes[field];
const isNumber =
type !== undefined &&
(BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type));
const column: ColumnType<TableItem> = {
field: field.name,
name: field.name,
sortable: true,
truncateText: true,
};
const render = (d: any, fullItem: EsDoc) => {
if (format !== undefined) {
d = format.convert(d, 'text');
return d;
}
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 (
<EuiToolTip
content={i18n.translate(
'xpack.ml.dataframe.analytics.classificationExploration.indexArrayToolTipContent',
{
defaultMessage:
'The full content of this array based column cannot be displayed.',
}
)}
>
<EuiBadge>
{i18n.translate(
'xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent',
{
defaultMessage: 'array',
}
)}
</EuiBadge>
</EuiToolTip>
);
}
return d;
};
if (isNumber) {
column.dataType = 'number';
column.render = render;
} else if (typeof type !== 'undefined') {
switch (type) {
case ES_FIELD_TYPES.BOOLEAN:
column.dataType = ES_FIELD_TYPES.BOOLEAN;
column.render = d => (d ? 'true' : 'false');
break;
case ES_FIELD_TYPES.DATE:
column.align = 'right';
if (format !== undefined) {
column.render = render;
} else {
column.render = (d: any) => {
if (d !== undefined) {
return formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
}
return d;
};
}
break;
default:
column.render = render;
break;
}
} else {
column.render = render;
schema = 'numeric';
}
return column;
switch (type) {
case 'date':
schema = 'datetime';
break;
case 'geo_point':
schema = 'json';
break;
case 'boolean':
schema = 'boolean';
break;
}
if (field === `${resultsField}.feature_importance`) {
isSortable = false;
}
return { id: field, schema, isSortable };
});
const docFieldsCount = docFields.length;
useEffect(() => {
if (
jobConfig !== undefined &&
columns.length > 0 &&
selectedFields.length > 0 &&
sortField !== undefined &&
sortDirection !== undefined &&
selectedFields.some(field => field.name === sortField)
) {
let field = sortField;
// If sorting by predictedField use dependentVar type
if (predictedFieldName === sortField) {
field = dependentVariable;
}
const requiresKeyword = isKeywordAndTextType(field);
loadExploreData({
field: sortField,
direction: sortDirection,
searchQuery,
requiresKeyword,
});
}
}, [JSON.stringify(searchQuery)]);
useEffect(() => {
// By default set sorting to descending on the prediction field (`<dependent_varible or prediction_field_name>_prediction`).
// if that's not available sort ascending on the first column. Check if the current sorting field is still available.
if (
jobConfig !== undefined &&
columns.length > 0 &&
selectedFields.length > 0 &&
!selectedFields.some(field => field.name === sortField)
) {
const predictedFieldSelected = selectedFields.some(
field => field.name === predictedFieldName
);
// CHECK IF keyword suffix is needed (if predicted field is selected we have to check the dependent variable type)
let sortByField = predictedFieldSelected ? dependentVariable : selectedFields[0].name;
const requiresKeyword = isKeywordAndTextType(sortByField);
sortByField = predictedFieldSelected ? predictedFieldName : sortByField;
const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
loadExploreData({ field: sortByField, direction, searchQuery, requiresKeyword });
}
}, [
jobConfig,
columns.length,
selectedFields.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) {
let field = sort.field;
// If sorting by predictedField use depVar for type check
if (predictedFieldName === sort.field) {
field = dependentVariable;
}
loadExploreData({
...sort,
searchQuery,
requiresKeyword: isKeywordAndTextType(field),
});
}
};
}
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);
// set query for use in evaluate panel
setEvaluateSearchQuery(esQueryDsl);
} 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',
}
),
},
{
value: true,
name: i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel',
{
defaultMessage: 'Training',
}
),
},
],
},
],
};
const docFieldsCount = tableFields.length;
if (jobConfig === undefined) {
return null;
@ -426,11 +152,6 @@ export const ResultsTable: FC<Props> = React.memo(
);
}
const tableError =
status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception')
? errorMessage
: searchError;
return (
<EuiPanel
grow={false}
@ -456,7 +177,7 @@ export const ResultsTable: FC<Props> = React.memo(
{docFieldsCount > MAX_COLUMNS && (
<EuiText size="s">
{i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection',
'xpack.ml.dataframe.analytics.classificationExploration.fieldSelection',
{
defaultMessage:
'{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected',
@ -466,52 +187,6 @@ export const ResultsTable: FC<Props> = React.memo(
</EuiText>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
<EuiPopover
id="popover"
button={
<EuiButtonIcon
iconType="gear"
onClick={toggleColumnsPopover}
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.selectColumnsAriaLabel',
{
defaultMessage: 'Select columns',
}
)}
/>
}
isOpen={isColumnsPopoverVisible}
closePopover={closeColumnsPopover}
ownFocus
>
<EuiPopoverTitle>
{i18n.translate(
'xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle',
{
defaultMessage: 'Select fields',
}
)}
</EuiPopoverTitle>
<div style={{ maxHeight: '400px', overflowY: 'scroll' }}>
{docFields.map(({ name }) => (
<EuiCheckbox
key={name}
id={name}
label={name}
checked={selectedFields.some(field => field.name === name)}
onChange={() => toggleColumn(name)}
disabled={
selectedFields.some(field => field.name === name) &&
selectedFields.length === 1
}
/>
))}
</div>
</EuiPopover>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
@ -520,28 +195,39 @@ export const ResultsTable: FC<Props> = React.memo(
<EuiProgress size="xs" color="accent" max={1} value={0} />
)}
{(columns.length > 0 || searchQuery !== defaultSearchQuery) && (
<Fragment>
<EuiFormRow
helpText={tableItems.length === SEARCH_SIZE ? showingFirstDocs : showingDocs}
>
<Fragment />
</EuiFormRow>
<EuiSpacer />
<MlInMemoryTableBasic
allowNeutralSort={false}
columns={columns}
compressed
hasActions={false}
isSelectable={false}
items={tableItems}
onTableChange={onTableChange}
pagination={pagination}
responsive={false}
search={search}
error={tableError}
sorting={sorting}
/>
</Fragment>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiSpacer size="s" />
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<ExplorationQueryBar
indexPattern={indexPattern}
setSearchQuery={setSearchQuery}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFormRow
helpText={tableItems.length === SEARCH_SIZE ? showingFirstDocs : showingDocs}
>
<Fragment />
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ClassificationExplorationDataGrid
columns={columns}
indexPattern={indexPattern}
pagination={pagination}
resultsField={jobConfig.dest.results_field}
rowCount={rowCount}
selectedFields={selectedFields}
setPagination={setPagination}
setSelectedFields={setSelectedFields}
setSortingColumns={setSortingColumns}
sortingColumns={sortingColumns}
tableItems={tableItems}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiPanel>
);

View file

@ -3,113 +3,158 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/*
* 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, { useEffect, useState } from 'react';
import { useEffect, useState, Dispatch, SetStateAction } from 'react';
import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui';
import { SearchResponse } from 'elasticsearch';
import { cloneDeep } from 'lodash';
import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_memory_table';
import { SORT_DIRECTION } from '../../../../../components/ml_in_memory_table';
import { ml } from '../../../../../services/ml_api_service';
import { getNestedProperty } from '../../../../../util/object_utils';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
import { Field } from '../../../../../../../common/types/fields';
import { isKeywordAndTextType } from '../../../../common/fields';
import { Dictionary } from '../../../../../../../common/types/common';
import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public';
import {
defaultSearchQuery,
ResultsSearchQuery,
isResultsSearchBoolQuery,
LoadExploreDataArg,
} from '../../../../common/analytics';
import {
getDefaultFieldsFromJobCaps,
getDependentVar,
getFlattenedFields,
getPredictedFieldName,
DataFrameAnalyticsConfig,
EsFieldName,
INDEX_STATUS,
SEARCH_SIZE,
SearchQuery,
} from '../../../../common';
import { SavedSearchQuery } from '../../../../../contexts/ml';
interface LoadClassificationExploreDataArg {
direction: SortDirection;
filterByIsTraining?: boolean;
field: string;
searchQuery: SavedSearchQuery;
requiresKeyword?: boolean;
pageIndex?: number;
pageSize?: number;
}
export type TableItem = Record<string, any>;
type Pagination = Pick<EuiDataGridPaginationProps, 'pageIndex' | 'pageSize'>;
export interface UseExploreDataReturnType {
errorMessage: string;
loadExploreData: (arg: LoadClassificationExploreDataArg) => void;
sortField: EsFieldName;
sortDirection: SortDirection;
fieldTypes: { [key: string]: ES_FIELD_TYPES };
pagination: Pagination;
rowCount: number;
searchQuery: SavedSearchQuery;
selectedFields: EsFieldName[];
setFilterByIsTraining: Dispatch<SetStateAction<undefined | boolean>>;
setPagination: Dispatch<SetStateAction<Pagination>>;
setSearchQuery: Dispatch<SetStateAction<SavedSearchQuery>>;
setSelectedFields: Dispatch<SetStateAction<EsFieldName[]>>;
setSortingColumns: Dispatch<SetStateAction<EuiDataGridSorting['columns']>>;
sortingColumns: EuiDataGridSorting['columns'];
status: INDEX_STATUS;
tableFields: string[];
tableItems: TableItem[];
}
type EsSorting = Dictionary<{
order: 'asc' | 'desc';
}>;
// The types specified in `@types/elasticsearch` are out of date and still have `total: number`.
interface SearchResponse7 extends SearchResponse<any> {
hits: SearchResponse<any>['hits'] & {
total: {
value: number;
relation: string;
};
};
}
export const useExploreData = (
jobConfig: DataFrameAnalyticsConfig | undefined,
needsDestIndexFields: boolean,
selectedFields: Field[],
setSelectedFields: React.Dispatch<React.SetStateAction<Field[]>>,
setDocFields: React.Dispatch<React.SetStateAction<Field[]>>,
setDepVarType: React.Dispatch<React.SetStateAction<ES_FIELD_TYPES | undefined>>
jobConfig: DataFrameAnalyticsConfig,
needsDestIndexFields: boolean
): UseExploreDataReturnType => {
const [errorMessage, setErrorMessage] = useState('');
const [status, setStatus] = useState(INDEX_STATUS.UNUSED);
const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]);
const [tableFields, setTableFields] = useState<string[]>([]);
const [tableItems, setTableItems] = useState<TableItem[]>([]);
const [sortField, setSortField] = useState<string>('');
const [sortDirection, setSortDirection] = useState<SortDirection>(SORT_DIRECTION.ASC);
const [fieldTypes, setFieldTypes] = useState<{ [key: string]: ES_FIELD_TYPES }>({});
const [rowCount, setRowCount] = useState(0);
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 });
const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery);
const [filterByIsTraining, setFilterByIsTraining] = useState<undefined | boolean>(undefined);
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
const predictedFieldName = getPredictedFieldName(
jobConfig.dest.results_field,
jobConfig.analysis
);
const dependentVariable = getDependentVar(jobConfig.analysis);
const getDefaultSelectedFields = () => {
const { fields } = newJobCapsService;
if (selectedFields.length === 0 && jobConfig !== undefined) {
const {
selectedFields: defaultSelected,
docFields,
depVarType,
} = getDefaultFieldsFromJobCaps(fields, jobConfig, needsDestIndexFields);
const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps(
fields,
jobConfig,
needsDestIndexFields
);
setDepVarType(depVarType);
setSelectedFields(defaultSelected);
setDocFields(docFields);
const types: { [key: string]: ES_FIELD_TYPES } = {};
const allFields: string[] = [];
docFields.forEach(field => {
types[field.id] = field.type;
allFields.push(field.id);
});
setFieldTypes(types);
setSelectedFields(defaultSelected.map(field => field.id));
setTableFields(allFields);
}
};
const loadExploreData = async ({
field,
direction,
searchQuery,
requiresKeyword,
}: LoadClassificationExploreDataArg) => {
filterByIsTraining: isTraining,
searchQuery: incomingQuery,
}: LoadExploreDataArg) => {
if (jobConfig !== undefined) {
setErrorMessage('');
setStatus(INDEX_STATUS.LOADING);
try {
const resultsField = jobConfig.dest.results_field;
const searchQueryClone: ResultsSearchQuery = cloneDeep(searchQuery);
const searchQueryClone: ResultsSearchQuery = cloneDeep(incomingQuery);
let query: ResultsSearchQuery;
const { pageIndex, pageSize } = pagination;
// If filterByIsTraining is defined - add that in to the final query
const trainingQuery =
isTraining !== undefined
? {
term: { [`${resultsField}.is_training`]: { value: isTraining } },
}
: undefined;
if (JSON.stringify(searchQuery) === JSON.stringify(defaultSearchQuery)) {
query = {
if (JSON.stringify(incomingQuery) === JSON.stringify(defaultSearchQuery)) {
const existsQuery = {
exists: {
field: resultsField,
},
};
query = {
bool: {
must: [existsQuery],
},
};
if (trainingQuery !== undefined && isResultsSearchBoolQuery(query)) {
query.bool.must.push(trainingQuery);
}
} else if (isResultsSearchBoolQuery(searchQueryClone)) {
if (searchQueryClone.bool.must === undefined) {
searchQueryClone.bool.must = [];
@ -121,33 +166,37 @@ export const useExploreData = (
},
});
if (trainingQuery !== undefined) {
searchQueryClone.bool.must.push(trainingQuery);
}
query = searchQueryClone;
} else {
query = searchQueryClone;
}
const body: SearchQuery = {
query,
};
const sort: EsSorting = sortingColumns
.map(column => {
const { id } = column;
column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id;
return column;
})
.reduce((s, column) => {
s[column.id] = { order: column.direction };
return s;
}, {} as EsSorting);
if (field !== undefined) {
body.sort = [
{
[`${field}${requiresKeyword ? '.keyword' : ''}`]: {
order: direction,
},
},
];
}
const resp: SearchResponse<any> = await ml.esSearch({
const resp: SearchResponse7 = await ml.esSearch({
index: jobConfig.dest.index,
size: SEARCH_SIZE,
body,
body: {
query,
from: pageIndex * pageSize,
size: pageSize,
...(Object.keys(sort).length > 0 ? { sort } : {}),
},
});
setSortField(field);
setSortDirection(direction);
setRowCount(resp.hits.total.value);
const docs = resp.hits.hits;
@ -199,10 +248,45 @@ export const useExploreData = (
};
useEffect(() => {
if (jobConfig !== undefined) {
getDefaultSelectedFields();
}
getDefaultSelectedFields();
}, [jobConfig && jobConfig.id]);
return { errorMessage, loadExploreData, sortField, sortDirection, status, tableItems };
// By default set sorting to descending on the prediction field (`<dependent_varible or prediction_field_name>_prediction`).
useEffect(() => {
const sortByField = isKeywordAndTextType(dependentVariable)
? `${predictedFieldName}.keyword`
: predictedFieldName;
const direction = SORT_DIRECTION.DESC;
setSortingColumns([{ id: sortByField, direction }]);
}, [jobConfig && jobConfig.id]);
useEffect(() => {
loadExploreData({ filterByIsTraining, searchQuery });
}, [
filterByIsTraining,
jobConfig && jobConfig.id,
pagination,
searchQuery,
selectedFields,
sortingColumns,
]);
return {
errorMessage,
fieldTypes,
pagination,
searchQuery,
selectedFields,
rowCount,
setFilterByIsTraining,
setPagination,
setSelectedFields,
setSortingColumns,
setSearchQuery,
sortingColumns,
status,
tableItems,
tableFields,
};
};

View file

@ -29,7 +29,7 @@ import { Dictionary } from '../../../../../../../common/types/common';
import { isKeywordAndTextType } from '../../../../common/fields';
import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public';
import {
LoadRegressionExploreDataArg,
LoadExploreDataArg,
defaultSearchQuery,
ResultsSearchQuery,
isResultsSearchBoolQuery,
@ -120,7 +120,7 @@ export const useExploreData = (
const loadExploreData = async ({
filterByIsTraining: isTraining,
searchQuery: incomingQuery,
}: LoadRegressionExploreDataArg) => {
}: LoadExploreDataArg) => {
if (jobConfig !== undefined) {
setErrorMessage('');
setStatus(INDEX_STATUS.LOADING);

View file

@ -9478,8 +9478,6 @@
"xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle": "分類ジョブID {jobId}の評価",
"xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す",
"xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました",
"xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent": "配列",
"xpack.ml.dataframe.analytics.classificationExploration.indexArrayToolTipContent": "この配列ベースの列の完全なコンテンツは表示できません。",
"xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。",
"xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。",
"xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage": "結果が見つかりませんでした。",
@ -9574,8 +9572,6 @@
"xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました",
"xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "一般化エラー",
"xpack.ml.dataframe.analytics.regressionExploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。",
"xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel": "テスト",
"xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel": "トレーニング",
"xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。",
"xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。",
"xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "平均二乗エラー",
@ -9587,9 +9583,6 @@
"xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage": "クエリをパースできません。",
"xpack.ml.dataframe.analytics.regressionExploration.rSquaredText": "R の二乗",
"xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent": "適合度を表します。モデルによる観察された結果の複製の効果を測定します。",
"xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder": "例: 平均>0.5",
"xpack.ml.dataframe.analytics.regressionExploration.selectColumnsAriaLabel": "列を選択",
"xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle": "フィールドを選択",
"xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle": "回帰ジョブID {jobId}のデスティネーションインデックス",
"xpack.ml.dataframe.analytics.regressionExploration.trainingDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました",
"xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle": "トレーニングエラー",

View file

@ -9481,8 +9481,6 @@
"xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle": "分类作业 ID {jobId} 的评估",
"xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档",
"xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估",
"xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent": "数组",
"xpack.ml.dataframe.analytics.classificationExploration.indexArrayToolTipContent": "此基于数组的列的完整内容无法显示。",
"xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError": "无法提取结果。加载索引的字段数据时发生错误。",
"xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError": "无法提取结果。加载作业配置数据时发生错误。",
"xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage": "未找到结果。",
@ -9577,8 +9575,6 @@
"xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估",
"xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "泛化误差",
"xpack.ml.dataframe.analytics.regressionExploration.indexError": "加载索引数据时出错。",
"xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel": "测试",
"xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel": "培训",
"xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError": "无法提取结果。加载索引的字段数据时发生错误。",
"xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError": "无法提取结果。加载作业配置数据时发生错误。",
"xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "均方误差",
@ -9590,9 +9586,6 @@
"xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage": "无法解析查询。",
"xpack.ml.dataframe.analytics.regressionExploration.rSquaredText": "R 平方",
"xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent": "表示拟合优度。度量模型复制被观察结果的优良性。",
"xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder": "例如 avg>0.5",
"xpack.ml.dataframe.analytics.regressionExploration.selectColumnsAriaLabel": "选择列",
"xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle": "选择字段",
"xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle": "回归作业 ID {jobId} 的目标索引",
"xpack.ml.dataframe.analytics.regressionExploration.trainingDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估",
"xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle": "训练误差",