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] DF Analytics Outlier detection results - add search bar #51235

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ interface RegressionAnalysis {

export const SEARCH_SIZE = 1000;

export const defaultSearchQuery = {
match_all: {},
};

export interface SearchQuery {
track_total_hits?: boolean;
query: SavedSearchQuery;
sort?: any;
}

export enum INDEX_STATUS {
UNUSED,
LOADING,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export {
getPredictedFieldName,
INDEX_STATUS,
SEARCH_SIZE,
defaultSearchQuery,
SearchQuery,
} from './analytics';

export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { FC, useEffect, useState } from 'react';
import React, { FC, Fragment, useEffect, useState } from 'react';
import moment from 'moment-timezone';

import { i18n } from '@kbn/i18n';
Expand All @@ -18,13 +18,16 @@ import {
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiPanel,
EuiPopover,
EuiPopoverTitle,
EuiProgress,
EuiSpacer,
EuiText,
EuiTitle,
EuiToolTip,
Query,
} from '@elastic/eui';

import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json';
Expand All @@ -51,12 +54,18 @@ import {
EsDoc,
MAX_COLUMNS,
INDEX_STATUS,
SEARCH_SIZE,
defaultSearchQuery,
} from '../../../../common';

import { getOutlierScoreFieldName } from './common';
import { useExploreData } from './use_explore_data';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
import {
DATA_FRAME_TASK_STATE,
Query as QueryType,
} from '../../../analytics_management/components/analytics_list/common';
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns';
import { SavedSearchQuery } from '../../../../../contexts/kibana';

const customColorScaleFactory = (n: number) => (t: number) => {
if (t < 1 / n) {
Expand Down Expand Up @@ -99,6 +108,10 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(25);

const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery);
const [searchError, setSearchError] = useState<any>(undefined);
const [searchString, setSearchString] = useState<string | undefined>(undefined);

useEffect(() => {
(async function() {
const analyticsConfigs: GetDataFrameAnalyticsResponse = await ml.dataFrameAnalytics.getDataFrameAnalytics(
Expand All @@ -119,23 +132,9 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
? euiThemeDark
: euiThemeLight;

const [clearTable, setClearTable] = useState(false);

const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]);
const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false);

// EuiInMemoryTable has an issue with dynamic sortable columns
// and will trigger a full page Kibana error in such a case.
// The following is a workaround until this is solved upstream:
// - If the sortable/columns config changes,
// the table will be unmounted/not rendered.
// This is what setClearTable(true) in toggleColumn() does.
// - After that on next render it gets re-enabled. To make sure React
// doesn't consolidate the state updates, setTimeout is used.
if (clearTable) {
setTimeout(() => setClearTable(false), 0);
}

function toggleColumnsPopover() {
setColumnsPopoverVisible(!isColumnsPopoverVisible);
}
Expand All @@ -146,7 +145,6 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {

function toggleColumn(column: EsFieldName) {
if (tableItems.length > 0 && jobConfig !== undefined) {
setClearTable(true);
// spread to a new array otherwise the component wouldn't re-render
setSelectedFields([...toggleSelectedField(selectedFields, column)]);
}
Expand Down Expand Up @@ -309,6 +307,17 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
);
}

useEffect(() => {
if (jobConfig !== undefined) {
const outlierScoreFieldName = getOutlierScoreFieldName(jobConfig);
const outlierScoreFieldSelected = selectedFields.includes(outlierScoreFieldName);

const field = outlierScoreFieldSelected ? outlierScoreFieldName : selectedFields[0];
const direction = outlierScoreFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
loadExploreData({ field, direction, searchQuery });
}
}, [JSON.stringify(searchQuery)]);

useEffect(() => {
// by default set the sorting to descending on the `outlier_score` field.
// if that's not available sort ascending on the first column.
Expand All @@ -319,7 +328,7 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {

const field = outlierScoreFieldSelected ? outlierScoreFieldName : selectedFields[0];
const direction = outlierScoreFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
loadExploreData({ field, direction });
loadExploreData({ field, direction, searchQuery });
return;
}
}, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]);
Expand All @@ -344,8 +353,7 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
setPageSize(size);

if (sort.field !== sortField || sort.direction !== sortDirection) {
setClearTable(true);
loadExploreData(sort);
loadExploreData({ ...sort, searchQuery });
}
};
}
Expand All @@ -358,11 +366,37 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
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.exploration.searchBoxPlaceholder', {
defaultMessage: 'E.g. avg>0.5',
}),
},
};

if (jobConfig === undefined) {
return null;
}

if (status === INDEX_STATUS.ERROR) {
// 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 (
<EuiPanel grow={false}>
<ExplorationTitle jobId={jobConfig.id} />
Expand All @@ -379,32 +413,16 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
);
}

