diff --git a/common/constants.ts b/common/constants.ts index 2d0bf50d..373d5286 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -90,6 +90,7 @@ export enum WORKFLOW_TYPE { SEMANTIC_SEARCH = 'Semantic search', MULTIMODAL_SEARCH = 'Multimodal search', HYBRID_SEARCH = 'Hybrid search', + SENTIMENT_ANALYSIS = 'Sentiment analysis', CUSTOM = 'Custom', UNKNOWN = 'Unknown', } @@ -181,9 +182,14 @@ export const SHARED_OPTIONAL_FIELDS = ['max_chunk_limit', 'description', 'tag']; /** * QUERY PRESETS */ +export const DEFAULT_TEXT_FIELD = 'my_text'; +export const DEFAULT_VECTOR_FIELD = 'my_embedding'; +export const DEFAULT_IMAGE_FIELD = 'my_image'; +export const DEFAULT_LABEL_FIELD = 'label'; export const VECTOR_FIELD_PATTERN = `{{vector_field}}`; export const TEXT_FIELD_PATTERN = `{{text_field}}`; export const IMAGE_FIELD_PATTERN = `{{image_field}}`; +export const LABEL_FIELD_PATTERN = `{{label_field}}`; export const QUERY_TEXT_PATTERN = `{{query_text}}`; export const QUERY_IMAGE_PATTERN = `{{query_image}}`; export const MODEL_ID_PATTERN = `{{model_id}}`; @@ -198,7 +204,7 @@ export const FETCH_ALL_QUERY = { }, size: 1000, }; -export const TERM_QUERY = { +export const TERM_QUERY_TEXT = { query: { term: { [TEXT_FIELD_PATTERN]: { @@ -207,6 +213,15 @@ export const TERM_QUERY = { }, }, }; +export const TERM_QUERY_LABEL = { + query: { + term: { + [LABEL_FIELD_PATTERN]: { + value: QUERY_TEXT_PATTERN, + }, + }, + }, +}; export const KNN_QUERY = { _source: { excludes: [VECTOR_FIELD_PATTERN], @@ -353,7 +368,7 @@ export const QUERY_PRESETS = [ }, { name: 'Term', - query: customStringify(TERM_QUERY), + query: customStringify(TERM_QUERY_TEXT), }, { name: 'Basic k-NN', diff --git a/common/interfaces.ts b/common/interfaces.ts index 2d46da26..4fe68f4b 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -483,10 +483,11 @@ export type QueryPreset = { }; export type QuickConfigureFields = { - embeddingModelId?: string; + modelId?: string; vectorField?: string; textField?: string; imageField?: string; + labelField?: string; embeddingLength?: number; }; diff --git a/public/pages/workflows/new_workflow/quick_configure_inputs.tsx b/public/pages/workflows/new_workflow/quick_configure_inputs.tsx index 1ce8fd68..956683be 100644 --- a/public/pages/workflows/new_workflow/quick_configure_inputs.tsx +++ b/public/pages/workflows/new_workflow/quick_configure_inputs.tsx @@ -17,6 +17,10 @@ import { } from '@elastic/eui'; import { COHERE_DIMENSIONS, + DEFAULT_IMAGE_FIELD, + DEFAULT_LABEL_FIELD, + DEFAULT_TEXT_FIELD, + DEFAULT_VECTOR_FIELD, MODEL_STATE, Model, OPENAI_DIMENSIONS, @@ -30,10 +34,6 @@ interface QuickConfigureInputsProps { setFields(fields: QuickConfigureFields): void; } -const DEFAULT_TEXT_FIELD = 'my_text'; -const DEFAULT_VECTOR_FIELD = 'my_embedding'; -const DEFAULT_IMAGE_FIELD = 'my_image'; - // Dynamic component to allow optional input configuration fields for different use cases. // Hooks back to the parent component with such field values export function QuickConfigureInputs(props: QuickConfigureInputsProps) { @@ -76,10 +76,17 @@ export function QuickConfigureInputs(props: QuickConfigureInputsProps) { imageField: DEFAULT_IMAGE_FIELD, }; } + if (props.workflowType === WORKFLOW_TYPE.SENTIMENT_ANALYSIS) { + defaultFieldValues = { + ...defaultFieldValues, + textField: DEFAULT_TEXT_FIELD, + labelField: DEFAULT_LABEL_FIELD, + }; + } if (deployedModels.length > 0) { defaultFieldValues = { ...defaultFieldValues, - embeddingModelId: deployedModels[0].id, + modelId: deployedModels[0].id, }; } setFieldValues(defaultFieldValues); @@ -93,7 +100,7 @@ export function QuickConfigureInputs(props: QuickConfigureInputsProps) { // Try to pre-fill the dimensions based on the chosen model useEffect(() => { const selectedModel = deployedModels.find( - (model) => model.id === fieldValues.embeddingModelId + (model) => model.id === fieldValues.modelId ); if (selectedModel?.connectorId !== undefined) { const connector = connectors[selectedModel.connectorId]; @@ -127,13 +134,14 @@ export function QuickConfigureInputs(props: QuickConfigureInputsProps) { } } } - }, [fieldValues.embeddingModelId, deployedModels, connectors]); + }, [fieldValues.modelId, deployedModels, connectors]); return ( <> {(props.workflowType === WORKFLOW_TYPE.SEMANTIC_SEARCH || props.workflowType === WORKFLOW_TYPE.MULTIMODAL_SEARCH || - props.workflowType === WORKFLOW_TYPE.HYBRID_SEARCH) && ( + props.workflowType === WORKFLOW_TYPE.HYBRID_SEARCH || + props.workflowType === WORKFLOW_TYPE.SENTIMENT_ANALYSIS) && ( <> ) )} - valueOfSelected={fieldValues?.embeddingModelId || ''} + valueOfSelected={fieldValues?.modelId || ''} onChange={(option: string) => { setFieldValues({ ...fieldValues, - embeddingModelId: option, + modelId: option, }); }} isInvalid={false} @@ -185,7 +201,11 @@ export function QuickConfigureInputs(props: QuickConfigureInputsProps) { )} - - { - setFieldValues({ - ...fieldValues, - vectorField: e.target.value, - }); - }} - /> - - - - { - setFieldValues({ - ...fieldValues, - embeddingLength: Number(e.target.value), - }); - }} - /> - + {(props.workflowType === WORKFLOW_TYPE.SEMANTIC_SEARCH || + props.workflowType === WORKFLOW_TYPE.MULTIMODAL_SEARCH || + props.workflowType === WORKFLOW_TYPE.HYBRID_SEARCH) && ( + <> + + { + setFieldValues({ + ...fieldValues, + vectorField: e.target.value, + }); + }} + /> + + + + { + setFieldValues({ + ...fieldValues, + embeddingLength: Number(e.target.value), + }); + }} + /> + + + )} + {props.workflowType === WORKFLOW_TYPE.SENTIMENT_ANALYSIS && ( + + { + setFieldValues({ + ...fieldValues, + labelField: e.target.value, + }); + }} + /> + + )} )} diff --git a/public/pages/workflows/new_workflow/quick_configure_modal.tsx b/public/pages/workflows/new_workflow/quick_configure_modal.tsx index e519eb46..257c02cf 100644 --- a/public/pages/workflows/new_workflow/quick_configure_modal.tsx +++ b/public/pages/workflows/new_workflow/quick_configure_modal.tsx @@ -17,8 +17,10 @@ import { EuiCompressedFormRow, } from '@elastic/eui'; import { - EMPTY_MAP_ENTRY, + DEFAULT_LABEL_FIELD, + DEFAULT_TEXT_FIELD, IMAGE_FIELD_PATTERN, + LABEL_FIELD_PATTERN, MODEL_ID_PATTERN, MapArrayFormValue, MapFormValue, @@ -32,6 +34,7 @@ import { Workflow, WorkflowConfig, customStringify, + isVectorSearchUseCase, } from '../../../../common'; import { APP_PATH } from '../../../utils'; import { processWorkflowName } from './utils'; @@ -89,10 +92,8 @@ export function QuickConfigureModal(props: QuickConfigureModalProps) { // fetching model interface if available. used to prefill some // of the input/output maps useEffect(() => { - setModelInterface( - models[quickConfigureFields.embeddingModelId || '']?.interface - ); - }, [models, quickConfigureFields.embeddingModelId]); + setModelInterface(models[quickConfigureFields.modelId || '']?.interface); + }, [models, quickConfigureFields.modelId]); return ( props.onClose()} style={{ width: '30vw' }}> @@ -182,16 +183,16 @@ function injectQuickConfigureFields( ): Workflow { if (workflow.ui_metadata?.type) { switch (workflow.ui_metadata?.type) { - // Semantic search / hybrid search: set defaults in the ingest processor, the index mappings, - // and the preset query case WORKFLOW_TYPE.SEMANTIC_SEARCH: case WORKFLOW_TYPE.HYBRID_SEARCH: - case WORKFLOW_TYPE.MULTIMODAL_SEARCH: { + case WORKFLOW_TYPE.MULTIMODAL_SEARCH: + case WORKFLOW_TYPE.SENTIMENT_ANALYSIS: { if (!isEmpty(quickConfigureFields) && workflow.ui_metadata?.config) { workflow.ui_metadata.config = updateIngestProcessorConfig( workflow.ui_metadata.config, quickConfigureFields, - modelInterface + modelInterface, + isVectorSearchUseCase(workflow) ); workflow.ui_metadata.config = updateIndexConfig( workflow.ui_metadata.config, @@ -204,7 +205,8 @@ function injectQuickConfigureFields( workflow.ui_metadata.config = updateSearchRequestProcessorConfig( workflow.ui_metadata.config, quickConfigureFields, - modelInterface + modelInterface, + isVectorSearchUseCase(workflow) ); } break; @@ -222,11 +224,12 @@ function injectQuickConfigureFields( function updateIngestProcessorConfig( config: WorkflowConfig, fields: QuickConfigureFields, - modelInterface: ModelInterface | undefined + modelInterface: ModelInterface | undefined, + isVectorSearchUseCase: boolean ): WorkflowConfig { config.ingest.enrich.processors[0].fields.forEach((field) => { - if (field.id === 'model' && fields.embeddingModelId) { - field.value = { id: fields.embeddingModelId }; + if (field.id === 'model' && fields.modelId) { + field.value = { id: fields.modelId }; } if (field.id === 'input_map') { const inputMap = generateMapFromModelInputs(modelInterface); @@ -260,14 +263,17 @@ function updateIngestProcessorConfig( } if (field.id === 'output_map') { const outputMap = generateMapFromModelOutputs(modelInterface); - if (fields.vectorField) { + const defaultField = isVectorSearchUseCase + ? fields.vectorField + : fields.labelField; + if (defaultField) { if (outputMap.length > 0) { outputMap[0] = { ...outputMap[0], - key: fields.vectorField, + key: defaultField, }; } else { - outputMap.push({ key: fields.vectorField, value: '' }); + outputMap.push({ key: defaultField, value: '' }); } } field.value = [outputMap] as MapArrayFormValue; @@ -282,36 +288,48 @@ function updateIngestProcessorConfig( function updateSearchRequestProcessorConfig( config: WorkflowConfig, fields: QuickConfigureFields, - modelInterface: ModelInterface | undefined + modelInterface: ModelInterface | undefined, + isVectorSearchUseCase: boolean ): WorkflowConfig { config.search.enrichRequest.processors[0].fields.forEach((field) => { - if (field.id === 'model' && fields.embeddingModelId) { - field.value = { id: fields.embeddingModelId }; + if (field.id === 'model' && fields.modelId) { + field.value = { id: fields.modelId }; } if (field.id === 'input_map') { const inputMap = generateMapFromModelInputs(modelInterface); - // TODO: pre-populate more if the query becomes standard - field.value = - inputMap.length > 0 - ? [inputMap] - : ([[EMPTY_MAP_ENTRY]] as MapArrayFormValue); + const defaultValue = `query.term.${ + isVectorSearchUseCase ? DEFAULT_TEXT_FIELD : DEFAULT_LABEL_FIELD + }.value`; + if (inputMap.length > 0) { + inputMap[0] = { + ...inputMap[0], + value: defaultValue, + }; + } else { + inputMap.push({ + key: '', + value: defaultValue, + }); + } + field.value = [inputMap] as MapArrayFormValue; } if (field.id === 'output_map') { - // prepopulate 'vector' constant as the model output transformed field, - // so it is consistent and used in the downstream query_template, if configured. const outputMap = generateMapFromModelOutputs(modelInterface); + const defaultKey = isVectorSearchUseCase + ? VECTOR + : `query.term.${DEFAULT_LABEL_FIELD}.value`; if (outputMap.length > 0) { outputMap[0] = { ...outputMap[0], - key: VECTOR, + key: defaultKey, }; } else { outputMap.push({ - key: VECTOR, + key: defaultKey, value: '', }); } - field.value = [outputMap]; + field.value = [outputMap] as MapArrayFormValue; } }); config.search.enrichRequest.processors[0].optionalFields = config.search.enrichRequest.processors[0].optionalFields?.map( @@ -378,6 +396,20 @@ function updateIndexConfig( }, }); } + if (fields.labelField) { + const existingMappings = JSON.parse( + config.ingest.index.mappings.value as string + ); + config.ingest.index.mappings.value = customStringify({ + ...existingMappings, + properties: { + ...(existingMappings.properties || {}), + [fields.labelField]: { + type: 'text', + }, + }, + }); + } return config; } @@ -387,10 +419,10 @@ function injectPlaceholderValues( fields: QuickConfigureFields ): string { let finalRequestString = requestString; - if (fields.embeddingModelId) { + if (fields.modelId) { finalRequestString = finalRequestString.replace( new RegExp(MODEL_ID_PATTERN, 'g'), - fields.embeddingModelId + fields.modelId ); } if (fields.textField) { @@ -411,7 +443,12 @@ function injectPlaceholderValues( fields.imageField ); } - + if (fields.labelField) { + finalRequestString = finalRequestString.replace( + new RegExp(LABEL_FIELD_PATTERN, 'g'), + fields.labelField + ); + } return finalRequestString; } diff --git a/public/pages/workflows/new_workflow/utils.ts b/public/pages/workflows/new_workflow/utils.ts index 59a5bb06..841290b8 100644 --- a/public/pages/workflows/new_workflow/utils.ts +++ b/public/pages/workflows/new_workflow/utils.ts @@ -17,7 +17,8 @@ import { WORKFLOW_TYPE, FETCH_ALL_QUERY, customStringify, - TERM_QUERY, + TERM_QUERY_TEXT, + TERM_QUERY_LABEL, MULTIMODAL_SEARCH_QUERY_BOOL, IProcessorConfig, VECTOR_TEMPLATE_PLACEHOLDER, @@ -45,6 +46,10 @@ export function enrichPresetWorkflowWithUiMetadata( uiMetadata = fetchHybridSearchMetadata(); break; } + case WORKFLOW_TYPE.SENTIMENT_ANALYSIS: { + uiMetadata = fetchSentimentAnalysisMetadata(); + break; + } default: { uiMetadata = fetchEmptyMetadata(); break; @@ -133,7 +138,7 @@ export function fetchSemanticSearchMetadata(): UIState { baseState.config.ingest.index.settings.value = customStringify({ [`index.knn`]: true, }); - baseState.config.search.request.value = customStringify(TERM_QUERY); + baseState.config.search.request.value = customStringify(TERM_QUERY_TEXT); baseState.config.search.enrichRequest.processors = [ injectQueryTemplateInProcessor( new MLSearchRequestProcessor().toObj(), @@ -171,7 +176,7 @@ export function fetchHybridSearchMetadata(): UIState { baseState.config.ingest.index.settings.value = customStringify({ [`index.knn`]: true, }); - baseState.config.search.request.value = customStringify(TERM_QUERY); + baseState.config.search.request.value = customStringify(TERM_QUERY_TEXT); baseState.config.search.enrichResponse.processors = [ injectDefaultWeightsInNormalizationProcessor( new NormalizationProcessor().toObj() @@ -186,6 +191,21 @@ export function fetchHybridSearchMetadata(): UIState { return baseState; } +export function fetchSentimentAnalysisMetadata(): UIState { + let baseState = fetchEmptyMetadata(); + baseState.type = WORKFLOW_TYPE.SENTIMENT_ANALYSIS; + baseState.config.ingest.enrich.processors = [new MLIngestProcessor().toObj()]; + baseState.config.ingest.index.name.value = generateId('knn_index', 6); + baseState.config.ingest.index.settings.value = customStringify({ + [`index.knn`]: true, + }); + baseState.config.search.request.value = customStringify(TERM_QUERY_LABEL); + baseState.config.search.enrichRequest.processors = [ + new MLSearchRequestProcessor().toObj(), + ]; + return baseState; +} + // Utility fn to process workflow names from their presentable/readable titles // on the UI, to a valid name format. // This leads to less friction if users decide to save the name later on. diff --git a/public/pages/workflows/workflow_list/workflow_list.tsx b/public/pages/workflows/workflow_list/workflow_list.tsx index e43999d9..8fdad6e0 100644 --- a/public/pages/workflows/workflow_list/workflow_list.tsx +++ b/public/pages/workflows/workflow_list/workflow_list.tsx @@ -44,33 +44,13 @@ const sorting = { }, }; -const filterOptions = [ +const filterOptions = Object.values(WORKFLOW_TYPE).map((type) => { // @ts-ignore - { - name: WORKFLOW_TYPE.SEMANTIC_SEARCH, + return { + name: type, checked: 'on', - } as EuiFilterSelectItem, - // @ts-ignore - { - name: WORKFLOW_TYPE.MULTIMODAL_SEARCH, - checked: 'on', - } as EuiFilterSelectItem, - // @ts-ignore - { - name: WORKFLOW_TYPE.HYBRID_SEARCH, - checked: 'on', - } as EuiFilterSelectItem, - // @ts-ignore - { - name: WORKFLOW_TYPE.CUSTOM, - checked: 'on', - } as EuiFilterSelectItem, - // @ts-ignore - { - name: WORKFLOW_TYPE.UNKNOWN, - checked: 'on', - } as EuiFilterSelectItem, -]; + } as EuiFilterSelectItem; +}); /** * The searchable list of created workflows. diff --git a/server/resources/templates/sentiment_analysis.json b/server/resources/templates/sentiment_analysis.json new file mode 100644 index 00000000..ed2d4c92 --- /dev/null +++ b/server/resources/templates/sentiment_analysis.json @@ -0,0 +1,14 @@ +{ + "name": "Sentiment Analysis", + "description": "A basic workflow containing the ingest pipeline, search pipeline, and index configurations for performing sentiment analysis", + "version": { + "template": "1.0.0", + "compatibility": [ + "2.17.0", + "3.0.0" + ] + }, + "ui_metadata": { + "type": "Sentiment analysis" + } +} \ No newline at end of file