Skip to content

Commit

Permalink
[Search] Add index errors in Search Index page (elastic#188682)
Browse files Browse the repository at this point in the history
## Summary

This adds an error callout to the index pages in Search if the mappings
contain a semantic text field that references a non-existent inference
ID, or an inference ID without a model that has started.
  • Loading branch information
sphilipse authored Jul 26, 2024
1 parent 754de3b commit 1457428
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* 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 { useEffect, useState } from 'react';

import React from 'react';

import { useActions, useValues } from 'kea';

import {
InferenceServiceSettings,
MappingProperty,
MappingPropertyBase,
MappingTypeMapping,
} from '@elastic/elasticsearch/lib/api/types';

import { EuiButton, EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

import { LocalInferenceServiceSettings } from '@kbn/ml-trained-models-utils/src/constants/trained_models';

import { KibanaLogic } from '../../../shared/kibana';
import { mappingsWithPropsApiLogic } from '../../api/mappings/mappings_logic';

export interface IndexErrorProps {
indexName: string;
}

interface SemanticTextProperty extends MappingPropertyBase {
inference_id: string;
type: 'semantic_text';
}

const parseMapping = (mappings: MappingTypeMapping) => {
const fields = mappings.properties;
if (!fields) {
return [];
}
return getSemanticTextFields(fields, '');
};

const getSemanticTextFields = (
fields: Record<string, MappingProperty>,
path: string
): Array<{ path: string; source: SemanticTextProperty }> => {
return Object.entries(fields).flatMap(([key, value]) => {
const currentPath: string = path ? `${path}.${key}` : key;
const currentField: Array<{ path: string; source: SemanticTextProperty }> =
// @ts-expect-error because semantic_text type isn't incorporated in API type yet
value.type === 'semantic_text' ? [{ path: currentPath, source: value }] : [];
if (hasProperties(value)) {
const childSemanticTextFields: Array<{ path: string; source: SemanticTextProperty }> =
value.properties ? getSemanticTextFields(value.properties, currentPath) : [];
return [...currentField, ...childSemanticTextFields];
}
return currentField;
});
};

function hasProperties(field: MappingProperty): field is MappingPropertyBase {
return !!(field as MappingPropertyBase).properties;
}

function isLocalModel(model: InferenceServiceSettings): model is LocalInferenceServiceSettings {
return Boolean((model as LocalInferenceServiceSettings).service_settings.model_id);
}

export const IndexError: React.FC<IndexErrorProps> = ({ indexName }) => {
const { makeRequest: makeMappingRequest } = useActions(mappingsWithPropsApiLogic(indexName));
const { data } = useValues(mappingsWithPropsApiLogic(indexName));
const { ml } = useValues(KibanaLogic);
const [errors, setErrors] = useState<
Array<{ error: string; field: { path: string; source: SemanticTextProperty } }>
>([]);

const [showErrors, setShowErrors] = useState(false);

useEffect(() => {
makeMappingRequest({ indexName });
}, [indexName]);

useEffect(() => {
const mappings = data?.mappings;
if (!mappings || !ml) {
return;
}

const semanticTextFields = parseMapping(mappings);
const fetchErrors = async () => {
const trainedModelStats = await ml?.mlApi?.trainedModels.getTrainedModelStats();
const endpoints = await ml?.mlApi?.inferenceModels.getAllInferenceEndpoints();
if (!trainedModelStats || !endpoints) {
return [];
}

const semanticTextFieldsWithErrors = semanticTextFields
.map((field) => {
const model = endpoints.endpoints.find(
(endpoint) => endpoint.model_id === field.source.inference_id
);
if (!model) {
return {
error: i18n.translate(
'xpack.enterpriseSearch.indexOverview.indexErrors.missingModelError',
{
defaultMessage: 'Model not found for inference endpoint {inferenceId}',
values: {
inferenceId: field.source.inference_id as string,
},
}
),
field,
};
}
if (isLocalModel(model)) {
const modelId = model.service_settings.model_id;
const modelStats = trainedModelStats?.trained_model_stats.find(
(value) => value.model_id === modelId
);
if (!modelStats || modelStats.deployment_stats?.state !== 'started') {
return {
error: i18n.translate(
'xpack.enterpriseSearch.indexOverview.indexErrors.missingModelError',
{
defaultMessage:
'Model {modelId} for inference endpoint {inferenceId} in field {fieldName} has not been started',
values: {
fieldName: field.path,
inferenceId: field.source.inference_id as string,
modelId,
},
}
),
field,
};
}
}
return { error: '', field };
})
.filter((value) => !!value.error);
setErrors(semanticTextFieldsWithErrors);
};

if (semanticTextFields.length) {
fetchErrors();
}
}, [data]);
return errors.length > 0 ? (
<EuiCallOut
data-test-subj="indexErrorCallout"
color="danger"
iconType="error"
title={i18n.translate('xpack.enterpriseSearch.indexOverview.indexErrors.title', {
defaultMessage: 'Index has errors',
})}
>
{showErrors && (
<>
<p>
{i18n.translate('xpack.enterpriseSearch.indexOverview.indexErrors.body', {
defaultMessage: 'Found errors in the following fields:',
})}
{errors.map(({ field, error }) => (
<li key={field.path}>
<strong>{field.path}</strong>: {error}
</li>
))}
</p>
<EuiButton
data-test-subj="enterpriseSearchIndexErrorHideFullErrorButton"
color="danger"
onClick={() => setShowErrors(false)}
>
{i18n.translate('xpack.enterpriseSearch.indexOverview.indexErrors.hideErrorsLabel', {
defaultMessage: 'Hide full error',
})}
</EuiButton>
</>
)}
{!showErrors && (
<EuiButton
data-test-subj="enterpriseSearchIndexErrorShowFullErrorButton"
color="danger"
onClick={() => setShowErrors(true)}
>
{i18n.translate('xpack.enterpriseSearch.indexOverview.indexErrors.showErrorsLabel', {
defaultMessage: 'Show full error',
})}
</EuiButton>
)}
</EuiCallOut>
) : null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { CrawlerConfiguration } from './crawler/crawler_configuration/crawler_co
import { SearchIndexDomainManagement } from './crawler/domain_management/domain_management';
import { NoConnectorRecord } from './crawler/no_connector_record';
import { SearchIndexDocuments } from './documents';
import { IndexError } from './index_error';
import { SearchIndexIndexMappings } from './index_mappings';
import { IndexNameLogic } from './index_name_logic';
import { IndexViewLogic } from './index_view_logic';
Expand Down Expand Up @@ -239,6 +240,7 @@ export const SearchIndex: React.FC = () => {
rightSideItems: getHeaderActions(index),
}}
>
<IndexError indexName={indexName} />
<Content
index={index}
errorConnectingMessage={errorConnectingMessage}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,18 @@ export function inferenceModelsApiProvider(httpService: HttpService) {
});
return result;
},
/**
* Gets all inference endpoints
*/
async getAllInferenceEndpoints() {
const result = await httpService.http<{
endpoints: estypes.InferenceModelConfigContainer[];
}>({
path: `${ML_INTERNAL_BASE_PATH}/_inference/all`,
method: 'GET',
version: '1',
});
return result;
},
};
}
37 changes: 37 additions & 0 deletions x-pack/plugins/ml/server/routes/inference_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import { schema } from '@kbn/config-schema';
import type { InferenceModelConfig, InferenceTaskType } from '@elastic/elasticsearch/lib/api/types';
import type { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils';
import type { RouteInitialization } from '../types';
import { createInferenceSchema } from './schemas/inference_schema';
import { modelsProvider } from '../models/model_management';
Expand Down Expand Up @@ -63,4 +64,40 @@ export function inferenceModelRoutes(
}
)
);
/**
* @apiGroup TrainedModels
*
* @api {put} /internal/ml/_inference/:taskType/:inferenceId Create Inference Endpoint
* @apiName CreateInferenceEndpoint
* @apiDescription Create Inference Endpoint
*/
router.versioned
.get({
path: `${ML_INTERNAL_BASE_PATH}/_inference/all`,
access: 'internal',
options: {
tags: ['access:ml:canGetTrainedModels'],
},
})
.addVersion(
{
version: '1',
validate: {},
},
routeGuard.fullLicenseAPIGuard(async ({ client, response }) => {
try {
const body = await client.asCurrentUser.transport.request<{
models: InferenceAPIConfigResponse[];
}>({
method: 'GET',
path: `/_inference/_all`,
});
return response.ok({
body,
});
} catch (e) {
return response.customError(wrapError(e));
}
})
);
}

0 comments on commit 1457428

Please sign in to comment.