[ML] ELSER v2 download in the Trained Models UI (#167407)

## Summary

Adds support for ELSER v2 download from the Trained Models UI.

- Marks an appropriate model version for the current cluster
configuration with the recommended flag.
- Updates the state column with better human-readable labels and colour
indicators.
- Adds a callout promoting a new version of ELSER

<img width="1686" alt="image"
src="0deea53a-6d37-4af6-97bc-9f46e36f113b">

#### Notes for reviews
- We need to wait for
https://github.com/elastic/elasticsearch/pull/99584 to get the start
deployment validation functionality. At the moment you can successfully
start deployment of the wrong model version.

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
Dima Arnautov 2023-10-03 11:19:49 +02:00 committed by GitHub
parent 105935ace5
commit 0c6dfbf209
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 375 additions and 77 deletions

View file

@ -510,6 +510,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
setUpgradeMode: `${ELASTICSEARCH_DOCS}ml-set-upgrade-mode.html`,
trainedModels: `${MACHINE_LEARNING_DOCS}ml-trained-models.html`,
startTrainedModelsDeployment: `${MACHINE_LEARNING_DOCS}ml-nlp-deploy-model.html`,
nlpElser: `${MACHINE_LEARNING_DOCS}ml-nlp-elser.html`,
},
transforms: {
guide: `${ELASTICSEARCH_DOCS}transforms.html`,

View file

@ -20,4 +20,9 @@ export {
type ModelDefinitionResponse,
type ElserVersion,
type GetElserOptions,
ELSER_ID_V1,
ELASTIC_MODEL_TAG,
ELASTIC_MODEL_TYPE,
MODEL_STATE,
type ModelState,
} from './src/constants/trained_models';

View file

@ -46,8 +46,12 @@ export const BUILT_IN_MODEL_TAG = 'prepackaged';
export const ELASTIC_MODEL_TAG = 'elastic';
export const ELSER_ID_V1 = '.elser_model_1' as const;
export const ELASTIC_MODEL_DEFINITIONS: Record<string, ModelDefinition> = Object.freeze({
'.elser_model_1': {
modelName: 'elser',
hidden: true,
version: 1,
config: {
input: {
@ -59,6 +63,7 @@ export const ELASTIC_MODEL_DEFINITIONS: Record<string, ModelDefinition> = Object
}),
},
'.elser_model_2_SNAPSHOT': {
modelName: 'elser',
version: 2,
default: true,
config: {
@ -71,6 +76,7 @@ export const ELASTIC_MODEL_DEFINITIONS: Record<string, ModelDefinition> = Object
}),
},
'.elser_model_2_linux-x86_64_SNAPSHOT': {
modelName: 'elser',
version: 2,
os: 'Linux',
arch: 'amd64',
@ -87,6 +93,7 @@ export const ELASTIC_MODEL_DEFINITIONS: Record<string, ModelDefinition> = Object
} as const);
export interface ModelDefinition {
modelName: string;
version: number;
config: object;
description: string;
@ -94,9 +101,10 @@ export interface ModelDefinition {
arch?: string;
default?: boolean;
recommended?: boolean;
hidden?: boolean;
}
export type ModelDefinitionResponse = ModelDefinition & {
export type ModelDefinitionResponse = Omit<ModelDefinition, 'modelName'> & {
name: string;
};
@ -106,6 +114,7 @@ export const MODEL_STATE = {
...DEPLOYMENT_STATE,
DOWNLOADING: 'downloading',
DOWNLOADED: 'downloaded',
NOT_DOWNLOADED: 'notDownloaded',
} as const;
export type ModelState = typeof MODEL_STATE[keyof typeof MODEL_STATE] | null;

View file

@ -15,6 +15,7 @@ export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference';
export const ML_ANOMALY_EXPLORER_PANELS = 'ml.anomalyExplorerPanels';
export const ML_NOTIFICATIONS_LAST_CHECKED_AT = 'ml.notificationsLastCheckedAt';
export const ML_OVERVIEW_PANELS = 'ml.overviewPanels';
export const ML_ELSER_CALLOUT_DISMISSED = 'ml.elserUpdateCalloutDismissed';
export type PartitionFieldConfig =
| {
@ -68,6 +69,7 @@ export interface MlStorageRecord {
[ML_ANOMALY_EXPLORER_PANELS]: AnomalyExplorerPanelsState | undefined;
[ML_NOTIFICATIONS_LAST_CHECKED_AT]: number | undefined;
[ML_OVERVIEW_PANELS]: OverviewPanelsState;
[ML_ELSER_CALLOUT_DISMISSED]: boolean | undefined;
}
export type MlStorage = Partial<MlStorageRecord> | null;
@ -88,6 +90,8 @@ export type TMlStorageMapped<T extends MlStorageKey> = T extends typeof ML_ENTIT
? number | undefined
: T extends typeof ML_OVERVIEW_PANELS
? OverviewPanelsState | undefined
: T extends typeof ML_ELSER_CALLOUT_DISMISSED
? boolean | undefined
: null;
export const ML_STORAGE_KEYS = [
@ -98,4 +102,5 @@ export const ML_STORAGE_KEYS = [
ML_ANOMALY_EXPLORER_PANELS,
ML_NOTIFICATIONS_LAST_CHECKED_AT,
ML_OVERVIEW_PANELS,
ML_ELSER_CALLOUT_DISMISSED,
] as const;

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { MODEL_STATE, ModelState } from '@kbn/ml-trained-models-utils';
import { EuiHealthProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export const getModelStateColor = (
state: ModelState
): { color: EuiHealthProps['color']; name: string } | null => {
switch (state) {
case MODEL_STATE.DOWNLOADED:
return {
color: 'subdued',
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.downloadedName', {
defaultMessage: 'Ready to deploy',
}),
};
case MODEL_STATE.DOWNLOADING:
return {
color: 'warning',
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.downloadingName', {
defaultMessage: 'Downloading...',
}),
};
case MODEL_STATE.STARTED:
return {
color: 'success',
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.startedName', {
defaultMessage: 'Deployed',
}),
};
case MODEL_STATE.STARTING:
return {
color: 'success',
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.startingName', {
defaultMessage: 'Starting deployment...',
}),
};
case MODEL_STATE.STOPPING:
return {
color: 'accent',
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.stoppingName', {
defaultMessage: 'Stopping deployment...',
}),
};
case MODEL_STATE.NOT_DOWNLOADED:
return {
color: '#d4dae5',
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelState.notDownloadedName', {
defaultMessage: 'Not downloaded',
}),
};
default:
return null;
}
};

