mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Search] Add index errors in Search Index page (#188682)
## 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.
This commit is contained in:
parent
754de3be4f
commit
1457428d7f
4 changed files with 249 additions and 0 deletions
|
@ -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;
|
||||
};
|
|
@ -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';
|
||||
|
@ -239,6 +240,7 @@ export const SearchIndex: React.FC = () => {
|
|||
rightSideItems: getHeaderActions(index),
|
||||
}}
|
||||
>
|
||||
<IndexError indexName={indexName} />
|
||||
<Content
|
||||
index={index}
|
||||
errorConnectingMessage={errorConnectingMessage}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -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));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue