Ver código fonte

[2.5] support null default value (#711)

* support null and default value on creating collection and schema page

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

* support display null and default value

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

* fix

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

---------

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
ryjiang 5 meses atrás
pai
commit
548a9286d5

+ 6 - 2
client/src/components/DataView/DataView.tsx

@@ -6,7 +6,7 @@ const DataView = (props: { type: string; value: any }) => {
 
   switch (type) {
     case 'VarChar':
-      return <MediaPreview value={value} />;
+      return <MediaPreview value={JSON.stringify(value)} />;
     case 'JSON':
     case 'Array':
     case 'SparseFloatVector':
@@ -25,7 +25,11 @@ const DataView = (props: { type: string; value: any }) => {
       return <Typography title={trimmedValue}>{trimmedValue}</Typography>;
 
     default:
-      return <Typography title={value}>{value}</Typography>;
+      return (
+        <Typography title={value}>
+          {JSON.stringify(value).replace(/^"|"$/g, '')}
+        </Typography>
+      );
   }
 };
 

+ 1 - 0
client/src/i18n/cn/button.ts

@@ -41,6 +41,7 @@ const btnTrans = {
   close: '关闭',
   modify: '修改',
   downloadSchema: '下载 Schema',
+  editDefaultValue: '编辑默认值',
 
   // tips
   loadColTooltip: '加载Collection',

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

@@ -64,6 +64,10 @@ const collectionTrans = {
   analyzer: '分词器',
   enableMatch: '启用匹配',
   textMatchTooltip: 'Milvus中的文本匹配能够基于特定术语实现精确的文档检索。',
+  nullable: 'Nullable',
+  nullableTooltip: '字段是否可以为 null,nullable 字段不能用作分区键。',
+  defaultValue: '默认值',
+  defaultValueTooltip: '字段的默认值, 不支持JSON 和 Array。',
 
   // load dialog
   loadTitle: '加载Collection',

+ 3 - 1
client/src/i18n/cn/common.ts

@@ -16,7 +16,7 @@ const commonTrans = {
     prometheusInstance: 'Prometheus实例',
     prometheusNamespace: 'Prometheus命名空间',
     connectionTip: '支持自托管Milvus或Zilliz云专用集群。',
-    checkHealth:  '检查健康状态',
+    checkHealth: '检查健康状态',
   },
   status: {
     loaded: '已加载',
@@ -83,6 +83,8 @@ const commonTrans = {
   tip: '以100k向量和1024段文件大小为例',
   disk: '磁盘',
   memory: '内存',
+  yes: '是',
+  no: '否',
 };
 
 export default commonTrans;

+ 1 - 0
client/src/i18n/en/button.ts

@@ -41,6 +41,7 @@ const btnTrans = {
   close: 'Close',
   modify: 'Modify',
   downloadSchema: 'Download Schema',
+  EditDefaultValue: 'Edit Default Value',
 
   // tips
   loadColTooltip: 'Load Collection',

+ 10 - 2
client/src/i18n/en/collection.ts

@@ -29,7 +29,8 @@ const collectionTrans = {
     'Consistency refers to the property that ensures every node or replica has the same view of data when writing or reading data at a given time.',
   entityCountInfo:
     'This count is an approximation and may be slightly delayed due to the unique mechanisms of Milvus. The actual count may vary and is updated periodically. Please note that this number should be used as a reference and not as an exact count.',
-  replicaTooltip: 'The number of replicas for the collection, it can not exceed the number of query nodes.',
+  replicaTooltip:
+    'The number of replicas for the collection, it can not exceed the number of query nodes.',
   modifyReplicaTooltip: 'Modify Replica Number',
 
   // create dialog
@@ -65,7 +66,14 @@ const collectionTrans = {
   enableDynamicSchema: 'Dynamic Schema',
   analyzer: 'Analyzer',
   enableMatch: 'Enable Match',
-  textMatchTooltip: 'Text match in Milvus enables precise document retrieval based on specific terms.',
+  textMatchTooltip:
+    'Text match in Milvus enables precise document retrieval based on specific terms.',
+  nullable: 'Nullable',
+  nullableTooltip:
+    'Whether the field can be null, nullable field cannot be used as partition keys.',
+  defaultValue: 'Default',
+  defaultValueTooltip:
+    'Default value of the field, JSON and Array types are not supported.',
 
   // load dialog
   loadTitle: 'Load Collection',

+ 2 - 0
client/src/i18n/en/common.ts

@@ -84,6 +84,8 @@ const commonTrans = {
   tip: 'Use 100k vectors and 1024 segment file size as example',
   disk: 'Disk',
   memory: 'Memory',
+  yes: 'Yes',
+  no: 'No',
 };
 
 export default commonTrans;

+ 105 - 42
client/src/pages/databases/collections/CreateFields.tsx

@@ -60,7 +60,9 @@ const useStyles = makeStyles((theme: Theme) => ({
     '& .MuiSelect-filled': {
       fontSize: 14,
     },
-    '& .MuiCheckbox-root': {},
+    '& .MuiCheckbox-root': {
+      padding: 4,
+    },
     '& .MuiFormControlLabel-label': {
       fontSize: 14,
     },
@@ -83,17 +85,18 @@ const useStyles = makeStyles((theme: Theme) => ({
     maxWidth: '80px',
   },
   descInput: {
-    width: '60px',
+    width: '64px',
   },
   btnTxt: {
     textTransform: 'uppercase',
   },
   iconBtn: {
-    marginLeft: 0,
     padding: 0,
     position: 'relative',
     top: '-8px',
-    width: 15,
+    '& svg': {
+      width: 15,
+    },
   },
   helperText: {
     lineHeight: '20px',
@@ -114,10 +117,13 @@ const useStyles = makeStyles((theme: Theme) => ({
     borderRadius: 4,
     display: 'flex',
     flexDirection: 'column',
-    gap: 8,
-    padding: 8,
     paddingLeft: 0,
     paddingTop: 0,
+    paddingRight: 8,
+    minHeight: 44,
+    alignSelf: 'flex-start',
+    alignItems: 'flex-start',
+    justifyContent: 'center',
   },
   analyzerInput: {
     paddingTop: 8,
@@ -125,7 +131,7 @@ const useStyles = makeStyles((theme: Theme) => ({
       width: '110px',
     },
   },
-  matchInput: { fontSize: 13 },
+  setting: { fontSize: 12, alignItems: 'center', display: 'flex' },
 }));
 
 type inputType = {
@@ -312,6 +318,35 @@ const CreateFields: FC<CreateFieldsProps> = ({
     });
   };
 
+  const generateDefaultValue = (field: FieldType) => {
+    let type: 'number' | 'text' = 'number';
+    switch (field.data_type) {
+      case DataTypeEnum.Int8:
+      case DataTypeEnum.Int16:
+      case DataTypeEnum.Int32:
+      case DataTypeEnum.Int64:
+      case DataTypeEnum.Float:
+      case DataTypeEnum.Double:
+        type = 'number';
+        break;
+      case DataTypeEnum.Bool:
+        type = 'text';
+        break;
+      default:
+        type = 'text';
+        break;
+    }
+
+    return getInput({
+      label: collectionTrans('defaultValue'),
+      value: field.default_value,
+      type: type,
+      handleChange: (value: string) =>
+        changeFields(field.id!, { default_value: value }),
+      inputClassName: classes.descInput,
+    });
+  };
+
   const generateDimension = (field: FieldType) => {
     // sparse dont support dimension
     if (field.data_type === DataTypeEnum.SparseFloatVector) {
@@ -432,36 +467,55 @@ const CreateFields: FC<CreateFieldsProps> = ({
     });
   };
 
-  const generatePartitionKeyToggle = (
+  const generatePartitionKeyCheckbox = (
     field: FieldType,
     fields: FieldType[]
   ) => {
     return (
-      <FormControlLabel
-        control={
-          <Switch
-            checked={!!field.is_partition_key}
-            disabled={
-              fields.some(f => f.is_partition_key) && !field.is_partition_key
-            }
-            size="small"
-            onChange={() => {
-              changeFields(field.id!, {
-                is_partition_key: !field.is_partition_key,
-              });
-            }}
-          />
-        }
-        label={
-          <CustomToolTip
-            title={collectionTrans('partitionKeyTooltip')}
-            placement="top"
-          >
-            {collectionTrans('partitionKey')}
-          </CustomToolTip>
-        }
-        className={classes.toggle}
-      />
+      <div className={classes.setting}>
+        <Checkbox
+          checked={!!field.is_partition_key}
+          size="small"
+          disabled={
+            (fields.some(f => f.is_partition_key) && !field.is_partition_key) ||
+            field.nullable
+          }
+          onChange={() => {
+            changeFields(field.id!, {
+              is_partition_key: !field.is_partition_key,
+            });
+          }}
+        />
+        <CustomToolTip
+          title={collectionTrans('partitionKeyTooltip')}
+          placement="top"
+        >
+          <>{collectionTrans('partitionKey')}</>
+        </CustomToolTip>
+      </div>
+    );
+  };
+
+  const generateNullableCheckbox = (field: FieldType, fields: FieldType[]) => {
+    return (
+      <div className={classes.setting}>
+        <Checkbox
+          checked={!!field.nullable}
+          size="small"
+          onChange={() => {
+            changeFields(field.id!, {
+              nullable: !field.nullable,
+              is_partition_key: false,
+            });
+          }}
+        />
+        <CustomToolTip
+          title={collectionTrans('nullableTooltip')}
+          placement="top"
+        >
+          <>{collectionTrans('nullable')}</>
+        </CustomToolTip>
+      </div>
     );
   };
 
@@ -474,7 +528,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
       update.enable_analyzer = true;
     }
     return (
-      <div className={classes.matchInput}>
+      <div className={classes.setting}>
         <Checkbox
           checked={!!field.enable_match}
           size="small"
@@ -706,6 +760,9 @@ const CreateFields: FC<CreateFieldsProps> = ({
     const isInt64 = field.data_type === DataTypeEnum.Int64;
     const isArray = field.data_type === DataTypeEnum.Array;
     const isElementVarChar = field.element_type === DataTypeEnum.VarChar;
+    const showDefaultValue =
+      field.data_type !== DataTypeEnum.Array &&
+      field.data_type !== DataTypeEnum.JSON;
 
     // handle default values
     if (isArray && typeof field.element_type === 'undefined') {
@@ -738,17 +795,23 @@ const CreateFields: FC<CreateFieldsProps> = ({
         {isArray ? generateMaxCapacity(field) : null}
         {isVarChar || isElementVarChar ? generateMaxLength(field) : null}
 
+        {showDefaultValue && generateDefaultValue(field)}
+
         {generateDesc(field)}
 
-        {isInt64 ? generatePartitionKeyToggle(field, fields) : null}
+        <div className={classes.paramsGrp}>
+          {isInt64 ? generatePartitionKeyCheckbox(field, fields) : null}
+
+          {isVarChar ? (
+            <>
+              {generateAnalyzerCheckBox(field, fields)}
+              {generateTextMatchCheckBox(field, fields)}
+              {generatePartitionKeyCheckbox(field, fields)}
+            </>
+          ) : null}
+          {generateNullableCheckbox(field, fields)}
+        </div>
 
-        {isVarChar ? (
-          <div className={classes.paramsGrp}>
-            {generateAnalyzerCheckBox(field, fields)}
-            {generateTextMatchCheckBox(field, fields)}
-            {generatePartitionKeyToggle(field, fields)}
-          </div>
-        ) : null}
         <IconButton
           onClick={() => {
             handleAddNewField(index);

+ 4 - 0
client/src/pages/databases/collections/Types.ts

@@ -32,6 +32,8 @@ export interface CreateField {
   enable_analyzer?: boolean;
   enable_match?: boolean;
   analyzer_params?: AnalyzerType | Record<AnalyzerType, any>;
+  nullable?: boolean;
+  default_value?: any;
 }
 
 export type CreateFieldType =
@@ -61,6 +63,8 @@ export type FieldType = {
   enable_analyzer?: boolean;
   enable_match?: boolean;
   analyzer_params?: any;
+  nullable?: boolean;
+  default_value?: any;
 };
 
 export interface CreateFieldsProps {

+ 18 - 0
client/src/pages/databases/collections/schema/Schema.tsx

@@ -126,6 +126,24 @@ const Overview = () => {
       },
       label: collectionTrans('fieldType'),
     },
+    {
+      id: 'nullable',
+      align: 'left',
+      disablePadding: false,
+      label: collectionTrans('nullable'),
+      formatter(f) {
+        return f.nullable ? commonTrans('yes') : commonTrans('no');
+      },
+    },
+    {
+      id: 'default_value',
+      align: 'left',
+      disablePadding: false,
+      label: collectionTrans('defaultValue'),
+      formatter(f) {
+        return f.default_value || '--';
+      },
+    },
     {
       id: 'name',
       align: 'left',

+ 1 - 1
client/src/pages/dialogs/CreateCollectionDialog.tsx

@@ -49,7 +49,7 @@ const useStyles = makeStyles((theme: Theme) => ({
     marginTop: theme.spacing(2),
   },
   dialog: {
-    width: 800,
+    minWidth: 820,
   },
 }));
 

+ 1 - 1
server/package.json

@@ -13,7 +13,7 @@
   },
   "dependencies": {
     "@json2csv/plainjs": "^7.0.3",
-    "@zilliz/milvus2-sdk-node": "2.5.0",
+    "@zilliz/milvus2-sdk-node": "2.5.1",
     "axios": "^1.7.7",
     "chalk": "4.1.2",
     "class-sanitizer": "^1.0.1",

+ 1 - 1
server/src/collections/collections.service.ts

@@ -45,7 +45,7 @@ import {
   VectorTypes,
   cloneObj,
 } from '../utils';
-import { QueryDto, ImportSampleDto, GetReplicasDto } from './dto';
+import { QueryDto, ImportSampleDto } from './dto';
 import {
   CollectionObject,
   CollectionLazyObject,

+ 5 - 2
server/src/utils/Helper.ts

@@ -109,13 +109,13 @@ export const genDataByType = (field: FieldSchema): any => {
     case 'SparseFloatVector':
       return makeRandomSparse(16);
     case 'VarChar':
-      const len = Number(findKeyValue(type_params, 'max_length'));
+      const len = Number(field.max_length);
       return makeRandomVarChar(len) || makeRandomId(len);
     case 'JSON':
       return makeRandomJSON();
     case 'Array':
       return Array.from({
-        length: Number(findKeyValue(type_params, 'max_capacity')),
+        length: Number(field.max_capacity),
       }).map(() => genDataByType({ ...field, data_type: element_type }));
   }
 };
@@ -143,6 +143,9 @@ export const genRow = (
   const result: any = {};
   fields.forEach(field => {
     if (!field.autoID) {
+      if ((field.nullable || field.default_value) && Math.random() < 0.5) {
+        return;
+      }
       result[field.name] = genDataByType(field);
     }
   });

+ 4 - 4
server/yarn.lock

@@ -1449,10 +1449,10 @@
   resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.9.tgz#b6ef7457e826be8049667ae673eda7876eb049be"
   integrity sha512-4VSbbcMoxc4KLjb1gs96SRmi7w4h1SF+fCoiK0XaQX62buCc1G5d0DC5bJ9xJBNPDSVCmIrcl8BiYxzjrqaaJA==
 
-"@zilliz/milvus2-sdk-node@2.5.0":
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/@zilliz/milvus2-sdk-node/-/milvus2-sdk-node-2.5.0.tgz#bc290be80abfd626c8615f985195a7651e54820a"
-  integrity sha512-YBM5/IEw7C5gg0HAH7nozhnpd8GvbE/sul8JTpBkjfjh9CUHM49bHZdi8hGJHjYPXA1/Cqv5XJa5Q43Czfewgg==
+"@zilliz/milvus2-sdk-node@2.5.1":
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/@zilliz/milvus2-sdk-node/-/milvus2-sdk-node-2.5.1.tgz#4afcb2e54826da8d96164a5fe60e96d56e7a356c"
+  integrity sha512-awb5Y2uT9Ku+YJ7m+16T6TphoPIzwgtOXxf7s53qK54mv4F+7YWN4mBE9R2ScVO5H499ra6k+QxbGRbxAuO53g==
   dependencies:
     "@grpc/grpc-js" "^1.12.1"
     "@grpc/proto-loader" "^0.7.10"