{noInfluencersConfigured && (
@@ -462,142 +325,12 @@ export class Explorer extends React.Component {
)}
-
-
-
-
-
-
-
- {showOverallSwimlane && (
-
- {(tooltipService) => (
-
- )}
-
- )}
-
-
- {viewBySwimlaneOptions.length > 0 && (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {viewByLoadedForTimeFormatted && (
-
- )}
- {viewByLoadedForTimeFormatted === undefined && (
-
- )}
- {filterActive === true && viewBySwimlaneFieldName === VIEW_BY_JOB_LABEL && (
-
- )}
-
-
-
-
-
- {showViewBySwimlane && (
- <>
-
-
-
- {(tooltipService) => (
-
- )}
-
-
- >
- )}
-
- {viewBySwimlaneDataLoading &&
}
-
- {!showViewBySwimlane &&
- !viewBySwimlaneDataLoading &&
- viewBySwimlaneFieldName !== null && (
-
- )}
- >
- )}
+
+
+
{annotationsData.length > 0 && (
<>
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts
index 1cfd29e2f60d2..d1adf8c7ad744 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts
@@ -37,10 +37,12 @@ export const FILTER_ACTION = {
REMOVE: '-',
};
-export enum SWIMLANE_TYPE {
- OVERALL = 'overall',
- VIEW_BY = 'viewBy',
-}
+export const SWIMLANE_TYPE = {
+ OVERALL: 'overall',
+ VIEW_BY: 'viewBy',
+} as const;
+
+export type SwimlaneType = typeof SWIMLANE_TYPE[keyof typeof SWIMLANE_TYPE];
export const CHART_TYPE = {
EVENT_DISTRIBUTION: 'event_distribution',
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx
index 18b5de1d51f9c..4e6dcdcc5129c 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx
@@ -22,7 +22,7 @@ import { numTicksForDateFormat } from '../util/chart_utils';
import { getSeverityColor } from '../../../common/util/anomaly_utils';
import { mlEscape } from '../util/string_utils';
import { ALLOW_CELL_RANGE_SELECTION, dragSelect$ } from './explorer_dashboard_service';
-import { DRAG_SELECT_ACTION } from './explorer_constants';
+import { DRAG_SELECT_ACTION, SwimlaneType } from './explorer_constants';
import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control';
import { TimeBuckets as TimeBucketsClass } from '../util/time_buckets';
import {
@@ -58,7 +58,7 @@ export interface ExplorerSwimlaneProps {
timeBuckets: InstanceType
;
swimlaneCellClick?: Function;
swimlaneData: OverallSwimlaneData;
- swimlaneType: string;
+ swimlaneType: SwimlaneType;
selection?: {
lanes: any[];
type: string;
diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts
index 0a2dbf5bcff35..4e1a2af9b13a6 100644
--- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts
+++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts
@@ -16,8 +16,9 @@ import {
AnomaliesTableData,
ExplorerJob,
AppStateSelectedCells,
- SwimlaneData,
TimeRangeBounds,
+ OverallSwimlaneData,
+ SwimlaneData,
} from '../../explorer_utils';
export interface ExplorerState {
@@ -35,7 +36,7 @@ export interface ExplorerState {
loading: boolean;
maskAll: boolean;
noInfluencersConfigured: boolean;
- overallSwimlaneData: SwimlaneData;
+ overallSwimlaneData: SwimlaneData | OverallSwimlaneData;
queryString: string;
selectedCells: AppStateSelectedCells | undefined;
selectedJobs: ExplorerJob[] | null;
@@ -45,7 +46,7 @@ export interface ExplorerState {
tableData: AnomaliesTableData;
tableQueryString: string;
viewByLoadedForTimeFormatted: string | null;
- viewBySwimlaneData: SwimlaneData;
+ viewBySwimlaneData: SwimlaneData | OverallSwimlaneData;
viewBySwimlaneDataLoading: boolean;
viewBySwimlaneFieldName?: string;
viewBySwimlaneOptions: string[];
diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx
index 7f7a8fc5a70bd..7a2df1a0f0535 100644
--- a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx
@@ -36,5 +36,5 @@ export const SelectLimit = () => {
setLimit(parseInt(e.target.value, 10));
}
- return ;
+ return ;
};
diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx
new file mode 100644
index 0000000000000..57d1fd81000b7
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx
@@ -0,0 +1,61 @@
+/*
+ * 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, useCallback, useState } from 'react';
+import { EuiResizeObserver, EuiText } from '@elastic/eui';
+
+import { throttle } from 'lodash';
+import {
+ ExplorerSwimlane,
+ ExplorerSwimlaneProps,
+} from '../../application/explorer/explorer_swimlane';
+
+import { MlTooltipComponent } from '../../application/components/chart_tooltip';
+
+const RESIZE_THROTTLE_TIME_MS = 500;
+
+export const SwimlaneContainer: FC<
+ Omit & {
+ onResize: (width: number) => void;
+ }
+> = ({ children, onResize, ...props }) => {
+ const [chartWidth, setChartWidth] = useState(0);
+
+ const resizeHandler = useCallback(
+ throttle((e: { width: number; height: number }) => {
+ const labelWidth = 200;
+ setChartWidth(e.width - labelWidth);
+ onResize(e.width);
+ }, RESIZE_THROTTLE_TIME_MS),
+ []
+ );
+
+ return (
+
+ {(resizeRef) => (
+ {
+ resizeRef(el);
+ }}
+ >
+
+
+
+ {(tooltipService) => (
+
+ )}
+
+
+
+
+ )}
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts
new file mode 100644
index 0000000000000..6cab23eb187c7
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts
@@ -0,0 +1,174 @@
+/*
+ * 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 { dashboardServiceProvider } from './dashboard_service';
+import { savedObjectsServiceMock } from '../../../../../../src/core/public/mocks';
+import { SavedObjectDashboard } from '../../../../../../src/plugins/dashboard/public/saved_dashboards';
+import {
+ DashboardUrlGenerator,
+ SavedDashboardPanel,
+} from '../../../../../../src/plugins/dashboard/public';
+
+jest.mock('@elastic/eui', () => {
+ return {
+ htmlIdGenerator: jest.fn(() => {
+ return jest.fn(() => 'test-panel-id');
+ }),
+ };
+});
+
+describe('DashboardService', () => {
+ const mockSavedObjectClient = savedObjectsServiceMock.createStartContract().client;
+ const dashboardUrlGenerator = ({
+ createUrl: jest.fn(),
+ } as unknown) as DashboardUrlGenerator;
+ const dashboardService = dashboardServiceProvider(
+ mockSavedObjectClient,
+ '8.0.0',
+ dashboardUrlGenerator
+ );
+
+ test('should fetch dashboard', () => {
+ // act
+ dashboardService.fetchDashboards('test');
+ // assert
+ expect(mockSavedObjectClient.find).toHaveBeenCalledWith({
+ type: 'dashboard',
+ perPage: 10,
+ search: `test*`,
+ searchFields: ['title^3', 'description'],
+ });
+ });
+
+ test('should attach panel to the dashboard', () => {
+ // act
+ dashboardService.attachPanels(
+ 'test-dashboard',
+ ({
+ title: 'ML Test',
+ hits: 0,
+ description: '',
+ panelsJSON: JSON.stringify([
+ {
+ version: '8.0.0',
+ type: 'ml_anomaly_swimlane',
+ gridData: { x: 0, y: 0, w: 24, h: 15, i: 'i63c960b1-ab1b-11ea-809d-f5c60c43347f' },
+ panelIndex: 'i63c960b1-ab1b-11ea-809d-f5c60c43347f',
+ embeddableConfig: {
+ title: 'Panel test!',
+ jobIds: ['cw_multi_1'],
+ swimlaneType: 'overall',
+ },
+ title: 'Panel test!',
+ },
+ {
+ version: '8.0.0',
+ type: 'ml_anomaly_swimlane',
+ gridData: { x: 24, y: 0, w: 24, h: 15, i: '0aa334bd-8308-4ded-9462-80dbd37680ee' },
+ panelIndex: '0aa334bd-8308-4ded-9462-80dbd37680ee',
+ embeddableConfig: {
+ title: 'ML anomaly swimlane for fb_population_1',
+ jobIds: ['fb_population_1'],
+ limit: 5,
+ swimlaneType: 'overall',
+ },
+ title: 'ML anomaly swimlane for fb_population_1',
+ },
+ {
+ version: '8.0.0',
+ gridData: { x: 0, y: 15, w: 24, h: 15, i: 'abd36eb7-4774-4216-891e-12100752b46d' },
+ panelIndex: 'abd36eb7-4774-4216-891e-12100752b46d',
+ embeddableConfig: {},
+ panelRefName: 'panel_2',
+ },
+ ]),
+ optionsJSON: '{"hidePanelTitles":false,"useMargins":true}',
+ version: 1,
+ timeRestore: false,
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: '{"query":{"language":"kuery","query":""},"filter":[]}',
+ },
+ } as unknown) as SavedObjectDashboard,
+ [{ title: 'Test title', type: 'test-panel', embeddableConfig: { testConfig: '' } }]
+ );
+ // assert
+ expect(mockSavedObjectClient.update).toHaveBeenCalledWith('dashboard', 'test-dashboard', {
+ title: 'ML Test',
+ hits: 0,
+ description: '',
+ panelsJSON: JSON.stringify([
+ {
+ version: '8.0.0',
+ type: 'ml_anomaly_swimlane',
+ gridData: { x: 0, y: 0, w: 24, h: 15, i: 'i63c960b1-ab1b-11ea-809d-f5c60c43347f' },
+ panelIndex: 'i63c960b1-ab1b-11ea-809d-f5c60c43347f',
+ embeddableConfig: {
+ title: 'Panel test!',
+ jobIds: ['cw_multi_1'],
+ swimlaneType: 'overall',
+ },
+ title: 'Panel test!',
+ },
+ {
+ version: '8.0.0',
+ type: 'ml_anomaly_swimlane',
+ gridData: { x: 24, y: 0, w: 24, h: 15, i: '0aa334bd-8308-4ded-9462-80dbd37680ee' },
+ panelIndex: '0aa334bd-8308-4ded-9462-80dbd37680ee',
+ embeddableConfig: {
+ title: 'ML anomaly swimlane for fb_population_1',
+ jobIds: ['fb_population_1'],
+ limit: 5,
+ swimlaneType: 'overall',
+ },
+ title: 'ML anomaly swimlane for fb_population_1',
+ },
+ {
+ version: '8.0.0',
+ gridData: { x: 0, y: 15, w: 24, h: 15, i: 'abd36eb7-4774-4216-891e-12100752b46d' },
+ panelIndex: 'abd36eb7-4774-4216-891e-12100752b46d',
+ embeddableConfig: {},
+ panelRefName: 'panel_2',
+ },
+ {
+ panelIndex: 'test-panel-id',
+ embeddableConfig: { testConfig: '' },
+ title: 'Test title',
+ type: 'test-panel',
+ version: '8.0.0',
+ gridData: { h: 15, i: 'test-panel-id', w: 24, x: 24, y: 15 },
+ },
+ ]),
+ optionsJSON: '{"hidePanelTitles":false,"useMargins":true}',
+ version: 1,
+ timeRestore: false,
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: '{"query":{"language":"kuery","query":""},"filter":[]}',
+ },
+ });
+ });
+
+ test('should generate edit url to the dashboard', () => {
+ dashboardService.getDashboardEditUrl('test-id');
+ expect(dashboardUrlGenerator.createUrl).toHaveBeenCalledWith({
+ dashboardId: 'test-id',
+ useHash: false,
+ viewMode: 'edit',
+ });
+ });
+
+ test('should find the panel positioned at the end', () => {
+ expect(
+ dashboardService.getLastPanel([
+ { gridData: { y: 15, x: 7 } },
+ { gridData: { y: 17, x: 9 } },
+ { gridData: { y: 15, x: 1 } },
+ { gridData: { y: 17, x: 10 } },
+ { gridData: { y: 15, x: 22 } },
+ { gridData: { y: 17, x: 9 } },
+ ] as SavedDashboardPanel[])
+ ).toEqual({ gridData: { y: 17, x: 10 } });
+ });
+});
diff --git a/x-pack/plugins/ml/public/application/services/dashboard_service.ts b/x-pack/plugins/ml/public/application/services/dashboard_service.ts
new file mode 100644
index 0000000000000..7f2bb71d18eb9
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/services/dashboard_service.ts
@@ -0,0 +1,136 @@
+/*
+ * 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 { SavedObjectsClientContract } from 'kibana/public';
+import { htmlIdGenerator } from '@elastic/eui';
+import { useMemo } from 'react';
+import {
+ DASHBOARD_APP_URL_GENERATOR,
+ DashboardUrlGenerator,
+ SavedDashboardPanel,
+ SavedObjectDashboard,
+} from '../../../../../../src/plugins/dashboard/public';
+import { useMlKibana } from '../contexts/kibana';
+import { ViewMode } from '../../../../../../src/plugins/embeddable/public';
+
+export type DashboardService = ReturnType;
+
+export function dashboardServiceProvider(
+ savedObjectClient: SavedObjectsClientContract,
+ kibanaVersion: string,
+ dashboardUrlGenerator: DashboardUrlGenerator
+) {
+ const generateId = htmlIdGenerator();
+ const DEFAULT_PANEL_WIDTH = 24;
+ const DEFAULT_PANEL_HEIGHT = 15;
+
+ return {
+ /**
+ * Fetches dashboards
+ */
+ async fetchDashboards(query?: string) {
+ return await savedObjectClient.find({
+ type: 'dashboard',
+ perPage: 10,
+ search: query ? `${query}*` : '',
+ searchFields: ['title^3', 'description'],
+ });
+ },
+ /**
+ * Resolves the last positioned panel from the collection.
+ */
+ getLastPanel(panels: SavedDashboardPanel[]): SavedDashboardPanel | null {
+ return panels.length > 0
+ ? panels.reduce((prev, current) =>
+ prev.gridData.y >= current.gridData.y
+ ? prev.gridData.y === current.gridData.y
+ ? prev.gridData.x > current.gridData.x
+ ? prev
+ : current
+ : prev
+ : current
+ )
+ : null;
+ },
+ /**
+ * Attaches embeddable panels to the dashboard
+ */
+ async attachPanels(
+ dashboardId: string,
+ dashboardAttributes: SavedObjectDashboard,
+ panelsData: Array>
+ ) {
+ const panels = JSON.parse(dashboardAttributes.panelsJSON) as SavedDashboardPanel[];
+ const version = kibanaVersion;
+ const rowWidth = DEFAULT_PANEL_WIDTH * 2;
+
+ for (const panelData of panelsData) {
+ const panelIndex = generateId();
+ const lastPanel = this.getLastPanel(panels);
+
+ const xOffset = lastPanel ? lastPanel.gridData.w + lastPanel.gridData.x : 0;
+ const availableRowSpace = rowWidth - xOffset;
+ const xPosition = availableRowSpace - DEFAULT_PANEL_WIDTH >= 0 ? xOffset : 0;
+
+ panels.push({
+ panelIndex,
+ embeddableConfig: panelData.embeddableConfig as { [key: string]: any },
+ title: panelData.title,
+ type: panelData.type,
+ version,
+ gridData: {
+ h: DEFAULT_PANEL_HEIGHT,
+ i: panelIndex,
+ w: DEFAULT_PANEL_WIDTH,
+ x: xPosition,
+ y: lastPanel
+ ? xPosition > 0
+ ? lastPanel.gridData.y
+ : lastPanel.gridData.y + lastPanel.gridData.h
+ : 0,
+ },
+ });
+ }
+
+ await savedObjectClient.update('dashboard', dashboardId, {
+ ...dashboardAttributes,
+ panelsJSON: JSON.stringify(panels),
+ });
+ },
+ /**
+ * Generates dashboard url with edit mode
+ */
+ async getDashboardEditUrl(dashboardId: string) {
+ return await dashboardUrlGenerator.createUrl({
+ dashboardId,
+ useHash: false,
+ viewMode: ViewMode.EDIT,
+ });
+ },
+ };
+}
+
+/**
+ * Hook to use {@link DashboardService} in react components
+ */
+export function useDashboardService(): DashboardService {
+ const {
+ services: {
+ savedObjects: { client: savedObjectClient },
+ kibanaVersion,
+ share: { urlGenerators },
+ },
+ } = useMlKibana();
+ return useMemo(
+ () =>
+ dashboardServiceProvider(
+ savedObjectClient,
+ kibanaVersion,
+ urlGenerators.getUrlGenerator(DASHBOARD_APP_URL_GENERATOR)
+ ),
+ [savedObjectClient, kibanaVersion]
+ );
+}
diff --git a/x-pack/plugins/ml/public/application/services/http_service.ts b/x-pack/plugins/ml/public/application/services/http_service.ts
index 7144411c2885d..bd927dc0e3011 100644
--- a/x-pack/plugins/ml/public/application/services/http_service.ts
+++ b/x-pack/plugins/ml/public/application/services/http_service.ts
@@ -37,6 +37,8 @@ function getFetchOptions(
/**
* Function for making HTTP requests to Kibana's backend.
* Wrapper for Kibana's HttpHandler.
+ *
+ * @deprecated use {@link HttpService} instead
*/
export async function http(options: HttpFetchOptionsWithPath): Promise {
const { path, fetchOptions } = getFetchOptions(options);
@@ -46,6 +48,8 @@ export async function http(options: HttpFetchOptionsWithPath): Promise {
/**
* Function for making HTTP requests to Kibana's backend which returns an Observable
* with request cancellation support.
+ *
+ * @deprecated use {@link HttpService} instead
*/
export function http$(options: HttpFetchOptionsWithPath): Observable {
const { path, fetchOptions } = getFetchOptions(options);
@@ -55,7 +59,7 @@ export function http$(options: HttpFetchOptionsWithPath): Observable {
/**
* Creates an Observable from Kibana's HttpHandler.
*/
-export function fromHttpHandler(input: string, init?: RequestInit): Observable {
+function fromHttpHandler(input: string, init?: RequestInit): Observable {
return new Observable((subscriber) => {
const controller = new AbortController();
const signal = controller.signal;
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
index fdaa3c2ffe79e..6d32fca6a645c 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
@@ -5,12 +5,13 @@
*/
import { Observable } from 'rxjs';
-import { http, http$ } from '../http_service';
+import { HttpStart } from 'kibana/public';
+import { HttpService } from '../http_service';
import { annotations } from './annotations';
import { dataFrameAnalytics } from './data_frame_analytics';
import { filters } from './filters';
-import { results } from './results';
+import { resultsApiProvider } from './results';
import { jobs } from './jobs';
import { fileDatavisualizer } from './datavisualizer';
import { MlServerDefaults, MlServerLimits } from '../../../../common/types/ml_server_info';
@@ -28,6 +29,7 @@ import {
import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types';
import { FieldRequestConfig } from '../../datavisualizer/index_based/common';
import { DataRecognizerConfigResponse, Module } from '../../../../common/types/modules';
+import { getHttp } from '../../util/dependency_cache';
export interface MlInfoResponse {
defaults: MlServerDefaults;
@@ -87,327 +89,330 @@ export function basePath() {
return '/api/ml';
}
-export const ml = {
- getJobs(obj?: { jobId?: string }) {
- const jobId = obj && obj.jobId ? `/${obj.jobId}` : '';
- return http({
- path: `${basePath()}/anomaly_detectors${jobId}`,
- });
- },
-
- getJobStats(obj: { jobId?: string }) {
- const jobId = obj && obj.jobId ? `/${obj.jobId}` : '';
- return http({
- path: `${basePath()}/anomaly_detectors${jobId}/_stats`,
- });
- },
-
- addJob({ jobId, job }: { jobId: string; job: Job }) {
- const body = JSON.stringify(job);
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}`,
- method: 'PUT',
- body,
- });
- },
-
- openJob({ jobId }: { jobId: string }) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/_open`,
- method: 'POST',
- });
- },
-
- closeJob({ jobId }: { jobId: string }) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/_close`,
- method: 'POST',
- });
- },
-
- forceCloseJob({ jobId }: { jobId: string }) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/_close?force=true`,
- method: 'POST',
- });
- },
-
- deleteJob({ jobId }: { jobId: string }) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}`,
- method: 'DELETE',
- });
- },
-
- forceDeleteJob({ jobId }: { jobId: string }) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}?force=true`,
- method: 'DELETE',
- });
- },
-
- updateJob({ jobId, job }: { jobId: string; job: Job }) {
- const body = JSON.stringify(job);
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/_update`,
- method: 'POST',
- body,
- });
- },
-
- estimateBucketSpan(obj: BucketSpanEstimatorData) {
- const body = JSON.stringify(obj);
- return http({
- path: `${basePath()}/validate/estimate_bucket_span`,
- method: 'POST',
- body,
- });
- },
-
- validateJob(payload: {
- job: Job;
- duration: {
- start?: number;
- end?: number;
- };
- fields?: any[];
- }) {
- const body = JSON.stringify(payload);
- return http({
- path: `${basePath()}/validate/job`,
- method: 'POST',
- body,
- });
- },
-
- validateCardinality$(job: CombinedJob): Observable {
- const body = JSON.stringify(job);
- return http$({
- path: `${basePath()}/validate/cardinality`,
- method: 'POST',
- body,
- });
- },
-
- getDatafeeds(obj: { datafeedId: string }) {
- const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : '';
- return http({
- path: `${basePath()}/datafeeds${datafeedId}`,
- });
- },
-
- getDatafeedStats(obj: { datafeedId: string }) {
- const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : '';
- return http({
- path: `${basePath()}/datafeeds${datafeedId}/_stats`,
- });
- },
-
- addDatafeed({ datafeedId, datafeedConfig }: { datafeedId: string; datafeedConfig: Datafeed }) {
- const body = JSON.stringify(datafeedConfig);
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}`,
- method: 'PUT',
- body,
- });
- },
-
- updateDatafeed({ datafeedId, datafeedConfig }: { datafeedId: string; datafeedConfig: Datafeed }) {
- const body = JSON.stringify(datafeedConfig);
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}/_update`,
- method: 'POST',
- body,
- });
- },
-
- deleteDatafeed({ datafeedId }: { datafeedId: string }) {
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}`,
- method: 'DELETE',
- });
- },
-
- forceDeleteDatafeed({ datafeedId }: { datafeedId: string }) {
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}?force=true`,
- method: 'DELETE',
- });
- },
-
- startDatafeed({ datafeedId, start, end }: { datafeedId: string; start: number; end: number }) {
- const body = JSON.stringify({
- ...(start !== undefined ? { start } : {}),
- ...(end !== undefined ? { end } : {}),
- });
-
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}/_start`,
- method: 'POST',
- body,
- });
- },
-
- stopDatafeed({ datafeedId }: { datafeedId: string }) {
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}/_stop`,
- method: 'POST',
- });
- },
-
- forceStopDatafeed({ datafeedId }: { datafeedId: string }) {
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}/_stop?force=true`,
- method: 'POST',
- });
- },
-
- datafeedPreview({ datafeedId }: { datafeedId: string }) {
- return http({
- path: `${basePath()}/datafeeds/${datafeedId}/_preview`,
- method: 'GET',
- });
- },
-
- validateDetector({ detector }: { detector: Detector }) {
- const body = JSON.stringify(detector);
- return http({
- path: `${basePath()}/anomaly_detectors/_validate/detector`,
- method: 'POST',
- body,
- });
- },
-
- forecast({ jobId, duration }: { jobId: string; duration?: string }) {
- const body = JSON.stringify({
- ...(duration !== undefined ? { duration } : {}),
- });
-
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/_forecast`,
- method: 'POST',
- body,
- });
- },
-
- overallBuckets({
- jobId,
- topN,
- bucketSpan,
- start,
- end,
- }: {
- jobId: string;
- topN: string;
- bucketSpan: string;
- start: number;
- end: number;
- }) {
- const body = JSON.stringify({ topN, bucketSpan, start, end });
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/results/overall_buckets`,
- method: 'POST',
- body,
- });
- },
-
- hasPrivileges(obj: any) {
- const body = JSON.stringify(obj);
- return http({
- path: `${basePath()}/_has_privileges`,
- method: 'POST',
- body,
- });
- },
-
- checkMlCapabilities() {
- return http({
- path: `${basePath()}/ml_capabilities`,
- method: 'GET',
- });
- },
-
- checkManageMLCapabilities() {
- return http({
- path: `${basePath()}/ml_capabilities`,
- method: 'GET',
- });
- },
-
- getNotificationSettings() {
- return http({
- path: `${basePath()}/notification_settings`,
- method: 'GET',
- });
- },
-
- getFieldCaps({ index, fields }: { index: string; fields: string[] }) {
- const body = JSON.stringify({
- ...(index !== undefined ? { index } : {}),
- ...(fields !== undefined ? { fields } : {}),
- });
-
- return http({
- path: `${basePath()}/indices/field_caps`,
- method: 'POST',
- body,
- });
- },
-
- recognizeIndex({ indexPatternTitle }: { indexPatternTitle: string }) {
- return http({
- path: `${basePath()}/modules/recognize/${indexPatternTitle}`,
- method: 'GET',
- });
- },
-
- listDataRecognizerModules() {
- return http({
- path: `${basePath()}/modules/get_module`,
- method: 'GET',
- });
- },
-
- getDataRecognizerModule({ moduleId }: { moduleId: string }) {
- return http({
- path: `${basePath()}/modules/get_module/${moduleId}`,
- method: 'GET',
- });
- },
-
- dataRecognizerModuleJobsExist({ moduleId }: { moduleId: string }) {
- return http({
- path: `${basePath()}/modules/jobs_exist/${moduleId}`,
- method: 'GET',
- });
+/**
+ * Temp solution to allow {@link ml} service to use http from
+ * the dependency_cache.
+ */
+const proxyHttpStart = new Proxy(({} as unknown) as HttpStart, {
+ get(obj, prop: keyof HttpStart) {
+ try {
+ return getHttp()[prop];
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ }
},
-
- setupDataRecognizerConfig({
- moduleId,
- prefix,
- groups,
- indexPatternName,
- query,
- useDedicatedIndex,
- startDatafeed,
- start,
- end,
- jobOverrides,
- estimateModelMemory,
- }: {
- moduleId: string;
- prefix?: string;
- groups?: string[];
- indexPatternName?: string;
- query?: any;
- useDedicatedIndex?: boolean;
- startDatafeed?: boolean;
- start?: number;
- end?: number;
- jobOverrides?: Array>;
- estimateModelMemory?: boolean;
- }) {
- const body = JSON.stringify({
+});
+
+export type MlApiServices = ReturnType;
+
+export const ml = mlApiServicesProvider(new HttpService(proxyHttpStart));
+
+export function mlApiServicesProvider(httpService: HttpService) {
+ const { http } = httpService;
+ return {
+ getJobs(obj?: { jobId?: string }) {
+ const jobId = obj && obj.jobId ? `/${obj.jobId}` : '';
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors${jobId}`,
+ });
+ },
+
+ getJobStats(obj: { jobId?: string }) {
+ const jobId = obj && obj.jobId ? `/${obj.jobId}` : '';
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors${jobId}/_stats`,
+ });
+ },
+
+ addJob({ jobId, job }: { jobId: string; job: Job }) {
+ const body = JSON.stringify(job);
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/${jobId}`,
+ method: 'PUT',
+ body,
+ });
+ },
+
+ openJob({ jobId }: { jobId: string }) {
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/_open`,
+ method: 'POST',
+ });
+ },
+
+ closeJob({ jobId }: { jobId: string }) {
+ return http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/_close`,
+ method: 'POST',
+ });
+ },
+
+ forceCloseJob({ jobId }: { jobId: string }) {
+ return http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/_close?force=true`,
+ method: 'POST',
+ });
+ },
+
+ deleteJob({ jobId }: { jobId: string }) {
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/${jobId}`,
+ method: 'DELETE',
+ });
+ },
+
+ forceDeleteJob({ jobId }: { jobId: string }) {
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/${jobId}?force=true`,
+ method: 'DELETE',
+ });
+ },
+
+ updateJob({ jobId, job }: { jobId: string; job: Job }) {
+ const body = JSON.stringify(job);
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/_update`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ estimateBucketSpan(obj: BucketSpanEstimatorData) {
+ const body = JSON.stringify(obj);
+ return httpService.http({
+ path: `${basePath()}/validate/estimate_bucket_span`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ validateJob(payload: {
+ job: Job;
+ duration: {
+ start?: number;
+ end?: number;
+ };
+ fields?: any[];
+ }) {
+ const body = JSON.stringify(payload);
+ return httpService.http({
+ path: `${basePath()}/validate/job`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ validateCardinality$(job: CombinedJob): Observable {
+ const body = JSON.stringify(job);
+ return httpService.http$({
+ path: `${basePath()}/validate/cardinality`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ getDatafeeds(obj: { datafeedId: string }) {
+ const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : '';
+ return httpService.http({
+ path: `${basePath()}/datafeeds${datafeedId}`,
+ });
+ },
+
+ getDatafeedStats(obj: { datafeedId: string }) {
+ const datafeedId = obj && obj.datafeedId ? `/${obj.datafeedId}` : '';
+ return httpService.http({
+ path: `${basePath()}/datafeeds${datafeedId}/_stats`,
+ });
+ },
+
+ addDatafeed({ datafeedId, datafeedConfig }: { datafeedId: string; datafeedConfig: Datafeed }) {
+ const body = JSON.stringify(datafeedConfig);
+ return httpService.http({
+ path: `${basePath()}/datafeeds/${datafeedId}`,
+ method: 'PUT',
+ body,
+ });
+ },
+
+ updateDatafeed({
+ datafeedId,
+ datafeedConfig,
+ }: {
+ datafeedId: string;
+ datafeedConfig: Datafeed;
+ }) {
+ const body = JSON.stringify(datafeedConfig);
+ return httpService.http({
+ path: `${basePath()}/datafeeds/${datafeedId}/_update`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ deleteDatafeed({ datafeedId }: { datafeedId: string }) {
+ return httpService.http({
+ path: `${basePath()}/datafeeds/${datafeedId}`,
+ method: 'DELETE',
+ });
+ },
+
+ forceDeleteDatafeed({ datafeedId }: { datafeedId: string }) {
+ return httpService.http({
+ path: `${basePath()}/datafeeds/${datafeedId}?force=true`,
+ method: 'DELETE',
+ });
+ },
+
+ startDatafeed({ datafeedId, start, end }: { datafeedId: string; start: number; end: number }) {
+ const body = JSON.stringify({
+ ...(start !== undefined ? { start } : {}),
+ ...(end !== undefined ? { end } : {}),
+ });
+
+ return httpService.http({
+ path: `${basePath()}/datafeeds/${datafeedId}/_start`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ stopDatafeed({ datafeedId }: { datafeedId: string }) {
+ return http({
+ path: `${basePath()}/datafeeds/${datafeedId}/_stop`,
+ method: 'POST',
+ });
+ },
+
+ forceStopDatafeed({ datafeedId }: { datafeedId: string }) {
+ return http({
+ path: `${basePath()}/datafeeds/${datafeedId}/_stop?force=true`,
+ method: 'POST',
+ });
+ },
+
+ datafeedPreview({ datafeedId }: { datafeedId: string }) {
+ return httpService.http({
+ path: `${basePath()}/datafeeds/${datafeedId}/_preview`,
+ method: 'GET',
+ });
+ },
+
+ validateDetector({ detector }: { detector: Detector }) {
+ const body = JSON.stringify(detector);
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/_validate/detector`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ forecast({ jobId, duration }: { jobId: string; duration?: string }) {
+ const body = JSON.stringify({
+ ...(duration !== undefined ? { duration } : {}),
+ });
+
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/_forecast`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ overallBuckets({
+ jobId,
+ topN,
+ bucketSpan,
+ start,
+ end,
+ }: {
+ jobId: string;
+ topN: string;
+ bucketSpan: string;
+ start: number;
+ end: number;
+ }) {
+ const body = JSON.stringify({ topN, bucketSpan, start, end });
+ return httpService.http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/results/overall_buckets`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ hasPrivileges(obj: any) {
+ const body = JSON.stringify(obj);
+ return httpService.http({
+ path: `${basePath()}/_has_privileges`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ checkMlCapabilities() {
+ return httpService.http({
+ path: `${basePath()}/ml_capabilities`,
+ method: 'GET',
+ });
+ },
+
+ checkManageMLCapabilities() {
+ return httpService.http({
+ path: `${basePath()}/ml_capabilities`,
+ method: 'GET',
+ });
+ },
+
+ getNotificationSettings() {
+ return httpService.http({
+ path: `${basePath()}/notification_settings`,
+ method: 'GET',
+ });
+ },
+
+ getFieldCaps({ index, fields }: { index: string; fields: string[] }) {
+ const body = JSON.stringify({
+ ...(index !== undefined ? { index } : {}),
+ ...(fields !== undefined ? { fields } : {}),
+ });
+
+ return httpService.http({
+ path: `${basePath()}/indices/field_caps`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ recognizeIndex({ indexPatternTitle }: { indexPatternTitle: string }) {
+ return httpService.http({
+ path: `${basePath()}/modules/recognize/${indexPatternTitle}`,
+ method: 'GET',
+ });
+ },
+
+ listDataRecognizerModules() {
+ return httpService.http({
+ path: `${basePath()}/modules/get_module`,
+ method: 'GET',
+ });
+ },
+
+ getDataRecognizerModule({ moduleId }: { moduleId: string }) {
+ return httpService.http({
+ path: `${basePath()}/modules/get_module/${moduleId}`,
+ method: 'GET',
+ });
+ },
+
+ dataRecognizerModuleJobsExist({ moduleId }: { moduleId: string }) {
+ return httpService.http({
+ path: `${basePath()}/modules/jobs_exist/${moduleId}`,
+ method: 'GET',
+ });
+ },
+
+ setupDataRecognizerConfig({
+ moduleId,
prefix,
groups,
indexPatternName,
@@ -418,37 +423,41 @@ export const ml = {
end,
jobOverrides,
estimateModelMemory,
- });
-
- return http({
- path: `${basePath()}/modules/setup/${moduleId}`,
- method: 'POST',
- body,
- });
- },
-
- getVisualizerFieldStats({
- indexPatternTitle,
- query,
- timeFieldName,
- earliest,
- latest,
- samplerShardSize,
- interval,
- fields,
- maxExamples,
- }: {
- indexPatternTitle: string;
- query: any;
- timeFieldName?: string;
- earliest?: number;
- latest?: number;
- samplerShardSize?: number;
- interval?: string;
- fields?: FieldRequestConfig[];
- maxExamples?: number;
- }) {
- const body = JSON.stringify({
+ }: {
+ moduleId: string;
+ prefix?: string;
+ groups?: string[];
+ indexPatternName?: string;
+ query?: any;
+ useDedicatedIndex?: boolean;
+ startDatafeed?: boolean;
+ start?: number;
+ end?: number;
+ jobOverrides?: Array>;
+ estimateModelMemory?: boolean;
+ }) {
+ const body = JSON.stringify({
+ prefix,
+ groups,
+ indexPatternName,
+ query,
+ useDedicatedIndex,
+ startDatafeed,
+ start,
+ end,
+ jobOverrides,
+ estimateModelMemory,
+ });
+
+ return httpService.http({
+ path: `${basePath()}/modules/setup/${moduleId}`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ getVisualizerFieldStats({
+ indexPatternTitle,
query,
timeFieldName,
earliest,
@@ -457,35 +466,37 @@ export const ml = {
interval,
fields,
maxExamples,
- });
-
- return http({
- path: `${basePath()}/data_visualizer/get_field_stats/${indexPatternTitle}`,
- method: 'POST',
- body,
- });
- },
-
- getVisualizerOverallStats({
- indexPatternTitle,
- query,
- timeFieldName,
- earliest,
- latest,
- samplerShardSize,
- aggregatableFields,
- nonAggregatableFields,
- }: {
- indexPatternTitle: string;
- query: any;
- timeFieldName?: string;
- earliest?: number;
- latest?: number;
- samplerShardSize?: number;
- aggregatableFields: string[];
- nonAggregatableFields: string[];
- }) {
- const body = JSON.stringify({
+ }: {
+ indexPatternTitle: string;
+ query: any;
+ timeFieldName?: string;
+ earliest?: number;
+ latest?: number;
+ samplerShardSize?: number;
+ interval?: string;
+ fields?: FieldRequestConfig[];
+ maxExamples?: number;
+ }) {
+ const body = JSON.stringify({
+ query,
+ timeFieldName,
+ earliest,
+ latest,
+ samplerShardSize,
+ interval,
+ fields,
+ maxExamples,
+ });
+
+ return httpService.http({
+ path: `${basePath()}/data_visualizer/get_field_stats/${indexPatternTitle}`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ getVisualizerOverallStats({
+ indexPatternTitle,
query,
timeFieldName,
earliest,
@@ -493,204 +504,230 @@ export const ml = {
samplerShardSize,
aggregatableFields,
nonAggregatableFields,
- });
-
- return http({
- path: `${basePath()}/data_visualizer/get_overall_stats/${indexPatternTitle}`,
- method: 'POST',
- body,
- });
- },
-
- /**
- * Gets a list of calendars
- * @param obj
- * @returns {Promise}
- */
- calendars(obj?: { calendarId?: CalendarId; calendarIds?: CalendarId[] }) {
- const { calendarId, calendarIds } = obj || {};
- let calendarIdsPathComponent = '';
- if (calendarId) {
- calendarIdsPathComponent = `/${calendarId}`;
- } else if (calendarIds) {
- calendarIdsPathComponent = `/${calendarIds.join(',')}`;
- }
- return http({
- path: `${basePath()}/calendars${calendarIdsPathComponent}`,
- method: 'GET',
- });
- },
-
- addCalendar(obj: Calendar) {
- const body = JSON.stringify(obj);
- return http({
- path: `${basePath()}/calendars`,
- method: 'PUT',
- body,
- });
- },
-
- updateCalendar(obj: UpdateCalendar) {
- const calendarId = obj && obj.calendarId ? `/${obj.calendarId}` : '';
- const body = JSON.stringify(obj);
- return http({
- path: `${basePath()}/calendars${calendarId}`,
- method: 'PUT',
- body,
- });
- },
-
- deleteCalendar({ calendarId }: { calendarId?: string }) {
- return http({
- path: `${basePath()}/calendars/${calendarId}`,
- method: 'DELETE',
- });
- },
-
- mlNodeCount() {
- return http<{ count: number }>({
- path: `${basePath()}/ml_node_count`,
- method: 'GET',
- });
- },
-
- mlInfo() {
- return http({
- path: `${basePath()}/info`,
- method: 'GET',
- });
- },
-
- calculateModelMemoryLimit$({
- analysisConfig,
- indexPattern,
- query,
- timeFieldName,
- earliestMs,
- latestMs,
- }: {
- analysisConfig: AnalysisConfig;
- indexPattern: string;
- query: any;
- timeFieldName: string;
- earliestMs: number;
- latestMs: number;
- }) {
- const body = JSON.stringify({
+ }: {
+ indexPatternTitle: string;
+ query: any;
+ timeFieldName?: string;
+ earliest?: number;
+ latest?: number;
+ samplerShardSize?: number;
+ aggregatableFields: string[];
+ nonAggregatableFields: string[];
+ }) {
+ const body = JSON.stringify({
+ query,
+ timeFieldName,
+ earliest,
+ latest,
+ samplerShardSize,
+ aggregatableFields,
+ nonAggregatableFields,
+ });
+
+ return httpService.http({
+ path: `${basePath()}/data_visualizer/get_overall_stats/${indexPatternTitle}`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ /**
+ * Gets a list of calendars
+ * @param obj
+ * @returns {Promise}
+ */
+ calendars(obj?: { calendarId?: CalendarId; calendarIds?: CalendarId[] }) {
+ const { calendarId, calendarIds } = obj || {};
+ let calendarIdsPathComponent = '';
+ if (calendarId) {
+ calendarIdsPathComponent = `/${calendarId}`;
+ } else if (calendarIds) {
+ calendarIdsPathComponent = `/${calendarIds.join(',')}`;
+ }
+ return httpService.http({
+ path: `${basePath()}/calendars${calendarIdsPathComponent}`,
+ method: 'GET',
+ });
+ },
+
+ addCalendar(obj: Calendar) {
+ const body = JSON.stringify(obj);
+ return httpService.http({
+ path: `${basePath()}/calendars`,
+ method: 'PUT',
+ body,
+ });
+ },
+
+ updateCalendar(obj: UpdateCalendar) {
+ const calendarId = obj && obj.calendarId ? `/${obj.calendarId}` : '';
+ const body = JSON.stringify(obj);
+ return httpService.http({
+ path: `${basePath()}/calendars${calendarId}`,
+ method: 'PUT',
+ body,
+ });
+ },
+
+ deleteCalendar({ calendarId }: { calendarId?: string }) {
+ return httpService.http({
+ path: `${basePath()}/calendars/${calendarId}`,
+ method: 'DELETE',
+ });
+ },
+
+ mlNodeCount() {
+ return httpService.http<{ count: number }>({
+ path: `${basePath()}/ml_node_count`,
+ method: 'GET',
+ });
+ },
+
+ mlInfo() {
+ return httpService.http({
+ path: `${basePath()}/info`,
+ method: 'GET',
+ });
+ },
+
+ calculateModelMemoryLimit$({
analysisConfig,
indexPattern,
query,
timeFieldName,
earliestMs,
latestMs,
- });
-
- return http$<{ modelMemoryLimit: string }>({
- path: `${basePath()}/validate/calculate_model_memory_limit`,
- method: 'POST',
- body,
- });
- },
-
- getCardinalityOfFields({
- index,
- fieldNames,
- query,
- timeFieldName,
- earliestMs,
- latestMs,
- }: {
- index: string;
- fieldNames: string[];
- query: any;
- timeFieldName: string;
- earliestMs: number;
- latestMs: number;
- }) {
- const body = JSON.stringify({ index, fieldNames, query, timeFieldName, earliestMs, latestMs });
-
- return http({
- path: `${basePath()}/fields_service/field_cardinality`,
- method: 'POST',
- body,
- });
- },
-
- getTimeFieldRange({
- index,
- timeFieldName,
- query,
- }: {
- index: string;
- timeFieldName?: string;
- query: any;
- }) {
- const body = JSON.stringify({ index, timeFieldName, query });
-
- return http({
- path: `${basePath()}/fields_service/time_field_range`,
- method: 'POST',
- body,
- });
- },
-
- esSearch(obj: any) {
- const body = JSON.stringify(obj);
- return http({
- path: `${basePath()}/es_search`,
- method: 'POST',
- body,
- });
- },
-
- esSearch$(obj: any) {
- const body = JSON.stringify(obj);
- return http$({
- path: `${basePath()}/es_search`,
- method: 'POST',
- body,
- });
- },
-
- getIndices() {
- const tempBasePath = '/api';
- return http>({
- path: `${tempBasePath}/index_management/indices`,
- method: 'GET',
- });
- },
-
- getModelSnapshots(jobId: string, snapshotId?: string) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots${
- snapshotId !== undefined ? `/${snapshotId}` : ''
- }`,
- });
- },
-
- updateModelSnapshot(
- jobId: string,
- snapshotId: string,
- body: { description?: string; retain?: boolean }
- ) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}/_update`,
- method: 'POST',
- body: JSON.stringify(body),
- });
- },
-
- deleteModelSnapshot(jobId: string, snapshotId: string) {
- return http({
- path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}`,
- method: 'DELETE',
- });
- },
-
- annotations,
- dataFrameAnalytics,
- filters,
- results,
- jobs,
- fileDatavisualizer,
-};
+ }: {
+ analysisConfig: AnalysisConfig;
+ indexPattern: string;
+ query: any;
+ timeFieldName: string;
+ earliestMs: number;
+ latestMs: number;
+ }) {
+ const body = JSON.stringify({
+ analysisConfig,
+ indexPattern,
+ query,
+ timeFieldName,
+ earliestMs,
+ latestMs,
+ });
+
+ return httpService.http$<{ modelMemoryLimit: string }>({
+ path: `${basePath()}/validate/calculate_model_memory_limit`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ getCardinalityOfFields({
+ index,
+ fieldNames,
+ query,
+ timeFieldName,
+ earliestMs,
+ latestMs,
+ }: {
+ index: string;
+ fieldNames: string[];
+ query: any;
+ timeFieldName: string;
+ earliestMs: number;
+ latestMs: number;
+ }) {
+ const body = JSON.stringify({
+ index,
+ fieldNames,
+ query,
+ timeFieldName,
+ earliestMs,
+ latestMs,
+ });
+
+ return httpService.http({
+ path: `${basePath()}/fields_service/field_cardinality`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ getTimeFieldRange({
+ index,
+ timeFieldName,
+ query,
+ }: {
+ index: string;
+ timeFieldName?: string;
+ query: any;
+ }) {
+ const body = JSON.stringify({ index, timeFieldName, query });
+
+ return httpService.http({
+ path: `${basePath()}/fields_service/time_field_range`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ esSearch(obj: any) {
+ const body = JSON.stringify(obj);
+ return httpService.http({
+ path: `${basePath()}/es_search`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ esSearch$(obj: any) {
+ const body = JSON.stringify(obj);
+ return httpService.http$({
+ path: `${basePath()}/es_search`,
+ method: 'POST',
+ body,
+ });
+ },
+
+ getIndices() {
+ const tempBasePath = '/api';
+ return httpService.http>({
+ path: `${tempBasePath}/index_management/indices`,
+ method: 'GET',
+ });
+ },
+
+ getModelSnapshots(jobId: string, snapshotId?: string) {
+ return http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots${
+ snapshotId !== undefined ? `/${snapshotId}` : ''
+ }`,
+ });
+ },
+
+ updateModelSnapshot(
+ jobId: string,
+ snapshotId: string,
+ body: { description?: string; retain?: boolean }
+ ) {
+ return http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}/_update`,
+ method: 'POST',
+ body: JSON.stringify(body),
+ });
+ },
+
+ deleteModelSnapshot(jobId: string, snapshotId: string) {
+ return http({
+ path: `${basePath()}/anomaly_detectors/${jobId}/model_snapshots/${snapshotId}`,
+ method: 'DELETE',
+ });
+ },
+
+ annotations,
+ dataFrameAnalytics,
+ filters,
+ results: resultsApiProvider(httpService),
+ jobs,
+ fileDatavisualizer,
+ };
+}
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
index 830e6fab4163a..521fd306847eb 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/results.ts
@@ -5,14 +5,14 @@
*/
// Service for obtaining data for the ML Results dashboards.
-import { http, http$ } from '../http_service';
+import { HttpService } from '../http_service';
import { basePath } from './index';
import { JobId } from '../../../../common/types/anomaly_detection_jobs';
import { PartitionFieldsDefinition } from '../results_service/result_service_rx';
-export const results = {
+export const resultsApiProvider = (httpService: HttpService) => ({
getAnomaliesTableData(
jobIds: string[],
criteriaFields: string[],
@@ -40,7 +40,7 @@ export const results = {
influencersFilterQuery,
});
- return http$({
+ return httpService.http$({
path: `${basePath()}/results/anomalies_table_data`,
method: 'POST',
body,
@@ -53,7 +53,7 @@ export const results = {
earliestMs,
latestMs,
});
- return http({
+ return httpService.http({
path: `${basePath()}/results/max_anomaly_score`,
method: 'POST',
body,
@@ -62,7 +62,7 @@ export const results = {
getCategoryDefinition(jobId: string, categoryId: string) {
const body = JSON.stringify({ jobId, categoryId });
- return http({
+ return httpService.http({
path: `${basePath()}/results/category_definition`,
method: 'POST',
body,
@@ -75,7 +75,7 @@ export const results = {
categoryIds,
maxExamples,
});
- return http({
+ return httpService.http({
path: `${basePath()}/results/category_examples`,
method: 'POST',
body,
@@ -90,10 +90,10 @@ export const results = {
latestMs: number
) {
const body = JSON.stringify({ jobId, searchTerm, criteriaFields, earliestMs, latestMs });
- return http$({
+ return httpService.http$({
path: `${basePath()}/results/partition_fields_values`,
method: 'POST',
body,
});
},
-};
+});
diff --git a/x-pack/plugins/ml/public/application/services/results_service/index.ts b/x-pack/plugins/ml/public/application/services/results_service/index.ts
index cc02248f4d5a9..6c508422e7063 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/index.ts
+++ b/x-pack/plugins/ml/public/application/services/results_service/index.ts
@@ -4,47 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- getMetricData,
- getModelPlotOutput,
- getRecordsForCriteria,
- getScheduledEventsByBucket,
- fetchPartitionFieldsValues,
-} from './result_service_rx';
-import {
- getEventDistributionData,
- getEventRateData,
- getInfluencerValueMaxScoreByTime,
- getOverallBucketScores,
- getRecordInfluencers,
- getRecordMaxScoreByTime,
- getRecords,
- getRecordsForDetector,
- getRecordsForInfluencer,
- getScoresByBucket,
- getTopInfluencers,
- getTopInfluencerValues,
-} from './results_service';
-
-export const mlResultsService = {
- getScoresByBucket,
- getScheduledEventsByBucket,
- getTopInfluencers,
- getTopInfluencerValues,
- getOverallBucketScores,
- getInfluencerValueMaxScoreByTime,
- getRecordInfluencers,
- getRecordsForInfluencer,
- getRecordsForDetector,
- getRecords,
- getRecordsForCriteria,
- getMetricData,
- getEventRateData,
- getEventDistributionData,
- getModelPlotOutput,
- getRecordMaxScoreByTime,
- fetchPartitionFieldsValues,
-};
+import { resultsServiceRxProvider } from './result_service_rx';
+import { resultsServiceProvider } from './results_service';
+import { ml, MlApiServices } from '../ml_api_service';
export type MlResultsService = typeof mlResultsService;
@@ -57,3 +19,12 @@ export interface CriteriaField {
fieldName: string;
fieldValue: any;
}
+
+export const mlResultsService = mlResultsServiceProvider(ml);
+
+export function mlResultsServiceProvider(mlApiServices: MlApiServices) {
+ return {
+ ...resultsServiceProvider(mlApiServices),
+ ...resultsServiceRxProvider(mlApiServices),
+ };
+}
diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
index a21d0caaedd33..1bcbd8dbcdd63 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
+++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
@@ -17,7 +17,7 @@ import _ from 'lodash';
import { Dictionary } from '../../../../common/types/common';
import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils';
import { JobId } from '../../../../common/types/anomaly_detection_jobs';
-import { ml } from '../ml_api_service';
+import { MlApiServices } from '../ml_api_service';
import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns';
import { CriteriaField } from './index';
@@ -46,524 +46,528 @@ export type PartitionFieldsDefinition = {
[field in FieldTypes]: FieldDefinition;
};
-export function getMetricData(
- index: string,
- entityFields: any[],
- query: object | undefined,
- metricFunction: string, // ES aggregation name
- metricFieldName: string,
- timeFieldName: string,
- earliestMs: number,
- latestMs: number,
- interval: string
-): Observable {
- // Build the criteria to use in the bool filter part of the request.
- // Add criteria for the time range, entity fields,
- // plus any additional supplied query.
- const shouldCriteria: object[] = [];
- const mustCriteria: object[] = [
- {
- range: {
- [timeFieldName]: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
- },
- },
- },
- ...(query ? [query] : []),
- ];
-
- entityFields.forEach((entity) => {
- if (entity.fieldValue.length !== 0) {
- mustCriteria.push({
- term: {
- [entity.fieldName]: entity.fieldValue,
- },
- });
- } else {
- // Add special handling for blank entity field values, checking for either
- // an empty string or the field not existing.
- shouldCriteria.push({
- bool: {
- must: [
- {
- term: {
- [entity.fieldName]: '',
- },
- },
- ],
- },
- });
- shouldCriteria.push({
- bool: {
- must_not: [
- {
- exists: { field: entity.fieldName },
- },
- ],
- },
- });
- }
- });
-
- const body: any = {
- query: {
- bool: {
- must: mustCriteria,
- },
- },
- size: 0,
- _source: {
- excludes: [],
- },
- aggs: {
- byTime: {
- date_histogram: {
- field: timeFieldName,
- interval,
- min_doc_count: 0,
- },
- },
- },
- };
-
- if (shouldCriteria.length > 0) {
- body.query.bool.should = shouldCriteria;
- body.query.bool.minimum_should_match = shouldCriteria.length / 2;
- }
-
- if (metricFieldName !== undefined && metricFieldName !== '') {
- body.aggs.byTime.aggs = {};
-
- const metricAgg: any = {
- [metricFunction]: {
- field: metricFieldName,
- },
- };
-
- if (metricFunction === 'percentiles') {
- metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS];
- }
- body.aggs.byTime.aggs.metric = metricAgg;
- }
-
- return ml.esSearch$({ index, body }).pipe(
- map((resp: any) => {
- const obj: MetricData = { success: true, results: {} };
- const dataByTime = resp?.aggregations?.byTime?.buckets ?? [];
- dataByTime.forEach((dataForTime: any) => {
- if (metricFunction === 'count') {
- obj.results[dataForTime.key] = dataForTime.doc_count;
- } else {
- const value = dataForTime?.metric?.value;
- const values = dataForTime?.metric?.values;
- if (dataForTime.doc_count === 0) {
- obj.results[dataForTime.key] = null;
- } else if (value !== undefined) {
- obj.results[dataForTime.key] = value;
- } else if (values !== undefined) {
- // Percentiles agg currently returns NaN rather than null when none of the docs in the
- // bucket contain the field used in the aggregation
- // (see elasticsearch issue https://github.com/elastic/elasticsearch/issues/29066).
- // Store as null, so values can be handled in the same manner downstream as other aggs
- // (min, mean, max) which return null.
- const medianValues = values[ML_MEDIAN_PERCENTS];
- obj.results[dataForTime.key] = !isNaN(medianValues) ? medianValues : null;
- } else {
- obj.results[dataForTime.key] = null;
- }
- }
- });
-
- return obj;
- })
- );
-}
-
export interface ModelPlotOutput extends ResultResponse {
results: Record;
}
-export function getModelPlotOutput(
- jobId: string,
- detectorIndex: number,
- criteriaFields: any[],
- earliestMs: number,
- latestMs: number,
- interval: string,
- aggType?: { min: any; max: any }
-): Observable {
- const obj: ModelPlotOutput = {
- success: true,
- results: {},
- };
+export interface RecordsForCriteria extends ResultResponse {
+ records: any[];
+}
- // if an aggType object has been passed in, use it.
- // otherwise default to min and max aggs for the upper and lower bounds
- const modelAggs =
- aggType === undefined
- ? { max: 'max', min: 'min' }
- : {
- max: aggType.max,
- min: aggType.min,
- };
+export interface ScheduledEventsByBucket extends ResultResponse {
+ events: Record;
+}
- // Build the criteria to use in the bool filter part of the request.
- // Add criteria for the job ID and time range.
- const mustCriteria: object[] = [
- {
- term: { job_id: jobId },
- },
- {
- range: {
- timestamp: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
- },
- },
- },
- ];
-
- // Add in term queries for each of the specified criteria.
- _.each(criteriaFields, (criteria) => {
- mustCriteria.push({
- term: {
- [criteria.fieldName]: criteria.fieldValue,
- },
- });
- });
-
- // Add criteria for the detector index. Results from jobs created before 6.1 will not
- // contain a detector_index field, so use a should criteria with a 'not exists' check.
- const shouldCriteria = [
- {
- term: { detector_index: detectorIndex },
- },
- {
- bool: {
- must_not: [
- {
- exists: { field: 'detector_index' },
+export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
+ return {
+ getMetricData(
+ index: string,
+ entityFields: any[],
+ query: object | undefined,
+ metricFunction: string, // ES aggregation name
+ metricFieldName: string,
+ timeFieldName: string,
+ earliestMs: number,
+ latestMs: number,
+ interval: string
+ ): Observable {
+ // Build the criteria to use in the bool filter part of the request.
+ // Add criteria for the time range, entity fields,
+ // plus any additional supplied query.
+ const shouldCriteria: object[] = [];
+ const mustCriteria: object[] = [
+ {
+ range: {
+ [timeFieldName]: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
+ },
},
- ],
- },
- },
- ];
+ },
+ ...(query ? [query] : []),
+ ];
+
+ entityFields.forEach((entity) => {
+ if (entity.fieldValue.length !== 0) {
+ mustCriteria.push({
+ term: {
+ [entity.fieldName]: entity.fieldValue,
+ },
+ });
+ } else {
+ // Add special handling for blank entity field values, checking for either
+ // an empty string or the field not existing.
+ shouldCriteria.push({
+ bool: {
+ must: [
+ {
+ term: {
+ [entity.fieldName]: '',
+ },
+ },
+ ],
+ },
+ });
+ shouldCriteria.push({
+ bool: {
+ must_not: [
+ {
+ exists: { field: entity.fieldName },
+ },
+ ],
+ },
+ });
+ }
+ });
- return ml
- .esSearch$({
- index: ML_RESULTS_INDEX_PATTERN,
- size: 0,
- body: {
+ const body: any = {
query: {
bool: {
- filter: [
- {
- query_string: {
- query: 'result_type:model_plot',
- analyze_wildcard: true,
- },
- },
- {
- bool: {
- must: mustCriteria,
- should: shouldCriteria,
- minimum_should_match: 1,
- },
- },
- ],
+ must: mustCriteria,
},
},
+ size: 0,
+ _source: {
+ excludes: [],
+ },
aggs: {
- times: {
+ byTime: {
date_histogram: {
- field: 'timestamp',
+ field: timeFieldName,
interval,
min_doc_count: 0,
},
- aggs: {
- actual: {
- avg: {
- field: 'actual',
- },
- },
- modelUpper: {
- [modelAggs.max]: {
- field: 'model_upper',
- },
- },
- modelLower: {
- [modelAggs.min]: {
- field: 'model_lower',
- },
- },
- },
},
},
- },
- })
- .pipe(
- map((resp) => {
- const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []);
- _.each(aggregationsByTime, (dataForTime: any) => {
- const time = dataForTime.key;
- const modelUpper: number | undefined = _.get(dataForTime, ['modelUpper', 'value']);
- const modelLower: number | undefined = _.get(dataForTime, ['modelLower', 'value']);
- const actual = _.get(dataForTime, ['actual', 'value']);
-
- obj.results[time] = {
- actual,
- modelUpper:
- modelUpper === undefined || isFinite(modelUpper) === false ? null : modelUpper,
- modelLower:
- modelLower === undefined || isFinite(modelLower) === false ? null : modelLower,
- };
- });
+ };
- return obj;
- })
- );
-}
+ if (shouldCriteria.length > 0) {
+ body.query.bool.should = shouldCriteria;
+ body.query.bool.minimum_should_match = shouldCriteria.length / 2;
+ }
-export interface RecordsForCriteria extends ResultResponse {
- records: any[];
-}
+ if (metricFieldName !== undefined && metricFieldName !== '') {
+ body.aggs.byTime.aggs = {};
-// Queries Elasticsearch to obtain the record level results matching the given criteria,
-// for the specified job(s), time range, and record score threshold.
-// criteriaFields parameter must be an array, with each object in the array having 'fieldName'
-// 'fieldValue' properties.
-// Pass an empty array or ['*'] to search over all job IDs.
-export function getRecordsForCriteria(
- jobIds: string[] | undefined,
- criteriaFields: CriteriaField[],
- threshold: any,
- earliestMs: number,
- latestMs: number,
- maxResults: number | undefined
-): Observable {
- const obj: RecordsForCriteria = { success: true, records: [] };
-
- // Build the criteria to use in the bool filter part of the request.
- // Add criteria for the time range, record score, plus any specified job IDs.
- const boolCriteria: any[] = [
- {
- range: {
- timestamp: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
- },
- },
- },
- {
- range: {
- record_score: {
- gte: threshold,
- },
- },
- },
- ];
+ const metricAgg: any = {
+ [metricFunction]: {
+ field: metricFieldName,
+ },
+ };
- if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
- let jobIdFilterStr = '';
- _.each(jobIds, (jobId, i) => {
- if (i > 0) {
- jobIdFilterStr += ' OR ';
+ if (metricFunction === 'percentiles') {
+ metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS];
+ }
+ body.aggs.byTime.aggs.metric = metricAgg;
}
- jobIdFilterStr += 'job_id:';
- jobIdFilterStr += jobId;
- });
- boolCriteria.push({
- query_string: {
- analyze_wildcard: false,
- query: jobIdFilterStr,
- },
- });
- }
-
- // Add in term queries for each of the specified criteria.
- _.each(criteriaFields, (criteria) => {
- boolCriteria.push({
- term: {
- [criteria.fieldName]: criteria.fieldValue,
- },
- });
- });
-
- return ml
- .esSearch$({
- index: ML_RESULTS_INDEX_PATTERN,
- rest_total_hits_as_int: true,
- size: maxResults !== undefined ? maxResults : 100,
- body: {
- query: {
- bool: {
- filter: [
- {
- query_string: {
- query: 'result_type:record',
- analyze_wildcard: false,
- },
- },
- {
- bool: {
- must: boolCriteria,
- },
- },
- ],
- },
- },
- sort: [{ record_score: { order: 'desc' } }],
- },
- })
- .pipe(
- map((resp) => {
- if (resp.hits.total !== 0) {
- _.each(resp.hits.hits, (hit: any) => {
- obj.records.push(hit._source);
+
+ return mlApiServices.esSearch$({ index, body }).pipe(
+ map((resp: any) => {
+ const obj: MetricData = { success: true, results: {} };
+ const dataByTime = resp?.aggregations?.byTime?.buckets ?? [];
+ dataByTime.forEach((dataForTime: any) => {
+ if (metricFunction === 'count') {
+ obj.results[dataForTime.key] = dataForTime.doc_count;
+ } else {
+ const value = dataForTime?.metric?.value;
+ const values = dataForTime?.metric?.values;
+ if (dataForTime.doc_count === 0) {
+ obj.results[dataForTime.key] = null;
+ } else if (value !== undefined) {
+ obj.results[dataForTime.key] = value;
+ } else if (values !== undefined) {
+ // Percentiles agg currently returns NaN rather than null when none of the docs in the
+ // bucket contain the field used in the aggregation
+ // (see elasticsearch issue https://github.com/elastic/elasticsearch/issues/29066).
+ // Store as null, so values can be handled in the same manner downstream as other aggs
+ // (min, mean, max) which return null.
+ const medianValues = values[ML_MEDIAN_PERCENTS];
+ obj.results[dataForTime.key] = !isNaN(medianValues) ? medianValues : null;
+ } else {
+ obj.results[dataForTime.key] = null;
+ }
+ }
});
- }
- return obj;
- })
- );
-}
-export interface ScheduledEventsByBucket extends ResultResponse {
- events: Record;
-}
+ return obj;
+ })
+ );
+ },
-// Obtains a list of scheduled events by job ID and time.
-// Pass an empty array or ['*'] to search over all job IDs.
-// Returned response contains a events property, which will only
-// contains keys for jobs which have scheduled events for the specified time range.
-export function getScheduledEventsByBucket(
- jobIds: string[] | undefined,
- earliestMs: number,
- latestMs: number,
- interval: string,
- maxJobs: number,
- maxEvents: number
-): Observable {
- const obj: ScheduledEventsByBucket = {
- success: true,
- events: {},
- };
+ getModelPlotOutput(
+ jobId: string,
+ detectorIndex: number,
+ criteriaFields: any[],
+ earliestMs: number,
+ latestMs: number,
+ interval: string,
+ aggType?: { min: any; max: any }
+ ): Observable {
+ const obj: ModelPlotOutput = {
+ success: true,
+ results: {},
+ };
+
+ // if an aggType object has been passed in, use it.
+ // otherwise default to min and max aggs for the upper and lower bounds
+ const modelAggs =
+ aggType === undefined
+ ? { max: 'max', min: 'min' }
+ : {
+ max: aggType.max,
+ min: aggType.min,
+ };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Add criteria for the job ID and time range.
+ const mustCriteria: object[] = [
+ {
+ term: { job_id: jobId },
+ },
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
+ },
+ },
+ },
+ ];
- // Build the criteria to use in the bool filter part of the request.
- // Adds criteria for the time range plus any specified job IDs.
- const boolCriteria: any[] = [
- {
- range: {
- timestamp: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
+ // Add in term queries for each of the specified criteria.
+ _.each(criteriaFields, (criteria) => {
+ mustCriteria.push({
+ term: {
+ [criteria.fieldName]: criteria.fieldValue,
+ },
+ });
+ });
+
+ // Add criteria for the detector index. Results from jobs created before 6.1 will not
+ // contain a detector_index field, so use a should criteria with a 'not exists' check.
+ const shouldCriteria = [
+ {
+ term: { detector_index: detectorIndex },
},
- },
- },
- {
- exists: { field: 'scheduled_events' },
- },
- ];
-
- if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
- let jobIdFilterStr = '';
- _.each(jobIds, (jobId, i) => {
- jobIdFilterStr += `${i > 0 ? ' OR ' : ''}job_id:${jobId}`;
- });
- boolCriteria.push({
- query_string: {
- analyze_wildcard: false,
- query: jobIdFilterStr,
- },
- });
- }
-
- return ml
- .esSearch$({
- index: ML_RESULTS_INDEX_PATTERN,
- size: 0,
- body: {
- query: {
+ {
bool: {
- filter: [
+ must_not: [
{
- query_string: {
- query: 'result_type:bucket',
- analyze_wildcard: false,
- },
- },
- {
- bool: {
- must: boolCriteria,
- },
+ exists: { field: 'detector_index' },
},
],
},
},
- aggs: {
- jobs: {
- terms: {
- field: 'job_id',
- min_doc_count: 1,
- size: maxJobs,
+ ];
+
+ return mlApiServices
+ .esSearch$({
+ index: ML_RESULTS_INDEX_PATTERN,
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ query_string: {
+ query: 'result_type:model_plot',
+ analyze_wildcard: true,
+ },
+ },
+ {
+ bool: {
+ must: mustCriteria,
+ should: shouldCriteria,
+ minimum_should_match: 1,
+ },
+ },
+ ],
+ },
},
aggs: {
times: {
date_histogram: {
field: 'timestamp',
interval,
- min_doc_count: 1,
+ min_doc_count: 0,
},
aggs: {
- events: {
- terms: {
- field: 'scheduled_events',
- size: maxEvents,
+ actual: {
+ avg: {
+ field: 'actual',
+ },
+ },
+ modelUpper: {
+ [modelAggs.max]: {
+ field: 'model_upper',
+ },
+ },
+ modelLower: {
+ [modelAggs.min]: {
+ field: 'model_lower',
},
},
},
},
},
},
+ })
+ .pipe(
+ map((resp) => {
+ const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []);
+ _.each(aggregationsByTime, (dataForTime: any) => {
+ const time = dataForTime.key;
+ const modelUpper: number | undefined = _.get(dataForTime, ['modelUpper', 'value']);
+ const modelLower: number | undefined = _.get(dataForTime, ['modelLower', 'value']);
+ const actual = _.get(dataForTime, ['actual', 'value']);
+
+ obj.results[time] = {
+ actual,
+ modelUpper:
+ modelUpper === undefined || isFinite(modelUpper) === false ? null : modelUpper,
+ modelLower:
+ modelLower === undefined || isFinite(modelLower) === false ? null : modelLower,
+ };
+ });
+
+ return obj;
+ })
+ );
+ },
+
+ // Queries Elasticsearch to obtain the record level results matching the given criteria,
+ // for the specified job(s), time range, and record score threshold.
+ // criteriaFields parameter must be an array, with each object in the array having 'fieldName'
+ // 'fieldValue' properties.
+ // Pass an empty array or ['*'] to search over all job IDs.
+ getRecordsForCriteria(
+ jobIds: string[] | undefined,
+ criteriaFields: CriteriaField[],
+ threshold: any,
+ earliestMs: number,
+ latestMs: number,
+ maxResults: number | undefined
+ ): Observable {
+ const obj: RecordsForCriteria = { success: true, records: [] };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Add criteria for the time range, record score, plus any specified job IDs.
+ const boolCriteria: any[] = [
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
+ },
+ },
},
- },
- })
- .pipe(
- map((resp) => {
- const dataByJobId = _.get(resp, ['aggregations', 'jobs', 'buckets'], []);
- _.each(dataByJobId, (dataForJob: any) => {
- const jobId: string = dataForJob.key;
- const resultsForTime: Record = {};
- const dataByTime = _.get(dataForJob, ['times', 'buckets'], []);
- _.each(dataByTime, (dataForTime: any) => {
- const time: string = dataForTime.key;
- const events: object[] = _.get(dataForTime, ['events', 'buckets']);
- resultsForTime[time] = _.map(events, 'key');
- });
- obj.events[jobId] = resultsForTime;
+ {
+ range: {
+ record_score: {
+ gte: threshold,
+ },
+ },
+ },
+ ];
+
+ if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
+ let jobIdFilterStr = '';
+ _.each(jobIds, (jobId, i) => {
+ if (i > 0) {
+ jobIdFilterStr += ' OR ';
+ }
+ jobIdFilterStr += 'job_id:';
+ jobIdFilterStr += jobId;
+ });
+ boolCriteria.push({
+ query_string: {
+ analyze_wildcard: false,
+ query: jobIdFilterStr,
+ },
});
+ }
- return obj;
- })
- );
-}
+ // Add in term queries for each of the specified criteria.
+ _.each(criteriaFields, (criteria) => {
+ boolCriteria.push({
+ term: {
+ [criteria.fieldName]: criteria.fieldValue,
+ },
+ });
+ });
-export function fetchPartitionFieldsValues(
- jobId: JobId,
- searchTerm: Dictionary,
- criteriaFields: Array<{ fieldName: string; fieldValue: any }>,
- earliestMs: number,
- latestMs: number
-) {
- return ml.results.fetchPartitionFieldsValues(
- jobId,
- searchTerm,
- criteriaFields,
- earliestMs,
- latestMs
- );
+ return mlApiServices
+ .esSearch$({
+ index: ML_RESULTS_INDEX_PATTERN,
+ rest_total_hits_as_int: true,
+ size: maxResults !== undefined ? maxResults : 100,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ query_string: {
+ query: 'result_type:record',
+ analyze_wildcard: false,
+ },
+ },
+ {
+ bool: {
+ must: boolCriteria,
+ },
+ },
+ ],
+ },
+ },
+ sort: [{ record_score: { order: 'desc' } }],
+ },
+ })
+ .pipe(
+ map((resp) => {
+ if (resp.hits.total !== 0) {
+ _.each(resp.hits.hits, (hit: any) => {
+ obj.records.push(hit._source);
+ });
+ }
+ return obj;
+ })
+ );
+ },
+
+ // Obtains a list of scheduled events by job ID and time.
+ // Pass an empty array or ['*'] to search over all job IDs.
+ // Returned response contains a events property, which will only
+ // contains keys for jobs which have scheduled events for the specified time range.
+ getScheduledEventsByBucket(
+ jobIds: string[] | undefined,
+ earliestMs: number,
+ latestMs: number,
+ interval: string,
+ maxJobs: number,
+ maxEvents: number
+ ): Observable {
+ const obj: ScheduledEventsByBucket = {
+ success: true,
+ events: {},
+ };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Adds criteria for the time range plus any specified job IDs.
+ const boolCriteria: any[] = [
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
+ },
+ },
+ },
+ {
+ exists: { field: 'scheduled_events' },
+ },
+ ];
+
+ if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
+ let jobIdFilterStr = '';
+ _.each(jobIds, (jobId, i) => {
+ jobIdFilterStr += `${i > 0 ? ' OR ' : ''}job_id:${jobId}`;
+ });
+ boolCriteria.push({
+ query_string: {
+ analyze_wildcard: false,
+ query: jobIdFilterStr,
+ },
+ });
+ }
+
+ return mlApiServices
+ .esSearch$({
+ index: ML_RESULTS_INDEX_PATTERN,
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ query_string: {
+ query: 'result_type:bucket',
+ analyze_wildcard: false,
+ },
+ },
+ {
+ bool: {
+ must: boolCriteria,
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ jobs: {
+ terms: {
+ field: 'job_id',
+ min_doc_count: 1,
+ size: maxJobs,
+ },
+ aggs: {
+ times: {
+ date_histogram: {
+ field: 'timestamp',
+ interval,
+ min_doc_count: 1,
+ },
+ aggs: {
+ events: {
+ terms: {
+ field: 'scheduled_events',
+ size: maxEvents,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ })
+ .pipe(
+ map((resp) => {
+ const dataByJobId = _.get(resp, ['aggregations', 'jobs', 'buckets'], []);
+ _.each(dataByJobId, (dataForJob: any) => {
+ const jobId: string = dataForJob.key;
+ const resultsForTime: Record = {};
+ const dataByTime = _.get(dataForJob, ['times', 'buckets'], []);
+ _.each(dataByTime, (dataForTime: any) => {
+ const time: string = dataForTime.key;
+ const events: object[] = _.get(dataForTime, ['events', 'buckets']);
+ resultsForTime[time] = _.map(events, 'key');
+ });
+ obj.events[jobId] = resultsForTime;
+ });
+
+ return obj;
+ })
+ );
+ },
+
+ fetchPartitionFieldsValues(
+ jobId: JobId,
+ searchTerm: Dictionary,
+ criteriaFields: Array<{ fieldName: string; fieldValue: any }>,
+ earliestMs: number,
+ latestMs: number
+ ) {
+ return mlApiServices.results.fetchPartitionFieldsValues(
+ jobId,
+ searchTerm,
+ criteriaFields,
+ earliestMs,
+ latestMs
+ );
+ },
+ };
}
diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
index 4af08994432bd..1b2c01ab73fce 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
+++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
@@ -4,43 +4,49 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export function getScoresByBucket(
- jobIds: string[],
- earliestMs: number,
- latestMs: number,
- interval: string | number,
- maxResults: number
-): Promise;
-export function getTopInfluencers(): Promise;
-export function getTopInfluencerValues(): Promise;
-export function getOverallBucketScores(
- jobIds: any,
- topN: any,
- earliestMs: any,
- latestMs: any,
- interval?: any
-): Promise;
-export function getInfluencerValueMaxScoreByTime(
- jobIds: string[],
- influencerFieldName: string,
- influencerFieldValues: string[],
- earliestMs: number,
- latestMs: number,
- interval: string,
- maxResults: number,
- influencersFilterQuery: any
-): Promise;
-export function getRecordInfluencers(): Promise;
-export function getRecordsForInfluencer(): Promise;
-export function getRecordsForDetector(): Promise;
-export function getRecords(): Promise;
-export function getEventRateData(
- index: string,
- query: any,
- timeFieldName: string,
- earliestMs: number,
- latestMs: number,
- interval: string | number
-): Promise;
-export function getEventDistributionData(): Promise;
-export function getRecordMaxScoreByTime(): Promise;
+import { MlApiServices } from '../ml_api_service';
+
+export function resultsServiceProvider(
+ mlApiServices: MlApiServices
+): {
+ getScoresByBucket(
+ jobIds: string[],
+ earliestMs: number,
+ latestMs: number,
+ interval: string | number,
+ maxResults: number
+ ): Promise;
+ getTopInfluencers(): Promise;
+ getTopInfluencerValues(): Promise;
+ getOverallBucketScores(
+ jobIds: any,
+ topN: any,
+ earliestMs: any,
+ latestMs: any,
+ interval?: any
+ ): Promise;
+ getInfluencerValueMaxScoreByTime(
+ jobIds: string[],
+ influencerFieldName: string,
+ influencerFieldValues: string[],
+ earliestMs: number,
+ latestMs: number,
+ interval: string,
+ maxResults: number,
+ influencersFilterQuery: any
+ ): Promise;
+ getRecordInfluencers(): Promise;
+ getRecordsForInfluencer(): Promise;
+ getRecordsForDetector(): Promise;
+ getRecords(): Promise;
+ getEventRateData(
+ index: string,
+ query: any,
+ timeFieldName: string,
+ earliestMs: number,
+ latestMs: number,
+ interval: string | number
+ ): Promise;
+ getEventDistributionData(): Promise;
+ getRecordMaxScoreByTime(): Promise;
+};
diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js
index 4fccc4d789370..9e3fed189b6f4 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js
+++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js
@@ -4,1322 +4,1331 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// Service for carrying out Elasticsearch queries to obtain data for the
-// Ml Results dashboards.
import _ from 'lodash';
-// import d3 from 'd3';
import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils';
import { escapeForElasticsearchQuery } from '../../util/string_utils';
import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns';
-import { ml } from '../ml_api_service';
-
-// Obtains the maximum bucket anomaly scores by job ID and time.
-// Pass an empty array or ['*'] to search over all job IDs.
-// Returned response contains a results property, with a key for job
-// which has results for the specified time range.
-export function getScoresByBucket(jobIds, earliestMs, latestMs, interval, maxResults) {
- return new Promise((resolve, reject) => {
- const obj = {
- success: true,
- results: {},
- };
-
- // Build the criteria to use in the bool filter part of the request.
- // Adds criteria for the time range plus any specified job IDs.
- const boolCriteria = [
- {
- range: {
- timestamp: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
- },
- },
- },
- ];
-
- if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
- let jobIdFilterStr = '';
- _.each(jobIds, (jobId, i) => {
- if (i > 0) {
- jobIdFilterStr += ' OR ';
- }
- jobIdFilterStr += 'job_id:';
- jobIdFilterStr += jobId;
- });
- boolCriteria.push({
- query_string: {
- analyze_wildcard: false,
- query: jobIdFilterStr,
- },
- });
- }
-
- ml.esSearch({
- index: ML_RESULTS_INDEX_PATTERN,
- size: 0,
- body: {
- query: {
- bool: {
- filter: [
- {
- query_string: {
- query: 'result_type:bucket',
- analyze_wildcard: false,
- },
- },
- {
- bool: {
- must: boolCriteria,
- },
+/**
+ * Service for carrying out Elasticsearch queries to obtain data for the Ml Results dashboards.
+ */
+export function resultsServiceProvider(mlApiServices) {
+ const SAMPLER_TOP_TERMS_SHARD_SIZE = 20000;
+ const ENTITY_AGGREGATION_SIZE = 10;
+ const AGGREGATION_MIN_DOC_COUNT = 1;
+ const CARDINALITY_PRECISION_THRESHOLD = 100;
+
+ return {
+ // Obtains the maximum bucket anomaly scores by job ID and time.
+ // Pass an empty array or ['*'] to search over all job IDs.
+ // Returned response contains a results property, with a key for job
+ // which has results for the specified time range.
+ getScoresByBucket(jobIds, earliestMs, latestMs, interval, maxResults) {
+ return new Promise((resolve, reject) => {
+ const obj = {
+ success: true,
+ results: {},
+ };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Adds criteria for the time range plus any specified job IDs.
+ const boolCriteria = [
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
},
- ],
+ },
},
- },
- aggs: {
- jobId: {
- terms: {
- field: 'job_id',
- size: maxResults !== undefined ? maxResults : 5,
- order: {
- anomalyScore: 'desc',
- },
+ ];
+
+ if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
+ let jobIdFilterStr = '';
+ _.each(jobIds, (jobId, i) => {
+ if (i > 0) {
+ jobIdFilterStr += ' OR ';
+ }
+ jobIdFilterStr += 'job_id:';
+ jobIdFilterStr += jobId;
+ });
+ boolCriteria.push({
+ query_string: {
+ analyze_wildcard: false,
+ query: jobIdFilterStr,
},
- aggs: {
- anomalyScore: {
- max: {
- field: 'anomaly_score',
+ });
+ }
+
+ mlApiServices
+ .esSearch({
+ index: ML_RESULTS_INDEX_PATTERN,
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ query_string: {
+ query: 'result_type:bucket',
+ analyze_wildcard: false,
+ },
+ },
+ {
+ bool: {
+ must: boolCriteria,
+ },
+ },
+ ],
},
},
- byTime: {
- date_histogram: {
- field: 'timestamp',
- interval: interval,
- min_doc_count: 1,
- extended_bounds: {
- min: earliestMs,
- max: latestMs,
+ aggs: {
+ jobId: {
+ terms: {
+ field: 'job_id',
+ size: maxResults !== undefined ? maxResults : 5,
+ order: {
+ anomalyScore: 'desc',
+ },
},
- },
- aggs: {
- anomalyScore: {
- max: {
- field: 'anomaly_score',
+ aggs: {
+ anomalyScore: {
+ max: {
+ field: 'anomaly_score',
+ },
+ },
+ byTime: {
+ date_histogram: {
+ field: 'timestamp',
+ interval: interval,
+ min_doc_count: 1,
+ extended_bounds: {
+ min: earliestMs,
+ max: latestMs,
+ },
+ },
+ aggs: {
+ anomalyScore: {
+ max: {
+ field: 'anomaly_score',
+ },
+ },
+ },
},
},
},
},
},
+ })
+ .then((resp) => {
+ const dataByJobId = _.get(resp, ['aggregations', 'jobId', 'buckets'], []);
+ _.each(dataByJobId, (dataForJob) => {
+ const jobId = dataForJob.key;
+
+ const resultsForTime = {};
+
+ const dataByTime = _.get(dataForJob, ['byTime', 'buckets'], []);
+ _.each(dataByTime, (dataForTime) => {
+ const value = _.get(dataForTime, ['anomalyScore', 'value']);
+ if (value !== undefined) {
+ const time = dataForTime.key;
+ resultsForTime[time] = _.get(dataForTime, ['anomalyScore', 'value']);
+ }
+ });
+ obj.results[jobId] = resultsForTime;
+ });
+
+ resolve(obj);
+ })
+ .catch((resp) => {
+ reject(resp);
+ });
+ });
+ },
+
+ // Obtains the top influencers, by maximum influencer score, for the specified index, time range and job ID(s).
+ // Pass an empty array or ['*'] to search over all job IDs.
+ // An optional array of influencers may be supplied, with each object in the array having 'fieldName'
+ // and 'fieldValue' properties, to limit data to the supplied list of influencers.
+ // Returned response contains an influencers property, with a key for each of the influencer field names,
+ // whose value is an array of objects containing influencerFieldValue, maxAnomalyScore and sumAnomalyScore keys.
+ getTopInfluencers(
+ jobIds,
+ earliestMs,
+ latestMs,
+ maxFieldValues = 10,
+ influencers = [],
+ influencersFilterQuery
+ ) {
+ return new Promise((resolve, reject) => {
+ const obj = { success: true, influencers: {} };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Adds criteria for the time range plus any specified job IDs.
+ const boolCriteria = [
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
+ },
+ },
},
- },
- },
- })
- .then((resp) => {
- const dataByJobId = _.get(resp, ['aggregations', 'jobId', 'buckets'], []);
- _.each(dataByJobId, (dataForJob) => {
- const jobId = dataForJob.key;
-
- const resultsForTime = {};
-
- const dataByTime = _.get(dataForJob, ['byTime', 'buckets'], []);
- _.each(dataByTime, (dataForTime) => {
- const value = _.get(dataForTime, ['anomalyScore', 'value']);
- if (value !== undefined) {
- const time = dataForTime.key;
- resultsForTime[time] = _.get(dataForTime, ['anomalyScore', 'value']);
+ {
+ range: {
+ influencer_score: {
+ gt: 0,
+ },
+ },
+ },
+ ];
+
+ if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
+ let jobIdFilterStr = '';
+ _.each(jobIds, (jobId, i) => {
+ if (i > 0) {
+ jobIdFilterStr += ' OR ';
}
+ jobIdFilterStr += 'job_id:';
+ jobIdFilterStr += jobId;
});
- obj.results[jobId] = resultsForTime;
- });
+ boolCriteria.push({
+ query_string: {
+ analyze_wildcard: false,
+ query: jobIdFilterStr,
+ },
+ });
+ }
- resolve(obj);
- })
- .catch((resp) => {
- reject(resp);
- });
- });
-}
+ if (influencersFilterQuery !== undefined) {
+ boolCriteria.push(influencersFilterQuery);
+ }
-// Obtains the top influencers, by maximum influencer score, for the specified index, time range and job ID(s).
-// Pass an empty array or ['*'] to search over all job IDs.
-// An optional array of influencers may be supplied, with each object in the array having 'fieldName'
-// and 'fieldValue' properties, to limit data to the supplied list of influencers.
-// Returned response contains an influencers property, with a key for each of the influencer field names,
-// whose value is an array of objects containing influencerFieldValue, maxAnomalyScore and sumAnomalyScore keys.
-export function getTopInfluencers(
- jobIds,
- earliestMs,
- latestMs,
- maxFieldValues = 10,
- influencers = [],
- influencersFilterQuery
-) {
- return new Promise((resolve, reject) => {
- const obj = { success: true, influencers: {} };
-
- // Build the criteria to use in the bool filter part of the request.
- // Adds criteria for the time range plus any specified job IDs.
- const boolCriteria = [
- {
- range: {
- timestamp: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
- },
- },
- },
- {
- range: {
- influencer_score: {
- gt: 0,
- },
- },
- },
- ];
-
- if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
- let jobIdFilterStr = '';
- _.each(jobIds, (jobId, i) => {
- if (i > 0) {
- jobIdFilterStr += ' OR ';
+ // Add a should query to filter for each of the specified influencers.
+ if (influencers.length > 0) {
+ boolCriteria.push({
+ bool: {
+ should: influencers.map((influencer) => {
+ return {
+ bool: {
+ must: [
+ { term: { influencer_field_name: influencer.fieldName } },
+ { term: { influencer_field_value: influencer.fieldValue } },
+ ],
+ },
+ };
+ }),
+ minimum_should_match: 1,
+ },
+ });
}
- jobIdFilterStr += 'job_id:';
- jobIdFilterStr += jobId;
- });
- boolCriteria.push({
- query_string: {
- analyze_wildcard: false,
- query: jobIdFilterStr,
- },
- });
- }
-
- if (influencersFilterQuery !== undefined) {
- boolCriteria.push(influencersFilterQuery);
- }
-
- // Add a should query to filter for each of the specified influencers.
- if (influencers.length > 0) {
- boolCriteria.push({
- bool: {
- should: influencers.map((influencer) => {
- return {
- bool: {
- must: [
- { term: { influencer_field_name: influencer.fieldName } },
- { term: { influencer_field_value: influencer.fieldValue } },
- ],
- },
- };
- }),
- minimum_should_match: 1,
- },
- });
- }
-
- ml.esSearch({
- index: ML_RESULTS_INDEX_PATTERN,
- size: 0,
- body: {
- query: {
- bool: {
- filter: [
- {
- query_string: {
- query: 'result_type:influencer',
- analyze_wildcard: false,
- },
- },
- {
+
+ mlApiServices
+ .esSearch({
+ index: ML_RESULTS_INDEX_PATTERN,
+ size: 0,
+ body: {
+ query: {
bool: {
- must: boolCriteria,
- },
- },
- ],
- },
- },
- aggs: {
- influencerFieldNames: {
- terms: {
- field: 'influencer_field_name',
- size: 5,
- order: {
- maxAnomalyScore: 'desc',
- },
- },
- aggs: {
- maxAnomalyScore: {
- max: {
- field: 'influencer_score',
+ filter: [
+ {
+ query_string: {
+ query: 'result_type:influencer',
+ analyze_wildcard: false,
+ },
+ },
+ {
+ bool: {
+ must: boolCriteria,
+ },
+ },
+ ],
},
},
- influencerFieldValues: {
- terms: {
- field: 'influencer_field_value',
- size: maxFieldValues,
- order: {
- maxAnomalyScore: 'desc',
- },
- },
- aggs: {
- maxAnomalyScore: {
- max: {
- field: 'influencer_score',
+ aggs: {
+ influencerFieldNames: {
+ terms: {
+ field: 'influencer_field_name',
+ size: 5,
+ order: {
+ maxAnomalyScore: 'desc',
},
},
- sumAnomalyScore: {
- sum: {
- field: 'influencer_score',
+ aggs: {
+ maxAnomalyScore: {
+ max: {
+ field: 'influencer_score',
+ },
+ },
+ influencerFieldValues: {
+ terms: {
+ field: 'influencer_field_value',
+ size: maxFieldValues,
+ order: {
+ maxAnomalyScore: 'desc',
+ },
+ },
+ aggs: {
+ maxAnomalyScore: {
+ max: {
+ field: 'influencer_score',
+ },
+ },
+ sumAnomalyScore: {
+ sum: {
+ field: 'influencer_score',
+ },
+ },
+ },
},
},
},
},
},
- },
- },
- },
- })
- .then((resp) => {
- const fieldNameBuckets = _.get(
- resp,
- ['aggregations', 'influencerFieldNames', 'buckets'],
- []
- );
- _.each(fieldNameBuckets, (nameBucket) => {
- const fieldName = nameBucket.key;
- const fieldValues = [];
-
- const fieldValueBuckets = _.get(nameBucket, ['influencerFieldValues', 'buckets'], []);
- _.each(fieldValueBuckets, (valueBucket) => {
- const fieldValueResult = {
- influencerFieldValue: valueBucket.key,
- maxAnomalyScore: valueBucket.maxAnomalyScore.value,
- sumAnomalyScore: valueBucket.sumAnomalyScore.value,
- };
- fieldValues.push(fieldValueResult);
- });
-
- obj.influencers[fieldName] = fieldValues;
- });
-
- resolve(obj);
- })
- .catch((resp) => {
- reject(resp);
- });
- });
-}
+ })
+ .then((resp) => {
+ const fieldNameBuckets = _.get(
+ resp,
+ ['aggregations', 'influencerFieldNames', 'buckets'],
+ []
+ );
+ _.each(fieldNameBuckets, (nameBucket) => {
+ const fieldName = nameBucket.key;
+ const fieldValues = [];
+
+ const fieldValueBuckets = _.get(nameBucket, ['influencerFieldValues', 'buckets'], []);
+ _.each(fieldValueBuckets, (valueBucket) => {
+ const fieldValueResult = {
+ influencerFieldValue: valueBucket.key,
+ maxAnomalyScore: valueBucket.maxAnomalyScore.value,
+ sumAnomalyScore: valueBucket.sumAnomalyScore.value,
+ };
+ fieldValues.push(fieldValueResult);
+ });
+
+ obj.influencers[fieldName] = fieldValues;
+ });
-// Obtains the top influencer field values, by maximum anomaly score, for a
-// particular index, field name and job ID(s).
-// Pass an empty array or ['*'] to search over all job IDs.
-// Returned response contains a results property, which is an array of objects
-// containing influencerFieldValue, maxAnomalyScore and sumAnomalyScore keys.
-export function getTopInfluencerValues(
- jobIds,
- influencerFieldName,
- earliestMs,
- latestMs,
- maxResults
-) {
- return new Promise((resolve, reject) => {
- const obj = { success: true, results: [] };
-
- // Build the criteria to use in the bool filter part of the request.
- // Adds criteria for the time range plus any specified job IDs.
- const boolCriteria = [
- {
- range: {
- timestamp: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
- },
- },
- },
- ];
-
- if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
- let jobIdFilterStr = '';
- _.each(jobIds, (jobId, i) => {
- if (i > 0) {
- jobIdFilterStr += ' OR ';
- }
- jobIdFilterStr += 'job_id:';
- jobIdFilterStr += jobId;
- });
- boolCriteria.push({
- query_string: {
- analyze_wildcard: false,
- query: jobIdFilterStr,
- },
+ resolve(obj);
+ })
+ .catch((resp) => {
+ reject(resp);
+ });
});
- }
-
- ml.esSearch({
- index: ML_RESULTS_INDEX_PATTERN,
- size: 0,
- body: {
- query: {
- bool: {
- filter: [
- {
- query_string: {
- query: `result_type:influencer AND influencer_field_name: ${escapeForElasticsearchQuery(
- influencerFieldName
- )}`,
- analyze_wildcard: false,
- },
+ },
+
+ // Obtains the top influencer field values, by maximum anomaly score, for a
+ // particular index, field name and job ID(s).
+ // Pass an empty array or ['*'] to search over all job IDs.
+ // Returned response contains a results property, which is an array of objects
+ // containing influencerFieldValue, maxAnomalyScore and sumAnomalyScore keys.
+ getTopInfluencerValues(jobIds, influencerFieldName, earliestMs, latestMs, maxResults) {
+ return new Promise((resolve, reject) => {
+ const obj = { success: true, results: [] };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Adds criteria for the time range plus any specified job IDs.
+ const boolCriteria = [
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
},
- {
- bool: {
- must: boolCriteria,
- },
- },
- ],
+ },
},
- },
- aggs: {
- influencerFieldValues: {
- terms: {
- field: 'influencer_field_value',
- size: maxResults !== undefined ? maxResults : 2,
- order: {
- maxAnomalyScore: 'desc',
- },
+ ];
+
+ if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
+ let jobIdFilterStr = '';
+ _.each(jobIds, (jobId, i) => {
+ if (i > 0) {
+ jobIdFilterStr += ' OR ';
+ }
+ jobIdFilterStr += 'job_id:';
+ jobIdFilterStr += jobId;
+ });
+ boolCriteria.push({
+ query_string: {
+ analyze_wildcard: false,
+ query: jobIdFilterStr,
},
- aggs: {
- maxAnomalyScore: {
- max: {
- field: 'influencer_score',
+ });
+ }
+
+ mlApiServices
+ .esSearch({
+ index: ML_RESULTS_INDEX_PATTERN,
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ query_string: {
+ query: `result_type:influencer AND influencer_field_name: ${escapeForElasticsearchQuery(
+ influencerFieldName
+ )}`,
+ analyze_wildcard: false,
+ },
+ },
+ {
+ bool: {
+ must: boolCriteria,
+ },
+ },
+ ],
},
},
- sumAnomalyScore: {
- sum: {
- field: 'influencer_score',
+ aggs: {
+ influencerFieldValues: {
+ terms: {
+ field: 'influencer_field_value',
+ size: maxResults !== undefined ? maxResults : 2,
+ order: {
+ maxAnomalyScore: 'desc',
+ },
+ },
+ aggs: {
+ maxAnomalyScore: {
+ max: {
+ field: 'influencer_score',
+ },
+ },
+ sumAnomalyScore: {
+ sum: {
+ field: 'influencer_score',
+ },
+ },
+ },
},
},
},
- },
- },
- },
- })
- .then((resp) => {
- const buckets = _.get(resp, ['aggregations', 'influencerFieldValues', 'buckets'], []);
- _.each(buckets, (bucket) => {
- const result = {
- influencerFieldValue: bucket.key,
- maxAnomalyScore: bucket.maxAnomalyScore.value,
- sumAnomalyScore: bucket.sumAnomalyScore.value,
- };
- obj.results.push(result);
- });
+ })
+ .then((resp) => {
+ const buckets = _.get(resp, ['aggregations', 'influencerFieldValues', 'buckets'], []);
+ _.each(buckets, (bucket) => {
+ const result = {
+ influencerFieldValue: bucket.key,
+ maxAnomalyScore: bucket.maxAnomalyScore.value,
+ sumAnomalyScore: bucket.sumAnomalyScore.value,
+ };
+ obj.results.push(result);
+ });
- resolve(obj);
- })
- .catch((resp) => {
- reject(resp);
+ resolve(obj);
+ })
+ .catch((resp) => {
+ reject(resp);
+ });
});
- });
-}
-
-// Obtains the overall bucket scores for the specified job ID(s).
-// Pass ['*'] to search over all job IDs.
-// Returned response contains a results property as an object of max score by time.
-export function getOverallBucketScores(jobIds, topN, earliestMs, latestMs, interval) {
- return new Promise((resolve, reject) => {
- const obj = { success: true, results: {} };
-
- ml.overallBuckets({
- jobId: jobIds,
- topN: topN,
- bucketSpan: interval,
- start: earliestMs,
- end: latestMs,
- })
- .then((resp) => {
- const dataByTime = _.get(resp, ['overall_buckets'], []);
- _.each(dataByTime, (dataForTime) => {
- const value = _.get(dataForTime, ['overall_score']);
- if (value !== undefined) {
- obj.results[dataForTime.timestamp] = value;
- }
- });
+ },
+
+ // Obtains the overall bucket scores for the specified job ID(s).
+ // Pass ['*'] to search over all job IDs.
+ // Returned response contains a results property as an object of max score by time.
+ getOverallBucketScores(jobIds, topN, earliestMs, latestMs, interval) {
+ return new Promise((resolve, reject) => {
+ const obj = { success: true, results: {} };
+
+ mlApiServices
+ .overallBuckets({
+ jobId: jobIds,
+ topN: topN,
+ bucketSpan: interval,
+ start: earliestMs,
+ end: latestMs,
+ })
+ .then((resp) => {
+ const dataByTime = _.get(resp, ['overall_buckets'], []);
+ _.each(dataByTime, (dataForTime) => {
+ const value = _.get(dataForTime, ['overall_score']);
+ if (value !== undefined) {
+ obj.results[dataForTime.timestamp] = value;
+ }
+ });
- resolve(obj);
- })
- .catch((resp) => {
- reject(resp);
+ resolve(obj);
+ })
+ .catch((resp) => {
+ reject(resp);
+ });
});
- });
-}
-
-// Obtains the maximum score by influencer_field_value and by time for the specified job ID(s)
-// (pass an empty array or ['*'] to search over all job IDs), and specified influencer field
-// values (pass an empty array to search over all field values).
-// Returned response contains a results property with influencer field values keyed
-// against max score by time.
-export function getInfluencerValueMaxScoreByTime(
- jobIds,
- influencerFieldName,
- influencerFieldValues,
- earliestMs,
- latestMs,
- interval,
- maxResults,
- influencersFilterQuery
-) {
- return new Promise((resolve, reject) => {
- const obj = { success: true, results: {} };
-
- // Build the criteria to use in the bool filter part of the request.
- // Adds criteria for the time range plus any specified job IDs.
- const boolCriteria = [
- {
- range: {
- timestamp: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
+ },
+
+ // Obtains the maximum score by influencer_field_value and by time for the specified job ID(s)
+ // (pass an empty array or ['*'] to search over all job IDs), and specified influencer field
+ // values (pass an empty array to search over all field values).
+ // Returned response contains a results property with influencer field values keyed
+ // against max score by time.
+ getInfluencerValueMaxScoreByTime(
+ jobIds,
+ influencerFieldName,
+ influencerFieldValues,
+ earliestMs,
+ latestMs,
+ interval,
+ maxResults,
+ influencersFilterQuery
+ ) {
+ return new Promise((resolve, reject) => {
+ const obj = { success: true, results: {} };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Adds criteria for the time range plus any specified job IDs.
+ const boolCriteria = [
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
+ },
+ },
},
- },
- },
- {
- range: {
- influencer_score: {
- gt: 0,
+ {
+ range: {
+ influencer_score: {
+ gt: 0,
+ },
+ },
},
- },
- },
- ];
-
- if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
- let jobIdFilterStr = '';
- _.each(jobIds, (jobId, i) => {
- if (i > 0) {
- jobIdFilterStr += ' OR ';
- }
- jobIdFilterStr += `job_id:${jobId}`;
- });
- boolCriteria.push({
- query_string: {
- analyze_wildcard: false,
- query: jobIdFilterStr,
- },
- });
- }
+ ];
- if (influencersFilterQuery !== undefined) {
- boolCriteria.push(influencersFilterQuery);
- }
+ if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
+ let jobIdFilterStr = '';
+ _.each(jobIds, (jobId, i) => {
+ if (i > 0) {
+ jobIdFilterStr += ' OR ';
+ }
+ jobIdFilterStr += `job_id:${jobId}`;
+ });
+ boolCriteria.push({
+ query_string: {
+ analyze_wildcard: false,
+ query: jobIdFilterStr,
+ },
+ });
+ }
- if (influencerFieldValues && influencerFieldValues.length > 0) {
- let influencerFilterStr = '';
- _.each(influencerFieldValues, (value, i) => {
- if (i > 0) {
- influencerFilterStr += ' OR ';
+ if (influencersFilterQuery !== undefined) {
+ boolCriteria.push(influencersFilterQuery);
}
- if (value.trim().length > 0) {
- influencerFilterStr += `influencer_field_value:${escapeForElasticsearchQuery(value)}`;
- } else {
- // Wrap whitespace influencer field values in quotes for the query_string query.
- influencerFilterStr += `influencer_field_value:"${value}"`;
+
+ if (influencerFieldValues && influencerFieldValues.length > 0) {
+ let influencerFilterStr = '';
+ _.each(influencerFieldValues, (value, i) => {
+ if (i > 0) {
+ influencerFilterStr += ' OR ';
+ }
+ if (value.trim().length > 0) {
+ influencerFilterStr += `influencer_field_value:${escapeForElasticsearchQuery(value)}`;
+ } else {
+ // Wrap whitespace influencer field values in quotes for the query_string query.
+ influencerFilterStr += `influencer_field_value:"${value}"`;
+ }
+ });
+ boolCriteria.push({
+ query_string: {
+ analyze_wildcard: false,
+ query: influencerFilterStr,
+ },
+ });
}
- });
- boolCriteria.push({
- query_string: {
- analyze_wildcard: false,
- query: influencerFilterStr,
- },
- });
- }
-
- ml.esSearch({
- index: ML_RESULTS_INDEX_PATTERN,
- size: 0,
- body: {
- query: {
- bool: {
- filter: [
- {
- query_string: {
- query: `result_type:influencer AND influencer_field_name: ${escapeForElasticsearchQuery(
- influencerFieldName
- )}`,
- analyze_wildcard: false,
+
+ mlApiServices
+ .esSearch({
+ index: ML_RESULTS_INDEX_PATTERN,
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ query_string: {
+ query: `result_type:influencer AND influencer_field_name: ${escapeForElasticsearchQuery(
+ influencerFieldName
+ )}`,
+ analyze_wildcard: false,
+ },
+ },
+ {
+ bool: {
+ must: boolCriteria,
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ influencerFieldValues: {
+ terms: {
+ field: 'influencer_field_value',
+ size: maxResults !== undefined ? maxResults : 10,
+ order: {
+ maxAnomalyScore: 'desc',
+ },
+ },
+ aggs: {
+ maxAnomalyScore: {
+ max: {
+ field: 'influencer_score',
+ },
+ },
+ byTime: {
+ date_histogram: {
+ field: 'timestamp',
+ interval,
+ min_doc_count: 1,
+ },
+ aggs: {
+ maxAnomalyScore: {
+ max: {
+ field: 'influencer_score',
+ },
+ },
+ },
+ },
+ },
},
},
- {
+ },
+ })
+ .then((resp) => {
+ const fieldValueBuckets = _.get(
+ resp,
+ ['aggregations', 'influencerFieldValues', 'buckets'],
+ []
+ );
+ _.each(fieldValueBuckets, (valueBucket) => {
+ const fieldValue = valueBucket.key;
+ const fieldValues = {};
+
+ const timeBuckets = _.get(valueBucket, ['byTime', 'buckets'], []);
+ _.each(timeBuckets, (timeBucket) => {
+ const time = timeBucket.key;
+ const score = timeBucket.maxAnomalyScore.value;
+ fieldValues[time] = score;
+ });
+
+ obj.results[fieldValue] = fieldValues;
+ });
+
+ resolve(obj);
+ })
+ .catch((resp) => {
+ reject(resp);
+ });
+ });
+ },
+
+ // Queries Elasticsearch to obtain record level results containing the influencers
+ // for the specified job(s), record score threshold, and time range.
+ // Pass an empty array or ['*'] to search over all job IDs.
+ // Returned response contains a records property, with each record containing
+ // only the fields job_id, detector_index, record_score and influencers.
+ getRecordInfluencers(jobIds, threshold, earliestMs, latestMs, maxResults) {
+ return new Promise((resolve, reject) => {
+ const obj = { success: true, records: [] };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Adds criteria for the existence of the nested influencers field, time range,
+ // record score, plus any specified job IDs.
+ const boolCriteria = [
+ {
+ nested: {
+ path: 'influencers',
+ query: {
bool: {
- must: boolCriteria,
+ must: [
+ {
+ exists: { field: 'influencers' },
+ },
+ ],
},
},
- ],
+ },
},
- },
- aggs: {
- influencerFieldValues: {
- terms: {
- field: 'influencer_field_value',
- size: maxResults !== undefined ? maxResults : 10,
- order: {
- maxAnomalyScore: 'desc',
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
},
},
- aggs: {
- maxAnomalyScore: {
- max: {
- field: 'influencer_score',
- },
+ },
+ {
+ range: {
+ record_score: {
+ gte: threshold,
},
- byTime: {
- date_histogram: {
- field: 'timestamp',
- interval,
- min_doc_count: 1,
- },
- aggs: {
- maxAnomalyScore: {
- max: {
- field: 'influencer_score',
+ },
+ },
+ ];
+
+ if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
+ let jobIdFilterStr = '';
+ _.each(jobIds, (jobId, i) => {
+ if (i > 0) {
+ jobIdFilterStr += ' OR ';
+ }
+ jobIdFilterStr += 'job_id:';
+ jobIdFilterStr += jobId;
+ });
+ boolCriteria.push({
+ query_string: {
+ analyze_wildcard: false,
+ query: jobIdFilterStr,
+ },
+ });
+ }
+
+ mlApiServices
+ .esSearch({
+ index: ML_RESULTS_INDEX_PATTERN,
+ size: maxResults !== undefined ? maxResults : 100,
+ rest_total_hits_as_int: true,
+ body: {
+ _source: ['job_id', 'detector_index', 'influencers', 'record_score'],
+ query: {
+ bool: {
+ filter: [
+ {
+ query_string: {
+ query: 'result_type:record',
+ analyze_wildcard: false,
+ },
},
- },
+ {
+ bool: {
+ must: boolCriteria,
+ },
+ },
+ ],
},
},
+ sort: [{ record_score: { order: 'desc' } }],
},
- },
- },
- },
- })
- .then((resp) => {
- const fieldValueBuckets = _.get(
- resp,
- ['aggregations', 'influencerFieldValues', 'buckets'],
- []
- );
- _.each(fieldValueBuckets, (valueBucket) => {
- const fieldValue = valueBucket.key;
- const fieldValues = {};
-
- const timeBuckets = _.get(valueBucket, ['byTime', 'buckets'], []);
- _.each(timeBuckets, (timeBucket) => {
- const time = timeBucket.key;
- const score = timeBucket.maxAnomalyScore.value;
- fieldValues[time] = score;
+ })
+ .then((resp) => {
+ if (resp.hits.total !== 0) {
+ _.each(resp.hits.hits, (hit) => {
+ obj.records.push(hit._source);
+ });
+ }
+ resolve(obj);
+ })
+ .catch((resp) => {
+ reject(resp);
});
+ });
+ },
+
+ // Queries Elasticsearch to obtain the record level results containing the specified influencer(s),
+ // for the specified job(s), time range, and record score threshold.
+ // influencers parameter must be an array, with each object in the array having 'fieldName'
+ // 'fieldValue' properties. The influencer array uses 'should' for the nested bool query,
+ // so this returns record level results which have at least one of the influencers.
+ // Pass an empty array or ['*'] to search over all job IDs.
+ getRecordsForInfluencer(
+ jobIds,
+ influencers,
+ threshold,
+ earliestMs,
+ latestMs,
+ maxResults,
+ influencersFilterQuery
+ ) {
+ return new Promise((resolve, reject) => {
+ const obj = { success: true, records: [] };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Add criteria for the time range, record score, plus any specified job IDs.
+ const boolCriteria = [
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
+ },
+ },
+ },
+ {
+ range: {
+ record_score: {
+ gte: threshold,
+ },
+ },
+ },
+ ];
- obj.results[fieldValue] = fieldValues;
- });
+ if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
+ let jobIdFilterStr = '';
+ _.each(jobIds, (jobId, i) => {
+ if (i > 0) {
+ jobIdFilterStr += ' OR ';
+ }
+ jobIdFilterStr += 'job_id:';
+ jobIdFilterStr += jobId;
+ });
+ boolCriteria.push({
+ query_string: {
+ analyze_wildcard: false,
+ query: jobIdFilterStr,
+ },
+ });
+ }
- resolve(obj);
- })
- .catch((resp) => {
- reject(resp);
- });
- });
-}
+ if (influencersFilterQuery !== undefined) {
+ boolCriteria.push(influencersFilterQuery);
+ }
-// Queries Elasticsearch to obtain record level results containing the influencers
-// for the specified job(s), record score threshold, and time range.
-// Pass an empty array or ['*'] to search over all job IDs.
-// Returned response contains a records property, with each record containing
-// only the fields job_id, detector_index, record_score and influencers.
-export function getRecordInfluencers(jobIds, threshold, earliestMs, latestMs, maxResults) {
- return new Promise((resolve, reject) => {
- const obj = { success: true, records: [] };
-
- // Build the criteria to use in the bool filter part of the request.
- // Adds criteria for the existence of the nested influencers field, time range,
- // record score, plus any specified job IDs.
- const boolCriteria = [
- {
- nested: {
- path: 'influencers',
- query: {
+ // Add a nested query to filter for each of the specified influencers.
+ if (influencers.length > 0) {
+ boolCriteria.push({
bool: {
- must: [
- {
- exists: { field: 'influencers' },
+ should: influencers.map((influencer) => {
+ return {
+ nested: {
+ path: 'influencers',
+ query: {
+ bool: {
+ must: [
+ {
+ match: {
+ 'influencers.influencer_field_name': influencer.fieldName,
+ },
+ },
+ {
+ match: {
+ 'influencers.influencer_field_values': influencer.fieldValue,
+ },
+ },
+ ],
+ },
+ },
+ },
+ };
+ }),
+ minimum_should_match: 1,
+ },
+ });
+ }
+
+ mlApiServices
+ .esSearch({
+ index: ML_RESULTS_INDEX_PATTERN,
+ size: maxResults !== undefined ? maxResults : 100,
+ rest_total_hits_as_int: true,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ query_string: {
+ query: 'result_type:record',
+ analyze_wildcard: false,
+ },
+ },
+ {
+ bool: {
+ must: boolCriteria,
+ },
+ },
+ ],
},
- ],
+ },
+ sort: [{ record_score: { order: 'desc' } }],
+ },
+ })
+ .then((resp) => {
+ if (resp.hits.total !== 0) {
+ _.each(resp.hits.hits, (hit) => {
+ obj.records.push(hit._source);
+ });
+ }
+ resolve(obj);
+ })
+ .catch((resp) => {
+ reject(resp);
+ });
+ });
+ },
+
+ // Queries Elasticsearch to obtain the record level results for the specified job and detector,
+ // time range, record score threshold, and whether to only return results containing influencers.
+ // An additional, optional influencer field name and value may also be provided.
+ getRecordsForDetector(
+ jobId,
+ detectorIndex,
+ checkForInfluencers,
+ influencerFieldName,
+ influencerFieldValue,
+ threshold,
+ earliestMs,
+ latestMs,
+ maxResults
+ ) {
+ return new Promise((resolve, reject) => {
+ const obj = { success: true, records: [] };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Add criteria for the time range, record score, plus any specified job IDs.
+ const boolCriteria = [
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
+ },
},
},
- },
- },
- {
- range: {
- timestamp: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
+ {
+ term: { job_id: jobId },
},
- },
- },
- {
- range: {
- record_score: {
- gte: threshold,
+ {
+ term: { detector_index: detectorIndex },
},
- },
- },
- ];
-
- if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
- let jobIdFilterStr = '';
- _.each(jobIds, (jobId, i) => {
- if (i > 0) {
- jobIdFilterStr += ' OR ';
- }
- jobIdFilterStr += 'job_id:';
- jobIdFilterStr += jobId;
- });
- boolCriteria.push({
- query_string: {
- analyze_wildcard: false,
- query: jobIdFilterStr,
- },
- });
- }
-
- ml.esSearch({
- index: ML_RESULTS_INDEX_PATTERN,
- size: maxResults !== undefined ? maxResults : 100,
- rest_total_hits_as_int: true,
- body: {
- _source: ['job_id', 'detector_index', 'influencers', 'record_score'],
- query: {
- bool: {
- filter: [
- {
- query_string: {
- query: 'result_type:record',
- analyze_wildcard: false,
- },
+ {
+ range: {
+ record_score: {
+ gte: threshold,
},
- {
+ },
+ },
+ ];
+
+ // Add a nested query to filter for the specified influencer field name and value.
+ if (influencerFieldName && influencerFieldValue) {
+ boolCriteria.push({
+ nested: {
+ path: 'influencers',
+ query: {
bool: {
- must: boolCriteria,
+ must: [
+ {
+ match: {
+ 'influencers.influencer_field_name': influencerFieldName,
+ },
+ },
+ {
+ match: {
+ 'influencers.influencer_field_values': influencerFieldValue,
+ },
+ },
+ ],
},
},
- ],
- },
- },
- sort: [{ record_score: { order: 'desc' } }],
- },
- })
- .then((resp) => {
- if (resp.hits.total !== 0) {
- _.each(resp.hits.hits, (hit) => {
- obj.records.push(hit._source);
+ },
});
- }
- resolve(obj);
- })
- .catch((resp) => {
- reject(resp);
- });
- });
-}
-
-// Queries Elasticsearch to obtain the record level results containing the specified influencer(s),
-// for the specified job(s), time range, and record score threshold.
-// influencers parameter must be an array, with each object in the array having 'fieldName'
-// 'fieldValue' properties. The influencer array uses 'should' for the nested bool query,
-// so this returns record level results which have at least one of the influencers.
-// Pass an empty array or ['*'] to search over all job IDs.
-export function getRecordsForInfluencer(
- jobIds,
- influencers,
- threshold,
- earliestMs,
- latestMs,
- maxResults,
- influencersFilterQuery
-) {
- return new Promise((resolve, reject) => {
- const obj = { success: true, records: [] };
-
- // Build the criteria to use in the bool filter part of the request.
- // Add criteria for the time range, record score, plus any specified job IDs.
- const boolCriteria = [
- {
- range: {
- timestamp: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
- },
- },
- },
- {
- range: {
- record_score: {
- gte: threshold,
- },
- },
- },
- ];
-
- if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
- let jobIdFilterStr = '';
- _.each(jobIds, (jobId, i) => {
- if (i > 0) {
- jobIdFilterStr += ' OR ';
- }
- jobIdFilterStr += 'job_id:';
- jobIdFilterStr += jobId;
- });
- boolCriteria.push({
- query_string: {
- analyze_wildcard: false,
- query: jobIdFilterStr,
- },
- });
- }
-
- if (influencersFilterQuery !== undefined) {
- boolCriteria.push(influencersFilterQuery);
- }
-
- // Add a nested query to filter for each of the specified influencers.
- if (influencers.length > 0) {
- boolCriteria.push({
- bool: {
- should: influencers.map((influencer) => {
- return {
+ } else {
+ if (checkForInfluencers === true) {
+ boolCriteria.push({
nested: {
path: 'influencers',
query: {
bool: {
must: [
{
- match: {
- 'influencers.influencer_field_name': influencer.fieldName,
- },
- },
- {
- match: {
- 'influencers.influencer_field_values': influencer.fieldValue,
- },
+ exists: { field: 'influencers' },
},
],
},
},
},
- };
- }),
- minimum_should_match: 1,
- },
- });
- }
-
- ml.esSearch({
- index: ML_RESULTS_INDEX_PATTERN,
- size: maxResults !== undefined ? maxResults : 100,
- rest_total_hits_as_int: true,
- body: {
- query: {
- bool: {
- filter: [
- {
- query_string: {
- query: 'result_type:record',
- analyze_wildcard: false,
- },
- },
- {
- bool: {
- must: boolCriteria,
- },
- },
- ],
- },
- },
- sort: [{ record_score: { order: 'desc' } }],
- },
- })
- .then((resp) => {
- if (resp.hits.total !== 0) {
- _.each(resp.hits.hits, (hit) => {
- obj.records.push(hit._source);
- });
+ });
+ }
}
- resolve(obj);
- })
- .catch((resp) => {
- reject(resp);
- });
- });
-}
-// Queries Elasticsearch to obtain the record level results for the specified job and detector,
-// time range, record score threshold, and whether to only return results containing influencers.
-// An additional, optional influencer field name and value may also be provided.
-export function getRecordsForDetector(
- jobId,
- detectorIndex,
- checkForInfluencers,
- influencerFieldName,
- influencerFieldValue,
- threshold,
- earliestMs,
- latestMs,
- maxResults
-) {
- return new Promise((resolve, reject) => {
- const obj = { success: true, records: [] };
-
- // Build the criteria to use in the bool filter part of the request.
- // Add criteria for the time range, record score, plus any specified job IDs.
- const boolCriteria = [
- {
- range: {
- timestamp: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
- },
- },
- },
- {
- term: { job_id: jobId },
- },
- {
- term: { detector_index: detectorIndex },
- },
- {
- range: {
- record_score: {
- gte: threshold,
- },
- },
- },
- ];
-
- // Add a nested query to filter for the specified influencer field name and value.
- if (influencerFieldName && influencerFieldValue) {
- boolCriteria.push({
- nested: {
- path: 'influencers',
- query: {
- bool: {
- must: [
- {
- match: {
- 'influencers.influencer_field_name': influencerFieldName,
- },
- },
- {
- match: {
- 'influencers.influencer_field_values': influencerFieldValue,
- },
+ mlApiServices
+ .esSearch({
+ index: ML_RESULTS_INDEX_PATTERN,
+ size: maxResults !== undefined ? maxResults : 100,
+ rest_total_hits_as_int: true,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ query_string: {
+ query: 'result_type:record',
+ analyze_wildcard: false,
+ },
+ },
+ {
+ bool: {
+ must: boolCriteria,
+ },
+ },
+ ],
},
- ],
+ },
+ sort: [{ record_score: { order: 'desc' } }],
},
- },
- },
+ })
+ .then((resp) => {
+ if (resp.hits.total !== 0) {
+ _.each(resp.hits.hits, (hit) => {
+ obj.records.push(hit._source);
+ });
+ }
+ resolve(obj);
+ })
+ .catch((resp) => {
+ reject(resp);
+ });
});
- } else {
- if (checkForInfluencers === true) {
- boolCriteria.push({
- nested: {
- path: 'influencers',
- query: {
- bool: {
- must: [
- {
- exists: { field: 'influencers' },
- },
- ],
+ },
+
+ // Queries Elasticsearch to obtain all the record level results for the specified job(s), time range,
+ // and record score threshold.
+ // Pass an empty array or ['*'] to search over all job IDs.
+ // Returned response contains a records property, which is an array of the matching results.
+ getRecords(jobIds, threshold, earliestMs, latestMs, maxResults) {
+ return this.getRecordsForInfluencer(jobIds, [], threshold, earliestMs, latestMs, maxResults);
+ },
+
+ // Queries Elasticsearch to obtain event rate data i.e. the count
+ // of documents over time.
+ // index can be a String, or String[], of index names to search.
+ // Extra query object can be supplied, or pass null if no additional query.
+ // Returned response contains a results property, which is an object
+ // of document counts against time (epoch millis).
+ getEventRateData(index, query, timeFieldName, earliestMs, latestMs, interval) {
+ return new Promise((resolve, reject) => {
+ const obj = { success: true, results: {} };
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Add criteria for the time range, entity fields,
+ // plus any additional supplied query.
+ const mustCriteria = [
+ {
+ range: {
+ [timeFieldName]: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
},
},
},
- });
- }
- }
-
- ml.esSearch({
- index: ML_RESULTS_INDEX_PATTERN,
- size: maxResults !== undefined ? maxResults : 100,
- rest_total_hits_as_int: true,
- body: {
- query: {
- bool: {
- filter: [
- {
- query_string: {
- query: 'result_type:record',
- analyze_wildcard: false,
+ ];
+
+ if (query) {
+ mustCriteria.push(query);
+ }
+
+ mlApiServices
+ .esSearch({
+ index,
+ rest_total_hits_as_int: true,
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ must: mustCriteria,
},
},
- {
- bool: {
- must: boolCriteria,
+ _source: {
+ excludes: [],
+ },
+ aggs: {
+ eventRate: {
+ date_histogram: {
+ field: timeFieldName,
+ interval: interval,
+ min_doc_count: 0,
+ extended_bounds: {
+ min: earliestMs,
+ max: latestMs,
+ },
+ },
},
},
- ],
- },
- },
- sort: [{ record_score: { order: 'desc' } }],
- },
- })
- .then((resp) => {
- if (resp.hits.total !== 0) {
- _.each(resp.hits.hits, (hit) => {
- obj.records.push(hit._source);
+ },
+ })
+ .then((resp) => {
+ const dataByTimeBucket = _.get(resp, ['aggregations', 'eventRate', 'buckets'], []);
+ _.each(dataByTimeBucket, (dataForTime) => {
+ const time = dataForTime.key;
+ obj.results[time] = dataForTime.doc_count;
+ });
+ obj.total = resp.hits.total;
+
+ resolve(obj);
+ })
+ .catch((resp) => {
+ reject(resp);
});
- }
- resolve(obj);
- })
- .catch((resp) => {
- reject(resp);
});
- });
-}
-
-// Queries Elasticsearch to obtain all the record level results for the specified job(s), time range,
-// and record score threshold.
-// Pass an empty array or ['*'] to search over all job IDs.
-// Returned response contains a records property, which is an array of the matching results.
-export function getRecords(jobIds, threshold, earliestMs, latestMs, maxResults) {
- return this.getRecordsForInfluencer(jobIds, [], threshold, earliestMs, latestMs, maxResults);
-}
+ },
-// Queries Elasticsearch to obtain event rate data i.e. the count
-// of documents over time.
-// index can be a String, or String[], of index names to search.
-// Extra query object can be supplied, or pass null if no additional query.
-// Returned response contains a results property, which is an object
-// of document counts against time (epoch millis).
-export function getEventRateData(index, query, timeFieldName, earliestMs, latestMs, interval) {
- return new Promise((resolve, reject) => {
- const obj = { success: true, results: {} };
-
- // Build the criteria to use in the bool filter part of the request.
- // Add criteria for the time range, entity fields,
- // plus any additional supplied query.
- const mustCriteria = [
- {
- range: {
- [timeFieldName]: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
- },
- },
- },
- ];
-
- if (query) {
- mustCriteria.push(query);
- }
+ // Queries Elasticsearch to obtain event distribution i.e. the count
+ // of entities over time.
+ // index can be a String, or String[], of index names to search.
+ // Extra query object can be supplied, or pass null if no additional query.
+ // Returned response contains a results property, which is an object
+ // of document counts against time (epoch millis).
- ml.esSearch({
+ getEventDistributionData(
index,
- rest_total_hits_as_int: true,
- size: 0,
- body: {
- query: {
- bool: {
- must: mustCriteria,
- },
- },
- _source: {
- excludes: [],
- },
- aggs: {
- eventRate: {
- date_histogram: {
- field: timeFieldName,
- interval: interval,
- min_doc_count: 0,
- extended_bounds: {
- min: earliestMs,
- max: latestMs,
- },
+ splitField,
+ filterField = null,
+ query,
+ metricFunction, // ES aggregation name
+ metricFieldName,
+ timeFieldName,
+ earliestMs,
+ latestMs,
+ interval
+ ) {
+ return new Promise((resolve, reject) => {
+ if (splitField === undefined) {
+ return resolve([]);
+ }
+
+ // Build the criteria to use in the bool filter part of the request.
+ // Add criteria for the time range, entity fields,
+ // plus any additional supplied query.
+ const mustCriteria = [];
+
+ mustCriteria.push({
+ range: {
+ [timeFieldName]: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
},
},
- },
- },
- })
- .then((resp) => {
- const dataByTimeBucket = _.get(resp, ['aggregations', 'eventRate', 'buckets'], []);
- _.each(dataByTimeBucket, (dataForTime) => {
- const time = dataForTime.key;
- obj.results[time] = dataForTime.doc_count;
});
- obj.total = resp.hits.total;
- resolve(obj);
- })
- .catch((resp) => {
- reject(resp);
- });
- });
-}
+ if (query) {
+ mustCriteria.push(query);
+ }
-// Queries Elasticsearch to obtain event distribution i.e. the count
-// of entities over time.
-// index can be a String, or String[], of index names to search.
-// Extra query object can be supplied, or pass null if no additional query.
-// Returned response contains a results property, which is an object
-// of document counts against time (epoch millis).
-const SAMPLER_TOP_TERMS_SHARD_SIZE = 20000;
-const ENTITY_AGGREGATION_SIZE = 10;
-const AGGREGATION_MIN_DOC_COUNT = 1;
-const CARDINALITY_PRECISION_THRESHOLD = 100;
-export function getEventDistributionData(
- index,
- splitField,
- filterField = null,
- query,
- metricFunction, // ES aggregation name
- metricFieldName,
- timeFieldName,
- earliestMs,
- latestMs,
- interval
-) {
- return new Promise((resolve, reject) => {
- if (splitField === undefined) {
- return resolve([]);
- }
-
- // Build the criteria to use in the bool filter part of the request.
- // Add criteria for the time range, entity fields,
- // plus any additional supplied query.
- const mustCriteria = [];
-
- mustCriteria.push({
- range: {
- [timeFieldName]: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
- },
- },
- });
-
- if (query) {
- mustCriteria.push(query);
- }
-
- if (filterField !== null) {
- mustCriteria.push({
- term: {
- [filterField.fieldName]: filterField.fieldValue,
- },
- });
- }
-
- const body = {
- query: {
- // using function_score and random_score to get a random sample of documents.
- // otherwise all documents would have the same score and the sampler aggregation
- // would pick the first N documents instead of a random set.
- function_score: {
- query: {
- bool: {
- must: mustCriteria,
+ if (filterField !== null) {
+ mustCriteria.push({
+ term: {
+ [filterField.fieldName]: filterField.fieldValue,
},
- },
- functions: [
- {
- random_score: {
- // static seed to get same randomized results on every request
- seed: 10,
- field: '_seq_no',
+ });
+ }
+
+ const body = {
+ query: {
+ // using function_score and random_score to get a random sample of documents.
+ // otherwise all documents would have the same score and the sampler aggregation
+ // would pick the first N documents instead of a random set.
+ function_score: {
+ query: {
+ bool: {
+ must: mustCriteria,
+ },
},
+ functions: [
+ {
+ random_score: {
+ // static seed to get same randomized results on every request
+ seed: 10,
+ field: '_seq_no',
+ },
+ },
+ ],
},
- ],
- },
- },
- size: 0,
- _source: {
- excludes: [],
- },
- aggs: {
- sample: {
- sampler: {
- shard_size: SAMPLER_TOP_TERMS_SHARD_SIZE,
+ },
+ size: 0,
+ _source: {
+ excludes: [],
},
aggs: {
- byTime: {
- date_histogram: {
- field: timeFieldName,
- interval: interval,
- min_doc_count: AGGREGATION_MIN_DOC_COUNT,
+ sample: {
+ sampler: {
+ shard_size: SAMPLER_TOP_TERMS_SHARD_SIZE,
},
aggs: {
- entities: {
- terms: {
- field: splitField.fieldName,
- size: ENTITY_AGGREGATION_SIZE,
+ byTime: {
+ date_histogram: {
+ field: timeFieldName,
+ interval: interval,
min_doc_count: AGGREGATION_MIN_DOC_COUNT,
},
+ aggs: {
+ entities: {
+ terms: {
+ field: splitField.fieldName,
+ size: ENTITY_AGGREGATION_SIZE,
+ min_doc_count: AGGREGATION_MIN_DOC_COUNT,
+ },
+ },
+ },
},
},
},
},
- },
- },
- };
-
- if (metricFieldName !== undefined && metricFieldName !== '') {
- body.aggs.sample.aggs.byTime.aggs.entities.aggs = {};
-
- const metricAgg = {
- [metricFunction]: {
- field: metricFieldName,
- },
- };
-
- if (metricFunction === 'percentiles') {
- metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS];
- }
-
- if (metricFunction === 'cardinality') {
- metricAgg[metricFunction].precision_threshold = CARDINALITY_PRECISION_THRESHOLD;
- }
- body.aggs.sample.aggs.byTime.aggs.entities.aggs.metric = metricAgg;
- }
-
- ml.esSearch({
- index,
- body,
- rest_total_hits_as_int: true,
- })
- .then((resp) => {
- // Because of the sampling, results of metricFunctions which use sum or count
- // can be significantly skewed. Taking into account totalHits we calculate a
- // a factor to normalize results for these metricFunctions.
- const totalHits = _.get(resp, ['hits', 'total'], 0);
- const successfulShards = _.get(resp, ['_shards', 'successful'], 0);
-
- let normalizeFactor = 1;
- if (totalHits > successfulShards * SAMPLER_TOP_TERMS_SHARD_SIZE) {
- normalizeFactor = totalHits / (successfulShards * SAMPLER_TOP_TERMS_SHARD_SIZE);
+ };
+
+ if (metricFieldName !== undefined && metricFieldName !== '') {
+ body.aggs.sample.aggs.byTime.aggs.entities.aggs = {};
+
+ const metricAgg = {
+ [metricFunction]: {
+ field: metricFieldName,
+ },
+ };
+
+ if (metricFunction === 'percentiles') {
+ metricAgg[metricFunction].percents = [ML_MEDIAN_PERCENTS];
+ }
+
+ if (metricFunction === 'cardinality') {
+ metricAgg[metricFunction].precision_threshold = CARDINALITY_PRECISION_THRESHOLD;
+ }
+ body.aggs.sample.aggs.byTime.aggs.entities.aggs.metric = metricAgg;
}
- const dataByTime = _.get(resp, ['aggregations', 'sample', 'byTime', 'buckets'], []);
- const data = dataByTime.reduce((d, dataForTime) => {
- const date = +dataForTime.key;
- const entities = _.get(dataForTime, ['entities', 'buckets'], []);
- entities.forEach((entity) => {
- let value = metricFunction === 'count' ? entity.doc_count : entity.metric.value;
-
- if (
- metricFunction === 'count' ||
- metricFunction === 'cardinality' ||
- metricFunction === 'sum'
- ) {
- value = value * normalizeFactor;
+ mlApiServices
+ .esSearch({
+ index,
+ body,
+ rest_total_hits_as_int: true,
+ })
+ .then((resp) => {
+ // Because of the sampling, results of metricFunctions which use sum or count
+ // can be significantly skewed. Taking into account totalHits we calculate a
+ // a factor to normalize results for these metricFunctions.
+ const totalHits = _.get(resp, ['hits', 'total'], 0);
+ const successfulShards = _.get(resp, ['_shards', 'successful'], 0);
+
+ let normalizeFactor = 1;
+ if (totalHits > successfulShards * SAMPLER_TOP_TERMS_SHARD_SIZE) {
+ normalizeFactor = totalHits / (successfulShards * SAMPLER_TOP_TERMS_SHARD_SIZE);
}
- d.push({
- date,
- entity: entity.key,
- value,
- });
+ const dataByTime = _.get(resp, ['aggregations', 'sample', 'byTime', 'buckets'], []);
+ const data = dataByTime.reduce((d, dataForTime) => {
+ const date = +dataForTime.key;
+ const entities = _.get(dataForTime, ['entities', 'buckets'], []);
+ entities.forEach((entity) => {
+ let value = metricFunction === 'count' ? entity.doc_count : entity.metric.value;
+
+ if (
+ metricFunction === 'count' ||
+ metricFunction === 'cardinality' ||
+ metricFunction === 'sum'
+ ) {
+ value = value * normalizeFactor;
+ }
+
+ d.push({
+ date,
+ entity: entity.key,
+ value,
+ });
+ });
+ return d;
+ }, []);
+ resolve(data);
+ })
+ .catch((resp) => {
+ reject(resp);
});
- return d;
- }, []);
- resolve(data);
- })
- .catch((resp) => {
- reject(resp);
});
- });
-}
-
-// Queries Elasticsearch to obtain the max record score over time for the specified job,
-// criteria, time range, and aggregation interval.
-// criteriaFields parameter must be an array, with each object in the array having 'fieldName'
-// 'fieldValue' properties.
-export function getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, interval) {
- return new Promise((resolve, reject) => {
- const obj = {
- success: true,
- results: {},
- };
-
- // Build the criteria to use in the bool filter part of the request.
- const mustCriteria = [
- {
- range: {
- timestamp: {
- gte: earliestMs,
- lte: latestMs,
- format: 'epoch_millis',
- },
- },
- },
- { term: { job_id: jobId } },
- ];
-
- _.each(criteriaFields, (criteria) => {
- mustCriteria.push({
- term: {
- [criteria.fieldName]: criteria.fieldValue,
- },
- });
- });
-
- ml.esSearch({
- index: ML_RESULTS_INDEX_PATTERN,
- size: 0,
- body: {
- query: {
- bool: {
- filter: [
- {
- query_string: {
- query: 'result_type:record',
- analyze_wildcard: true,
- },
+ },
+
+ // Queries Elasticsearch to obtain the max record score over time for the specified job,
+ // criteria, time range, and aggregation interval.
+ // criteriaFields parameter must be an array, with each object in the array having 'fieldName'
+ // 'fieldValue' properties.
+ getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, interval) {
+ return new Promise((resolve, reject) => {
+ const obj = {
+ success: true,
+ results: {},
+ };
+
+ // Build the criteria to use in the bool filter part of the request.
+ const mustCriteria = [
+ {
+ range: {
+ timestamp: {
+ gte: earliestMs,
+ lte: latestMs,
+ format: 'epoch_millis',
},
- {
+ },
+ },
+ { term: { job_id: jobId } },
+ ];
+
+ _.each(criteriaFields, (criteria) => {
+ mustCriteria.push({
+ term: {
+ [criteria.fieldName]: criteria.fieldValue,
+ },
+ });
+ });
+
+ mlApiServices
+ .esSearch({
+ index: ML_RESULTS_INDEX_PATTERN,
+ size: 0,
+ body: {
+ query: {
bool: {
- must: mustCriteria,
+ filter: [
+ {
+ query_string: {
+ query: 'result_type:record',
+ analyze_wildcard: true,
+ },
+ },
+ {
+ bool: {
+ must: mustCriteria,
+ },
+ },
+ ],
},
},
- ],
- },
- },
- aggs: {
- times: {
- date_histogram: {
- field: 'timestamp',
- interval: interval,
- min_doc_count: 1,
- },
- aggs: {
- recordScore: {
- max: {
- field: 'record_score',
+ aggs: {
+ times: {
+ date_histogram: {
+ field: 'timestamp',
+ interval: interval,
+ min_doc_count: 1,
+ },
+ aggs: {
+ recordScore: {
+ max: {
+ field: 'record_score',
+ },
+ },
+ },
},
},
},
- },
- },
- },
- })
- .then((resp) => {
- const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []);
- _.each(aggregationsByTime, (dataForTime) => {
- const time = dataForTime.key;
- obj.results[time] = {
- score: _.get(dataForTime, ['recordScore', 'value']),
- };
- });
+ })
+ .then((resp) => {
+ const aggregationsByTime = _.get(resp, ['aggregations', 'times', 'buckets'], []);
+ _.each(aggregationsByTime, (dataForTime) => {
+ const time = dataForTime.key;
+ obj.results[time] = {
+ score: _.get(dataForTime, ['recordScore', 'value']),
+ };
+ });
- resolve(obj);
- })
- .catch((resp) => {
- reject(resp);
+ resolve(obj);
+ })
+ .catch((resp) => {
+ reject(resp);
+ });
});
- });
+ },
+ };
}
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx
index b53b08e5f6146..b4b25db452bdb 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { CoreStart } from 'kibana/public';
+import { i18n } from '@kbn/i18n';
import { Subject } from 'rxjs';
import {
Embeddable,
@@ -25,12 +26,19 @@ import {
RefreshInterval,
TimeRange,
} from '../../../../../../src/plugins/data/common';
+import { SwimlaneType } from '../../application/explorer/explorer_constants';
export const ANOMALY_SWIMLANE_EMBEDDABLE_TYPE = 'ml_anomaly_swimlane';
+export const getDefaultPanelTitle = (jobIds: JobId[]) =>
+ i18n.translate('xpack.ml.swimlaneEmbeddable.title', {
+ defaultMessage: 'ML anomaly swimlane for {jobIds}',
+ values: { jobIds: jobIds.join(', ') },
+ });
+
export interface AnomalySwimlaneEmbeddableCustomInput {
jobIds: JobId[];
- swimlaneType: string;
+ swimlaneType: SwimlaneType;
viewBy?: string;
limit?: number;
@@ -43,9 +51,12 @@ export interface AnomalySwimlaneEmbeddableCustomInput {
export type AnomalySwimlaneEmbeddableInput = EmbeddableInput & AnomalySwimlaneEmbeddableCustomInput;
-export interface AnomalySwimlaneEmbeddableOutput extends EmbeddableOutput {
+export type AnomalySwimlaneEmbeddableOutput = EmbeddableOutput &
+ AnomalySwimlaneEmbeddableCustomOutput;
+
+export interface AnomalySwimlaneEmbeddableCustomOutput {
jobIds: JobId[];
- swimlaneType: string;
+ swimlaneType: SwimlaneType;
viewBy?: string;
limit?: number;
}
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts
index e86d738d8b809..09091b21e49b6 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts
@@ -23,8 +23,9 @@ import { MlStartDependencies } from '../../plugin';
import { HttpService } from '../../application/services/http_service';
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
import { ExplorerService } from '../../application/services/explorer_service';
-import { mlResultsService } from '../../application/services/results_service';
+import { mlResultsServiceProvider } from '../../application/services/results_service';
import { resolveAnomalySwimlaneUserInput } from './anomaly_swimlane_setup_flyout';
+import { mlApiServicesProvider } from '../../application/services/ml_api_service';
export class AnomalySwimlaneEmbeddableFactory
implements EmbeddableFactoryDefinition {
@@ -64,8 +65,7 @@ export class AnomalySwimlaneEmbeddableFactory
const explorerService = new ExplorerService(
pluginsStart.data.query.timefilter.timefilter,
coreStart.uiSettings,
- // TODO mlResultsService to use DI
- mlResultsService
+ mlResultsServiceProvider(mlApiServicesProvider(httpService))
);
return [coreStart, pluginsStart, { anomalyDetectorService, explorerService }];
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx
index 00d47c0d897c7..4c93b9ef23239 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx
@@ -20,7 +20,7 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { SWIMLANE_TYPE } from '../../application/explorer/explorer_constants';
+import { SWIMLANE_TYPE, SwimlaneType } from '../../application/explorer/explorer_constants';
import { AnomalySwimlaneEmbeddableInput } from './anomaly_swimlane_embeddable';
export interface AnomalySwimlaneInitializerProps {
@@ -31,7 +31,7 @@ export interface AnomalySwimlaneInitializerProps {
>;
onCreate: (swimlaneProps: {
panelTitle: string;
- swimlaneType: string;
+ swimlaneType: SwimlaneType;
viewBy?: string;
limit?: number;
}) => void;
@@ -51,8 +51,8 @@ export const AnomalySwimlaneInitializer: FC = (
initialInput,
}) => {
const [panelTitle, setPanelTitle] = useState(defaultTitle);
- const [swimlaneType, setSwimlaneType] = useState(
- (initialInput?.swimlaneType ?? SWIMLANE_TYPE.OVERALL) as SWIMLANE_TYPE
+ const [swimlaneType, setSwimlaneType] = useState(
+ initialInput?.swimlaneType ?? SWIMLANE_TYPE.OVERALL
);
const [viewBySwimlaneFieldName, setViewBySwimlaneFieldName] = useState(initialInput?.viewBy);
const [limit, setLimit] = useState(initialInput?.limit ?? 5);
@@ -135,7 +135,7 @@ export const AnomalySwimlaneInitializer: FC = (
})}
options={swimlaneTypeOptions}
idSelected={swimlaneType}
- onChange={(id) => setSwimlaneType(id as SWIMLANE_TYPE)}
+ onChange={(id) => setSwimlaneType(id as SwimlaneType)}
/>
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx
index 83f9833109bf4..54f50d2d3da32 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx
@@ -6,7 +6,6 @@
import React from 'react';
import { IUiSettingsClient, OverlayStart } from 'kibana/public';
-import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { VIEW_BY_JOB_LABEL } from '../../application/explorer/explorer_constants';
import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
@@ -14,7 +13,10 @@ import { AnomalySwimlaneInitializer } from './anomaly_swimlane_initializer';
import { JobSelectorFlyout } from '../../application/components/job_selector/job_selector_flyout';
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
import { getInitialGroupsMap } from '../../application/components/job_selector/job_selector';
-import { AnomalySwimlaneEmbeddableInput } from './anomaly_swimlane_embeddable';
+import {
+ AnomalySwimlaneEmbeddableInput,
+ getDefaultPanelTitle,
+} from './anomaly_swimlane_embeddable';
export async function resolveAnomalySwimlaneUserInput(
{
@@ -52,12 +54,7 @@ export async function resolveAnomalySwimlaneUserInput(
reject();
}}
onSelectionConfirmed={async ({ jobIds, groups }) => {
- const title =
- input?.title ??
- i18n.translate('xpack.ml.swimlaneEmbeddable.title', {
- defaultMessage: 'ML anomaly swimlane for {jobIds}',
- values: { jobIds: jobIds.join(', ') },
- });
+ const title = input?.title ?? getDefaultPanelTitle(jobIds);
const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx
index e5d8584683c55..0bba9b59f7bf7 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { FC, useState } from 'react';
+import React, { FC, useCallback, useState } from 'react';
import {
EuiCallOut,
EuiFlexGroup,
@@ -28,6 +28,7 @@ import {
} from './anomaly_swimlane_embeddable';
import { MlTooltipComponent } from '../../application/components/chart_tooltip';
import { useSwimlaneInputResolver } from './swimlane_input_resolver';
+import { SwimlaneType } from '../../application/explorer/explorer_constants';
const RESIZE_THROTTLE_TIME_MS = 500;
@@ -54,10 +55,13 @@ export const ExplorerSwimlaneContainer: FC = ({
chartWidth
);
- const onResize = throttle((e: { width: number; height: number }) => {
- const labelWidth = 200;
- setChartWidth(e.width - labelWidth);
- }, RESIZE_THROTTLE_TIME_MS);
+ const onResize = useCallback(
+ throttle((e: { width: number; height: number }) => {
+ const labelWidth = 200;
+ setChartWidth(e.width - labelWidth);
+ }, RESIZE_THROTTLE_TIME_MS),
+ []
+ );
if (error) {
return (
@@ -91,14 +95,14 @@ export const ExplorerSwimlaneContainer: FC = ({
{chartWidth > 0 && swimlaneData && swimlaneType ? (
-
+
{(tooltipService) => (
)}
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts
index e704582d5d61a..3829bbce5e5c9 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts
@@ -24,7 +24,7 @@ import {
AnomalySwimlaneServices,
} from './anomaly_swimlane_embeddable';
import { MlStartDependencies } from '../../plugin';
-import { SWIMLANE_TYPE } from '../../application/explorer/explorer_constants';
+import { SWIMLANE_TYPE, SwimlaneType } from '../../application/explorer/explorer_constants';
import { Filter } from '../../../../../../src/plugins/data/common/es_query/filters';
import { Query } from '../../../../../../src/plugins/data/common/query';
import { esKuery, UI_SETTINGS } from '../../../../../../src/plugins/data/public';
@@ -55,7 +55,7 @@ export function useSwimlaneInputResolver(
const [{ uiSettings }, , { explorerService, anomalyDetectorService }] = services;
const [swimlaneData, setSwimlaneData] = useState();
- const [swimlaneType, setSwimlaneType] = useState();
+ const [swimlaneType, setSwimlaneType] = useState();
const [error, setError] = useState();
const chartWidth$ = useMemo(() => new Subject(), []);
diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts
index a9ffb1a5bf579..5a956651c86d8 100755
--- a/x-pack/plugins/ml/public/index.ts
+++ b/x-pack/plugins/ml/public/index.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { PluginInitializer } from 'kibana/public';
+import { PluginInitializer, PluginInitializerContext } from 'kibana/public';
import './index.scss';
import {
MlPlugin,
@@ -19,7 +19,7 @@ export const plugin: PluginInitializer<
MlPluginStart,
MlSetupDependencies,
MlStartDependencies
-> = () => new MlPlugin();
+> = (initializerContext: PluginInitializerContext) => new MlPlugin(initializerContext);
export { MlPluginSetup, MlPluginStart };
export * from './shared';
diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts
index fe9f602bc3637..be2ebb3caa416 100644
--- a/x-pack/plugins/ml/public/plugin.ts
+++ b/x-pack/plugins/ml/public/plugin.ts
@@ -5,7 +5,13 @@
*/
import { i18n } from '@kbn/i18n';
-import { Plugin, CoreStart, CoreSetup, AppMountParameters } from 'kibana/public';
+import {
+ Plugin,
+ CoreStart,
+ CoreSetup,
+ AppMountParameters,
+ PluginInitializerContext,
+} from 'kibana/public';
import { ManagementSetup } from 'src/plugins/management/public';
import { SharePluginStart } from 'src/plugins/share/public';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
@@ -38,9 +44,13 @@ export interface MlSetupDependencies {
home: HomePublicPluginSetup;
embeddable: EmbeddableSetup;
uiActions: UiActionsSetup;
+ kibanaVersion: string;
+ share: SharePluginStart;
}
export class MlPlugin implements Plugin {
+ constructor(private initializerContext: PluginInitializerContext) {}
+
setup(core: CoreSetup, pluginsSetup: MlSetupDependencies) {
core.application.register({
id: PLUGIN_ID,
@@ -53,6 +63,7 @@ export class MlPlugin implements Plugin {
category: DEFAULT_APP_CATEGORIES.kibana,
mount: async (params: AppMountParameters) => {
const [coreStart, pluginsStart] = await core.getStartServices();
+ const kibanaVersion = this.initializerContext.env.packageInfo.version;
const { renderApp } = await import('./application/app');
return renderApp(
coreStart,
@@ -67,6 +78,7 @@ export class MlPlugin implements Plugin {
home: pluginsSetup.home,
embeddable: pluginsSetup.embeddable,
uiActions: pluginsSetup.uiActions,
+ kibanaVersion,
},
{
element: params.element,
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts
index 56d00a4e11390..c23abead458f1 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts
@@ -61,6 +61,7 @@ export default function ({ getService }: FtrProviderContext) {
before(async () => {
await esArchiver.loadIfNeeded('ml/farequote');
await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp');
+ await ml.testResources.createMLTestDashboardIfNeeded();
await ml.testResources.setKibanaTimeZoneToUTC();
await ml.securityUI.loginAsMlPowerUser();
@@ -125,6 +126,12 @@ export default function ({ getService }: FtrProviderContext) {
it('anomalies table is not empty', async () => {
await ml.anomaliesTable.assertTableNotEmpty();
});
+
+ // should be the last step because it navigates away from the Anomaly Explorer page
+ it('should allow to attach anomaly swimlane embeddable to the dashboard', async () => {
+ await ml.anomalyExplorer.openAddToDashboardControl();
+ await ml.anomalyExplorer.addAndEditSwimlaneInDashboard('ML Test');
+ });
});
}
});
diff --git a/x-pack/test/functional/apps/ml/index.ts b/x-pack/test/functional/apps/ml/index.ts
index 92e836e0c4c1b..2d8aac3b8dddf 100644
--- a/x-pack/test/functional/apps/ml/index.ts
+++ b/x-pack/test/functional/apps/ml/index.ts
@@ -22,6 +22,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
await ml.securityCommon.cleanMlRoles();
await ml.testResources.deleteSavedSearches();
+ await ml.testResources.deleteDashboards();
await ml.testResources.deleteIndexPatternByTitle('ft_farequote');
await ml.testResources.deleteIndexPatternByTitle('ft_ecommerce');
diff --git a/x-pack/test/functional/services/ml/anomaly_explorer.ts b/x-pack/test/functional/services/ml/anomaly_explorer.ts
index 6ec72c76bb9cf..7c479a4234673 100644
--- a/x-pack/test/functional/services/ml/anomaly_explorer.ts
+++ b/x-pack/test/functional/services/ml/anomaly_explorer.ts
@@ -66,5 +66,38 @@ export function MachineLearningAnomalyExplorerProvider({ getService }: FtrProvid
async assertSwimlaneViewByExists() {
await testSubjects.existOrFail('mlAnomalyExplorerSwimlaneViewBy');
},
+
+ async openAddToDashboardControl() {
+ await testSubjects.click('mlAnomalyTimelinePanelMenu');
+ await testSubjects.click('mlAnomalyTimelinePanelAddToDashboardButton');
+ await testSubjects.existOrFail('mlAddToDashboardModal');
+ },
+
+ async addAndEditSwimlaneInDashboard(dashboardTitle: string) {
+ await this.filterWithSearchString(dashboardTitle);
+ await testSubjects.isDisplayed('mlDashboardSelectionTable > checkboxSelectAll');
+ await testSubjects.click('mlDashboardSelectionTable > checkboxSelectAll');
+ expect(await testSubjects.isChecked('mlDashboardSelectionTable > checkboxSelectAll')).to.be(
+ true
+ );
+ await testSubjects.clickWhenNotDisabled('mlAddAndEditDashboardButton');
+ const embeddable = await testSubjects.find('mlAnomalySwimlaneEmbeddableWrapper');
+ const swimlane = await embeddable.findByClassName('ml-swimlanes');
+ expect(await swimlane.isDisplayed()).to.eql(
+ true,
+ 'Anomaly swimlane should be displayed in dashboard'
+ );
+ },
+
+ async waitForDashboardsToLoad() {
+ await testSubjects.existOrFail('~mlDashboardSelectionTable', { timeout: 60 * 1000 });
+ },
+
+ async filterWithSearchString(filter: string) {
+ await this.waitForDashboardsToLoad();
+ const searchBarInput = await testSubjects.find('mlDashboardsSearchBox');
+ await searchBarInput.clearValueWithKeyboard();
+ await searchBarInput.type(filter);
+ },
};
}
diff --git a/x-pack/test/functional/services/ml/test_resources.ts b/x-pack/test/functional/services/ml/test_resources.ts
index 4e3d1d9d86271..9927c987bbea5 100644
--- a/x-pack/test/functional/services/ml/test_resources.ts
+++ b/x-pack/test/functional/services/ml/test_resources.ts
@@ -5,7 +5,7 @@
*/
import { ProvidedType } from '@kbn/test/types/ftr';
-import { savedSearches } from './test_resources_data';
+import { savedSearches, dashboards } from './test_resources_data';
import { COMMON_REQUEST_HEADERS } from './common';
import { FtrProviderContext } from '../../ftr_provider_context';
@@ -137,6 +137,20 @@ export function MachineLearningTestResourcesProvider({ getService }: FtrProvider
return createResponse.id;
},
+ async createDashboard(title: string, body: object): Promise {
+ log.debug(`Creating dashboard with title '${title}'`);
+
+ const createResponse = await supertest
+ .post(`/api/saved_objects/${SavedObjectType.DASHBOARD}`)
+ .set(COMMON_REQUEST_HEADERS)
+ .send(body)
+ .expect(200)
+ .then((res: any) => res.body);
+
+ log.debug(` > Created with id '${createResponse.id}'`);
+ return createResponse.id;
+ },
+
async createSavedSearchIfNeeded(savedSearch: any): Promise