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