diff --git a/x-pack/plugins/enterprise_search/common/types/error_codes.ts b/x-pack/plugins/enterprise_search/common/types/error_codes.ts index 79f1d2e69856c..a92f0025b8602 100644 --- a/x-pack/plugins/enterprise_search/common/types/error_codes.ts +++ b/x-pack/plugins/enterprise_search/common/types/error_codes.ts @@ -11,6 +11,7 @@ export enum ErrorCode { ANALYTICS_COLLECTION_NAME_INVALID = 'analytics_collection_name_invalid', CONNECTOR_DOCUMENT_ALREADY_EXISTS = 'connector_document_already_exists', CRAWLER_ALREADY_EXISTS = 'crawler_already_exists', + DOCUMENT_NOT_FOUND = 'document_not_found', INDEX_ALREADY_EXISTS = 'index_already_exists', INDEX_NOT_FOUND = 'index_not_found', PIPELINE_ALREADY_EXISTS = 'pipeline_already_exists', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/documents/get_document_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/documents/get_document_logic.ts new file mode 100644 index 0000000000000..5f5cf247f57f8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/documents/get_document_logic.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GetResponse } from '@elastic/elasticsearch/lib/api/types'; + +import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { HttpLogic } from '../../../shared/http'; + +export interface GetDocumentsArgs { + documentId: string; + indexName: string; +} + +export type GetDocumentsResponse = GetResponse; + +export const getDocument = async ({ indexName, documentId }: GetDocumentsArgs) => { + const route = `/internal/enterprise_search/indices/${indexName}/document/${documentId}`; + + return await HttpLogic.values.http.get(route); +}; + +export const GetDocumentsApiLogic = createApiLogic(['get_documents_logic'], getDocument); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/documents/get_documents_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/documents/get_documents_logic.test.ts new file mode 100644 index 0000000000000..fb01683fff4cb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/documents/get_documents_logic.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockHttpValues } from '../../../__mocks__/kea_logic'; + +import { nextTick } from '@kbn/test-jest-helpers'; + +import { getDocument } from './get_document_logic'; + +describe('getDocumentApiLogic', () => { + const { http } = mockHttpValues; + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getDocument', () => { + it('calls correct api', async () => { + const promise = Promise.resolve({ + _id: 'test-id', + _index: 'indexName', + _source: {}, + found: true, + }); + http.get.mockReturnValue(promise); + const result = getDocument({ documentId: '123123', indexName: 'indexName' }); + await nextTick(); + expect(http.get).toHaveBeenCalledWith( + '/internal/enterprise_search/indices/indexName/document/123123' + ); + await expect(result).resolves.toEqual({ + _id: 'test-id', + _index: 'indexName', + _source: {}, + found: true, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts index 2805b8389913d..fdc5bd5a92fd5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts @@ -13,6 +13,7 @@ import { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_ import { ErrorResponse, HttpError, Status } from '../../../../../../../common/types/api'; import { TrainedModelState } from '../../../../../../../common/types/pipelines'; +import { GetDocumentsApiLogic } from '../../../../api/documents/get_document_logic'; import { MappingsApiLogic } from '../../../../api/mappings/mappings_logic'; import { MLModelsApiLogic } from '../../../../api/ml_models/ml_models_logic'; import { AttachMlInferencePipelineApiLogic } from '../../../../api/pipelines/attach_ml_inference_pipeline'; @@ -35,22 +36,8 @@ const DEFAULT_VALUES: MLInferenceProcessorsValues = { ...EMPTY_PIPELINE_CONFIGURATION, }, indexName: '', - simulateBody: ` -[ - { - "_index": "index", - "_id": "id", - "_source": { - "foo": "bar" - } - }, - { - "_index": "index", - "_id": "id", - "_source": { - "foo": "baz" - } - } + simulateBody: `[ + ]`, step: AddInferencePipelineSteps.Configuration, }, @@ -61,7 +48,12 @@ const DEFAULT_VALUES: MLInferenceProcessorsValues = { pipelineName: 'Field is required.', sourceField: 'Field is required.', }, + getDocumentApiErrorMessage: undefined, + getDocumentApiStatus: Status.IDLE, + getDocumentData: undefined, + getDocumentsErr: '', index: null, + isGetDocumentsLoading: false, isLoading: true, isPipelineDataValid: false, mappingData: undefined, @@ -71,6 +63,7 @@ const DEFAULT_VALUES: MLInferenceProcessorsValues = { mlInferencePipelinesData: undefined, mlModelsData: undefined, mlModelsStatus: 0, + showGetDocumentErrors: false, simulateExistingPipelineData: undefined, simulateExistingPipelineStatus: 0, simulatePipelineData: undefined, @@ -103,6 +96,7 @@ describe('MlInferenceLogic', () => { const { mount: mountFetchMlInferencePipelinesApiLogic } = new LogicMounter( FetchMlInferencePipelinesApiLogic ); + const { mount: mountGetDocumentsApiLogic } = new LogicMounter(GetDocumentsApiLogic); beforeEach(() => { jest.clearAllMocks(); @@ -114,6 +108,7 @@ describe('MlInferenceLogic', () => { mountSimulateMlInterfacePipelineApiLogic(); mountCreateMlInferencePipelineApiLogic(); mountAttachMlInferencePipelineApiLogic(); + mountGetDocumentsApiLogic(); mount(); }); @@ -197,13 +192,35 @@ describe('MlInferenceLogic', () => { expect(MLInferenceLogic.values.createErrors).not.toHaveLength(0); MLInferenceLogic.actions.makeCreatePipelineRequest({ indexName: 'test', - pipelineName: 'unit-test', modelId: 'test-model', + pipelineName: 'unit-test', sourceField: 'body', }); expect(MLInferenceLogic.values.createErrors).toHaveLength(0); }); }); + describe('getDocumentApiSuccess', () => { + it('sets simulateBody text to the returned document', () => { + GetDocumentsApiLogic.actions.apiSuccess({ + _id: 'test-index-123', + _index: 'test-index', + found: true, + }); + expect(MLInferenceLogic.values.addInferencePipelineModal.simulateBody).toEqual( + JSON.stringify( + [ + { + _id: 'test-index-123', + _index: 'test-index', + found: true, + }, + ], + undefined, + 2 + ) + ); + }); + }); }); describe('selectors', () => { @@ -331,9 +348,9 @@ describe('MlInferenceLogic', () => { { destinationField: 'test-field', disabled: false, - pipelineName: 'unit-test', - modelType: '', modelId: 'test-model', + modelType: '', + pipelineName: 'unit-test', sourceField: 'body', }, ]); @@ -361,9 +378,9 @@ describe('MlInferenceLogic', () => { destinationField: 'test-field', disabled: true, disabledReason: expect.any(String), - pipelineName: 'unit-test', - modelType: '', modelId: 'test-model', + modelType: '', + pipelineName: 'unit-test', sourceField: 'body_content', }, ]); @@ -507,6 +524,46 @@ describe('MlInferenceLogic', () => { expect(MLInferenceLogic.values.mlInferencePipeline).toEqual(existingPipeline); }); }); + describe('getDocumentsErr', () => { + it('returns empty string when no error is present', () => { + GetDocumentsApiLogic.actions.apiSuccess({ + _id: 'test-123', + _index: 'test', + found: true, + }); + expect(MLInferenceLogic.values.getDocumentsErr).toEqual(''); + }); + it('returns extracted error message from the http response', () => { + GetDocumentsApiLogic.actions.apiError({ + body: { + error: 'document-not-found', + message: 'not-found', + statusCode: 404, + }, + } as HttpError); + expect(MLInferenceLogic.values.getDocumentsErr).toEqual('not-found'); + }); + }); + describe('showGetDocumentErrors', () => { + it('returns false when no error is present', () => { + GetDocumentsApiLogic.actions.apiSuccess({ + _id: 'test-123', + _index: 'test', + found: true, + }); + expect(MLInferenceLogic.values.showGetDocumentErrors).toEqual(false); + }); + it('returns true when an error message is present', () => { + GetDocumentsApiLogic.actions.apiError({ + body: { + error: 'document-not-found', + message: 'not-found', + statusCode: 404, + }, + } as HttpError); + expect(MLInferenceLogic.values.showGetDocumentErrors).toEqual(true); + }); + }); }); describe('listeners', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts index 4b92c43b2b304..001d4391bd94f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.ts @@ -18,11 +18,17 @@ import { getMlModelTypesForModelConfig, parseMlInferenceParametersFromPipeline, } from '../../../../../../../common/ml_inference_pipeline'; -import { Status } from '../../../../../../../common/types/api'; +import { Status, HttpError } from '../../../../../../../common/types/api'; import { MlInferencePipeline } from '../../../../../../../common/types/pipelines'; import { Actions } from '../../../../../shared/api_logic/create_api_logic'; import { getErrorsFromHttpResponse } from '../../../../../shared/flash_messages/handle_api_errors'; + +import { + GetDocumentsApiLogic, + GetDocumentsArgs, + GetDocumentsResponse, +} from '../../../../api/documents/get_document_logic'; import { CachedFetchIndexApiLogic, CachedFetchIndexApiLogicValues, @@ -127,6 +133,8 @@ interface MLInferenceProcessorsActions { CreateMlInferencePipelineResponse >['apiSuccess']; createPipeline: () => void; + getDocumentApiError: Actions['apiError']; + getDocumentApiSuccess: Actions['apiSuccess']; makeAttachPipelineRequest: Actions< AttachMlInferencePipelineApiLogicArgs, AttachMlInferencePipelineResponse @@ -135,6 +143,7 @@ interface MLInferenceProcessorsActions { CreateMlInferencePipelineApiLogicArgs, CreateMlInferencePipelineResponse >['makeRequest']; + makeGetDocumentRequest: Actions['makeRequest']; makeMLModelsRequest: Actions['makeRequest']; makeMappingRequest: Actions['makeRequest']; makeMlInferencePipelinesRequest: Actions< @@ -204,7 +213,12 @@ export interface MLInferenceProcessorsValues { createErrors: string[]; existingInferencePipelines: MLInferencePipelineOption[]; formErrors: AddInferencePipelineFormErrors; + getDocumentApiErrorMessage: HttpError | undefined; + getDocumentApiStatus: Status; + getDocumentData: typeof GetDocumentsApiLogic.values.data; + getDocumentsErr: string; index: CachedFetchIndexApiLogicValues['indexData']; + isGetDocumentsLoading: boolean; isLoading: boolean; isPipelineDataValid: boolean; mappingData: typeof MappingsApiLogic.values.data; @@ -214,6 +228,7 @@ export interface MLInferenceProcessorsValues { mlInferencePipelinesData: FetchMlInferencePipelinesResponse | undefined; mlModelsData: TrainedModelConfigResponse[] | undefined; mlModelsStatus: Status; + showGetDocumentErrors: boolean; simulateExistingPipelineData: typeof SimulateExistingMlInterfacePipelineApiLogic.values.data; simulateExistingPipelineStatus: Status; simulatePipelineData: typeof SimulateMlInterfacePipelineApiLogic.values.data; @@ -278,6 +293,12 @@ export const MLInferenceLogic = kea< 'apiSuccess as attachApiSuccess', 'makeRequest as makeAttachPipelineRequest', ], + GetDocumentsApiLogic, + [ + 'apiError as getDocumentApiError', + 'apiSuccess as getDocumentApiSuccess', + 'makeRequest as makeGetDocumentRequest', + ], ], values: [ CachedFetchIndexApiLogic, @@ -294,6 +315,12 @@ export const MLInferenceLogic = kea< ['data as simulatePipelineData', 'status as simulatePipelineStatus'], FetchMlInferencePipelineProcessorsApiLogic, ['data as mlInferencePipelineProcessors'], + GetDocumentsApiLogic, + [ + 'data as getDocumentData', + 'status as getDocumentApiStatus', + 'error as getDocumentApiErrorMessage', + ], ], }, events: {}, @@ -375,26 +402,16 @@ export const MLInferenceLogic = kea< ...EMPTY_PIPELINE_CONFIGURATION, }, indexName: '', - simulateBody: ` -[ - { - "_index": "index", - "_id": "id", - "_source": { - "foo": "bar" - } - }, - { - "_index": "index", - "_id": "id", - "_source": { - "foo": "baz" - } - } + simulateBody: `[ + ]`, step: AddInferencePipelineSteps.Configuration, }, { + getDocumentApiSuccess: (modal, doc) => ({ + ...modal, + simulateBody: JSON.stringify([doc], undefined, 2), + }), setAddInferencePipelineStep: (modal, { step }) => ({ ...modal, step }), setIndexName: (modal, { indexName }) => ({ ...modal, indexName }), setInferencePipelineConfiguration: (modal, { configuration }) => ({ @@ -420,8 +437,8 @@ export const MLInferenceLogic = kea< [], { setSimulatePipelineErrors: (_, { errors }) => errors, - simulatePipelineApiError: (_, error) => getErrorsFromHttpResponse(error), simulateExistingPipelineApiError: (_, error) => getErrorsFromHttpResponse(error), + simulatePipelineApiError: (_, error) => getErrorsFromHttpResponse(error), }, ], }, @@ -431,6 +448,19 @@ export const MLInferenceLogic = kea< (modal: AddInferencePipelineModal) => validateInferencePipelineConfiguration(modal.configuration), ], + getDocumentsErr: [ + () => [selectors.getDocumentApiErrorMessage], + (err: MLInferenceProcessorsValues['getDocumentApiErrorMessage']) => { + if (!err) return ''; + return getErrorsFromHttpResponse(err)[0]; + }, + ], + isGetDocumentsLoading: [ + () => [selectors.getDocumentApiStatus], + (status) => { + return status === Status.LOADING; + }, + ], isLoading: [ () => [selectors.mlModelsStatus, selectors.mappingStatus], (mlModelsStatus, mappingStatus) => @@ -441,6 +471,12 @@ export const MLInferenceLogic = kea< () => [selectors.formErrors], (errors: AddInferencePipelineFormErrors) => Object.keys(errors).length === 0, ], + showGetDocumentErrors: [ + () => [selectors.getDocumentApiStatus], + (status: MLInferenceProcessorsValues['getDocumentApiStatus']) => { + return status === Status.ERROR; + }, + ], mlInferencePipeline: [ () => [ selectors.isPipelineDataValid, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline.tsx index bd5b561426cfa..a1f7b316b9d48 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/test_pipeline.tsx @@ -5,23 +5,26 @@ * 2.0. */ -import React from 'react'; +import React, { useRef } from 'react'; import { useValues, useActions } from 'kea'; import { - EuiCodeBlock, - EuiResizableContainer, EuiButton, - EuiText, + EuiCode, + EuiCodeBlock, + EuiFieldText, EuiFlexGroup, EuiFlexItem, - useIsWithinMaxBreakpoint, + EuiFormRow, + EuiResizableContainer, EuiSpacer, + EuiText, + useIsWithinMaxBreakpoint, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - +import { FormattedMessage } from '@kbn/i18n-react'; import { CodeEditor } from '@kbn/kibana-react-plugin/public'; import { MLInferenceLogic } from './ml_inference_logic'; @@ -30,25 +33,81 @@ import './add_ml_inference_pipeline_modal.scss'; export const TestPipeline: React.FC = () => { const { - addInferencePipelineModal: { simulateBody }, + addInferencePipelineModal: { + configuration: { sourceField }, + indexName, + simulateBody, + }, + getDocumentsErr, + isGetDocumentsLoading, + showGetDocumentErrors, simulatePipelineResult, simulatePipelineErrors, } = useValues(MLInferenceLogic); - const { simulatePipeline, setPipelineSimulateBody } = useActions(MLInferenceLogic); + const { simulatePipeline, setPipelineSimulateBody, makeGetDocumentRequest } = + useActions(MLInferenceLogic); const isSmallerViewport = useIsWithinMaxBreakpoint('s'); + const inputRef = useRef(); return ( - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.test.title', - { defaultMessage: 'Review pipeline results (optional)' } - )} -