if (status === INDEX_STATUS.LOADED && tableItems.length === 0) {
return (
<EuiPanel grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<ExplorationTitle jobId={jobConfig.id} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<span>{getTaskStateBadge(jobStatus)}</span>
</EuiFlexItem>
</EuiFlexGroup>
<EuiCallOut
title={i18n.translate('xpack.ml.dataframe.analytics.exploration.noDataCalloutTitle', {
defaultMessage: 'Empty index query result.',
})}
color="primary"
>
<p>
{i18n.translate('xpack.ml.dataframe.analytics.exploration.noDataCalloutBody', {
defaultMessage:
'The query for the index returned no results. Please make sure the index contains documents and your query is not too restrictive.',
})}
</p>
</EuiCallOut>
</EuiPanel>
);
let tableError =
status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception')
? errorMessage
: searchError;

if (status === INDEX_STATUS.LOADED && tableItems.length === 0 && tableError === undefined) {
tableError = i18n.translate('xpack.ml.dataframe.analytics.exploration.noDataCalloutBody', {
defaultMessage:
'The query for the index returned no results. Please make sure the index contains documents and your query is not too restrictive.',
});
}

return (
Expand Down Expand Up @@ -483,20 +501,38 @@ export const Exploration: FC<Props> = React.memo(({ jobId, jobStatus }) => {
{status !== INDEX_STATUS.LOADING && (
<EuiProgress size="xs" color="accent" max={1} value={0} />
)}
{clearTable === false && columns.length > 0 && sortField !== '' && (
<MlInMemoryTableBasic
allowNeutralSort={false}
className="mlDataFrameAnalyticsExploration"
columns={columns}
compressed
hasActions={false}
isSelectable={false}
items={tableItems}
onTableChange={onTableChange}
pagination={pagination}
responsive={false}
sorting={sorting}
/>
{(columns.length > 0 || searchQuery !== defaultSearchQuery) && sortField !== '' && (
<Fragment>
{tableItems.length === SEARCH_SIZE && (
<EuiFormRow
helpText={i18n.translate(
'xpack.ml.dataframe.analytics.exploration.documentsShownHelpText',
{
defaultMessage: 'Showing first {searchSize} documents',
values: { searchSize: SEARCH_SIZE },
}
)}
>
<Fragment />
</EuiFormRow>
)}
<EuiSpacer />
<MlInMemoryTableBasic
allowNeutralSort={false}
className="mlDataFrameAnalyticsExploration"
columns={columns}
compressed
hasActions={false}
isSelectable={false}
items={tableItems}
onTableChange={onTableChange}
pagination={pagination}
responsive={false}
sorting={sorting}
search={search}
error={tableError}
/>
</Fragment>
)}
</EuiPanel>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,21 @@ import {
EsFieldName,
INDEX_STATUS,
SEARCH_SIZE,
defaultSearchQuery,
SearchQuery,
} from '../../../../common';

import { getOutlierScoreFieldName } from './common';
import { SavedSearchQuery } from '../../../../../contexts/kibana';

type TableItem = Record<string, any>;

interface LoadExploreDataArg {
field: string;
direction: SortDirection;
searchQuery: SavedSearchQuery;
}

export interface UseExploreDataReturnType {
errorMessage: string;
loadExploreData: (arg: LoadExploreDataArg) => void;
Expand All @@ -50,27 +55,32 @@ export const useExploreData = (
const [sortField, setSortField] = useState<string>('');
const [sortDirection, setSortDirection] = useState<SortDirection>(SORT_DIRECTION.ASC);

const loadExploreData = async ({ field, direction }: LoadExploreDataArg) => {
const loadExploreData = async ({ field, direction, searchQuery }: LoadExploreDataArg) => {
if (jobConfig !== undefined) {
setErrorMessage('');
setStatus(INDEX_STATUS.LOADING);

try {
const resultsField = jobConfig.dest.results_field;

const body: SearchQuery = {
query: searchQuery,
};

if (field !== undefined) {
body.sort = [
{
[field]: {
order: direction,
},
},
];
}

const resp: SearchResponse<any> = await ml.esSearch({
index: jobConfig.dest.index,
size: SEARCH_SIZE,
body: {
query: { match_all: {} },
sort: [
{
[field]: {
order: direction,
},
},
],
},
body,
});

setSortField(field);
Expand Down Expand Up @@ -135,6 +145,7 @@ export const useExploreData = (
loadExploreData({
field: getOutlierScoreFieldName(jobConfig),
direction: SORT_DIRECTION.DESC,
searchQuery: defaultSearchQuery,
});
}
}, [jobConfig && jobConfig.id]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import {
getEvalQueryBody,
isRegressionResultsSearchBoolQuery,
RegressionResultsSearchQuery,
SearchQuery,
} from '../../../../common/analytics';
import { SearchQuery } from './use_explore_data';

interface Props {
jobConfig: DataFrameAnalyticsConfig;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ 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';
import { RegressionResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics';

interface GetDataFrameAnalyticsResponse {
count: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,12 @@ import {
getPredictedFieldName,
INDEX_STATUS,
SEARCH_SIZE,
defaultSearchQuery,
} 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, defaultSearchQuery } from './use_explore_data';
import { useExploreData } from './use_explore_data';
import { ExplorationTitle } from './regression_exploration';

const PAGE_SIZE_OPTIONS = [5, 10, 25, 50];
Expand Down
Loading