Browse Source

[2.5] support text match (#709)

* support enable match and enable analyzer for varchar field

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* support show analyzer and enable match on the schema page

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* support editing anlayzer params

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>

* if enable match is true, we should enable analyzer as well

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* support copy anayzer params

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* optmize data generator of varchar field

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* support text match for adv filter

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

---------

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
Signed-off-by: shanghaikid <jiangruiyi@gmail.com>
ryjiang 7 months ago
parent
commit
09455c6163

+ 1 - 1
client/src/components/advancedSearch/Filter.tsx

@@ -96,7 +96,7 @@ const Filter = forwardRef((props: FilterProps, ref) => {
       let newExpr = `${n} ${op} ${value}`;
       let newExpr = `${n} ${op} ${value}`;
 
 
       // rewrite expression if the op is JSON_CONTAINS/ARRAY_CONTAINS
       // rewrite expression if the op is JSON_CONTAINS/ARRAY_CONTAINS
-      if (op === 'JSON_CONTAINS' || op === 'ARRAY_CONTAINS') {
+      if (op === 'JSON_CONTAINS' || op === 'ARRAY_CONTAINS' || op ==='TEXT_MATCH') {
         newExpr = `${op}(${n}, ${value})`;
         newExpr = `${op}(${n}, ${value})`;
       }
       }
       // rewrite expression if the op is ARRAY_CONTAINS_ALL/ARRAY_CONTAINS_ANY
       // rewrite expression if the op is ARRAY_CONTAINS_ALL/ARRAY_CONTAINS_ANY

+ 4 - 0
client/src/consts/Util.ts

@@ -42,6 +42,10 @@ export const LOGICAL_OPERATORS = [
     value: 'like',
     value: 'like',
     label: 'like',
     label: 'like',
   },
   },
+  {
+    value: 'TEXT_MATCH',
+    label: 'TEXT_MATCH',
+  },
   {
   {
     value: 'JSON_CONTAINS',
     value: 'JSON_CONTAINS',
     label: 'JSON_CONTAINS',
     label: 'JSON_CONTAINS',

+ 11 - 0
client/src/consts/default.ts

@@ -3,3 +3,14 @@ export const DEFAULT_ATTU_VARCHAR_MAX_LENGTH = 32;
 export const DEFAULT_ATTU_ELEMENT_TYPE = 4; // int32
 export const DEFAULT_ATTU_ELEMENT_TYPE = 4; // int32
 export const DEFAULT_ATTU_DIM = 128;
 export const DEFAULT_ATTU_DIM = 128;
 export const MIN_INT64 = `-9223372036854775807`; // safe int64 min value
 export const MIN_INT64 = `-9223372036854775807`; // safe int64 min value
+export const DEFAULT_ANALYZER_PARAMS = {
+  standard: {
+    type: 'standard',
+  },
+  english: {
+    type: 'english',
+  },
+  chinese: {
+    type: 'chinese',
+  },
+};

+ 17 - 45
client/src/context/Root.tsx

@@ -1,8 +1,5 @@
 import { useState, useCallback, useEffect, useContext } from 'react';
 import { useState, useCallback, useEffect, useContext } from 'react';
 import React from 'react';
 import React from 'react';
-import { Theme } from '@mui/material';
-import { makeStyles } from '@mui/styles';
-import { SwipeableDrawer } from '@mui/material';
 import { authContext } from '@/context';
 import { authContext } from '@/context';
 import {
 import {
   RootContextType,
   RootContextType,
@@ -33,31 +30,22 @@ export const rootContext = React.createContext<RootContextType>({
     position = { vertical: 'top', horizontal: 'right' }
     position = { vertical: 'top', horizontal: 'right' }
   ) => {},
   ) => {},
   dialog: DefaultDialogConfigs,
   dialog: DefaultDialogConfigs,
+  dialog2: DefaultDialogConfigs,
   setDialog: params => {},
   setDialog: params => {},
+  setDialog2: params => {},
   handleCloseDialog: () => {},
   handleCloseDialog: () => {},
-  setDrawer: (params: any) => {},
+  handleCloseDialog2: () => {},
   versionInfo: { attu: '', sdk: '' },
   versionInfo: { attu: '', sdk: '' },
 });
 });
 
 
 const { Provider } = rootContext;
 const { Provider } = rootContext;
 
 
-const useStyles = makeStyles((theme: Theme) => ({
-  paper: {
-    minWidth: '300px',
-  },
-  paperAnchorRight: {
-    width: '40vw',
-  },
-}));
-
 // Dialog has two type : normal | custom;
 // Dialog has two type : normal | custom;
 // notice type mean it's a notice dialog you need to set props like title, content, actions
 // notice type mean it's a notice dialog you need to set props like title, content, actions
 // custom type could have own state, you could set a complete component in dialog.
 // custom type could have own state, you could set a complete component in dialog.
 export const RootProvider = (props: { children: React.ReactNode }) => {
 export const RootProvider = (props: { children: React.ReactNode }) => {
   const { isAuth } = useContext(authContext);
   const { isAuth } = useContext(authContext);
 
 
-  const classes = useStyles();
-
   const [snackBar, setSnackBar] = useState<SnackBarType>({
   const [snackBar, setSnackBar] = useState<SnackBarType>({
     open: false,
     open: false,
     type: 'success',
     type: 'success',
@@ -67,11 +55,8 @@ export const RootProvider = (props: { children: React.ReactNode }) => {
     autoHideDuration: 1000,
     autoHideDuration: 1000,
   });
   });
   const [dialog, setDialog] = useState<DialogType>(DefaultDialogConfigs);
   const [dialog, setDialog] = useState<DialogType>(DefaultDialogConfigs);
-  const [drawer, setDrawer]: any = useState({
-    anchor: 'right',
-    open: false,
-    child: <></>,
-  });
+  const [dialog2, setDialog2] = useState<DialogType>(DefaultDialogConfigs);
+
   const [versionInfo, setVersionInfo] = useState({ attu: '', sdk: '' });
   const [versionInfo, setVersionInfo] = useState({ attu: '', sdk: '' });
 
 
   const handleSnackBarClose = () => {
   const handleSnackBarClose = () => {
@@ -101,19 +86,12 @@ export const RootProvider = (props: { children: React.ReactNode }) => {
       open: false,
       open: false,
     });
     });
   };
   };
-
-  const toggleDrawer =
-    (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
-      if (
-        event.type === 'keydown' &&
-        ((event as React.KeyboardEvent).key === 'Tab' ||
-          (event as React.KeyboardEvent).key === 'Shift')
-      ) {
-        return;
-      }
-
-      setDrawer({ ...drawer, open: open });
-    };
+  const handleCloseDialog2 = () => {
+    setDialog2({
+      ...dialog2,
+      open: false,
+    });
+  };
 
 
   useEffect(() => {
   useEffect(() => {
     if (isAuth) {
     if (isAuth) {
@@ -144,25 +122,19 @@ export const RootProvider = (props: { children: React.ReactNode }) => {
       value={{
       value={{
         openSnackBar,
         openSnackBar,
         dialog,
         dialog,
+        dialog2,
         setDialog,
         setDialog,
+        setDialog2,
         handleCloseDialog,
         handleCloseDialog,
-        setDrawer,
+        handleCloseDialog2,
         versionInfo,
         versionInfo,
       }}
       }}
     >
     >
-      <CustomSnackBar {...snackBar} onClose={handleSnackBarClose} />
       {props.children}
       {props.children}
-      <CustomDialog {...dialog} onClose={handleCloseDialog} />
 
 
-      <SwipeableDrawer
-        anchor={drawer.anchor}
-        open={drawer.open}
-        onClose={toggleDrawer(false)}
-        onOpen={toggleDrawer(true)}
-        classes={{ paperAnchorRight: classes.paperAnchorRight }}
-      >
-        {drawer.child}
-      </SwipeableDrawer>
+      <CustomSnackBar {...snackBar} onClose={handleSnackBarClose} />
+      <CustomDialog {...dialog} onClose={handleCloseDialog} />
+      <CustomDialog {...dialog2} onClose={handleCloseDialog2} />
     </Provider>
     </Provider>
   );
   );
 };
 };

+ 3 - 1
client/src/context/Types.ts

@@ -15,9 +15,11 @@ import { AuthObject } from '@server/types';
 export type RootContextType = {
 export type RootContextType = {
   openSnackBar: OpenSnackBarType;
   openSnackBar: OpenSnackBarType;
   dialog: DialogType;
   dialog: DialogType;
+  dialog2: DialogType;
   setDialog: (params: DialogType) => void;
   setDialog: (params: DialogType) => void;
+  setDialog2: (params: DialogType) => void;
   handleCloseDialog: () => void;
   handleCloseDialog: () => void;
-  setDrawer: (params: any) => void;
+  handleCloseDialog2: () => void;
   versionInfo: { attu: string; sdk: string };
   versionInfo: { attu: string; sdk: string };
 };
 };
 
 

+ 3 - 0
client/src/i18n/cn/collection.ts

@@ -61,6 +61,9 @@ const collectionTrans = {
   partitionKeyTooltip:
   partitionKeyTooltip:
     'Milvus将根据分区键字段中的值在分区中存储entities。只支持一个Int64或VarChar字段。',
     'Milvus将根据分区键字段中的值在分区中存储entities。只支持一个Int64或VarChar字段。',
   enableDynamicSchema: '启用动态Schema',
   enableDynamicSchema: '启用动态Schema',
+  analyzer: '分词器',
+  enableMatch: '启用匹配',
+  textMatchTooltip: 'Milvus中的文本匹配能够基于特定术语实现精确的文档检索。',
 
 
   // load dialog
   // load dialog
   loadTitle: '加载Collection',
   loadTitle: '加载Collection',

+ 4 - 0
client/src/i18n/cn/dialog.ts

@@ -1,3 +1,5 @@
+import { Edit } from '@mui/icons-material';
+
 const dialogTrans = {
 const dialogTrans = {
   value: '值',
   value: '值',
   deleteTipAction: '输入',
   deleteTipAction: '输入',
@@ -12,6 +14,7 @@ const dialogTrans = {
   loadTitle: `加载 {{type}}`,
   loadTitle: `加载 {{type}}`,
   editEntityTitle: `编辑 Entity`,
   editEntityTitle: `编辑 Entity`,
   modifyReplicaTitle: `修改 {{type}} 的副本`,
   modifyReplicaTitle: `修改 {{type}} 的副本`,
+  editAnalyzerTitle: `编辑 {{type}} 分析器`,
 
 
   loadContent: `您正在尝试加载带有数据的 {{type}}。只有已加载的 {{type}} 可以被搜索。`,
   loadContent: `您正在尝试加载带有数据的 {{type}}。只有已加载的 {{type}} 可以被搜索。`,
   releaseContent: `您正在尝试发布带有数据的 {{type}}。请注意,数据将不再可用于搜索。`,
   releaseContent: `您正在尝试发布带有数据的 {{type}}。请注意,数据将不再可用于搜索。`,
@@ -28,6 +31,7 @@ const dialogTrans = {
   emptyDataDialogInfo: `您正在尝试清空数据。此操作无法撤销,请谨慎操作。`,
   emptyDataDialogInfo: `您正在尝试清空数据。此操作无法撤销,请谨慎操作。`,
   resetPropertyInfo: '您确定要重置属性吗?',
   resetPropertyInfo: '您确定要重置属性吗?',
   editEntityInfo: `注意:编辑id字段将创建一个新的实体。`,
   editEntityInfo: `注意:编辑id字段将创建一个新的实体。`,
+  editAnalyzerInfo: `分析器以JSON格式定义,请参考milvus.io 了解<a href='https://milvus.io/docs/analyzer-overview.md' target='_blank'>更多信息</a>。`,
 };
 };
 
 
 export default dialogTrans;
 export default dialogTrans;

+ 3 - 0
client/src/i18n/en/collection.ts

@@ -63,6 +63,9 @@ const collectionTrans = {
   partitionKeyTooltip:
   partitionKeyTooltip:
     ' Milvus will store entities in a partition according to the values in the partition key field. Only one Int64 or VarChar field is supported.',
     ' Milvus will store entities in a partition according to the values in the partition key field. Only one Int64 or VarChar field is supported.',
   enableDynamicSchema: 'Dynamic Schema',
   enableDynamicSchema: 'Dynamic Schema',
+  analyzer: 'Analyzer',
+  enableMatch: 'Enable Match',
+  textMatchTooltip: 'Text match in Milvus enables precise document retrieval based on specific terms.',
 
 
   // load dialog
   // load dialog
   loadTitle: 'Load Collection',
   loadTitle: 'Load Collection',

+ 3 - 1
client/src/i18n/en/dialog.ts

@@ -11,6 +11,7 @@ const dialogTrans = {
   flush: `Flush data for {{type}}`,
   flush: `Flush data for {{type}}`,
   loadTitle: `Load {{type}}`,
   loadTitle: `Load {{type}}`,
   editEntityTitle: `Edit Entity(JSON)`,
   editEntityTitle: `Edit Entity(JSON)`,
+  editAnalyzerTitle: `Edit analyzer for {{type}}`,
   modifyReplicaTitle: `Modify replica for {{type}}`,
   modifyReplicaTitle: `Modify replica for {{type}}`,
 
 
   loadContent: `You are trying to load a {{type}} with data. Only loaded {{type}} can be searched.`,
   loadContent: `You are trying to load a {{type}} with data. Only loaded {{type}} can be searched.`,
@@ -27,7 +28,8 @@ const dialogTrans = {
   flushDialogInfo: `Flush is a process that seals and indexes any remaining segments after data is upserted into Milvus. This avoids brute force searches on unsealed segments.  <br /><br />It's best to use flush at the end of an upsert session to prevent data fragmentation. <br /><br /><strong>Note: that this operation may take some time for large datasets.</strong>`,
   flushDialogInfo: `Flush is a process that seals and indexes any remaining segments after data is upserted into Milvus. This avoids brute force searches on unsealed segments.  <br /><br />It's best to use flush at the end of an upsert session to prevent data fragmentation. <br /><br /><strong>Note: that this operation may take some time for large datasets.</strong>`,
   emptyDataDialogInfo: `You are attempting to empty the data. This action cannot be undone, please proceed with caution.`,
   emptyDataDialogInfo: `You are attempting to empty the data. This action cannot be undone, please proceed with caution.`,
   resetPropertyInfo: `Are you sure you want to reset the property?`,
   resetPropertyInfo: `Are you sure you want to reset the property?`,
-  editEntityInfo: `NOTE: Edit id field will create a new entity.`
+  editEntityInfo: `NOTE: Edit id field will create a new entity.`,
+  editAnalyzerInfo: `Analyzer is defined in JSON format, please refer to milvus.io for <a href='https://milvus.io/docs/analyzer-overview.md' target='_blank'>more information</a>.`,
 };
 };
 
 
 export default dialogTrans;
 export default dialogTrans;

+ 15 - 0
client/src/pages/databases/collections/Constants.ts

@@ -108,3 +108,18 @@ export const PRIMARY_FIELDS_OPTIONS: LabelValuePair[] = [
     value: DataTypeEnum.VarChar,
     value: DataTypeEnum.VarChar,
   },
   },
 ];
 ];
+
+export const ANALYZER_OPTIONS: LabelValuePair[] = [
+  {
+    label: 'Standard',
+    value: 'standard',
+  },
+  {
+    label: 'English',
+    value: 'english',
+  },
+  {
+    label: 'Chinese',
+    value: 'chinese',
+  },
+];

+ 169 - 54
client/src/pages/databases/collections/CreateFields.tsx

@@ -4,8 +4,9 @@ import {
   IconButton,
   IconButton,
   Switch,
   Switch,
   FormControlLabel,
   FormControlLabel,
+  Checkbox,
 } from '@mui/material';
 } from '@mui/material';
-import { FC, Fragment, ReactElement, useMemo } from 'react';
+import { FC, Fragment, ReactElement, useMemo, useContext } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import CustomSelector from '@/components/customSelector/CustomSelector';
 import CustomSelector from '@/components/customSelector/CustomSelector';
 import icons from '@/components/icons/Icons';
 import icons from '@/components/icons/Icons';
@@ -16,11 +17,14 @@ import {
   checkEmptyValid,
   checkEmptyValid,
   checkRange,
   checkRange,
   getCheckResult,
   getCheckResult,
+  getAnalyzerParams,
 } from '@/utils';
 } from '@/utils';
+import { rootContext } from '@/context';
 import {
 import {
   ALL_OPTIONS,
   ALL_OPTIONS,
   PRIMARY_FIELDS_OPTIONS,
   PRIMARY_FIELDS_OPTIONS,
   VECTOR_FIELDS_OPTIONS,
   VECTOR_FIELDS_OPTIONS,
+  ANALYZER_OPTIONS,
 } from './Constants';
 } from './Constants';
 import { CreateFieldsProps, CreateFieldType, FieldType } from './Types';
 import { CreateFieldsProps, CreateFieldType, FieldType } from './Types';
 import { DataTypeEnum, VectorTypes } from '@/consts';
 import { DataTypeEnum, VectorTypes } from '@/consts';
@@ -31,6 +35,8 @@ import {
   DEFAULT_ATTU_ELEMENT_TYPE,
   DEFAULT_ATTU_ELEMENT_TYPE,
 } from '@/consts';
 } from '@/consts';
 import { makeStyles } from '@mui/styles';
 import { makeStyles } from '@mui/styles';
+import CustomIconButton from '@/components/customButton/CustomIconButton';
+import EditAnalyzerDialog from '@/pages/dialogs/EditAnalyzerDialog';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   optionalWrapper: {
   optionalWrapper: {
@@ -44,33 +50,40 @@ const useStyles = makeStyles((theme: Theme) => ({
     alignItems: 'center',
     alignItems: 'center',
     gap: '8px',
     gap: '8px',
     flex: '1 0 auto',
     flex: '1 0 auto',
-  },
-  input: {
-    fontSize: '14px',
+    marginBottom: 4,
+    '& .MuiFormLabel-root': {
+      fontSize: 14,
+    },
+    '& .MuiInputBase-root': {
+      fontSize: 14,
+    },
+    '& .MuiSelect-filled': {
+      fontSize: 14,
+    },
+    '& .MuiCheckbox-root': {},
+    '& .MuiFormControlLabel-label': {
+      fontSize: 14,
+    },
   },
   },
   fieldInput: {
   fieldInput: {
-    width: '170px',
+    width: '130px',
   },
   },
   select: {
   select: {
-    width: '180px',
+    width: '150px',
     marginTop: '-20px',
     marginTop: '-20px',
-
-    '&:first-child': {
-      marginLeft: 0,
-    },
   },
   },
   autoIdSelect: {
   autoIdSelect: {
     width: '120px',
     width: '120px',
     marginTop: '-20px',
     marginTop: '-20px',
   },
   },
   numberBox: {
   numberBox: {
-    width: '97px',
+    width: '80px',
   },
   },
   maxLength: {
   maxLength: {
     maxWidth: '80px',
     maxWidth: '80px',
   },
   },
   descInput: {
   descInput: {
-    width: '120px',
+    width: '60px',
   },
   },
   btnTxt: {
   btnTxt: {
     textTransform: 'uppercase',
     textTransform: 'uppercase',
@@ -78,10 +91,9 @@ const useStyles = makeStyles((theme: Theme) => ({
   iconBtn: {
   iconBtn: {
     marginLeft: 0,
     marginLeft: 0,
     padding: 0,
     padding: 0,
-    width: '16px',
-    height: '16px',
     position: 'relative',
     position: 'relative',
     top: '-8px',
     top: '-8px',
+    width: 15,
   },
   },
   helperText: {
   helperText: {
     lineHeight: '20px',
     lineHeight: '20px',
@@ -90,7 +102,6 @@ const useStyles = makeStyles((theme: Theme) => ({
     marginLeft: '11px',
     marginLeft: '11px',
   },
   },
   toggle: {
   toggle: {
-    marginBottom: theme.spacing(2),
     marginLeft: theme.spacing(0.5),
     marginLeft: theme.spacing(0.5),
     marginRight: theme.spacing(0.5),
     marginRight: theme.spacing(0.5),
   },
   },
@@ -98,6 +109,23 @@ const useStyles = makeStyles((theme: Theme) => ({
     fontSize: '14px',
     fontSize: '14px',
     marginLeft: theme.spacing(0.5),
     marginLeft: theme.spacing(0.5),
   },
   },
+  paramsGrp: {
+    border: `1px dashed ${theme.palette.divider}`,
+    borderRadius: 4,
+    display: 'flex',
+    flexDirection: 'column',
+    gap: 8,
+    padding: 8,
+    paddingLeft: 0,
+    paddingTop: 0,
+  },
+  analyzerInput: {
+    paddingTop: 8,
+    '& .select': {
+      width: '110px',
+    },
+  },
+  matchInput: { fontSize: 13 },
 }));
 }));
 
 
 type inputType = {
 type inputType = {
@@ -118,6 +146,8 @@ const CreateFields: FC<CreateFieldsProps> = ({
   autoID,
   autoID,
   setFieldsValidation,
   setFieldsValidation,
 }) => {
 }) => {
+  const { setDialog2, handleCloseDialog2 } = useContext(rootContext);
+
   const { t: collectionTrans } = useTranslation('collection');
   const { t: collectionTrans } = useTranslation('collection');
   const { t: warningTrans } = useTranslation('warning');
   const { t: warningTrans } = useTranslation('warning');
 
 
@@ -245,14 +275,14 @@ const CreateFields: FC<CreateFieldsProps> = ({
     label?: string,
     label?: string,
     className?: string
     className?: string
   ) => {
   ) => {
-    const defaultLabal = collectionTrans(
+    const defaultLabel = collectionTrans(
       VectorTypes.includes(field.data_type) ? 'vectorFieldName' : 'fieldName'
       VectorTypes.includes(field.data_type) ? 'vectorFieldName' : 'fieldName'
     );
     );
 
 
     return getInput({
     return getInput({
-      label: label || defaultLabal,
+      label: label || defaultLabel,
       value: field.name,
       value: field.name,
-      className: className || classes.fieldInput,
+      className: `${classes.fieldInput} ${className}`,
       handleChange: (value: string) => {
       handleChange: (value: string) => {
         const isValid = checkEmptyValid(value);
         const isValid = checkEmptyValid(value);
         setFieldsValidation(v =>
         setFieldsValidation(v =>
@@ -261,7 +291,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
           )
           )
         );
         );
 
 
-        changeFields(field.id!, 'name', value);
+        changeFields(field.id!, { name: value });
       },
       },
       validate: (value: any) => {
       validate: (value: any) => {
         if (value === null) return ' ';
         if (value === null) return ' ';
@@ -277,7 +307,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
       label: collectionTrans('description'),
       label: collectionTrans('description'),
       value: field.description,
       value: field.description,
       handleChange: (value: string) =>
       handleChange: (value: string) =>
-        changeFields(field.id!, 'description', value),
+        changeFields(field.id!, { description: value }),
       inputClassName: classes.descInput,
       inputClassName: classes.descInput,
     });
     });
   };
   };
@@ -311,7 +341,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
     };
     };
     return getInput({
     return getInput({
       label: collectionTrans('dimension'),
       label: collectionTrans('dimension'),
-      value: field.dimension as number,
+      value: field.dim as number,
       inputClassName: classes.numberBox,
       inputClassName: classes.numberBox,
       handleChange: (value: string) => {
       handleChange: (value: string) => {
         const { isPositive, isMultiple } = validateDimension(value);
         const { isPositive, isMultiple } = validateDimension(value);
@@ -320,11 +350,11 @@ const CreateFields: FC<CreateFieldsProps> = ({
             ? !!isMultiple && isPositive
             ? !!isMultiple && isPositive
             : isPositive;
             : isPositive;
 
 
-        changeFields(field.id!, 'dimension', `${value}`);
+        changeFields(field.id!, { dim: value });
 
 
         setFieldsValidation(v =>
         setFieldsValidation(v =>
           v.map(item =>
           v.map(item =>
-            item.id === field.id! ? { ...item, dimension: isValid } : item
+            item.id === field.id! ? { ...item, dim: isValid } : item
           )
           )
         );
         );
       },
       },
@@ -343,7 +373,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
   const generateMaxLength = (field: FieldType) => {
   const generateMaxLength = (field: FieldType) => {
     // update data if needed
     // update data if needed
     if (typeof field.max_length === 'undefined') {
     if (typeof field.max_length === 'undefined') {
-      changeFields(field.id!, 'max_length', DEFAULT_ATTU_VARCHAR_MAX_LENGTH);
+      changeFields(field.id!, { max_length: DEFAULT_ATTU_VARCHAR_MAX_LENGTH });
     }
     }
     return getInput({
     return getInput({
       label: 'Max Length',
       label: 'Max Length',
@@ -351,7 +381,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
       type: 'number',
       type: 'number',
       inputClassName: classes.maxLength,
       inputClassName: classes.maxLength,
       handleChange: (value: string) =>
       handleChange: (value: string) =>
-        changeFields(field.id!, 'max_length', value),
+        changeFields(field.id!, { max_length: value }),
       validate: (value: any) => {
       validate: (value: any) => {
         if (value === null) return ' ';
         if (value === null) return ' ';
         const isEmptyValid = checkEmptyValid(value);
         const isEmptyValid = checkEmptyValid(value);
@@ -380,7 +410,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
       type: 'number',
       type: 'number',
       inputClassName: classes.maxLength,
       inputClassName: classes.maxLength,
       handleChange: (value: string) =>
       handleChange: (value: string) =>
-        changeFields(field.id!, 'max_capacity', value),
+        changeFields(field.id!, { max_capacity: value }),
       validate: (value: any) => {
       validate: (value: any) => {
         if (value === null) return ' ';
         if (value === null) return ' ';
         const isEmptyValid = checkEmptyValid(value);
         const isEmptyValid = checkEmptyValid(value);
@@ -416,11 +446,9 @@ const CreateFields: FC<CreateFieldsProps> = ({
             }
             }
             size="small"
             size="small"
             onChange={() => {
             onChange={() => {
-              changeFields(
-                field.id!,
-                'is_partition_key',
-                !field.is_partition_key
-              );
+              changeFields(field.id!, {
+                is_partition_key: !field.is_partition_key,
+              });
             }}
             }}
           />
           />
         }
         }
@@ -429,10 +457,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
             title={collectionTrans('partitionKeyTooltip')}
             title={collectionTrans('partitionKeyTooltip')}
             placement="top"
             placement="top"
           >
           >
-            <>
-              {collectionTrans('partitionKey')}
-              {/* <InfoIcon classes={{ root: classes.icon }} /> */}
-            </>
+            {collectionTrans('partitionKey')}
           </CustomToolTip>
           </CustomToolTip>
         }
         }
         className={classes.toggle}
         className={classes.toggle}
@@ -440,7 +465,93 @@ const CreateFields: FC<CreateFieldsProps> = ({
     );
     );
   };
   };
 
 
-  const changeFields = (id: string, key: keyof FieldType, value: any) => {
+  const generateTextMatchCheckBox = (field: FieldType, fields: FieldType[]) => {
+    const update: Partial<FieldType> = {
+      enable_match: !field.enable_match,
+    };
+
+    if (!field.enable_match) {
+      update.enable_analyzer = true;
+    }
+    return (
+      <div className={classes.matchInput}>
+        <Checkbox
+          checked={!!field.enable_match}
+          size="small"
+          onChange={() => {
+            changeFields(field.id!, update);
+          }}
+        />
+        <CustomToolTip
+          title={collectionTrans('textMatchTooltip')}
+          placement="top"
+        >
+          <>{collectionTrans('enableMatch')}</>
+        </CustomToolTip>
+      </div>
+    );
+  };
+
+  const generateAnalyzerCheckBox = (field: FieldType, fields: FieldType[]) => {
+    let analyzer = '';
+    if (typeof field.analyzer_params === 'object') {
+      analyzer = field.analyzer_params.tokenizer || field.analyzer_params.type;
+    } else {
+      analyzer = field.analyzer_params || 'standard';
+    }
+
+    return (
+      <div className={classes.analyzerInput}>
+        <Checkbox
+          checked={!!field.enable_analyzer}
+          size="small"
+          onChange={() => {
+            changeFields(field.id!, {
+              enable_analyzer: !field.enable_analyzer,
+            });
+          }}
+        />
+        <CustomSelector
+          wrapperClass="select"
+          options={ANALYZER_OPTIONS}
+          size="small"
+          onChange={e => {
+            changeFields(field.id!, { analyzer_params: e.target.value });
+          }}
+          disabled={!field.enable_analyzer}
+          value={analyzer}
+          variant="filled"
+          label={collectionTrans('analyzer')}
+        />
+        <CustomIconButton
+          disabled={!field.enable_analyzer}
+          onClick={() => {
+            setDialog2({
+              open: true,
+              type: 'custom',
+              params: {
+                component: (
+                  <EditAnalyzerDialog
+                    data={getAnalyzerParams(
+                      field.analyzer_params || 'standard'
+                    )}
+                    handleConfirm={data => {
+                      changeFields(field.id!, { analyzer_params: data });
+                    }}
+                    handleCloseDialog={handleCloseDialog2}
+                  />
+                ),
+              },
+            });
+          }}
+        >
+          <icons.settings className={classes.icon} />
+        </CustomIconButton>
+      </div>
+    );
+  };
+
+  const changeFields = (id: string, changes: Partial<FieldType>) => {
     const newFields = fields.map(f => {
     const newFields = fields.map(f => {
       if (f.id !== id) {
       if (f.id !== id) {
         return f;
         return f;
@@ -448,7 +559,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
 
 
       const updatedField = {
       const updatedField = {
         ...f,
         ...f,
-        [key]: value,
+        ...changes,
       };
       };
 
 
       // remove array params, if not array
       // remove array params, if not array
@@ -470,12 +581,10 @@ const CreateFields: FC<CreateFieldsProps> = ({
         !VectorTypes.includes(updatedField.data_type) ||
         !VectorTypes.includes(updatedField.data_type) ||
         updatedField.data_type === DataTypeEnum.SparseFloatVector
         updatedField.data_type === DataTypeEnum.SparseFloatVector
       ) {
       ) {
-        delete updatedField.dimension;
+        delete updatedField.dim;
       } else {
       } else {
         // add dimension if not exist
         // add dimension if not exist
-        updatedField.dimension = Number(
-          updatedField.dimension || DEFAULT_ATTU_DIM
-        );
+        updatedField.dim = Number(updatedField.dim || DEFAULT_ATTU_DIM);
       }
       }
 
 
       return updatedField;
       return updatedField;
@@ -492,13 +601,13 @@ const CreateFields: FC<CreateFieldsProps> = ({
       is_primary_key: false,
       is_primary_key: false,
       description: '',
       description: '',
       isDefault: false,
       isDefault: false,
-      dimension: DEFAULT_ATTU_DIM,
+      dim: DEFAULT_ATTU_DIM,
       id,
       id,
     };
     };
     const newValidation = {
     const newValidation = {
       id,
       id,
       name: false,
       name: false,
-      dimension: true,
+      dim: true,
     };
     };
 
 
     fields.splice(index + 1, 0, newDefaultItem);
     fields.splice(index + 1, 0, newDefaultItem);
@@ -525,7 +634,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
           `${collectionTrans('idType')} `,
           `${collectionTrans('idType')} `,
           field.data_type,
           field.data_type,
           (value: DataTypeEnum) => {
           (value: DataTypeEnum) => {
-            changeFields(field.id!, 'data_type', value);
+            changeFields(field.id!, { data_type: value });
             if (value === DataTypeEnum.VarChar) {
             if (value === DataTypeEnum.VarChar) {
               setAutoID(false);
               setAutoID(false);
             }
             }
@@ -542,7 +651,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
               disabled={isVarChar}
               disabled={isVarChar}
               size="small"
               size="small"
               onChange={() => {
               onChange={() => {
-                changeFields(field.id!, 'autoID', !autoID);
+                changeFields(field.id!, { autoID: !autoID });
                 setAutoID(!autoID);
                 setAutoID(!autoID);
               }}
               }}
             />
             />
@@ -572,7 +681,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
           'vector',
           'vector',
           `${collectionTrans('vectorType')} `,
           `${collectionTrans('vectorType')} `,
           field.data_type,
           field.data_type,
-          (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
+          (value: DataTypeEnum) => changeFields(field.id!, { data_type: value })
         )}
         )}
         {generateDimension(field)}
         {generateDimension(field)}
         {generateDesc(field)}
         {generateDesc(field)}
@@ -600,10 +709,10 @@ const CreateFields: FC<CreateFieldsProps> = ({
 
 
     // handle default values
     // handle default values
     if (isArray && typeof field.element_type === 'undefined') {
     if (isArray && typeof field.element_type === 'undefined') {
-      changeFields(field.id!, 'element_type', DEFAULT_ATTU_ELEMENT_TYPE);
+      changeFields(field.id!, { element_type: DEFAULT_ATTU_ELEMENT_TYPE });
     }
     }
     if (isArray && typeof field.max_capacity === 'undefined') {
     if (isArray && typeof field.max_capacity === 'undefined') {
-      changeFields(field.id!, 'max_capacity', DEFAULT_ATTU_MAX_CAPACITY);
+      changeFields(field.id!, { max_capacity: DEFAULT_ATTU_MAX_CAPACITY });
     }
     }
 
 
     return (
     return (
@@ -613,7 +722,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
           'all',
           'all',
           collectionTrans('fieldType'),
           collectionTrans('fieldType'),
           field.data_type,
           field.data_type,
-          (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
+          (value: DataTypeEnum) => changeFields(field.id!, { data_type: value })
         )}
         )}
 
 
         {isArray
         {isArray
@@ -622,7 +731,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
               collectionTrans('elementType'),
               collectionTrans('elementType'),
               field.element_type || DEFAULT_ATTU_ELEMENT_TYPE,
               field.element_type || DEFAULT_ATTU_ELEMENT_TYPE,
               (value: DataTypeEnum) =>
               (value: DataTypeEnum) =>
-                changeFields(field.id!, 'element_type', value)
+                changeFields(field.id!, { element_type: value })
             )
             )
           : null}
           : null}
 
 
@@ -631,9 +740,15 @@ const CreateFields: FC<CreateFieldsProps> = ({
 
 
         {generateDesc(field)}
         {generateDesc(field)}
 
 
-        {isVarChar || isInt64
-          ? generatePartitionKeyToggle(field, fields)
-          : null}
+        {isInt64 ? generatePartitionKeyToggle(field, fields) : null}
+
+        {isVarChar ? (
+          <div className={classes.paramsGrp}>
+            {generateAnalyzerCheckBox(field, fields)}
+            {generateTextMatchCheckBox(field, fields)}
+            {generatePartitionKeyToggle(field, fields)}
+          </div>
+        ) : null}
         <IconButton
         <IconButton
           onClick={() => {
           onClick={() => {
             handleAddNewField(index);
             handleAddNewField(index);
@@ -667,7 +782,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
           'all',
           'all',
           collectionTrans('fieldType'),
           collectionTrans('fieldType'),
           field.data_type,
           field.data_type,
-          (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
+          (value: DataTypeEnum) => changeFields(field.id!, { data_type: value })
         )}
         )}
 
 
         {generateDimension(field)}
         {generateDimension(field)}

+ 11 - 7
client/src/pages/databases/collections/Types.ts

@@ -13,24 +13,25 @@ export interface CollectionCreateParam {
   consistency_level: string;
   consistency_level: string;
 }
 }
 
 
+export type AnalyzerType = 'standard' | 'english' | 'chinese';
+
 export interface CreateField {
 export interface CreateField {
   name: string | null;
   name: string | null;
   data_type: DataTypeEnum;
   data_type: DataTypeEnum;
   is_primary_key: boolean;
   is_primary_key: boolean;
   is_partition_key?: boolean;
   is_partition_key?: boolean;
   description: string;
   description: string;
-  dimension?: number | string;
   isDefault?: boolean;
   isDefault?: boolean;
   id?: string;
   id?: string;
-  type_params?: {
-    dim?: string | number;
-    max_length?: string | number;
-  };
+  dim?: string | number;
+  max_length?: string | number;
   createType?: CreateFieldType;
   createType?: CreateFieldType;
   element_type?: DataTypeEnum;
   element_type?: DataTypeEnum;
-  max_length?: string | number;
   max_capacity?: string | number;
   max_capacity?: string | number;
   autoID?: boolean;
   autoID?: boolean;
+  enable_analyzer?: boolean;
+  enable_match?: boolean;
+  analyzer_params?: AnalyzerType | Record<AnalyzerType, any>;
 }
 }
 
 
 export type CreateFieldType =
 export type CreateFieldType =
@@ -46,7 +47,7 @@ export type FieldType = {
   is_primary_key: boolean;
   is_primary_key: boolean;
   is_partition_key?: boolean;
   is_partition_key?: boolean;
   description: string;
   description: string;
-  dimension?: number | string;
+  dim?: number | string;
   isDefault?: boolean;
   isDefault?: boolean;
   id?: string;
   id?: string;
   type_params?: {
   type_params?: {
@@ -57,6 +58,9 @@ export type FieldType = {
   max_length?: string | number;
   max_length?: string | number;
   max_capacity?: string | number;
   max_capacity?: string | number;
   autoID?: boolean;
   autoID?: boolean;
+  enable_analyzer?: boolean;
+  enable_match?: boolean;
+  analyzer_params?: any;
 };
 };
 
 
 export interface CreateFieldsProps {
 export interface CreateFieldsProps {

+ 39 - 2
client/src/pages/databases/collections/schema/Schema.tsx

@@ -5,7 +5,7 @@ import AttuGrid from '@/components/grid/Grid';
 import { ColDefinitionsType } from '@/components/grid/Types';
 import { ColDefinitionsType } from '@/components/grid/Types';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import Icons from '@/components/icons/Icons';
 import Icons from '@/components/icons/Icons';
-import { formatFieldType, formatNumber } from '@/utils';
+import { formatFieldType, formatNumber, findKeyValue } from '@/utils';
 import { dataContext, rootContext, systemContext } from '@/context';
 import { dataContext, rootContext, systemContext } from '@/context';
 import IndexTypeElement from './IndexTypeElement';
 import IndexTypeElement from './IndexTypeElement';
 import { getLabelDisplayedRows } from '@/pages/search/Utils';
 import { getLabelDisplayedRows } from '@/pages/search/Utils';
@@ -72,7 +72,44 @@ const Overview = () => {
               />
               />
             ) : null}
             ) : null}
             {f.autoID ? (
             {f.autoID ? (
-              <Chip className={classes.chip} size="small" label="auto id" />
+              <Chip
+                className={classes.chip}
+                size="small"
+                label={collectionTrans('autoId')}
+              />
+            ) : null}
+            {findKeyValue(f.type_params, 'enable_match') ? (
+              <Chip
+                className={classes.chip}
+                size="small"
+                label={collectionTrans('enableMatch')}
+              />
+            ) : null}
+            {findKeyValue(f.type_params, 'enable_analyzer') ? (
+              <Tooltip
+                title={findKeyValue(f.type_params, 'analyzer_params')}
+                arrow
+              >
+                <Chip
+                  className={classes.chip}
+                  size="small"
+                  label={collectionTrans('analyzer')}
+                  onClick={() => {
+                    const textToCopy = findKeyValue(
+                      f.type_params,
+                      'analyzer_params'
+                    );
+                    navigator.clipboard
+                      .writeText(textToCopy as string)
+                      .then(() => {
+                        alert('Copied to clipboard!');
+                      })
+                      .catch(err => {
+                        alert('Failed to copy: ' + err);
+                      });
+                  }}
+                />
+              </Tooltip>
             ) : null}
             ) : null}
           </div>
           </div>
         );
         );

+ 1 - 0
client/src/pages/databases/collections/schema/Styles.tsx

@@ -81,6 +81,7 @@ export const useStyles = makeStyles((theme: Theme) => ({
     fontSize: '12px',
     fontSize: '12px',
     color: theme.palette.text.primary,
     color: theme.palette.text.primary,
     border: 'none',
     border: 'none',
+    cursor: 'normal',
   },
   },
   featureChip: {
   featureChip: {
     marginRight: 4,
     marginRight: 4,

+ 24 - 46
client/src/pages/dialogs/CreateCollectionDialog.tsx

@@ -7,13 +7,8 @@ import CustomSelector from '@/components/customSelector/CustomSelector';
 import { ITextfieldConfig } from '@/components/customInput/Types';
 import { ITextfieldConfig } from '@/components/customInput/Types';
 import { rootContext, dataContext } from '@/context';
 import { rootContext, dataContext } from '@/context';
 import { useFormValidation } from '@/hooks';
 import { useFormValidation } from '@/hooks';
-import { formatForm, TypeEnum } from '@/utils';
-import {
-  DataTypeEnum,
-  ConsistencyLevelEnum,
-  DEFAULT_ATTU_DIM,
-  VectorTypes,
-} from '@/consts';
+import { formatForm, getAnalyzerParams, TypeEnum } from '@/utils';
+import { DataTypeEnum, ConsistencyLevelEnum, DEFAULT_ATTU_DIM } from '@/consts';
 import CreateFields from '../databases/collections/CreateFields';
 import CreateFields from '../databases/collections/CreateFields';
 import {
 import {
   CollectionCreateParam,
   CollectionCreateParam,
@@ -28,16 +23,13 @@ const useStyles = makeStyles((theme: Theme) => ({
     width: '100%',
     width: '100%',
     display: 'flex',
     display: 'flex',
     alignItems: 'center',
     alignItems: 'center',
-    marginBottom: '16px',
     '&:nth-last-child(3)': {
     '&:nth-last-child(3)': {
       flexDirection: 'column',
       flexDirection: 'column',
       alignItems: 'flex-start',
       alignItems: 'flex-start',
       marginBottom: '0',
       marginBottom: '0',
     },
     },
-
     '& legend': {
     '& legend': {
       marginBottom: theme.spacing(1),
       marginBottom: theme.spacing(1),
-      color: theme.palette.text.secondary,
       lineHeight: '20px',
       lineHeight: '20px',
       fontSize: '14px',
       fontSize: '14px',
     },
     },
@@ -45,23 +37,20 @@ const useStyles = makeStyles((theme: Theme) => ({
   generalInfo: {
   generalInfo: {
     gap: 8,
     gap: 8,
   },
   },
-
   input: {
   input: {
     width: '100%',
     width: '100%',
   },
   },
   select: {
   select: {
-    width: '170px',
-
     '&:first-child': {
     '&:first-child': {
       marginLeft: 0,
       marginLeft: 0,
     },
     },
   },
   },
   consistencySelect: {
   consistencySelect: {
-    '& .MuiSelect-filled': {
-      padding: 12,
-    },
+    marginTop: theme.spacing(2),
+  },
+  dialog: {
+    width: 800,
   },
   },
-  dialog: {},
 }));
 }));
 
 
 const CreateCollectionDialog: FC<CollectionCreateProps> = ({ onCreate }) => {
 const CreateCollectionDialog: FC<CollectionCreateProps> = ({ onCreate }) => {
@@ -96,7 +85,7 @@ const CreateCollectionDialog: FC<CollectionCreateProps> = ({ onCreate }) => {
       data_type: DataTypeEnum.FloatVector,
       data_type: DataTypeEnum.FloatVector,
       is_primary_key: false,
       is_primary_key: false,
       name: null,
       name: null,
-      dimension: DEFAULT_ATTU_DIM,
+      dim: DEFAULT_ATTU_DIM,
       description: '',
       description: '',
       isDefault: true,
       isDefault: true,
       id: '2',
       id: '2',
@@ -109,7 +98,7 @@ const CreateCollectionDialog: FC<CollectionCreateProps> = ({ onCreate }) => {
     }[]
     }[]
   >([
   >([
     { id: '1', name: false },
     { id: '1', name: false },
-    { id: '2', name: false, dimension: true },
+    { id: '2', name: false, dim: true },
   ]);
   ]);
 
 
   const allFieldsValid = useMemo(() => {
   const allFieldsValid = useMemo(() => {
@@ -206,17 +195,19 @@ const CreateCollectionDialog: FC<CollectionCreateProps> = ({ onCreate }) => {
       ...form,
       ...form,
       fields: fields.map(v => {
       fields: fields.map(v => {
         let data: CreateField = {
         let data: CreateField = {
+          ...v,
           name: v.name,
           name: v.name,
           description: v.description,
           description: v.description,
-          is_primary_key: v.is_primary_key,
-          is_partition_key: v.is_partition_key,
+          is_primary_key: !!v.is_primary_key,
+          is_partition_key: !!v.is_partition_key,
           data_type: v.data_type,
           data_type: v.data_type,
         };
         };
 
 
         // if we need
         // if we need
-        if (typeof v.dimension !== undefined) {
-          data.dimension = Number(v.dimension);
+        if (typeof v.dim !== undefined && !isNaN(Number(v.dim))) {
+          data.dim = Number(v.dim);
         }
         }
+
         if (typeof v.max_length === 'number') {
         if (typeof v.max_length === 'number') {
           data.max_length = Number(v.max_length);
           data.max_length = Number(v.max_length);
         }
         }
@@ -227,32 +218,19 @@ const CreateCollectionDialog: FC<CollectionCreateProps> = ({ onCreate }) => {
           data.max_capacity = Number(v.max_capacity);
           data.max_capacity = Number(v.max_capacity);
         }
         }
 
 
-        v.is_primary_key && (data.autoID = form.autoID);
+        if (v.analyzer_params && v.enable_analyzer) {
+          // if analyzer_params is string, we need to use default value
+          data.analyzer_params = getAnalyzerParams(v.analyzer_params);
+        }
 
 
-        const param = VectorTypes.includes(v.data_type)
-          ? {
-              ...data,
-              type_params: {
-                // if data type is vector, dimension must exist.
-                dim: Number(data.dimension!),
-              },
-            }
-          : v.data_type === DataTypeEnum.VarChar ||
-            v.element_type === DataTypeEnum.VarChar
-          ? {
-              ...v,
-              type_params: {
-                max_length: Number(v.max_length!),
-              },
-            }
-          : { ...data };
+        v.is_primary_key && (data.autoID = form.autoID);
 
 
         // delete sparse vector dime
         // delete sparse vector dime
-        if (param.data_type === DataTypeEnum.SparseFloatVector) {
-          delete param.type_params!.dim;
+        if (data.data_type === DataTypeEnum.SparseFloatVector) {
+          delete data.dim;
         }
         }
 
 
-        return param;
+        return data;
       }),
       }),
       consistency_level: consistencyLevel,
       consistency_level: consistencyLevel,
     };
     };
@@ -317,14 +295,14 @@ const CreateCollectionDialog: FC<CollectionCreateProps> = ({ onCreate }) => {
         </fieldset>
         </fieldset>
 
 
         <fieldset className={classes.fieldset}>
         <fieldset className={classes.fieldset}>
-          <legend>{collectionTrans('consistency')}</legend>
           <CustomSelector
           <CustomSelector
             wrapperClass={`${classes.select} ${classes.consistencySelect}`}
             wrapperClass={`${classes.select} ${classes.consistencySelect}`}
             size="small"
             size="small"
             options={CONSISTENCY_LEVEL_OPTIONS}
             options={CONSISTENCY_LEVEL_OPTIONS}
-            onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
+            onChange={e => {
               setConsistencyLevel(e.target.value as ConsistencyLevelEnum);
               setConsistencyLevel(e.target.value as ConsistencyLevelEnum);
             }}
             }}
+            label={collectionTrans('consistency')}
             value={consistencyLevel}
             value={consistencyLevel}
             variant="filled"
             variant="filled"
           />
           />

+ 153 - 0
client/src/pages/dialogs/EditAnalyzerDialog.tsx

@@ -0,0 +1,153 @@
+import { FC, useEffect, useRef, useState } from 'react';
+import { Theme, useTheme } from '@mui/material';
+import { EditorState, Compartment } from '@codemirror/state';
+import { EditorView, keymap, ViewUpdate } from '@codemirror/view';
+import { insertTab } from '@codemirror/commands';
+import { indentUnit } from '@codemirror/language';
+import { basicSetup } from 'codemirror';
+import { json, jsonParseLinter } from '@codemirror/lang-json';
+import { linter } from '@codemirror/lint';
+import { useTranslation } from 'react-i18next';
+import DialogTemplate from '@/components/customDialog/DialogTemplate';
+import { makeStyles } from '@mui/styles';
+import { githubLight } from '@ddietr/codemirror-themes/github-light';
+import { githubDark } from '@ddietr/codemirror-themes/github-dark';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  code: {
+    border: `1px solid ${theme.palette.divider}`,
+    overflow: 'auto',
+  },
+  tip: {
+    fontSize: 12,
+    marginBottom: 8,
+    width: 480,
+    lineHeight: '20px',
+  },
+}));
+
+type EditAnalyzerDialogProps = {
+  data: { [key: string]: any };
+  handleConfirm: (data: { [key: string]: any }) => void;
+  handleCloseDialog: () => void;
+  cb?: () => void;
+};
+
+// json linter for cm
+const linterExtension = linter(jsonParseLinter());
+
+const EditAnalyzerDialog: FC<EditAnalyzerDialogProps> = props => {
+  const theme = useTheme();
+  const themeCompartment = new Compartment();
+
+  // props
+  const { data, handleCloseDialog, handleConfirm } = props;
+  // UI states
+  const [disabled, setDisabled] = useState(true);
+  // context
+  // translations
+  const { t: btnTrans } = useTranslation('btn');
+  const { t: dialogTrans } = useTranslation('dialog');
+  // refs
+  const editorEl = useRef<HTMLDivElement>(null);
+  const editor = useRef<EditorView>();
+  // styles
+  const classes = useStyles();
+
+  const originalData = JSON.stringify(data, null, 4) + '\n';
+
+  // create editor
+  useEffect(() => {
+    if (!editor.current) {
+      const startState = EditorState.create({
+        doc: originalData,
+        extensions: [
+          basicSetup,
+          json(),
+          linterExtension,
+          keymap.of([{ key: 'Tab', run: insertTab }]), // fix tab behaviour
+          indentUnit.of('    '), // fix tab indentation
+          EditorView.theme({
+            '&.cm-editor': {
+              '&.cm-focused': {
+                outline: 'none',
+              },
+            },
+            '.cm-content': {
+              fontSize: '12px',
+            },
+            '.cm-tooltip-lint': {
+              width: '80%',
+            },
+          }),
+          themeCompartment.of(
+            theme.palette.mode === 'light' ? githubLight : githubDark
+          ),
+          EditorView.lineWrapping,
+          EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
+            if (viewUpdate.docChanged) {
+              const d = jsonParseLinter()(view);
+              if (d.length !== 0) {
+                setDisabled(true);
+                return;
+              }
+
+              const doc = viewUpdate.state.doc;
+              const value = doc.toString();
+
+              setDisabled(value === originalData);
+            }
+          }),
+        ],
+      });
+
+      const view = new EditorView({
+        state: startState,
+        parent: editorEl.current!,
+      });
+
+      editor.current = view;
+
+      return () => {
+        view.destroy();
+        editor.current = undefined;
+      };
+    }
+  }, [JSON.stringify(data)]);
+
+  // handle confirm
+  const _handleConfirm = async () => {
+    handleConfirm(JSON.parse(editor.current!.state.doc.toString()));
+    handleCloseDialog();
+  };
+
+  return (
+    <DialogTemplate
+      title={dialogTrans('editAnalyzerTitle')}
+      handleClose={handleCloseDialog}
+      children={
+        <>
+          <div
+            className={classes.tip}
+            dangerouslySetInnerHTML={{
+              __html: dialogTrans('editAnalyzerInfo'),
+            }}
+          ></div>
+          <div
+            className={`${classes.code} cm-editor`}
+            ref={editorEl}
+            onClick={() => {
+              if (editor.current) editor.current.focus();
+            }}
+          ></div>
+        </>
+      }
+      confirmDisabled={disabled}
+      confirmLabel={btnTrans('edit')}
+      handleConfirm={_handleConfirm}
+      showCancel={true}
+    />
+  );
+};
+
+export default EditAnalyzerDialog;

+ 4 - 0
client/src/utils/Common.ts

@@ -1,5 +1,6 @@
 import { saveAs } from 'file-saver';
 import { saveAs } from 'file-saver';
 import { Parser } from '@json2csv/plainjs';
 import { Parser } from '@json2csv/plainjs';
+import { KeyValuePair } from '@server/types';
 
 
 export const copyToCommand = (
 export const copyToCommand = (
   value: string,
   value: string,
@@ -95,3 +96,6 @@ export const saveCsvAs = (csvObj: any, as: string) => {
     console.error(err);
     console.error(err);
   }
   }
 };
 };
+
+export const findKeyValue = (obj: KeyValuePair[], key: string) =>
+  obj.find(v => v.key === key)?.value;

+ 12 - 0
client/src/utils/Format.ts

@@ -4,6 +4,7 @@ import {
   DEFAULT_PROMETHEUS_PORT,
   DEFAULT_PROMETHEUS_PORT,
   VectorTypes,
   VectorTypes,
   DataTypeStringEnum,
   DataTypeStringEnum,
+  DEFAULT_ANALYZER_PARAMS,
 } from '@/consts';
 } from '@/consts';
 import {
 import {
   CreateFieldType,
   CreateFieldType,
@@ -11,6 +12,7 @@ import {
 } from '@/pages/databases/collections/Types';
 } from '@/pages/databases/collections/Types';
 import { FieldObject } from '@server/types';
 import { FieldObject } from '@server/types';
 import { generateVector } from '.';
 import { generateVector } from '.';
+import { AnalyzerType } from '@/pages/databases/collections/Types';
 
 
 /**
 /**
  * transform large capacity to capacity in b.
  * transform large capacity to capacity in b.
@@ -336,3 +338,13 @@ export const getColumnWidth = (field: FieldObject): number => {
       return 350;
       return 350;
   }
   }
 };
 };
+
+export const getAnalyzerParams = (
+  analyzerParams: AnalyzerType | Record<AnalyzerType, any>
+): Record<string, any> => {
+  if (typeof analyzerParams === 'string') {
+    return DEFAULT_ANALYZER_PARAMS[analyzerParams as AnalyzerType];
+  }
+
+  return analyzerParams;
+};

+ 1 - 1
client/src/utils/index.ts

@@ -5,4 +5,4 @@ export * from './Insert';
 export * from './Metric';
 export * from './Metric';
 export * from './search';
 export * from './search';
 export * from './Sort';
 export * from './Sort';
-export * from './Validation';
+export * from './Validation';

+ 43 - 1
server/src/utils/Helper.ts

@@ -41,6 +41,47 @@ export const makeRandomSparse = (dim: number) => {
   return sparseObject;
   return sparseObject;
 };
 };
 
 
+export const makeRandomVarChar = (maxLength: number) => {
+  const words = [
+    'quick',
+    'brown',
+    'fox',
+    'jumps',
+    'over',
+    'lazy',
+    'dog',
+    'runs',
+    'forest',
+    'grace',
+    'speed',
+    'bright',
+    'sky',
+    'beautiful',
+    'day',
+    'adventure',
+    'beyond',
+    'horizon',
+    'silent',
+  ];
+
+  let text = '';
+  const space = ' ';
+
+  while (text.length < maxLength) {
+    // Pick a random word from the list
+    const nextWord = words[Math.floor(Math.random() * words.length)];
+    const newLength = text.length + nextWord.length + (text ? space.length : 0);
+
+    if (newLength <= maxLength) {
+      text += (text ? space : '') + nextWord;
+    } else {
+      break; // Stop adding words when the limit is reached
+    }
+  }
+
+  return text;
+};
+
 export const genDataByType = (field: FieldSchema): any => {
 export const genDataByType = (field: FieldSchema): any => {
   const { data_type, type_params, element_type } = field;
   const { data_type, type_params, element_type } = field;
   switch (data_type) {
   switch (data_type) {
@@ -68,7 +109,8 @@ export const genDataByType = (field: FieldSchema): any => {
     case 'SparseFloatVector':
     case 'SparseFloatVector':
       return makeRandomSparse(16);
       return makeRandomSparse(16);
     case 'VarChar':
     case 'VarChar':
-      return makeRandomId(Number(findKeyValue(type_params, 'max_length')));
+      const len = Number(findKeyValue(type_params, 'max_length'));
+      return makeRandomVarChar(len) || makeRandomId(len);
     case 'JSON':
     case 'JSON':
       return makeRandomJSON();
       return makeRandomJSON();
     case 'Array':
     case 'Array':