Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Enterprise Search] Support for starting ELSER model deployment #156080

Merged
merged 19 commits into from
May 1, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 { ELSER_MODEL_ID } from '../../../../../../common/ml_inference_pipeline';
import { Actions, createApiLogic } from '../../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../../shared/http';

export type StartTextExpansionModelArgs = undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick(non-blocking): Best practice is to specify an empty object rather than undefined.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this is breaking the type checks:

/var/lib/buildkite-agent/builds/kb-n2-2-spot-a440796eab9fe2c4/elastic/kibana-pull-request/kibana/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/ml_models/text_expansion/create_text_expansion_model_api_logic.ts
--
  | 2023-04-28 11:19:48 EDT | 11:18  error  An empty interface is equivalent to `{}`  @typescript-eslint/no-empty-interface

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sphilipse I reverted to undefined because I couldn't find a way to fix the CI error.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, what you want to do instead is use {} wherever you're using StartTextExpansionModelArgs. But this is a nitpick anyway.


export interface StartTextExpansionModelResponse {
deploymentState: string;
modelId: string;
}

export const startTextExpansionModel = async (): Promise<StartTextExpansionModelResponse> => {
const route = `/internal/enterprise_search/ml/models/${ELSER_MODEL_ID}/deploy`;
return await HttpLogic.values.http.post<StartTextExpansionModelResponse>(route, {
body: undefined,
});
};

export const StartTextExpansionModelApiLogic = createApiLogic(
['start_text_expansion_model_api_logic'],
startTextExpansionModel
);

export type StartTextExpansionModelApiLogicActions = Actions<
StartTextExpansionModelArgs,
StartTextExpansionModelResponse
>;
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import {
ModelDeploymentInProgress,
ModelDeployed,
TextExpansionDismissButton,
ModelStarted,
} from './text_expansion_callout';

jest.mock('./text_expansion_callout_data', () => ({
useTextExpansionCallOutData: jest.fn(() => ({
dismiss: jest.fn(),
isCreateButtonDisabled: false,
isDismissable: false,
isStartButtonDisabled: false,
show: true,
})),
}));
Expand All @@ -34,6 +36,8 @@ const DEFAULT_VALUES = {
isCreateButtonDisabled: false,
isModelDownloadInProgress: false,
isModelDownloaded: false,
isModelStarted: false,
isStartButtonDisabled: false,
};