-
+ + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.test.title', + { defaultMessage: 'Review pipeline results (optional)' } + )} +

+
+ + +
+ {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.test.subtitle', + { defaultMessage: 'Documents' } + )} +
+
+
+ + + { + inputRef.current = ref; + }} + isInvalid={showGetDocumentErrors} + isLoading={isGetDocumentsLoading} + onKeyDown={(e) => { + if (e.key === 'Enter' && inputRef.current?.value.trim().length !== 0) { + makeGetDocumentRequest({ + documentId: inputRef.current?.value.trim() ?? '', + indexName, + }); + } + }} + /> + + +
{ - +

{i18n.translate( 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.test.description', @@ -98,6 +157,16 @@ export const TestPipeline: React.FC = () => { 'You can simulate your pipeline results by passing an array of documents.', } )} +
+ {`[{"_index":"index","_id":"id","_source":{"${sourceField}":"bar"}}]`} + ), + }} + />

diff --git a/x-pack/plugins/enterprise_search/server/lib/indices/document/get_document.ts b/x-pack/plugins/enterprise_search/server/lib/indices/document/get_document.ts new file mode 100644 index 0000000000000..a2d21d32fad31 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/indices/document/get_document.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GetResponse } from '@elastic/elasticsearch/lib/api/types'; +import { IScopedClusterClient } from '@kbn/core/server'; + +export const getDocument = async ( + client: IScopedClusterClient, + indexName: string, + documentId: string +): Promise> => { + const response = await client.asCurrentUser.get({ + id: documentId, + index: indexName, + }); + return response; +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/documents.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/documents.ts new file mode 100644 index 0000000000000..47d36cfff07f5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/documents.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { ErrorCode } from '../../../common/types/error_codes'; +import { getDocument } from '../../lib/indices/document/get_document'; +import { RouteDependencies } from '../../plugin'; +import { elasticsearchErrorHandler } from '../../utils/elasticsearch_error_handler'; +import { isNotFoundException } from '../../utils/identify_exceptions'; + +export function registerDocumentRoute({ router, log }: RouteDependencies) { + router.get( + { + path: '/internal/enterprise_search/indices/{index_name}/document/{document_id}', + validate: { + params: schema.object({ + document_id: schema.string(), + index_name: schema.string(), + }), + }, + }, + elasticsearchErrorHandler(log, async (context, request, response) => { + const indexName = decodeURIComponent(request.params.index_name); + const documentId = decodeURIComponent(request.params.document_id); + const { client } = (await context.core).elasticsearch; + + try { + const documentResponse = await getDocument(client, indexName, documentId); + return response.ok({ + body: documentResponse, + headers: { 'content-type': 'application/json' }, + }); + } catch (error) { + if (isNotFoundException(error)) { + return response.customError({ + body: { + attributes: { + error_code: ErrorCode.DOCUMENT_NOT_FOUND, + }, + message: `Could not find document ${documentId}`, + }, + statusCode: 404, + }); + } else { + // otherwise, default handler + throw error; + } + } + }) + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/index.ts index ea3a7f3805d3f..1a609a22da8b8 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/index.ts @@ -7,6 +7,7 @@ import { RouteDependencies } from '../../plugin'; +import { registerDocumentRoute } from './documents'; import { registerIndexRoutes } from './indices'; import { registerMappingRoute } from './mapping'; import { registerSearchRoute } from './search'; @@ -15,4 +16,5 @@ export const registerEnterpriseSearchRoutes = (dependencies: RouteDependencies) registerIndexRoutes(dependencies); registerMappingRoute(dependencies); registerSearchRoute(dependencies); + registerDocumentRoute(dependencies); }; diff --git a/x-pack/plugins/enterprise_search/server/utils/identify_exceptions.ts b/x-pack/plugins/enterprise_search/server/utils/identify_exceptions.ts index 9577eabfd31f3..5874659c690f8 100644 --- a/x-pack/plugins/enterprise_search/server/utils/identify_exceptions.ts +++ b/x-pack/plugins/enterprise_search/server/utils/identify_exceptions.ts @@ -33,3 +33,6 @@ export const isUnauthorizedException = (error: ElasticsearchResponseError) => export const isPipelineIsInUseException = (error: Error) => error.message === ErrorCode.PIPELINE_IS_IN_USE; + +export const isNotFoundException = (error: ElasticsearchResponseError) => + error.meta?.statusCode === 404;