View file

@ -177,17 +177,18 @@ export function useModelActions({
},
{
name: i18n.translate('xpack.ml.inference.modelsList.startModelDeploymentActionLabel', {
defaultMessage: 'Start deployment',
defaultMessage: 'Deploy',
}),
description: i18n.translate(
'xpack.ml.inference.modelsList.startModelDeploymentActionLabel',
'xpack.ml.inference.modelsList.startModelDeploymentActionDescription',
{
defaultMessage: 'Start deployment',
}
),
'data-test-subj': 'mlModelsTableRowStartDeploymentAction',
// @ts-ignore EUI has a type check issue when type "button" is combined with an icon.
icon: 'play',
type: 'icon',
type: 'button',
isPrimary: true,
enabled: (item) => {
return canStartStopTrainedModels && !isLoading && item.state !== MODEL_STATE.DOWNLOADING;
@ -311,10 +312,12 @@ export function useModelActions({
'data-test-subj': 'mlModelsTableRowStopDeploymentAction',
icon: 'stop',
type: 'icon',
isPrimary: true,
available: (item) => item.model_type === TRAINED_MODEL_TYPE.PYTORCH,
enabled: (item) =>
canStartStopTrainedModels && !isLoading && item.deployment_ids.length > 0,
isPrimary: false,
available: (item) =>
item.model_type === TRAINED_MODEL_TYPE.PYTORCH &&
canStartStopTrainedModels &&
(item.state === MODEL_STATE.STARTED || item.state === MODEL_STATE.STARTING),
enabled: (item) => !isLoading,
onClick: async (item) => {
const requireForceStop = isPopulatedObject(item.pipelines);
const hasMultipleDeployments = item.deployment_ids.length > 1;
@ -380,17 +383,19 @@ export function useModelActions({
},
{
name: i18n.translate('xpack.ml.inference.modelsList.downloadModelActionLabel', {
defaultMessage: 'Download model',
defaultMessage: 'Download',
}),
description: i18n.translate('xpack.ml.inference.modelsList.downloadModelActionLabel', {
defaultMessage: 'Download model',
defaultMessage: 'Download',
}),
'data-test-subj': 'mlModelsTableRowDownloadModelAction',
// @ts-ignore EUI has a type check issue when type "button" is combined with an icon.
icon: 'download',
type: 'icon',
type: 'button',
isPrimary: true,
available: (item) => item.tags.includes(ELASTIC_MODEL_TAG),
enabled: (item) => !item.state && !isLoading,
available: (item) =>
item.tags.includes(ELASTIC_MODEL_TAG) && item.state === MODEL_STATE.NOT_DOWNLOADED,
enabled: (item) => !isLoading,
onClick: async (item) => {
try {
onLoading(true);

View file

@ -10,12 +10,16 @@ import {
EuiBadge,
EuiButton,
EuiButtonIcon,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiHealth,
EuiInMemoryTable,
EuiSearchBarProps,
EuiLink,
type EuiSearchBarProps,
EuiSpacer,
EuiTitle,
EuiToolTip,
SearchFilterConfig,
} from '@elastic/eui';
import { groupBy } from 'lodash';
@ -28,18 +32,21 @@ import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { usePageUrlState } from '@kbn/ml-url-state';
import { useTimefilter } from '@kbn/ml-date-picker';
import {
BUILT_IN_MODEL_TYPE,
BUILT_IN_MODEL_TAG,
BUILT_IN_MODEL_TYPE,
DEPLOYMENT_STATE,
} from '@kbn/ml-trained-models-utils';
import { isDefined } from '@kbn/ml-is-defined';
import {
ELASTIC_MODEL_DEFINITIONS,
ELASTIC_MODEL_TAG,
ELASTIC_MODEL_TYPE,
ELSER_ID_V1,
MODEL_STATE,
ModelState,
} from '@kbn/ml-trained-models-utils/src/constants/trained_models';
type ModelState,
} from '@kbn/ml-trained-models-utils';
import { isDefined } from '@kbn/ml-is-defined';
import { css } from '@emotion/react';
import { useStorage } from '@kbn/ml-local-storage';
import { getModelStateColor } from './get_model_state_color';
import { ML_ELSER_CALLOUT_DISMISSED } from '../../../common/types/storage';
import { TechnicalPreviewBadge } from '../components/technical_preview_badge';
import { useModelActions } from './model_actions';
import { ModelsTableToConfigMapping } from '.';
@ -74,6 +81,7 @@ export type ModelItem = TrainedModelConfigResponse & {
deployment_ids: string[];
putModelConfig?: object;
state: ModelState;
recommended?: boolean;
};
export type ModelItemFull = Required<ModelItem>;
@ -83,10 +91,14 @@ interface PageUrlState {
pageUrlState: ListingPageUrlState;
}
const modelIdColumnName = i18n.translate('xpack.ml.trainedModels.modelsList.modelIdHeader', {
defaultMessage: 'ID',
});
export const getDefaultModelsListState = (): ListingPageUrlState => ({
pageIndex: 0,
pageSize: 10,
sortField: ModelsTableToConfigMapping.id,
sortField: modelIdColumnName,
sortDirection: 'asc',
});
@ -102,10 +114,17 @@ export const ModelsList: FC<Props> = ({
const {
services: {
application: { capabilities },
docLinks,
},
} = useMlKibana();
const nlpElserDocUrl = docLinks.links.ml.nlpElser;
const { isNLPEnabled } = useEnabledFeatures();
const [isElserCalloutDismissed, setIsElserCalloutDismissed] = useStorage(
ML_ELSER_CALLOUT_DISMISSED,
false
);
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: true });
@ -155,6 +174,11 @@ export const ModelsList: FC<Props> = ({
[]
);
// List of downloaded/existing models
const existingModels = useMemo(() => {
return items.filter((i) => !i.putModelConfig);
}, [items]);
/**
* Checks if the model download complete.
*/
@ -219,7 +243,35 @@ export const ModelsList: FC<Props> = ({
// TODO combine fetching models definitions and stats into a single function
await fetchModelsStats(newItems);
setItems(newItems);
let resultItems = newItems;
// don't add any of the built-in models (e.g. elser) if NLP is disabled
if (isNLPEnabled) {
const idMap = new Map<string, ModelItem>(
resultItems.map((model) => [model.model_id, model])
);
const forDownload = await trainedModelsApiService.getTrainedModelDownloads();
const notDownloaded: ModelItem[] = forDownload
.filter(({ name, hidden, recommended }) => {
if (recommended && idMap.has(name)) {
idMap.get(name)!.recommended = true;
}
return !idMap.has(name) && !hidden;
})
.map<ModelItem>((modelDefinition) => {
return {
model_id: modelDefinition.name,
type: [ELASTIC_MODEL_TYPE],
tags: [ELASTIC_MODEL_TAG],
putModelConfig: modelDefinition.config,
description: modelDefinition.description,
state: MODEL_STATE.NOT_DOWNLOADED,
recommended: !!modelDefinition.recommended,
} as ModelItem;
});
resultItems = [...resultItems, ...notDownloaded];
}
setItems(resultItems);
if (expandedItemsToRefresh.length > 0) {
await fetchModelsStats(expandedItemsToRefresh);
@ -242,7 +294,7 @@ export const ModelsList: FC<Props> = ({
setIsInitialized(true);
setIsLoading(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [itemIdToExpandedRowMap]);
}, [itemIdToExpandedRowMap, isNLPEnabled]);
useEffect(
function updateOnTimerRefresh() {
@ -257,13 +309,13 @@ export const ModelsList: FC<Props> = ({
return {
total: {
show: true,
value: items.length,
value: existingModels.length,
label: i18n.translate('xpack.ml.trainedModels.modelsList.totalAmountLabel', {
defaultMessage: 'Total trained models',
}),
},
};
}, [items]);
}, [existingModels]);
/**
* Fetches models stats and update the original object
@ -325,7 +377,7 @@ export const ModelsList: FC<Props> = ({
* Unique inference types from models
*/
const inferenceTypesOptions = useMemo(() => {
const result = items.reduce((acc, item) => {
const result = existingModels.reduce((acc, item) => {
const type = item.inference_config && Object.keys(item.inference_config)[0];
if (type) {
acc.add(type);
@ -339,13 +391,16 @@ export const ModelsList: FC<Props> = ({
value: v,
name: v,
}));
}, [items]);
}, [existingModels]);
const modelAndDeploymentIds = useMemo(
() => [
...new Set([...items.flatMap((v) => v.deployment_ids), ...items.map((i) => i.model_id)]),
...new Set([
...existingModels.flatMap((v) => v.deployment_ids),
...existingModels.map((i) => i.model_id),
]),
],
[items]
[existingModels]
);
/**
@ -400,30 +455,61 @@ export const ModelsList: FC<Props> = ({
'data-test-subj': 'mlModelsTableRowDetailsToggle',
},
{
field: ModelsTableToConfigMapping.id,
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelIdHeader', {
defaultMessage: 'ID',
}),
sortable: true,
name: modelIdColumnName,
width: '15%',
sortable: ({ model_id: modelId }: ModelItem) => modelId,
truncateText: false,
textOnly: false,
'data-test-subj': 'mlModelsTableColumnId',
render: ({ description, model_id: modelId }: ModelItem) => {
const isTechPreview = description?.includes('(Tech Preview)');
return (
<EuiFlexGroup gutterSize={'s'} alignItems={'center'} wrap={true}>
<EuiFlexItem grow={false}>{modelId}</EuiFlexItem>
{isTechPreview ? (
<EuiFlexItem grow={false}>
<TechnicalPreviewBadge compressed />
</EuiFlexItem>
) : null}
</EuiFlexGroup>
);
},
},
{
field: ModelsTableToConfigMapping.description,
width: '350px',
width: '35%',
name: i18n.translate('xpack.ml.trainedModels.modelsList.modelDescriptionHeader', {
defaultMessage: 'Description',
}),
sortable: false,
truncateText: false,
'data-test-subj': 'mlModelsTableColumnDescription',
render: (description: string) => {
render: ({ description, recommended }: ModelItem) => {
if (!description) return null;
const isTechPreview = description.includes('(Tech Preview)');
return (
<>
{description.replace('(Tech Preview)', '')}
{isTechPreview ? <TechnicalPreviewBadge compressed /> : null}
{recommended ? (
<EuiToolTip
content={
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.recommendedDownloadContent"
defaultMessage="Recommended ELSER model version for your cluster's hardware configuration"
/>
}
>
<b
css={css`
text-wrap: nowrap;
`}
>
&nbsp;
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.recommendedDownloadLabel"
defaultMessage="(Recommended)"
/>
</b>
</EuiToolTip>
) : null}
</>
);
},
@ -456,8 +542,13 @@ export const ModelsList: FC<Props> = ({
}),
align: 'left',
truncateText: false,
render: (state: string) => {
return state ? <EuiBadge color="hollow">{state}</EuiBadge> : null;
render: (state: ModelState) => {
const config = getModelStateColor(state);
return config ? (
<EuiHealth textSize={'xs'} color={config.color}>
{config.name}
</EuiHealth>
) : null;
},
'data-test-subj': 'mlModelsTableColumnDeploymentState',
},
@ -585,28 +676,8 @@ export const ModelsList: FC<Props> = ({
: {}),
};
const resultItems = useMemo<ModelItem[]>(() => {
if (isNLPEnabled === false) {
// don't add any of the built in models (e.g. elser) if NLP is disabled
return items;
}
const idSet = new Set(items.map((i) => i.model_id));
const notDownloaded: ModelItem[] = Object.entries(ELASTIC_MODEL_DEFINITIONS)
.filter(([modelId]) => !idSet.has(modelId))
.map(([modelId, modelDefinition]) => {
return {
model_id: modelId,
type: [ELASTIC_MODEL_TYPE],
tags: [ELASTIC_MODEL_TAG],
putModelConfig: modelDefinition.config,
description: modelDefinition.description,
} as ModelItem;
});
const result = [...items, ...notDownloaded];
return result;
}, [isNLPEnabled, items]);
const isElserCalloutVisible =
!isElserCalloutDismissed && items.findIndex((i) => i.model_id === ELSER_ID_V1) >= 0;
if (!isInitialized) return null;
@ -631,7 +702,7 @@ export const ModelsList: FC<Props> = ({
isExpandable={true}
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
isSelectable={false}
items={resultItems}
items={items}
itemId={ModelsTableToConfigMapping.id}
loading={isLoading}
search={search}
@ -643,6 +714,38 @@ export const ModelsList: FC<Props> = ({
onTableChange={onTableChange}
sorting={sorting}
data-test-subj={isLoading ? 'mlModelsTable loading' : 'mlModelsTable loaded'}
childrenBetween={
isElserCalloutVisible ? (
<>
<EuiCallOut
size="s"
title={
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.newElserModelTitle"
defaultMessage="New ELSER model now available"
/>
}
onDismiss={setIsElserCalloutDismissed.bind(null, true)}
>
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.newElserModelDescription"
defaultMessage="A new version of ELSER that shows faster performance and improved relevance is now available. {docLink} for information on how to start using it."
values={{
docLink: (
<EuiLink href={nlpElserDocUrl} external target={'_blank'}>
<FormattedMessage
id="xpack.ml.trainedModels.modelsList.startDeployment.viewElserDocLink"
defaultMessage="View documentation"
/>
</EuiLink>
),
}}
/>
</EuiCallOut>
<EuiSpacer size="m" />
</>
) : null
}
/>
</div>
{modelsToDelete.length > 0 && (

View file

@ -43,6 +43,89 @@ describe('modelsProvider', () => {
jest.clearAllMocks();
});
describe('getModelDownloads', () => {
test('provides a list of models with recommended and default flag', async () => {
const result = await modelService.getModelDownloads();
expect(result).toEqual([
{
config: { input: { field_names: ['text_field'] } },
description: 'Elastic Learned Sparse EncodeR v1 (Tech Preview)',
hidden: true,
name: '.elser_model_1',
version: 1,
},
{
config: { input: { field_names: ['text_field'] } },
default: true,
description: 'Elastic Learned Sparse EncodeR v2 (Tech Preview)',
name: '.elser_model_2_SNAPSHOT',
version: 2,
},
{
arch: 'amd64',
config: { input: { field_names: ['text_field'] } },
description:
'Elastic Learned Sparse EncodeR v2, optimized for linux-x86_64 (Tech Preview)',
name: '.elser_model_2_linux-x86_64_SNAPSHOT',
os: 'Linux',
recommended: true,
version: 2,
},
]);
});
test('provides a list of models with default model as recommended', async () => {
mockCloud.cloudId = undefined;
(mockClient.asInternalUser.transport.request as jest.Mock).mockResolvedValueOnce({
_nodes: {
total: 1,
successful: 1,
failed: 0,
},
cluster_name: 'default',
nodes: {
yYmqBqjpQG2rXsmMSPb9pQ: {
name: 'node-0',
roles: ['ml'],
attributes: {},
os: {
name: 'Mac OS X',
arch: 'aarch64',
},
},
},
});
const result = await modelService.getModelDownloads();
expect(result).toEqual([
{
config: { input: { field_names: ['text_field'] } },
description: 'Elastic Learned Sparse EncodeR v1 (Tech Preview)',
hidden: true,
name: '.elser_model_1',
version: 1,
},
{
config: { input: { field_names: ['text_field'] } },
recommended: true,
description: 'Elastic Learned Sparse EncodeR v2 (Tech Preview)',
name: '.elser_model_2_SNAPSHOT',
version: 2,
},
{
arch: 'amd64',
config: { input: { field_names: ['text_field'] } },
description:
'Elastic Learned Sparse EncodeR v2, optimized for linux-x86_64 (Tech Preview)',
name: '.elser_model_2_linux-x86_64_SNAPSHOT',
os: 'Linux',
version: 2,
},
]);
});
});
describe('getELSER', () => {
test('provides a recommended definition by default', async () => {
const result = await modelService.getELSER();

View file

@ -25,6 +25,7 @@ import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { PipelineDefinition } from '../../../common/types/trained_models';
export type ModelService = ReturnType<typeof modelsProvider>;
export const modelsProvider = (client: IScopedClusterClient, cloud?: CloudSetup) =>
new ModelsProvider(client, cloud);
@ -83,6 +84,7 @@ export class ModelsProvider {
return { [index]: null };
}
}
private getNodeId(
elementOriginalId: string,
nodeType: typeof JOB_MAP_NODE_TYPES[keyof typeof JOB_MAP_NODE_TYPES]
@ -446,18 +448,39 @@ export class ModelsProvider {
}
}
const result = Object.entries(ELASTIC_MODEL_DEFINITIONS).map(([name, def]) => {
const modelDefinitionMap = new Map<string, ModelDefinitionResponse[]>();
for (const [name, def] of Object.entries(ELASTIC_MODEL_DEFINITIONS)) {
const recommended =
(isCloud && def.os === 'Linux' && def.arch === 'amd64') ||
(sameArch && !!def?.os && def?.os === osName && def?.arch === arch);
return {
...def,
name,
...(recommended ? { recommended } : {}),
};
});
return result;
const { modelName, ...rest } = def;
const modelDefinitionResponse = {
...rest,
...(recommended ? { recommended } : {}),
name,
};
if (modelDefinitionMap.has(modelName)) {
modelDefinitionMap.get(modelName)!.push(modelDefinitionResponse);
} else {
modelDefinitionMap.set(modelName, [modelDefinitionResponse]);
}
}
// check if there is no recommended, so we mark default as recommended
for (const arr of modelDefinitionMap.values()) {
const defaultModel = arr.find((a) => a.default);
const recommendedModel = arr.find((a) => a.recommended);
if (defaultModel && !recommendedModel) {
delete defaultModel.default;
defaultModel.recommended = true;
}
}
return [...modelDefinitionMap.values()].flat();
}
/**

View file

@ -264,9 +264,11 @@ export function TrainedModelsTableProvider(
}
public async deleteModel(modelId: string) {
const fromContextMenu = await this.doesModelCollapsedActionsButtonExist(modelId);
await mlCommonUI.invokeTableRowAction(
this.rowSelector(modelId),
'mlModelsTableRowDeleteAction'
'mlModelsTableRowDeleteAction',
fromContextMenu
);
await this.assertDeleteModalExists();
await this.confirmDeleteModel();
@ -459,9 +461,10 @@ export function TrainedModelsTableProvider(
}
public async clickStopDeploymentAction(modelId: string) {
await testSubjects.clickWhenNotDisabled(
this.rowSelector(modelId, 'mlModelsTableRowStopDeploymentAction'),
{ timeout: 5000 }
await mlCommonUI.invokeTableRowAction(
this.rowSelector(modelId),
'mlModelsTableRowStopDeploymentAction',
true
);
}