describe('TextExpansionCallOut', () => {
Expand Down Expand Up @@ -63,6 +67,15 @@ describe('TextExpansionCallOut', () => {
const wrapper = shallow(<TextExpansionCallOut />);
expect(wrapper.find(ModelDeployed).length).toBe(1);
});
it('renders panel with deployment in progress status if the model has been started', () => {
setMockValues({
...DEFAULT_VALUES,
isModelStarted: true,
});

const wrapper = shallow(<TextExpansionCallOut />);
expect(wrapper.find(ModelStarted).length).toBe(1);
});

describe('DeployModel', () => {
it('renders deploy button', () => {
Expand Down Expand Up @@ -109,12 +122,43 @@ describe('TextExpansionCallOut', () => {
});

describe('ModelDeployed', () => {
it('renders start button', () => {
const wrapper = shallow(
<ModelDeployed dismiss={() => {}} isDismissable={false} isStartButtonDisabled={false} />
);
expect(wrapper.find(EuiButton).length).toBe(1);
const button = wrapper.find(EuiButton);
expect(button.prop('disabled')).toBe(false);
});
it('renders disabled start button if it is set to disabled', () => {
const wrapper = shallow(
<ModelDeployed dismiss={() => {}} isDismissable={false} isStartButtonDisabled />
);
expect(wrapper.find(EuiButton).length).toBe(1);
const button = wrapper.find(EuiButton);
expect(button.prop('disabled')).toBe(true);
});
it('renders dismiss button if it is set to dismissable', () => {
const wrapper = shallow(
<ModelDeployed dismiss={() => {}} isDismissable isStartButtonDisabled={false} />
);
expect(wrapper.find(TextExpansionDismissButton).length).toBe(1);
});
it('does not render dismiss button if it is set to non-dismissable', () => {
const wrapper = shallow(
<ModelDeployed dismiss={() => {}} isDismissable={false} isStartButtonDisabled={false} />
);
expect(wrapper.find(TextExpansionDismissButton).length).toBe(0);
});
});

describe('ModelStarted', () => {
it('renders dismiss button if it is set to dismissable', () => {
const wrapper = shallow(<ModelDeployed dismiss={() => {}} isDismissable />);
const wrapper = shallow(<ModelStarted dismiss={() => {}} isDismissable />);
expect(wrapper.find(TextExpansionDismissButton).length).toBe(1);
});
it('does not render dismiss button if it is set to non-dismissable', () => {
const wrapper = shallow(<ModelDeployed dismiss={() => {}} isDismissable={false} />);
const wrapper = shallow(<ModelStarted dismiss={() => {}} isDismissable={false} />);
expect(wrapper.find(TextExpansionDismissButton).length).toBe(0);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useActions, useValues } from 'kea';
import {
EuiBadge,
EuiButton,
EuiButtonEmpty,
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
Expand All @@ -25,6 +26,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedHTMLMessage } from '@kbn/i18n-react';

import { docLinks } from '../../../../../shared/doc_links';
import { KibanaLogic } from '../../../../../shared/kibana';

import { useTextExpansionCallOutData } from './text_expansion_callout_data';
import { TextExpansionCalloutLogic } from './text_expansion_callout_logic';
Expand All @@ -33,6 +35,7 @@ export interface TextExpansionCallOutState {
dismiss: () => void;
isCreateButtonDisabled: boolean;
isDismissable: boolean;
isStartButtonDisabled: boolean;
show: boolean;
}

Expand Down Expand Up @@ -182,6 +185,90 @@ export const ModelDeploymentInProgress = ({
export const ModelDeployed = ({
demjened marked this conversation as resolved.
Show resolved Hide resolved
dismiss,
isDismissable,
isStartButtonDisabled,
}: Pick<TextExpansionCallOutState, 'dismiss' | 'isDismissable' | 'isStartButtonDisabled'>) => {
const { startTextExpansionModel } = useActions(TextExpansionCalloutLogic);

return (
<EuiPanel color="success">
demjened marked this conversation as resolved.
Show resolved Hide resolved
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem grow>
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="checkInCircleFilled" />
</EuiFlexItem>
<EuiFlexItem grow>
<EuiTitle size="xs">
<h4>
demjened marked this conversation as resolved.
Show resolved Hide resolved
<FormattedMessage
id="xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.deployedTitle"
defaultMessage="Your ELSER model has deployed but not started." />
</h4>
</EuiTitle>
</EuiFlexItem>
{isDismissable && (
<EuiFlexItem grow={false}>
<TextExpansionDismissButton dismiss={dismiss} />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow>
<EuiText>
<FormattedMessage
id="xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.deployedBody"
defaultMessage="You may start the model in a single-threaded configuration for testing, or tune the performance for a production environment." />
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow>
<EuiFlexGroup direction="row" gutterSize="s" alignItems="center">
<EuiFlexItem>
<EuiButton
aria-label={i18n.translate(
demjened marked this conversation as resolved.
Show resolved Hide resolved
'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.startModelButton',
{ defaultMessage: 'Start ELSER model' }
)}
color="success"
disabled={isStartButtonDisabled}
iconType="playFilled"
onClick={() => startTextExpansionModel(undefined)}
>
{i18n.translate(
'xpack.enterpriseSearch.content.indices.pipelines.textExpansionCallOut.startModelButton.label',
{
defaultMessage: 'Start single-threaded',
}
)}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiButtonEmpty
aria-label={i18n.translate(
'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.fineTuneModelButton',
{ defaultMessage: 'Fine-tune ELSER model' }
)}
iconSide="left"
iconType="wrench"
onClick={() => KibanaLogic.values.navigateToUrl('/app/ml/trained_models', {
shouldNotCreateHref: true,
})}
demjened marked this conversation as resolved.
Show resolved Hide resolved
>
{i18n.translate('xpack.enterpriseSearch.content.engine.api.step1.viewKeysButton', {
defaultMessage: 'Fine-tune performance',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow />
demjened marked this conversation as resolved.
Show resolved Hide resolved
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};

export const ModelStarted = ({
dismiss,
isDismissable,
}: Pick<TextExpansionCallOutState, 'dismiss' | 'isDismissable'>) => (
<EuiPanel color="success">
<EuiFlexGroup direction="column" gutterSize="s">
Expand All @@ -194,8 +281,8 @@ export const ModelDeployed = ({
<EuiTitle size="xs">
<h4>
<FormattedMessage
id="xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.deployedTitle"
defaultMessage="Your ELSER model has deployed."
id="xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.startedTitle"
defaultMessage="Your ELSER model has started."
/>
</h4>
</EuiTitle>
Expand All @@ -221,15 +308,28 @@ export const ModelDeployed = ({

export const TextExpansionCallOut: React.FC<TextExpansionCallOutProps> = (props) => {
const { dismiss, isDismissable, show } = useTextExpansionCallOutData(props);
const { isCreateButtonDisabled, isModelDownloadInProgress, isModelDownloaded } =
useValues(TextExpansionCalloutLogic);
const {
isCreateButtonDisabled,
isModelDownloadInProgress,
isModelDownloaded,
isModelStarted,
isStartButtonDisabled,
} = useValues(TextExpansionCalloutLogic);

if (!show) return null;

if (!!isModelDownloadInProgress) {
demjened marked this conversation as resolved.
Show resolved Hide resolved
return <ModelDeploymentInProgress dismiss={dismiss} isDismissable={isDismissable} />;
} else if (!!isModelDownloaded) {
return <ModelDeployed dismiss={dismiss} isDismissable={isDismissable} />;
return (
<ModelDeployed
dismiss={dismiss}
isDismissable={isDismissable}
isStartButtonDisabled={isStartButtonDisabled}
/>
);
} else if (!!isModelStarted) {
demjened marked this conversation as resolved.
Show resolved Hide resolved
return <ModelStarted dismiss={dismiss} isDismissable={isDismissable} />;
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const isDismissed = () => localStorage.getItem(TEXT_EXPANSION_CALL_OUT_DISMISSED
export const useTextExpansionCallOutData = ({
isDismissable = false,
}: TextExpansionCallOutProps): TextExpansionCallOutState => {
const { isCreateButtonDisabled } = useValues(TextExpansionCalloutLogic);
const { isCreateButtonDisabled, isStartButtonDisabled } = useValues(TextExpansionCalloutLogic);

const [show, setShow] = useState<boolean>(() => {
if (!isDismissable) return true;
Expand Down Expand Up @@ -50,6 +50,7 @@ export const useTextExpansionCallOutData = ({
dismiss,
isCreateButtonDisabled,
isDismissable,
isStartButtonDisabled,
show,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ const DEFAULT_VALUES: TextExpansionCalloutValues = {
isCreateButtonDisabled: false,
isModelDownloadInProgress: false,
isModelDownloaded: false,
isModelStarted: false,
isPollingTextExpansionModelActive: false,
isStartButtonDisabled: false,
startTextExpansionModelStatus: Status.IDLE,
textExpansionModel: undefined,
textExpansionModelPollTimeoutId: null,
};
Expand Down Expand Up @@ -184,6 +187,19 @@ describe('TextExpansionCalloutLogic', () => {
});
});

describe('startTextExpansionModelSuccess', () => {
it('sets startedTextExpansionModel', () => {
jest.spyOn(TextExpansionCalloutLogic.actions, 'fetchTextExpansionModel');

TextExpansionCalloutLogic.actions.startTextExpansionModelSuccess({
deploymentState: MlModelDeploymentState.FullyAllocated,
modelId: 'mock-model-id',
});

expect(TextExpansionCalloutLogic.actions.fetchTextExpansionModel).toHaveBeenCalled();
});
});

describe('stopPollingTextExpansionModel', () => {
it('clears polling timeout and poll timeout ID if it is set', () => {
mount({
Expand Down Expand Up @@ -269,6 +285,23 @@ describe('TextExpansionCalloutLogic', () => {
});
});

describe('isModelStarted', () => {
it('is set to true if the model is started', () => {
FetchTextExpansionModelApiLogic.actions.apiSuccess({
deploymentState: MlModelDeploymentState.FullyAllocated,
modelId: 'mock-model-id',
});
expect(TextExpansionCalloutLogic.values.isModelStarted).toBe(true);
});
it('is set to false if the model is not started', () => {
FetchTextExpansionModelApiLogic.actions.apiSuccess({
deploymentState: MlModelDeploymentState.NotDeployed,
modelId: 'mock-model-id',
});
expect(TextExpansionCalloutLogic.values.isModelStarted).toBe(false);
});
});

describe('isPollingTextExpansionModelActive', () => {
it('is set to false if polling is not active', () => {
mount({
Expand Down
Loading