Browse Source

Merge branch 'milvus-io:main' into main

zhuanghong.chen 4 years ago
parent
commit
f72c5f2555
54 changed files with 2426 additions and 79 deletions
  1. 4 0
      client/src/assets/icons/copy.svg
  2. 3 0
      client/src/assets/icons/nav-search.svg
  3. 5 0
      client/src/assets/icons/search.svg
  4. 191 0
      client/src/components/advancedSearch/Condition.tsx
  5. 234 0
      client/src/components/advancedSearch/ConditionGroup.tsx
  6. 56 0
      client/src/components/advancedSearch/CopyButton.tsx
  7. 219 0
      client/src/components/advancedSearch/Dialog.tsx
  8. 320 0
      client/src/components/advancedSearch/Filter.tsx
  9. 83 0
      client/src/components/advancedSearch/Types.ts
  10. 3 0
      client/src/components/advancedSearch/index.tsx
  11. 18 13
      client/src/components/customDialog/DialogTemplate.tsx
  12. 1 0
      client/src/components/customDialog/Types.ts
  13. 16 0
      client/src/components/icons/Icons.tsx
  14. 6 1
      client/src/components/icons/Types.ts
  15. 9 0
      client/src/components/insert/Container.tsx
  16. 2 0
      client/src/components/insert/Import.tsx
  17. 1 3
      client/src/components/insert/Preview.tsx
  18. 2 0
      client/src/components/insert/Types.ts
  19. 30 7
      client/src/components/layout/Layout.tsx
  20. 9 3
      client/src/components/menu/SimpleMenu.tsx
  21. 2 0
      client/src/components/menu/Types.ts
  22. 74 12
      client/src/consts/Milvus.tsx
  23. 6 7
      client/src/hooks/Navigation.ts
  24. 6 0
      client/src/http/BaseModel.ts
  25. 8 0
      client/src/http/Collection.ts
  26. 4 0
      client/src/http/Index.ts
  27. 1 0
      client/src/i18n/cn/nav.ts
  28. 15 0
      client/src/i18n/cn/search.ts
  29. 2 0
      client/src/i18n/cn/warning.ts
  30. 1 0
      client/src/i18n/en/nav.ts
  31. 15 0
      client/src/i18n/en/search.ts
  32. 2 0
      client/src/i18n/en/warning.ts
  33. 4 0
      client/src/i18n/index.ts
  34. 4 1
      client/src/pages/collections/Create.tsx
  35. 1 1
      client/src/pages/connect/Connect.tsx
  36. 10 7
      client/src/pages/schema/Create.tsx
  37. 0 1
      client/src/pages/schema/Schema.tsx
  38. 2 0
      client/src/pages/schema/Types.ts
  39. 1 0
      client/src/pages/seach/Constants.ts
  40. 259 0
      client/src/pages/seach/SearchParams.tsx
  41. 119 0
      client/src/pages/seach/Styles.ts
  42. 62 0
      client/src/pages/seach/Types.ts
  43. 422 0
      client/src/pages/seach/VectorSearch.tsx
  44. 6 0
      client/src/router/Config.ts
  45. 2 2
      client/src/router/Types.ts
  46. 17 0
      client/src/utils/Common.ts
  47. 18 7
      client/src/utils/Format.ts
  48. 1 1
      client/src/utils/Insert.ts
  49. 29 5
      client/src/utils/Validation.ts
  50. 104 0
      client/src/utils/search.ts
  51. 1 1
      server/package.json
  52. 11 3
      server/src/collections/collections.controller.ts
  53. 1 0
      server/src/collections/collections.service.ts
  54. 4 4
      server/yarn.lock

+ 4 - 0
client/src/assets/icons/copy.svg

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.3333 6H7.33333C6.59695 6 6 6.59695 6 7.33333V13.3333C6 14.0697 6.59695 14.6667 7.33333 14.6667H13.3333C14.0697 14.6667 14.6667 14.0697 14.6667 13.3333V7.33333C14.6667 6.59695 14.0697 6 13.3333 6Z" stroke="#06AFF2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.33301 9.99992H2.66634C2.31272 9.99992 1.97358 9.85944 1.72353 9.60939C1.47348 9.35935 1.33301 9.02021 1.33301 8.66659V2.66659C1.33301 2.31296 1.47348 1.97382 1.72353 1.72378C1.97358 1.47373 2.31272 1.33325 2.66634 1.33325H8.66634C9.01996 1.33325 9.3591 1.47373 9.60915 1.72378C9.8592 1.97382 9.99967 2.31296 9.99967 2.66659V3.33325" stroke="#06AFF2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
client/src/assets/icons/nav-search.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.165 0.902214C11.5044 1.04952 11.706 1.40291 11.6601 1.76999L10.9438 7.49997H17.4998C17.8232 7.49997 18.1174 7.68702 18.2545 7.97984C18.3917 8.27266 18.347 8.61838 18.14 8.86679L9.8067 18.8668C9.56987 19.151 9.17403 19.245 8.83469 19.0977C8.49534 18.9504 8.29373 18.597 8.33962 18.2299L9.05586 12.5H2.49985C2.1765 12.5 1.88234 12.3129 1.74519 12.0201C1.60804 11.7273 1.65266 11.3815 1.85966 11.1331L10.193 1.13314C10.4298 0.84895 10.8257 0.754907 11.165 0.902214ZM4.27905 10.8333H9.99985C10.2389 10.8333 10.4664 10.9359 10.6246 11.1151C10.7828 11.2943 10.8564 11.5328 10.8267 11.77L10.346 15.6163L15.7206 9.16663H9.99985C9.76082 9.16663 9.5333 9.06399 9.37512 8.8848C9.21693 8.70561 9.1433 8.46712 9.17295 8.22994L9.65373 4.38368L4.27905 10.8333Z" fill="#06AFF2"/>
+</svg>

+ 5 - 0
client/src/assets/icons/search.svg

@@ -0,0 +1,5 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M29.29 17.7886C30.0117 16.9137 31.306 16.7894 32.181 17.511C33.0559 18.2326 33.1802 19.5269 32.4586 20.4019L22.9354 31.9485C22.2138 32.8234 20.9195 32.9477 20.0445 32.2261C19.1696 31.5044 19.0453 30.2101 19.7669 29.3352L29.29 17.7886Z" fill="#AEAEBB"/>
+<path d="M22 19.1014L15.3798 17.0207L22 14.8986V11L12 14.7785V19.2215L22 23V19.1014Z" fill="#AEAEBB"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M21 1C9.9543 1 1 9.9543 1 21C1 32.0457 9.9543 41 21 41C25.5938 41 29.8258 39.4512 33.2025 36.8474C33.3392 37.1868 33.545 37.5048 33.8201 37.7799L42.2201 46.1799C43.3136 47.2734 45.0864 47.2734 46.1799 46.1799C47.2734 45.0864 47.2734 43.3136 46.1799 42.2201L37.7799 33.8201C37.5048 33.545 37.1868 33.3392 36.8474 33.2025C39.4512 29.8258 41 25.5938 41 21C41 9.9543 32.0457 1 21 1ZM6.71429 21C6.71429 13.1102 13.1102 6.71429 21 6.71429C28.8898 6.71429 35.2857 13.1102 35.2857 21C35.2857 28.8898 28.8898 35.2857 21 35.2857C13.1102 35.2857 6.71429 28.8898 6.71429 21Z" fill="#AEAEBB"/>
+</svg>

+ 191 - 0
client/src/components/advancedSearch/Condition.tsx

@@ -0,0 +1,191 @@
+import React, { useState, useEffect, FC } from 'react';
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  IconButton,
+  TextField,
+} from '@material-ui/core';
+import CloseIcon from '@material-ui/icons/Close';
+import { ConditionProps, Field } from './Types';
+import CustomSelector from '../customSelector/CustomSelector';
+
+// Todo: Move to corrsponding Constant file.
+// Static logical operators.
+const LogicalOperators = [
+  {
+    value: '<',
+    label: '<',
+  },
+  {
+    value: '<=',
+    label: '<=',
+  },
+  {
+    value: '>',
+    label: '>',
+  },
+  {
+    value: '>=',
+    label: '>=',
+  },
+  {
+    value: '==',
+    label: '==',
+  },
+  {
+    value: '!=',
+    label: '!=',
+  },
+  {
+    value: 'in',
+    label: 'in',
+  },
+];
+
+const Condition: FC<ConditionProps> = props => {
+  const {
+    onDelete,
+    triggerChange,
+    fields = [],
+    id = '',
+    initData,
+    className = '',
+    ...others
+  } = props;
+  const [operator, setOperator] = useState(
+    initData?.op || LogicalOperators[0].value
+  );
+  const [conditionField, setConditionField] = useState<Field | any>(
+    initData?.field || fields[0] || {}
+  );
+  const [conditionValue, setConditionValue] = useState(initData?.value || '');
+  const [isValuelegal, setIsValueLegal] = useState(
+    initData?.isCorrect || false
+  );
+
+  /**
+   * Check condition's value by field's and operator's type.
+   * Trigger condition change event.
+   */
+  useEffect(() => {
+    const regInt = /^\d+$/;
+    const regFloat = /^\d+\.\d+$/;
+    const regIntInterval = /^\[\d+,\d+\]$/;
+    const regFloatInterval = /^\[\d+\.\d+,\d+\.\d+]$/;
+
+    const type = conditionField?.type;
+    const isIn = operator === 'in';
+    let isLegal = false;
+
+    switch (type) {
+      case 'int':
+        isLegal = isIn
+          ? regIntInterval.test(conditionValue)
+          : regInt.test(conditionValue);
+        break;
+      case 'float':
+        isLegal = isIn
+          ? regFloatInterval.test(conditionValue)
+          : regFloat.test(conditionValue);
+        break;
+      default:
+        isLegal = false;
+        break;
+    }
+    setIsValueLegal(isLegal);
+    triggerChange(id, {
+      field: conditionField,
+      op: operator,
+      value: conditionValue,
+      isCorrect: isLegal,
+      id,
+    });
+    // No need for 'id' and 'triggerChange'.
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [conditionField, operator, conditionValue]);
+
+  const classes = useStyles();
+
+  // Logic operator input change.
+  const handleOpChange = (event: React.ChangeEvent<{ value: unknown }>) => {
+    setOperator(event.target.value);
+  };
+  // Field Name input change.
+  const handleFieldNameChange = (
+    event: React.ChangeEvent<{ value: unknown }>
+  ) => {
+    const value = event.target.value;
+    const target = fields.find(field => field.name === value);
+    target && setConditionField(target);
+  };
+  // Value input change.
+  const handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const value = event.target.value;
+    setConditionValue(value);
+  };
+
+  return (
+    <div className={`${classes.wrapper} ${className}`} {...others}>
+      <CustomSelector
+        label="Field Name"
+        value={conditionField?.name}
+        onChange={handleFieldNameChange}
+        options={fields.map(i => ({ value: i.name, label: i.name }))}
+        variant="filled"
+        wrapperClass={classes.fieldName}
+      />
+      <CustomSelector
+        label="Logic"
+        value={operator}
+        onChange={handleOpChange}
+        options={LogicalOperators}
+        variant="filled"
+        wrapperClass={classes.logic}
+      />
+      <TextField
+        className={classes.value}
+        label="Value"
+        variant="filled"
+        // size="small"
+        onChange={handleValueChange}
+        value={conditionValue}
+        error={!isValuelegal}
+      />
+      <IconButton
+        aria-label="close"
+        className={classes.closeButton}
+        onClick={onDelete}
+        size="small"
+      >
+        <CloseIcon />
+      </IconButton>
+    </div>
+  );
+};
+
+Condition.displayName = 'Condition';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    wrapper: {
+      minWidth: '466px',
+      minHeight: '62px',
+      background: '#FFFFFF',
+      padding: '12px 16px',
+      display: 'flex',
+      flexDirection: 'row',
+      alignItems: 'center',
+    },
+    closeButton: {},
+    fieldName: {
+      minHeight: '38px',
+      minWidth: '130px',
+    },
+    logic: { minHeight: '38px', minWidth: '70px', margin: '0 24px' },
+    value: { minHeight: '38px', minWidth: '130px' },
+  })
+);
+
+export default Condition;

+ 234 - 0
client/src/components/advancedSearch/ConditionGroup.tsx

@@ -0,0 +1,234 @@
+import React, { useState, FC } from 'react';
+import { makeStyles, Theme, createStyles, Button } from '@material-ui/core';
+import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab';
+import ConditionItem from './Condition';
+import AddIcon from '@material-ui/icons/Add';
+import {
+  ConditionGroupProps,
+  BinaryLogicalOpProps,
+  AddConditionProps,
+} from './Types';
+
+// "And & or" operator component.
+const BinaryLogicalOp: FC<BinaryLogicalOpProps> = props => {
+  const { onChange, className, initValue = 'and' } = props;
+  const [operator, setOperator] = useState(initValue);
+  const handleChange = (
+    event: React.MouseEvent<HTMLElement>,
+    newOp: string
+  ) => {
+    if (newOp !== null) {
+      setOperator(newOp);
+      onChange(newOp);
+    }
+  };
+  return (
+    <>
+      <div className={`${className} op-${operator}`}>
+        <ToggleButtonGroup
+          value={operator}
+          exclusive
+          onChange={handleChange}
+          aria-label="Binary Logical Operator"
+        >
+          <ToggleButton value="and" aria-label="And">
+            AND
+          </ToggleButton>
+          <ToggleButton value="or" aria-label="Or">
+            OR
+          </ToggleButton>
+        </ToggleButtonGroup>
+        <div className="op-split" />
+      </div>
+    </>
+  );
+};
+
+// "+ Add condition" component.
+const AddCondition: FC<AddConditionProps> = props => {
+  const { className, onClick } = props;
+  return (
+    <Button onClick={onClick} color="primary" className={className}>
+      <AddIcon />
+      Add Condition
+    </Button>
+  );
+};
+
+// Condition group component which contains of BinaryLogicalOp, AddCondition and ConditionItem.
+const ConditionGroup = (props: ConditionGroupProps) => {
+  const {
+    fields = [],
+    handleConditions = {},
+    conditions: flatConditions = [],
+  } = props;
+  const {
+    addCondition,
+    removeCondition,
+    changeBinaryLogicalOp,
+    updateConditionData,
+  } = handleConditions;
+
+  const classes = useStyles();
+
+  const generateClassName = (conditions: any, currentIndex: number) => {
+    let className = '';
+    if (currentIndex === 0 || conditions[currentIndex - 1].type === 'break') {
+      className += 'radius-top';
+    }
+    if (
+      currentIndex === conditions.length - 1 ||
+      conditions[currentIndex + 1].type === 'break'
+    ) {
+      className ? (className = 'radius-all') : (className = 'radius-bottom');
+    }
+    return className;
+  };
+
+  // Generate condition items with operators and add condition btn.
+  const generateConditionItems = (conditions: any[]) => {
+    const conditionsLength = conditions.length;
+    const results = conditions.reduce((prev: any, condition, currentIndex) => {
+      if (currentIndex === conditionsLength - 1) {
+        prev.push(
+          <ConditionItem
+            key={condition.id}
+            id={condition.id}
+            onDelete={() => {
+              removeCondition(condition.id);
+            }}
+            fields={fields}
+            triggerChange={updateConditionData}
+            initData={condition?.data}
+            className={generateClassName(conditions, currentIndex)}
+          />
+        );
+        prev.push(
+          <AddCondition
+            key={`${condition.id}-add`}
+            className={classes.addBtn}
+            onClick={() => {
+              addCondition(condition.id);
+            }}
+          />
+        );
+      } else if (condition.type === 'break') {
+        prev.pop();
+        prev.push(
+          <AddCondition
+            key={`${condition.id}-add`}
+            className={classes.addBtn}
+            onClick={() => {
+              addCondition(condition.id, true);
+            }}
+          />
+        );
+        prev.push(
+          <BinaryLogicalOp
+            key={`${condition.id}-op`}
+            onChange={newOp => {
+              changeBinaryLogicalOp(newOp, condition.id);
+            }}
+            className={classes.binaryLogicOp}
+            initValue="or"
+          />
+        );
+      } else if (condition.type === 'condition') {
+        prev.push(
+          <ConditionItem
+            key={condition.id}
+            id={condition.id}
+            onDelete={() => {
+              removeCondition(condition.id);
+            }}
+            fields={fields}
+            triggerChange={updateConditionData}
+            initData={condition?.data}
+            className={generateClassName(conditions, currentIndex)}
+          />
+        );
+        prev.push(
+          <BinaryLogicalOp
+            key={`${condition.id}-op`}
+            onChange={newOp => {
+              changeBinaryLogicalOp(newOp, condition.id);
+            }}
+            className={classes.binaryLogicOp}
+          />
+        );
+      }
+      return prev;
+    }, []);
+    return results;
+  };
+
+  return (
+    <div className={classes.wrapper}>
+      {generateConditionItems(flatConditions)}
+      {flatConditions?.length === 0 && (
+        <AddCondition
+          className={classes.addBtn}
+          onClick={() => {
+            addCondition();
+          }}
+        />
+      )}
+    </div>
+  );
+};
+
+ConditionGroup.displayName = 'ConditionGroup';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    wrapper: {
+      display: 'flex',
+      flexDirection: 'column',
+      alignItems: 'center',
+
+      '& .op-or': {
+        backgroundColor: 'unset',
+        margin: '16px 0',
+      },
+
+      '& .radius-top': {
+        borderRadius: '8px 8px 0 0',
+      },
+      '& .radius-bottom': {
+        borderRadius: '0 0 8px 8px',
+      },
+      '& .radius-all': {
+        borderRadius: '8px',
+      },
+    },
+    addBtn: {},
+    binaryLogicOp: {
+      width: '100%',
+      backgroundColor: '#FFFFFF',
+      display: 'flex',
+      flexDirection: 'row',
+      alignItems: 'center',
+      '& .op-split': {
+        height: '1px',
+        backgroundColor: '#E9E9ED',
+        width: '100%',
+      },
+      '& button': {
+        width: '42px',
+        height: '32px',
+        color: '#010E29',
+      },
+      '& button.Mui-selected': {
+        backgroundColor: '#06AFF2',
+        color: '#FFFFFF',
+      },
+      '& button.Mui-selected:hover': {
+        backgroundColor: '#06AFF2',
+        color: '#FFFFFF',
+      },
+    },
+  })
+);
+
+export default ConditionGroup;

+ 56 - 0
client/src/components/advancedSearch/CopyButton.tsx

@@ -0,0 +1,56 @@
+import React, { useState, FC } from 'react';
+import { makeStyles, Theme, createStyles } from '@material-ui/core';
+import { CopyButtonProps } from './Types';
+import icons from '../icons/Icons';
+import CustomIconButton from '../customButton/CustomIconButton';
+
+const CopyIcon = icons.copyExpression;
+
+const CopyButton: FC<CopyButtonProps> = props => {
+  const {
+    label = 'copy button',
+    icon,
+    className,
+    value = '',
+    ...others
+  } = props;
+  const classes = useStyles();
+  const [tooltipTitle, setTooltipTitle] = useState('Copy');
+
+  const handleClick = (v: string) => {
+    setTooltipTitle('Copied!');
+    navigator.clipboard.writeText(v);
+    setTimeout(() => {
+      setTooltipTitle('Copy');
+    }, 1000);
+  };
+
+  return (
+    <CustomIconButton
+      tooltip={tooltipTitle}
+      aria-label={label}
+      className={`${classes.button} ${className}`}
+      onClick={() => handleClick(value || '')}
+      {...others}
+    >
+      {icon || <CopyIcon style={{ color: 'transparent' }} />}
+    </CustomIconButton>
+  );
+};
+
+CopyButton.displayName = 'CopyButton';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    button: {
+      '& svg': {
+        width: '16px',
+        height: '16px',
+      },
+    },
+    tooltip: {},
+  })
+);
+
+export default CopyButton;

+ 219 - 0
client/src/components/advancedSearch/Dialog.tsx

@@ -0,0 +1,219 @@
+import React, { useEffect } from 'react';
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  Typography,
+  Button,
+  IconButton,
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogTitle,
+} from '@material-ui/core';
+import CloseIcon from '@material-ui/icons/Close';
+import CachedIcon from '@material-ui/icons/Cached';
+import ConditionGroup from './ConditionGroup';
+import CopyBtn from './CopyButton';
+// import DialogTemplate from '../customDialog/DialogTemplate';
+import { DialogProps } from './Types';
+
+const AdvancedDialog = (props: DialogProps) => {
+  const {
+    open = false,
+    onClose,
+    onSubmit,
+    onReset,
+    onCancel,
+    handleConditions = {},
+    conditions: flatConditions = [],
+    isLegal = false,
+    expression: filterExpression = '',
+    title = 'Advanced Filter',
+    fields = [],
+    ...others
+  } = props;
+  const { addCondition } = handleConditions;
+  const classes = useStyles();
+
+  useEffect(() => {
+    flatConditions.length === 0 && addCondition();
+    // Only need add one condition after dialog's first mount.
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  return (
+    <>
+      <Dialog
+        onClose={onClose}
+        aria-labelledby="customized-dialog-title"
+        open={open}
+        maxWidth="xl"
+        className={classes.wrapper}
+        {...others}
+      >
+        <DialogTitle className={classes.dialogTitle} disableTypography>
+          <Typography variant="h5" component="h2">
+            {title}
+          </Typography>
+          <IconButton
+            aria-label="close"
+            className={classes.closeButton}
+            onClick={onCancel}
+            size="small"
+          >
+            <CloseIcon />
+          </IconButton>
+        </DialogTitle>
+        <DialogContent>
+          <div
+            className={`${classes.expResult} ${
+              !isLegal && 'disable-exp'
+            } testcopy`}
+          >
+            {`${isLegal ? filterExpression : 'Filter Expression'}`}
+            {isLegal && (
+              <CopyBtn label="copy expression" value={filterExpression} />
+            )}
+          </div>
+          <div className={classes.expWrapper}>
+            <ConditionGroup
+              fields={fields}
+              handleConditions={handleConditions}
+              conditions={flatConditions}
+            />
+          </div>
+        </DialogContent>
+        <DialogActions className={classes.dialogActions}>
+          <Button
+            onClick={onReset}
+            color="primary"
+            className={classes.resetBtn}
+            size="small"
+          >
+            <CachedIcon />
+            Reset
+          </Button>
+          <div>
+            <Button
+              autoFocus
+              onClick={onCancel}
+              color="primary"
+              className={classes.cancelBtn}
+            >
+              Cancel
+            </Button>
+            <Button
+              onClick={onSubmit}
+              variant="contained"
+              color="primary"
+              className={classes.applyBtn}
+              disabled={!isLegal}
+            >
+              Apply Filters
+            </Button>
+          </div>
+        </DialogActions>
+      </Dialog>
+      {/* <DialogTemplate
+        title={title}
+        handleClose={onClose}
+        showCloseIcon
+        handleConfirm={onSubmit}
+        confirmLabel="Apply Filters"
+        confirmDisabled={!isLegal}
+        handleCancel={onCancel}
+        cancelLabel="Cancel"
+        leftActions={
+          <Button
+            onClick={onReset}
+            color="primary"
+            className={classes.resetBtn}
+            size="small"
+          >
+            <CachedIcon />
+            Reset
+          </Button>
+        }
+      >
+        <div
+          className={`${classes.expResult} ${
+            !isLegal && 'disable-exp'
+          } testcopy`}
+        >
+          {`${isLegal ? filterExpression : 'Filter Expression'}`}
+          {isLegal && (
+            <CopyBtn label="copy expression" value={filterExpression} />
+          )}
+        </div>
+        <div className={classes.expWrapper}>
+          <ConditionGroup
+            fields={fields}
+            handleConditions={handleConditions}
+            conditions={flatConditions}
+          />
+        </div>
+      </DialogTemplate> */}
+    </>
+  );
+};
+
+AdvancedDialog.displayName = 'AdvancedDialog';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    wrapper: {
+      '& .disable-exp': {
+        userSelect: 'none',
+        color: '#AEAEBB',
+      },
+    },
+    closeButton: {
+      color: 'black',
+    },
+    dialogTitle: {
+      display: 'flex',
+      alignItems: 'center',
+      justifyContent: 'space-between',
+    },
+    dialogActions: {
+      justifyContent: 'space-between',
+    },
+    resetBtn: {},
+    cancelBtn: {
+      marginRight: '32px',
+    },
+    applyBtn: {
+      backgroundColor: '#06AFF2',
+      color: 'white',
+    },
+    copyButton: {
+      borderRadius: '0',
+    },
+    expResult: {
+      background: '#F9F9F9',
+      borderRadius: '8px',
+      display: 'flex',
+      justifyContent: 'space-between',
+      alignItems: 'center',
+      minHeight: '40px',
+      marginBottom: '16px',
+      padding: '0 16px',
+      fontFamily: 'Source Code Pro',
+      fontStyle: 'normal',
+      fontWeight: 'normal',
+      fontSize: '16px',
+      lineHeight: '24px',
+    },
+    expWrapper: {
+      background: '#F9F9F9',
+      borderRadius: '8px',
+      minWidth: '480px',
+      minHeight: '104px',
+      padding: '12px',
+    },
+  })
+);
+
+export default AdvancedDialog;

+ 320 - 0
client/src/components/advancedSearch/Filter.tsx

@@ -0,0 +1,320 @@
+import { useState, useEffect } from 'react';
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  Chip,
+  Tooltip,
+} from '@material-ui/core';
+import FilterListIcon from '@material-ui/icons/FilterList';
+import AdvancedDialog from './Dialog';
+import { FilterProps, ConditionData } from './Types';
+import { generateIdByHash } from '../../utils/Common';
+import CustomButton from '../customButton/CustomButton';
+
+const Filter = function Filter(props: FilterProps) {
+  const {
+    title = 'title',
+    showTitle = true,
+    className = '',
+    filterDisabled = false,
+    tooltipPlacement = 'top',
+    onSubmit,
+    fields = [],
+    ...others
+  } = props;
+  const classes = useStyles();
+
+  const [open, setOpen] = useState(false);
+  const [flatConditions, setFlatConditions] = useState<any[]>([]);
+  const [initConditions, setInitConditions] = useState<any[]>([]);
+  const [isConditionsLegal, setIsConditionsLegal] = useState(false);
+  const [filterExpression, setFilterExpression] = useState('');
+
+  // if fields if empty array, reset all conditions
+  useEffect(() => {
+    if (fields.length === 0) {
+      setFlatConditions([]);
+      setInitConditions([]);
+    }
+  }, [fields]);
+
+  // Check all conditions are all correct.
+  useEffect(() => {
+    // Calc the sum of conditions.
+    for (let i = 0; i < flatConditions.length; i++) {
+      const { data, type } = flatConditions[i];
+      if (type !== 'condition') continue;
+      if (!data) {
+        setIsConditionsLegal(false);
+        return;
+      }
+      if (!data.isCorrect) {
+        setIsConditionsLegal(data.isCorrect);
+        return;
+      }
+    }
+    setIsConditionsLegal(true);
+    generateExpression(flatConditions, setFilterExpression);
+  }, [flatConditions]);
+
+  const setFilteredFlatConditions = (conditions: any[]) => {
+    const newConditions = conditions.reduce((prev, item, currentIndex) => {
+      if (prev.length === 0 && item.type !== 'condition') return prev;
+      if (
+        prev.length &&
+        item.type !== 'condition' &&
+        prev[prev.length - 1].type !== 'condition'
+      )
+        return prev;
+      return [...prev, item];
+    }, []);
+    setFlatConditions(newConditions);
+  };
+
+  const generateExpression = (conditions: any[], func: any) => {
+    const expression = conditions.reduce((prev, item) => {
+      const { type, data } = item;
+      if (type === 'break') return `${prev} || `;
+      const {
+        field: { name },
+        op,
+        value,
+      } = data;
+      return `${prev}${
+        prev && !prev.endsWith('|| ') ? ' && ' : ''
+      }${name} ${op} ${value}`;
+    }, '');
+    func(expression);
+  };
+
+  /**
+   * Insert "OR" operator into specified position.
+   * @param targetId The break operator will be inserted after the target one.
+   */
+  const addOrOp = (targetId?: string) => {
+    if (!targetId) {
+      setFilteredFlatConditions([
+        ...flatConditions,
+        { id: generateIdByHash('break'), type: 'break' },
+        {
+          id: generateIdByHash('condition'),
+          type: 'condition',
+          field: '',
+          operator: '<',
+          value: '',
+        },
+      ]);
+      return;
+    }
+    const formerConditons = [...flatConditions];
+    const newConditions = formerConditons.reduce((prev, item) => {
+      if (item.id === targetId) {
+        return [
+          ...prev,
+          item,
+          { id: generateIdByHash('break'), type: 'break' },
+        ];
+      }
+      return [...prev, item];
+    }, []);
+    setFilteredFlatConditions(newConditions);
+  };
+  /**
+   * Remove "OR" operator in specified position.
+   * @param targetId Remove the break operator after the target one.
+   */
+  const removeOrOp = (targetId: string) => {
+    setFilteredFlatConditions(flatConditions.filter(i => i.id !== targetId));
+  };
+  /**
+   * Add new condition to specified position.
+   * @param targetId Position where new condition item will be inserted.
+   * Will be pushed to tail if empty.
+   * @param beforeTarget Will be inserted before the target item.
+   */
+  const addCondition = (targetId?: string, beforeTarget?: boolean) => {
+    const formerConditons = [...flatConditions];
+    const newItem = {
+      id: generateIdByHash('condition'),
+      type: 'condition',
+      field: '',
+      operator: '<',
+      value: '',
+    };
+    if (!targetId) {
+      formerConditons.push(newItem);
+      setFilteredFlatConditions(formerConditons);
+      return;
+    }
+    const newConditions = formerConditons.reduce((prev, item) => {
+      if (item.id === targetId) {
+        const newItems = [
+          item,
+          {
+            id: generateIdByHash('condition'),
+            type: 'condition',
+            field: '',
+            operator: '<',
+            value: '',
+          },
+        ];
+        beforeTarget && newItems.reverse();
+        return [...prev, ...newItems];
+      }
+      return [...prev, item];
+    }, []);
+    setIsConditionsLegal(false);
+    setFilteredFlatConditions(newConditions);
+  };
+  /**
+   * Remove condition from specified position.
+   * @param targetId Position where new condition item will be removed.
+   */
+  const removeCondition = (targetId: string) => {
+    setFilteredFlatConditions(flatConditions.filter(i => i.id !== targetId));
+  };
+  const changeBinaryLogicalOp = (value: string, targetId: string) => {
+    if (value === 'or') {
+      addOrOp(targetId);
+    } else if (value === 'and') {
+      removeOrOp(targetId);
+    }
+  };
+  /**
+   * Update specified condition's data.
+   * @param id Specify one item that will be updated.
+   * @param data Data that will be updated to condition.
+   */
+  const updateConditionData = (id: string, data: ConditionData) => {
+    const formerFlatConditions = flatConditions.map(i => {
+      if (i.id === id) return { ...i, data };
+      return i;
+    });
+    setFilteredFlatConditions(formerFlatConditions);
+  };
+  // Reset conditions.
+  const resetConditions = () => {
+    setIsConditionsLegal(false);
+    setFilteredFlatConditions([
+      {
+        id: generateIdByHash(),
+        type: 'condition',
+        field: '',
+        operator: '<',
+        value: '',
+      },
+    ]);
+  };
+  const getConditionsSum = () => {
+    const conds = flatConditions.filter(i => i.type === 'condition');
+    return conds.length;
+  };
+
+  const handleConditions = {
+    addOrOp,
+    removeOrOp,
+    addCondition,
+    removeCondition,
+    changeBinaryLogicalOp,
+    updateConditionData,
+    resetConditions,
+    getConditionsSum,
+    setFilteredFlatConditions,
+    setIsConditionsLegal,
+    setFilterExpression,
+  };
+
+  const handleClickOpen = () => {
+    setOpen(true);
+  };
+  const handleClose = () => {
+    setOpen(false);
+  };
+  const handleDeleteAll = () => {
+    setFlatConditions([]);
+    setInitConditions([]);
+    onSubmit('');
+  };
+  const handleCancel = () => {
+    setOpen(false);
+    handleFallback();
+  };
+  const handleSubmit = () => {
+    onSubmit(filterExpression);
+    setInitConditions(flatConditions);
+    setOpen(false);
+  };
+  const handleReset = () => {
+    setFilteredFlatConditions([
+      {
+        id: generateIdByHash('condition'),
+        type: 'condition',
+        field: '',
+        operator: '<',
+        value: '',
+      },
+    ]);
+  };
+  const handleFallback = () => {
+    setFilteredFlatConditions(initConditions);
+  };
+
+  return (
+    <>
+      <div className={`${classes.wrapper} ${className}`} {...others}>
+        <CustomButton
+          disabled={filterDisabled}
+          className={`${classes.afBtn} af-btn`}
+          onClick={handleClickOpen}
+        >
+          <FilterListIcon />
+          {showTitle ? title : ''}
+        </CustomButton>
+        {initConditions.length > 0 && (
+          <Tooltip
+            arrow
+            interactive
+            title={filterExpression}
+            placement={tooltipPlacement}
+          >
+            <Chip
+              label={initConditions.filter(i => i.type === 'condition').length}
+              onDelete={handleDeleteAll}
+              variant="outlined"
+              size="small"
+            />
+          </Tooltip>
+        )}
+        {open && (
+          <AdvancedDialog
+            open={open}
+            onClose={handleClose}
+            onCancel={handleCancel}
+            onSubmit={handleSubmit}
+            onReset={handleReset}
+            title="Advanced Filter"
+            fields={fields}
+            handleConditions={handleConditions}
+            conditions={flatConditions}
+            isLegal={isConditionsLegal}
+            expression={filterExpression}
+          />
+        )}
+      </div>
+    </>
+  );
+};
+
+Filter.displayName = 'AdvancedFilter';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    wrapper: {},
+    afBtn: {
+      color: '#06AFF2',
+    },
+  })
+);
+
+export default Filter;

+ 83 - 0
client/src/components/advancedSearch/Types.ts

@@ -0,0 +1,83 @@
+// import { ReactElement } from 'react';
+
+export interface ConditionProps {
+  others?: object;
+  onDelete: () => void;
+  triggerChange: (id: string, data: TriggerChangeData) => void;
+  fields: Field[];
+  id: string;
+  initData: any;
+  className?: string;
+}
+
+export interface Field {
+  name: string;
+  type: 'int' | 'float';
+}
+
+export interface TriggerChangeData {
+  field: Field;
+  op: string;
+  value: string;
+  isCorrect: boolean;
+  id: string;
+}
+
+export interface ConditionGroupProps {
+  others?: object;
+  fields: Field[];
+  handleConditions: any;
+  conditions: any[];
+}
+
+export interface BinaryLogicalOpProps {
+  onChange: (newOp: string) => void;
+  className?: string;
+  initValue?: string;
+}
+
+export interface AddConditionProps {
+  className?: string;
+  onClick?: () => void;
+}
+
+export interface CopyButtonProps {
+  className?: string;
+  icon?: any;
+  label: string;
+  value: string;
+  others?: any;
+}
+
+export interface DialogProps {
+  others?: object;
+  open: boolean;
+  onClose: () => void;
+  onSubmit: (data: any) => void;
+  onReset: () => void;
+  onCancel: () => void;
+  title: string;
+  fields: Field[];
+  handleConditions: any;
+  conditions: any[];
+  isLegal: boolean;
+  expression: string;
+}
+
+export interface FilterProps {
+  className?: string;
+  title: string;
+  showTitle?: boolean;
+  filterDisabled?: boolean;
+  others?: object;
+  onSubmit: (data: any) => void;
+  tooltipPlacement?: 'left' | 'right' | 'bottom' | 'top';
+  fields: Field[];
+}
+
+export interface ConditionData {
+  field: Field;
+  op: string;
+  value: string;
+  isCorrect: boolean;
+}

+ 3 - 0
client/src/components/advancedSearch/index.tsx

@@ -0,0 +1,3 @@
+import Filter from './Filter';
+
+export default Filter;

+ 18 - 13
client/src/components/customDialog/DialogTemplate.tsx

@@ -13,6 +13,7 @@ import CustomButton from '../customButton/CustomButton';
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   actions: {
   actions: {
     paddingTop: theme.spacing(2),
     paddingTop: theme.spacing(2),
+    justifyContent: 'space-between',
   },
   },
 }));
 }));
 
 
@@ -28,6 +29,7 @@ const DialogTemplate: FC<DialogContainerProps> = ({
   showActions = true,
   showActions = true,
   showCancel = true,
   showCancel = true,
   showCloseIcon = true,
   showCloseIcon = true,
+  leftActions,
 }) => {
 }) => {
   const { t } = useTranslation('btn');
   const { t } = useTranslation('btn');
   const cancel = cancelLabel || t('cancel');
   const cancel = cancelLabel || t('cancel');
@@ -43,20 +45,23 @@ const DialogTemplate: FC<DialogContainerProps> = ({
       <DialogContent>{children}</DialogContent>
       <DialogContent>{children}</DialogContent>
       {showActions && (
       {showActions && (
         <DialogActions className={classes.actions}>
         <DialogActions className={classes.actions}>
-          {showCancel && (
-            <CustomButton onClick={onCancel} color="default" name="cancel">
-              {cancel}
+          <div>{leftActions}</div>
+          <div>
+            {showCancel && (
+              <CustomButton onClick={onCancel} color="default" name="cancel">
+                {cancel}
+              </CustomButton>
+            )}
+            <CustomButton
+              variant="contained"
+              onClick={handleConfirm}
+              color="primary"
+              disabled={confirmDisabled}
+              name="confirm"
+            >
+              {confirm}
             </CustomButton>
             </CustomButton>
-          )}
-          <CustomButton
-            variant="contained"
-            onClick={handleConfirm}
-            color="primary"
-            disabled={confirmDisabled}
-            name="confirm"
-          >
-            {confirm}
-          </CustomButton>
+          </div>
         </DialogActions>
         </DialogActions>
       )}
       )}
     </>
     </>

+ 1 - 0
client/src/components/customDialog/Types.ts

@@ -31,4 +31,5 @@ export type DialogContainerProps = {
   confirmDisabled?: boolean;
   confirmDisabled?: boolean;
   showActions?: boolean;
   showActions?: boolean;
   showCancel?: boolean;
   showCancel?: boolean;
+  leftActions?: ReactElement;
 };
 };

+ 16 - 0
client/src/components/icons/Icons.tsx

@@ -19,6 +19,8 @@ import ExitToAppIcon from '@material-ui/icons/ExitToApp';
 import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
 import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
 import RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline';
 import RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline';
 import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
 import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
+import CachedIcon from '@material-ui/icons/Cached';
+import FilterListIcon from '@material-ui/icons/FilterList';
 import { SvgIcon } from '@material-ui/core';
 import { SvgIcon } from '@material-ui/core';
 import { ReactComponent as MilvusIcon } from '../../assets/icons/milvus.svg';
 import { ReactComponent as MilvusIcon } from '../../assets/icons/milvus.svg';
 import { ReactComponent as OverviewIcon } from '../../assets/icons/overview.svg';
 import { ReactComponent as OverviewIcon } from '../../assets/icons/overview.svg';
@@ -29,6 +31,9 @@ import { ReactComponent as ReleaseIcon } from '../../assets/icons/release.svg';
 import { ReactComponent as LoadIcon } from '../../assets/icons/load.svg';
 import { ReactComponent as LoadIcon } from '../../assets/icons/load.svg';
 import { ReactComponent as KeyIcon } from '../../assets/icons/key.svg';
 import { ReactComponent as KeyIcon } from '../../assets/icons/key.svg';
 import { ReactComponent as UploadIcon } from '../../assets/icons/upload.svg';
 import { ReactComponent as UploadIcon } from '../../assets/icons/upload.svg';
+import { ReactComponent as VectorSearchIcon } from '../../assets/icons/nav-search.svg';
+import { ReactComponent as SearchEmptyIcon } from '../../assets/icons/search.svg';
+import { ReactComponent as CopyIcon } from '../../assets/icons/copy.svg';
 
 
 const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
 const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   search: (props = {}) => <SearchIcon {...props} />,
   search: (props = {}) => <SearchIcon {...props} />,
@@ -50,6 +55,8 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   rightArrow: (props = {}) => <ArrowForwardIosIcon {...props} />,
   rightArrow: (props = {}) => <ArrowForwardIosIcon {...props} />,
   remove: (props = {}) => <RemoveCircleOutlineIcon {...props} />,
   remove: (props = {}) => <RemoveCircleOutlineIcon {...props} />,
   dropdown: (props = {}) => <ArrowDropDownIcon {...props} />,
   dropdown: (props = {}) => <ArrowDropDownIcon {...props} />,
+  refresh: (props = {}) => <CachedIcon {...props} />,
+  filter: (props = {}) => <FilterListIcon {...props} />,
 
 
   milvus: (props = {}) => (
   milvus: (props = {}) => (
     <SvgIcon viewBox="0 0 44 31" component={MilvusIcon} {...props} />
     <SvgIcon viewBox="0 0 44 31" component={MilvusIcon} {...props} />
@@ -63,6 +70,9 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   navConsole: (props = {}) => (
   navConsole: (props = {}) => (
     <SvgIcon viewBox="0 0 20 20" component={ConsoleIcon} {...props} />
     <SvgIcon viewBox="0 0 20 20" component={ConsoleIcon} {...props} />
   ),
   ),
+  navSearch: (props = {}) => (
+    <SvgIcon viewBox="0 0 20 20" component={VectorSearchIcon} {...props} />
+  ),
   info: (props = {}) => (
   info: (props = {}) => (
     <SvgIcon viewBox="0 0 16 16" component={InfoIcon} {...props} />
     <SvgIcon viewBox="0 0 16 16" component={InfoIcon} {...props} />
   ),
   ),
@@ -78,6 +88,12 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   upload: (props = {}) => (
   upload: (props = {}) => (
     <SvgIcon viewBox="0 0 16 16" component={UploadIcon} {...props} />
     <SvgIcon viewBox="0 0 16 16" component={UploadIcon} {...props} />
   ),
   ),
+  vectorSearch: (props = {}) => (
+    <SvgIcon viewBox="0 0 48 48" component={SearchEmptyIcon} {...props} />
+  ),
+  copyExpression: (props = {}) => (
+    <SvgIcon viewBox="0 0 16 16" component={CopyIcon} {...props} />
+  ),
 };
 };
 
 
 export default icons;
 export default icons;

+ 6 - 1
client/src/components/icons/Types.ts

@@ -15,6 +15,7 @@ export type IconsType =
   | 'navOverview'
   | 'navOverview'
   | 'navCollection'
   | 'navCollection'
   | 'navConsole'
   | 'navConsole'
+  | 'navSearch'
   | 'expandLess'
   | 'expandLess'
   | 'expandMore'
   | 'expandMore'
   | 'back'
   | 'back'
@@ -26,4 +27,8 @@ export type IconsType =
   | 'remove'
   | 'remove'
   | 'key'
   | 'key'
   | 'upload'
   | 'upload'
-  | 'dropdown';
+  | 'dropdown'
+  | 'vectorSearch'
+  | 'refresh'
+  | 'filter'
+  | 'copyExpression';

+ 9 - 0
client/src/components/insert/Container.tsx

@@ -72,6 +72,8 @@ const InsertContainer: FC<InsertContentProps> = ({
   const [isContainFieldNames, setIsContainFieldNames] = useState<number>(1);
   const [isContainFieldNames, setIsContainFieldNames] = useState<number>(1);
   // uploaded file name
   // uploaded file name
   const [fileName, setFileName] = useState<string>('');
   const [fileName, setFileName] = useState<string>('');
+  const [file, setFile] = useState<File | null>(null);
+
   // uploaded csv data (type: string)
   // uploaded csv data (type: string)
   const [csvData, setCsvData] = useState<any[]>([]);
   const [csvData, setCsvData] = useState<any[]>([]);
 
 
@@ -225,6 +227,7 @@ const InsertContainer: FC<InsertContentProps> = ({
       openSnackBar(insertTrans('uploadFieldNamesLenWarning'), 'error');
       openSnackBar(insertTrans('uploadFieldNamesLenWarning'), 'error');
       // reset uploader value and filename
       // reset uploader value and filename
       setFileName('');
       setFileName('');
+      setFile(null);
       uploader.value = null;
       uploader.value = null;
       return;
       return;
     }
     }
@@ -272,6 +275,10 @@ const InsertContainer: FC<InsertContentProps> = ({
     }
     }
   };
   };
 
 
+  const handleUploadFileChange = (file: File, upload: HTMLFormElement) => {
+    setFile(file);
+  };
+
   const handleBack = () => {
   const handleBack = () => {
     switch (activeStep) {
     switch (activeStep) {
       case InsertStepperEnum.import:
       case InsertStepperEnum.import:
@@ -299,6 +306,7 @@ const InsertContainer: FC<InsertContentProps> = ({
             handleCollectionChange={handleCollectionChange}
             handleCollectionChange={handleCollectionChange}
             handlePartitionChange={setPartitionValue}
             handlePartitionChange={setPartitionValue}
             handleUploadedData={handleUploadedData}
             handleUploadedData={handleUploadedData}
+            handleUploadFileChange={handleUploadFileChange}
             fileName={fileName}
             fileName={fileName}
             setFileName={setFileName}
             setFileName={setFileName}
           />
           />
@@ -311,6 +319,7 @@ const InsertContainer: FC<InsertContentProps> = ({
             tableHeads={tableHeads}
             tableHeads={tableHeads}
             setTableHeads={setTableHeads}
             setTableHeads={setTableHeads}
             isContainFieldNames={isContainFieldNames}
             isContainFieldNames={isContainFieldNames}
+            file={file}
             handleIsContainedChange={setIsContainFieldNames}
             handleIsContainedChange={setIsContainFieldNames}
           />
           />
         );
         );

+ 2 - 0
client/src/components/insert/Import.tsx

@@ -102,6 +102,7 @@ const InsertImport: FC<InsertImportProps> = ({
   handlePartitionChange,
   handlePartitionChange,
 
 
   handleUploadedData,
   handleUploadedData,
+  handleUploadFileChange,
   fileName,
   fileName,
   setFileName,
   setFileName,
 }) => {
 }) => {
@@ -162,6 +163,7 @@ const InsertImport: FC<InsertImportProps> = ({
             handleUploadedData={handleUploadedData}
             handleUploadedData={handleUploadedData}
             maxSize={parseByte('150m')}
             maxSize={parseByte('150m')}
             overSizeWarning={insertTrans('overSizeWarning')}
             overSizeWarning={insertTrans('overSizeWarning')}
+            handleUploadFileChange={handleUploadFileChange}
           />
           />
           <Typography className="text">
           <Typography className="text">
             {fileName || insertTrans('fileNamePlaceHolder')}
             {fileName || insertTrans('fileNamePlaceHolder')}

+ 1 - 3
client/src/components/insert/Preview.tsx

@@ -59,9 +59,6 @@ const getStyles = makeStyles((theme: Theme) => ({
     },
     },
   },
   },
 
 
-  active: {
-    color: theme.palette.primary.main,
-  },
   menuIcon: {
   menuIcon: {
     color: theme.palette.milvusGrey.dark,
     color: theme.palette.milvusGrey.dark,
   },
   },
@@ -91,6 +88,7 @@ const InsertPreview: FC<InsertPreviewProps> = ({
   handleIsContainedChange,
   handleIsContainedChange,
   tableHeads,
   tableHeads,
   setTableHeads,
   setTableHeads,
+  file,
 }) => {
 }) => {
   const classes = getStyles();
   const classes = getStyles();
   const { t: insertTrans } = useTranslation('insert');
   const { t: insertTrans } = useTranslation('insert');

+ 2 - 0
client/src/components/insert/Types.ts

@@ -54,6 +54,7 @@ export interface InsertImportProps {
   handlePartitionChange: (partitionName: string) => void;
   handlePartitionChange: (partitionName: string) => void;
   // handle uploaded data
   // handle uploaded data
   handleUploadedData: (data: string, uploader: HTMLFormElement) => void;
   handleUploadedData: (data: string, uploader: HTMLFormElement) => void;
+  handleUploadFileChange: (file: File, uploader: HTMLFormElement) => void;
   fileName: string;
   fileName: string;
   setFileName: (fileName: string) => void;
   setFileName: (fileName: string) => void;
 }
 }
@@ -67,6 +68,7 @@ export interface InsertPreviewProps {
 
 
   isContainFieldNames: number;
   isContainFieldNames: number;
   handleIsContainedChange: (isContained: number) => void;
   handleIsContainedChange: (isContained: number) => void;
+  file: File | null; // csv file
 }
 }
 
 
 export interface InsertStatusProps {
 export interface InsertStatusProps {

+ 30 - 7
client/src/components/layout/Layout.tsx

@@ -17,6 +17,18 @@ const useStyles = makeStyles((theme: Theme) =>
     },
     },
     content: {
     content: {
       display: 'flex',
       display: 'flex',
+
+      '& .normalSearchIcon': {
+        '& path': {
+          fill: theme.palette.milvusGrey.dark,
+        },
+      },
+
+      '& .activeSearchIcon': {
+        '& path': {
+          fill: theme.palette.primary.main,
+        },
+      },
     },
     },
     body: {
     body: {
       flex: 1,
       flex: 1,
@@ -34,13 +46,17 @@ const Layout = (props: any) => {
   const { t: navTrans } = useTranslation('nav');
   const { t: navTrans } = useTranslation('nav');
   const classes = useStyles();
   const classes = useStyles();
   const location = useLocation();
   const location = useLocation();
-  const defaultActive = useMemo(
-    () =>
-      location.pathname.includes('collection')
-        ? navTrans('collection')
-        : navTrans('overview'),
-    [location, navTrans]
-  );
+  const defaultActive = useMemo(() => {
+    if (location.pathname.includes('collection')) {
+      return navTrans('collection');
+    }
+
+    if (location.pathname.includes('search')) {
+      return navTrans('search');
+    }
+
+    return navTrans('overview');
+  }, [location, navTrans]);
 
 
   const menuItems: NavMenuItem[] = [
   const menuItems: NavMenuItem[] = [
     {
     {
@@ -53,6 +69,13 @@ const Layout = (props: any) => {
       label: navTrans('collection'),
       label: navTrans('collection'),
       onClick: () => history.push('/collections'),
       onClick: () => history.push('/collections'),
     },
     },
+    {
+      icon: icons.navSearch,
+      label: navTrans('search'),
+      onClick: () => history.push('/search'),
+      iconActiveClass: 'activeSearchIcon',
+      iconNormalClass: 'normalSearchIcon',
+    },
   ];
   ];
 
 
   return (
   return (

+ 9 - 3
client/src/components/menu/SimpleMenu.tsx

@@ -13,7 +13,7 @@ const getStyles = makeStyles((theme: Theme) => ({
     borderRadius: '4px',
     borderRadius: '4px',
   },
   },
   menuItem: {
   menuItem: {
-    minWidth: '160px',
+    minWidth: (props: { minWidth: string }) => props.minWidth,
     padding: theme.spacing(1),
     padding: theme.spacing(1),
 
 
     '&:hover': {
     '&:hover': {
@@ -23,10 +23,16 @@ const getStyles = makeStyles((theme: Theme) => ({
 }));
 }));
 
 
 const SimpleMenu: FC<SimpleMenuType> = props => {
 const SimpleMenu: FC<SimpleMenuType> = props => {
-  const { label, menuItems, buttonProps, className = '' } = props;
+  const {
+    label,
+    menuItems,
+    buttonProps,
+    menuItemWidth = '160px',
+    className = '',
+  } = props;
   const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
   const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
 
 
-  const classes = getStyles();
+  const classes = getStyles({ minWidth: menuItemWidth });
   const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
   const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
     setAnchorEl(event.currentTarget);
     setAnchorEl(event.currentTarget);
   };
   };

+ 2 - 0
client/src/components/menu/Types.ts

@@ -10,6 +10,8 @@ export type SimpleMenuType = {
   }[];
   }[];
   buttonProps?: ButtonProps;
   buttonProps?: ButtonProps;
   className?: string;
   className?: string;
+  // e.g. 160px
+  menuItemWidth?: string;
 };
 };
 
 
 export type NavMenuItem = {
 export type NavMenuItem = {

+ 74 - 12
client/src/consts/Milvus.tsx

@@ -1,3 +1,5 @@
+import { DataTypeEnum } from '../pages/collections/Types';
+
 export enum METRIC_TYPES_VALUES {
 export enum METRIC_TYPES_VALUES {
   L2 = 'L2',
   L2 = 'L2',
   IP = 'IP',
   IP = 'IP',
@@ -55,7 +57,7 @@ export type indexConfigType = {
     create: string[];
     create: string[];
     search: searchKeywordsType[];
     search: searchKeywordsType[];
   };
   };
-}
+};
 
 
 // index
 // index
 export const FLOAT_INDEX_CONFIG: indexConfigType = {
 export const FLOAT_INDEX_CONFIG: indexConfigType = {
@@ -67,10 +69,10 @@ export const FLOAT_INDEX_CONFIG: indexConfigType = {
     create: ['nlist', 'm'],
     create: ['nlist', 'm'],
     search: ['nprobe'],
     search: ['nprobe'],
   },
   },
-  // IVF_SQ8: {
-  //   create: ['nlist'],
-  //   search: ['nprobe'],
-  // },
+  IVF_SQ8: {
+    create: ['nlist'],
+    search: ['nprobe'],
+  },
   // IVF_SQ8_HYBRID: {
   // IVF_SQ8_HYBRID: {
   //   create: ['nlist'],
   //   create: ['nlist'],
   //   search: ['nprobe'],
   //   search: ['nprobe'],
@@ -91,9 +93,10 @@ export const FLOAT_INDEX_CONFIG: indexConfigType = {
   //   create: ['out_degree', 'candidate_pool_size', 'search_length', 'knng'],
   //   create: ['out_degree', 'candidate_pool_size', 'search_length', 'knng'],
   //   search: ['search_length'],
   //   search: ['search_length'],
   // },}
   // },}
-}
+};
 
 
 export const BINARY_INDEX_CONFIG: indexConfigType = {
 export const BINARY_INDEX_CONFIG: indexConfigType = {
+  // },
   BIN_FLAT: {
   BIN_FLAT: {
     create: ['nlist'],
     create: ['nlist'],
     search: ['nprobe'],
     search: ['nprobe'],
@@ -120,13 +123,72 @@ export const m_OPTIONS = [
 ];
 ];
 
 
 export const INDEX_OPTIONS_MAP = {
 export const INDEX_OPTIONS_MAP = {
-  FLOAT_INDEX: Object.keys(FLOAT_INDEX_CONFIG).map(v => ({ label: v, value: v })),
-  BINARY_INDEX: Object.keys(BINARY_INDEX_CONFIG).map(v => ({ label: v, value: v })),
+  [DataTypeEnum.FloatVector]: Object.keys(FLOAT_INDEX_CONFIG).map(v => ({
+    label: v,
+    value: v,
+  })),
+  [DataTypeEnum.BinaryVector]: Object.keys(BINARY_INDEX_CONFIG).map(v => ({
+    label: v,
+    value: v,
+  })),
 };
 };
 
 
 export const PRIMARY_KEY_FIELD = 'INT64 (Primary key)';
 export const PRIMARY_KEY_FIELD = 'INT64 (Primary key)';
 
 
-export enum EmbeddingTypeEnum {
-  float = 'FLOAT_INDEX',
-  binary = 'BINARY_INDEX',
-}
+export const METRIC_OPTIONS_MAP = {
+  [DataTypeEnum.FloatVector]: [
+    {
+      value: METRIC_TYPES_VALUES.L2,
+      label: METRIC_TYPES_VALUES.L2,
+    },
+    {
+      value: METRIC_TYPES_VALUES.IP,
+      label: METRIC_TYPES_VALUES.IP,
+    },
+  ],
+  [DataTypeEnum.BinaryVector]: [
+    {
+      value: METRIC_TYPES_VALUES.SUBSTRUCTURE,
+      label: METRIC_TYPES_VALUES.SUBSTRUCTURE,
+    },
+    {
+      value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
+      label: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
+    },
+    {
+      value: METRIC_TYPES_VALUES.HAMMING,
+      label: METRIC_TYPES_VALUES.HAMMING,
+    },
+    {
+      value: METRIC_TYPES_VALUES.JACCARD,
+      label: METRIC_TYPES_VALUES.JACCARD,
+    },
+    {
+      value: METRIC_TYPES_VALUES.TANIMOTO,
+      label: METRIC_TYPES_VALUES.TANIMOTO,
+    },
+  ],
+};
+
+/**
+ * use L2 as float default metric type
+ * use Hamming as binary default metric type
+ */
+export const DEFAULT_METRIC_VALUE_MAP = {
+  [DataTypeEnum.FloatVector]: METRIC_TYPES_VALUES.L2,
+  [DataTypeEnum.BinaryVector]: METRIC_TYPES_VALUES.HAMMING,
+};
+
+// search params default value map
+export const DEFAULT_SEARCH_PARAM_VALUE_MAP: {
+  [key in searchKeywordsType]: number;
+} = {
+  // range: [top_k, 32768]
+  ef: 250,
+  // range: [1, nlist]
+  nprobe: 1,
+  // range: {-1} ∪ [top_k, n × n_trees]
+  search_k: 250,
+  // range: [10, 300]
+  search_length: 10,
+};

+ 6 - 7
client/src/hooks/Navigation.ts

@@ -1,6 +1,5 @@
 import { useContext, useEffect } from 'react';
 import { useContext, useEffect } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-// import { useParams } from 'react-router-dom';
 import { navContext } from '../context/Navigation';
 import { navContext } from '../context/Navigation';
 import { ALL_ROUTER_TYPES, NavInfo } from '../router/Types';
 import { ALL_ROUTER_TYPES, NavInfo } from '../router/Types';
 
 
@@ -10,7 +9,7 @@ export const useNavigationHook = (
     collectionName: string;
     collectionName: string;
   }
   }
 ) => {
 ) => {
-  const { t } = useTranslation('nav');
+  const { t: navTrans } = useTranslation('nav');
   const { setNavInfo } = useContext(navContext);
   const { setNavInfo } = useContext(navContext);
   const { collectionName } = extraParam || { collectionName: '' };
   const { collectionName } = extraParam || { collectionName: '' };
 
 
@@ -18,7 +17,7 @@ export const useNavigationHook = (
     switch (type) {
     switch (type) {
       case ALL_ROUTER_TYPES.OVERVIEW: {
       case ALL_ROUTER_TYPES.OVERVIEW: {
         const navInfo: NavInfo = {
         const navInfo: NavInfo = {
-          navTitle: t('overview'),
+          navTitle: navTrans('overview'),
           backPath: '',
           backPath: '',
         };
         };
         setNavInfo(navInfo);
         setNavInfo(navInfo);
@@ -26,7 +25,7 @@ export const useNavigationHook = (
       }
       }
       case ALL_ROUTER_TYPES.COLLECTIONS: {
       case ALL_ROUTER_TYPES.COLLECTIONS: {
         const navInfo: NavInfo = {
         const navInfo: NavInfo = {
-          navTitle: t('collection'),
+          navTitle: navTrans('collection'),
           backPath: '',
           backPath: '',
         };
         };
         setNavInfo(navInfo);
         setNavInfo(navInfo);
@@ -40,9 +39,9 @@ export const useNavigationHook = (
         setNavInfo(navInfo);
         setNavInfo(navInfo);
         break;
         break;
       }
       }
-      case ALL_ROUTER_TYPES.CONSOLE: {
+      case ALL_ROUTER_TYPES.SEARCH: {
         const navInfo: NavInfo = {
         const navInfo: NavInfo = {
-          navTitle: t('console'),
+          navTitle: navTrans('search'),
           backPath: '',
           backPath: '',
         };
         };
         setNavInfo(navInfo);
         setNavInfo(navInfo);
@@ -51,5 +50,5 @@ export const useNavigationHook = (
       default:
       default:
         break;
         break;
     }
     }
-  }, [type, t, setNavInfo, collectionName]);
+  }, [type, navTrans, setNavInfo, collectionName]);
 };
 };

+ 6 - 0
client/src/http/BaseModel.ts

@@ -78,4 +78,10 @@ export default class BaseModel {
     const res = await http.post(path, data);
     const res = await http.post(path, data);
     return res.data;
     return res.data;
   }
   }
+
+  static async vectorSearch(options: updateParamsType) {
+    const { path, data } = options;
+    const res = await http.post(path, data);
+    return res.data.data;
+  }
 }
 }

+ 8 - 0
client/src/http/Collection.ts

@@ -1,6 +1,7 @@
 import { ChildrenStatusType, StatusEnum } from '../components/status/Types';
 import { ChildrenStatusType, StatusEnum } from '../components/status/Types';
 import { CollectionView, InsertDataParam } from '../pages/collections/Types';
 import { CollectionView, InsertDataParam } from '../pages/collections/Types';
 import { Field } from '../pages/schema/Types';
 import { Field } from '../pages/schema/Types';
+import { VectorSearchParam } from '../pages/seach/Types';
 import { IndexState, ShowCollectionsType } from '../types/Milvus';
 import { IndexState, ShowCollectionsType } from '../types/Milvus';
 import { formatNumber } from '../utils/Common';
 import { formatNumber } from '../utils/Common';
 import BaseModel from './BaseModel';
 import BaseModel from './BaseModel';
@@ -80,6 +81,13 @@ export class CollectionHttp extends BaseModel implements CollectionView {
     });
     });
   }
   }
 
 
+  static vectorSearchData(collectionName: string, params: VectorSearchParam) {
+    return super.vectorSearch({
+      path: `${this.COLLECTIONS_URL}/${collectionName}/search`,
+      data: params,
+    });
+  }
+
   get _autoId() {
   get _autoId() {
     return this.autoID;
     return this.autoID;
   }
   }

+ 4 - 0
client/src/http/Index.ts

@@ -76,4 +76,8 @@ export class IndexHttp extends BaseModel implements IndexView {
   get _fieldName() {
   get _fieldName() {
     return this.field_name;
     return this.field_name;
   }
   }
+
+  get _metricType() {
+    return this.params.find(p => p.key === 'metric_type')?.value || '';
+  }
 }
 }

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

@@ -2,6 +2,7 @@ const navTrans = {
   overview: 'Overview',
   overview: 'Overview',
   collection: 'Collection',
   collection: 'Collection',
   console: 'Search Console',
   console: 'Search Console',
+  search: 'Vector Search',
 };
 };
 
 
 export default navTrans;
 export default navTrans;

+ 15 - 0
client/src/i18n/cn/search.ts

@@ -0,0 +1,15 @@
+const searchTrans = {
+  firstTip: '1. Enter vector value',
+  secondTip: '2. Choose collection and field',
+  thirdTip: '3. Set search parameters',
+  vectorPlaceholder: 'Please input your vector value here, e.g. [1, 2, 3, 4]',
+  collection: 'Choose Collection',
+  field: 'Choose Field',
+  startTip: 'Start your vector search',
+  empty: 'No result',
+  result: 'Search Results',
+  topK: 'TopK {{number}}',
+  filter: 'Advanced Filter',
+};
+
+export default searchTrans;

+ 2 - 0
client/src/i18n/cn/warning.ts

@@ -3,6 +3,8 @@ const warningTrans = {
   positive: '{{name}} should be positive',
   positive: '{{name}} should be positive',
   integer: '{{name}} should be integers',
   integer: '{{name}} should be integers',
   range: 'range is {{min}} ~ {{max}}',
   range: 'range is {{min}} ~ {{max}}',
+  specValueOrRange:
+    '{{name}} should be {{specValue}}, or in range {{min}} ~ {{max}}',
 };
 };
 
 
 export default warningTrans;
 export default warningTrans;

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

@@ -2,6 +2,7 @@ const navTrans = {
   overview: 'Overview',
   overview: 'Overview',
   collection: 'Collection',
   collection: 'Collection',
   console: 'Search Console',
   console: 'Search Console',
+  search: 'Vector Search',
 };
 };
 
 
 export default navTrans;
 export default navTrans;

+ 15 - 0
client/src/i18n/en/search.ts

@@ -0,0 +1,15 @@
+const searchTrans = {
+  firstTip: '1. Enter vector value',
+  secondTip: '2. Choose collection and field',
+  thirdTip: '3. Set search parameters',
+  vectorPlaceholder: 'Please input your vector value here, e.g. [1, 2, 3, 4]',
+  collection: 'Choose Collection',
+  field: 'Choose Field',
+  startTip: 'Start your vector search',
+  empty: 'No result',
+  result: 'Search Results',
+  topK: 'TopK {{number}}',
+  filter: 'Advanced Filter',
+};
+
+export default searchTrans;

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

@@ -3,6 +3,8 @@ const warningTrans = {
   positive: '{{name}} should be positive',
   positive: '{{name}} should be positive',
   integer: '{{name}} should be integers',
   integer: '{{name}} should be integers',
   range: 'range is {{min}} ~ {{max}}',
   range: 'range is {{min}} ~ {{max}}',
+  specValueOrRange:
+    '{{name}} should be {{specValue}}, or in range {{min}} ~ {{max}}',
 };
 };
 
 
 export default warningTrans;
 export default warningTrans;

+ 4 - 0
client/src/i18n/index.ts

@@ -23,6 +23,8 @@ import indexEn from './en/index';
 import indexCn from './cn/index';
 import indexCn from './cn/index';
 import insertEn from './en/insert';
 import insertEn from './en/insert';
 import insertCn from './cn/insert';
 import insertCn from './cn/insert';
+import searchEn from './en/search';
+import searchCn from './cn/search';
 
 
 export const resources = {
 export const resources = {
   cn: {
   cn: {
@@ -37,6 +39,7 @@ export const resources = {
     success: successCn,
     success: successCn,
     index: indexCn,
     index: indexCn,
     insert: insertCn,
     insert: insertCn,
+    search: searchCn,
   },
   },
   en: {
   en: {
     translation: commonEn,
     translation: commonEn,
@@ -50,6 +53,7 @@ export const resources = {
     success: successEn,
     success: successEn,
     index: indexEn,
     index: indexEn,
     insert: insertEn,
     insert: insertEn,
+    search: searchEn,
   },
   },
 };
 };
 
 

+ 4 - 1
client/src/pages/collections/Create.tsx

@@ -160,13 +160,16 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
     const param: CollectionCreateParam = {
     const param: CollectionCreateParam = {
       ...form,
       ...form,
       fields: fields.map(v => {
       fields: fields.map(v => {
-        return {
+        const data: any = {
           name: v.name,
           name: v.name,
           description: v.description,
           description: v.description,
           is_primary_key: v.is_primary_key,
           is_primary_key: v.is_primary_key,
           data_type: v.data_type,
           data_type: v.data_type,
           dimension: vectorType.includes(v.data_type) ? v.dimension : undefined,
           dimension: vectorType.includes(v.data_type) ? v.dimension : undefined,
         };
         };
+
+        v.is_primary_key && (data.autoID = form.autoID);
+        return data;
       }),
       }),
     };
     };
     handleCreate(param);
     handleCreate(param);

+ 1 - 1
client/src/pages/connect/Connect.tsx

@@ -4,7 +4,7 @@ import { ITextfieldConfig } from '../../components/customInput/Types';
 import icons from '../../components/icons/Icons';
 import icons from '../../components/icons/Icons';
 import ConnectContainer from './ConnectContainer';
 import ConnectContainer from './ConnectContainer';
 import CustomInput from '../../components/customInput/CustomInput';
 import CustomInput from '../../components/customInput/CustomInput';
-import { useContext, useEffect, useMemo, useState } from 'react';
+import { useContext, useMemo, useState } from 'react';
 import { formatForm } from '../../utils/Form';
 import { formatForm } from '../../utils/Form';
 import { useFormValidation } from '../../hooks/Form';
 import { useFormValidation } from '../../hooks/Form';
 import CustomButton from '../../components/customButton/CustomButton';
 import CustomButton from '../../components/customButton/CustomButton';

+ 10 - 7
client/src/pages/schema/Create.tsx

@@ -2,7 +2,6 @@ import { useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import DialogTemplate from '../../components/customDialog/DialogTemplate';
 import DialogTemplate from '../../components/customDialog/DialogTemplate';
 import {
 import {
-  EmbeddingTypeEnum,
   INDEX_CONFIG,
   INDEX_CONFIG,
   INDEX_OPTIONS_MAP,
   INDEX_OPTIONS_MAP,
   MetricType,
   MetricType,
@@ -10,6 +9,7 @@ import {
 } from '../../consts/Milvus';
 } from '../../consts/Milvus';
 import { useFormValidation } from '../../hooks/Form';
 import { useFormValidation } from '../../hooks/Form';
 import { formatForm, getMetricOptions } from '../../utils/Form';
 import { formatForm, getMetricOptions } from '../../utils/Form';
+import { getEmbeddingType } from '../../utils/search';
 import { DataType } from '../collections/Types';
 import { DataType } from '../collections/Types';
 import CreateForm from './CreateForm';
 import CreateForm from './CreateForm';
 import { IndexType, ParamPair, INDEX_TYPES_ENUM } from './Types';
 import { IndexType, ParamPair, INDEX_TYPES_ENUM } from './Types';
@@ -26,8 +26,14 @@ const CreateIndex = (props: {
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
 
 
-  const defaultIndexType = fieldType === 'BinaryVector' ? INDEX_TYPES_ENUM.BIN_IVF_FLAT : INDEX_TYPES_ENUM.IVF_FLAT;
-  const defaultMetricType = fieldType === 'BinaryVector' ? METRIC_TYPES_VALUES.HAMMING : METRIC_TYPES_VALUES.L2;
+  const defaultIndexType =
+    fieldType === 'BinaryVector'
+      ? INDEX_TYPES_ENUM.BIN_IVF_FLAT
+      : INDEX_TYPES_ENUM.IVF_FLAT;
+  const defaultMetricType =
+    fieldType === 'BinaryVector'
+      ? METRIC_TYPES_VALUES.HAMMING
+      : METRIC_TYPES_VALUES.L2;
 
 
   const [indexSetting, setIndexSetting] = useState<{
   const [indexSetting, setIndexSetting] = useState<{
     index_type: IndexType;
     index_type: IndexType;
@@ -68,10 +74,7 @@ const CreateIndex = (props: {
   }, [indexCreateParams, indexSetting]);
   }, [indexCreateParams, indexSetting]);
 
 
   const indexOptions = useMemo(() => {
   const indexOptions = useMemo(() => {
-    const type =
-      fieldType === 'BinaryVector'
-        ? EmbeddingTypeEnum.binary
-        : EmbeddingTypeEnum.float;
+    const type = getEmbeddingType(fieldType);
     return INDEX_OPTIONS_MAP[type];
     return INDEX_OPTIONS_MAP[type];
   }, [fieldType]);
   }, [fieldType]);
 
 

+ 0 - 1
client/src/pages/schema/Schema.tsx

@@ -99,7 +99,6 @@ const Schema: FC<{
 
 
       try {
       try {
         const list = await fetchSchemaListWithIndex(collectionName);
         const list = await fetchSchemaListWithIndex(collectionName);
-        console.log(list);
         const fields: FieldView[] = list.map(f =>
         const fields: FieldView[] = list.map(f =>
           Object.assign(f, {
           Object.assign(f, {
             _fieldNameElement: (
             _fieldNameElement: (

+ 2 - 0
client/src/pages/schema/Types.ts

@@ -1,4 +1,5 @@
 import { ReactElement } from 'react';
 import { ReactElement } from 'react';
+import { MetricType } from '../../consts/Milvus';
 import { DataType } from '../collections/Types';
 import { DataType } from '../collections/Types';
 
 
 export enum INDEX_TYPES_ENUM {
 export enum INDEX_TYPES_ENUM {
@@ -47,6 +48,7 @@ export interface IndexView {
   _indexTypeElement?: ReactElement;
   _indexTypeElement?: ReactElement;
   _indexParameterPairs: { key: string; value: string }[];
   _indexParameterPairs: { key: string; value: string }[];
   _indexParamElement?: ReactElement;
   _indexParamElement?: ReactElement;
+  _metricType?: MetricType | string;
 }
 }
 
 
 export type IndexType =
 export type IndexType =

+ 1 - 0
client/src/pages/seach/Constants.ts

@@ -0,0 +1 @@
+export const TOP_K_OPTIONS = [50, 100, 150, 200, 250];

+ 259 - 0
client/src/pages/seach/SearchParams.tsx

@@ -0,0 +1,259 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { FC, useCallback, useEffect, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import CustomInput from '../../components/customInput/CustomInput';
+import { ITextfieldConfig } from '../../components/customInput/Types';
+import CustomSelector from '../../components/customSelector/CustomSelector';
+import { Option } from '../../components/customSelector/Types';
+import {
+  DEFAULT_SEARCH_PARAM_VALUE_MAP,
+  INDEX_CONFIG,
+  METRIC_OPTIONS_MAP,
+  searchKeywordsType,
+} from '../../consts/Milvus';
+import { useFormValidation } from '../../hooks/Form';
+import { formatForm } from '../../utils/Form';
+import { SearchParamInputConfig, SearchParamsProps } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  selector: {
+    width: '100%',
+    marginTop: theme.spacing(2),
+  },
+  input: {
+    marginTop: theme.spacing(2),
+  },
+}));
+
+const SearchParams: FC<SearchParamsProps> = ({
+  indexType,
+  indexParams,
+  searchParamsForm,
+  handleFormChange,
+  embeddingType,
+  metricType,
+  topK,
+  setParamsDisabled,
+  wrapperClass = '',
+}) => {
+  const { t: indexTrans } = useTranslation('index');
+  const { t: warningTrans } = useTranslation('warning');
+  const classes = getStyles();
+
+  const metricOptions: Option[] = METRIC_OPTIONS_MAP[embeddingType];
+
+  // search params key list, depends on index type
+  // e.g. ['nprobe']
+  const searchParams = useMemo(
+    () => (indexType !== '' ? INDEX_CONFIG[indexType].search : []),
+    [indexType]
+  );
+
+  const handleInputChange = useCallback(
+    (key: string, value: number) => {
+      const form = { ...searchParamsForm, [key]: value };
+      handleFormChange(form);
+    },
+    [handleFormChange, searchParamsForm]
+  );
+
+  /**
+   * function to transfer search params to CustomInput need config type
+   */
+  const getNumberInputConfig = useCallback(
+    (params: SearchParamInputConfig): ITextfieldConfig => {
+      const {
+        label,
+        key,
+        min,
+        max,
+        value,
+        handleChange,
+        isInt = true,
+      } = params;
+
+      // search_k range is special compared to others,need to be handled separately
+      // range: {-1} ∪ [top_k, n × n_trees]
+      const isSearchK = label === 'search_k';
+
+      const config: ITextfieldConfig = {
+        label,
+        key,
+        onChange: value => {
+          handleChange(value);
+        },
+        className: classes.input,
+        variant: 'filled',
+        type: 'number',
+        value,
+        validations: [
+          {
+            rule: 'require',
+            errorText: warningTrans('required', { name: label }),
+          },
+        ],
+      };
+      if (!isSearchK && min && max) {
+        config.validations?.push({
+          rule: 'range',
+          errorText: warningTrans('range', { min, max }),
+          extraParam: {
+            min,
+            max,
+            type: 'number',
+          },
+        });
+      }
+
+      if (isInt) {
+        config.validations?.push({
+          rule: 'integer',
+          errorText: warningTrans('integer', { name: label }),
+        });
+      }
+
+      // search_k
+      if (isSearchK) {
+        config.validations?.push({
+          rule: 'specValueOrRange',
+          errorText: warningTrans('specValueOrRange', {
+            name: label,
+            min,
+            max,
+            specValue: -1,
+          }),
+          extraParam: {
+            min,
+            max,
+            compareValue: -1,
+            type: 'number',
+          },
+        });
+      }
+      return config;
+    },
+    [warningTrans, classes.input]
+  );
+
+  const getSearchInputConfig = useCallback(
+    (paramKey: searchKeywordsType): ITextfieldConfig => {
+      const nlist = Number(
+        indexParams.find(p => p.key === 'nlist')?.value || 0
+      );
+
+      const configParamMap: {
+        [key in searchKeywordsType]: SearchParamInputConfig;
+      } = {
+        nprobe: {
+          label: 'nprobe',
+          key: 'nprobe',
+          value: searchParamsForm['nprobe'] || '',
+          min: 1,
+          max: nlist,
+          isInt: true,
+          handleChange: value => {
+            handleInputChange('nprobe', value);
+          },
+        },
+        ef: {
+          label: 'ef',
+          key: 'ef',
+          value: searchParamsForm['ef'] || '',
+          min: topK,
+          max: 32768,
+          isInt: true,
+          handleChange: value => {
+            handleInputChange('ef', value);
+          },
+        },
+        search_k: {
+          label: 'search_k',
+          key: 'search_k',
+          value: searchParamsForm['search_k'] || '',
+          min: topK,
+          // n * n_trees can be infinity
+          max: Infinity,
+          isInt: true,
+          handleChange: value => {
+            handleInputChange('search_k', value);
+          },
+        },
+        search_length: {
+          label: 'search_length',
+          key: 'search_length',
+          value: searchParamsForm['search_length'] || '',
+          min: 10,
+          max: 300,
+          isInt: true,
+          handleChange: value => {
+            handleInputChange('search_length', value);
+          },
+        },
+      };
+
+      const param = configParamMap[paramKey];
+      return getNumberInputConfig(param);
+    },
+    [
+      indexParams,
+      topK,
+      searchParamsForm,
+      getNumberInputConfig,
+      handleInputChange,
+    ]
+  );
+
+  useEffect(() => {
+    // generate different form according to search params
+    const form = searchParams.reduce(
+      (paramsForm, param) => ({
+        ...paramsForm,
+        [param]: DEFAULT_SEARCH_PARAM_VALUE_MAP[param],
+      }),
+      {}
+    );
+    handleFormChange(form);
+  }, [searchParams, handleFormChange]);
+
+  const checkedForm = useMemo(() => {
+    const { ...needCheckItems } = searchParamsForm;
+    return formatForm(needCheckItems);
+  }, [searchParamsForm]);
+
+  const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
+
+  useEffect(() => {
+    setParamsDisabled(disabled);
+  }, [disabled, setParamsDisabled]);
+
+  return (
+    <div className={wrapperClass}>
+      {/* metric type */}
+      <CustomSelector
+        options={metricOptions}
+        value={metricType}
+        label={indexTrans('metric')}
+        wrapperClass={classes.selector}
+        variant="filled"
+        onChange={(e: { target: { value: unknown } }) => {
+          // not selectable now, so not set onChange event
+        }}
+        // not selectable now
+        readOnly={true}
+      />
+
+      {/* dynamic params, now every type only has one param except metric type */}
+      {searchParams.map(param => (
+        <CustomInput
+          key={param}
+          type="text"
+          textConfig={getSearchInputConfig(param)}
+          checkValid={checkIsValid}
+          validInfo={validation}
+        />
+      ))}
+    </div>
+  );
+};
+
+export default SearchParams;

+ 119 - 0
client/src/pages/seach/Styles.ts

@@ -0,0 +1,119 @@
+import { makeStyles, Theme } from '@material-ui/core';
+
+export const getVectorSearchStyles = makeStyles((theme: Theme) => ({
+  form: {
+    display: 'flex',
+    justifyContent: 'space-between',
+
+    '& .field': {
+      display: 'flex',
+      flexDirection: 'column',
+      flexBasis: '33%',
+
+      padding: theme.spacing(2, 3, 3),
+      backgroundColor: '#fff',
+      borderRadius: theme.spacing(0.5),
+      boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
+
+      '& .textarea': {
+        border: `1px solid ${theme.palette.milvusGrey.main}`,
+        borderRadius: theme.spacing(0.5),
+        padding: theme.spacing(1),
+        paddingBottom: '18px',
+        marginTop: theme.spacing(2),
+      },
+
+      // reset default style
+      '& .textfield': {
+        padding: 0,
+        fontSize: '14px',
+        lineHeight: '20px',
+        fontWeight: 400,
+
+        '&::before': {
+          borderBottom: 'none',
+        },
+
+        '&::after': {
+          borderBottom: 'none',
+        },
+      },
+
+      '& .multiline': {
+        '& textarea': {
+          overflow: 'auto',
+          // change scrollbar style
+          '&::-webkit-scrollbar': {
+            width: '8px',
+          },
+
+          '&::-webkit-scrollbar-track': {
+            backgroundColor: '#f9f9f9',
+          },
+
+          '&::-webkit-scrollbar-thumb': {
+            borderRadius: '8px',
+            backgroundColor: '#eee',
+          },
+        },
+      },
+    },
+
+    '& .field-second': {
+      flexGrow: 1,
+      margin: theme.spacing(0, 1),
+    },
+
+    '& .text': {
+      color: theme.palette.milvusGrey.dark,
+      fontWeight: 500,
+    },
+  },
+  selector: {
+    width: '100%',
+    marginTop: theme.spacing(2),
+  },
+  paramsWrapper: {
+    display: 'flex',
+    flexDirection: 'column',
+  },
+  toolbar: {
+    display: 'flex',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+
+    padding: theme.spacing(2, 0),
+
+    '& .left': {
+      display: 'flex',
+      alignItems: 'center',
+
+      '& .text': {
+        color: theme.palette.milvusGrey.main,
+      },
+    },
+    '& .right': {
+      '& .btn': {
+        marginRight: theme.spacing(1),
+      },
+      '& .icon': {
+        fontSize: '16px',
+      },
+    },
+  },
+  menuLabel: {
+    minWidth: '108px',
+
+    padding: theme.spacing(0, 1),
+    margin: theme.spacing(0, 1),
+
+    backgroundColor: '#fff',
+    color: theme.palette.milvusGrey.dark,
+  },
+  menuItem: {
+    fontWeight: 500,
+    fontSize: '12px',
+    lineHeight: '16px',
+    color: theme.palette.milvusGrey.dark,
+  },
+}));

+ 62 - 0
client/src/pages/seach/Types.ts

@@ -0,0 +1,62 @@
+import { Option } from '../../components/customSelector/Types';
+import { searchKeywordsType } from '../../consts/Milvus';
+import { DataType, DataTypeEnum } from '../collections/Types';
+import { IndexView } from '../schema/Types';
+
+export interface SearchParamsProps {
+  // if user created index, pass metric type choosed when creating
+  // else pass empty string
+  metricType: string;
+  // used for getting metric type options
+  embeddingType: DataTypeEnum.FloatVector | DataTypeEnum.BinaryVector;
+  // default index type is FLAT
+  indexType: string;
+  // index extra params, e.g. nlist
+  indexParams: { key: string; value: string }[];
+  searchParamsForm: {
+    [key in string]: number;
+  };
+  topK: number;
+  handleFormChange: (form: { [key in string]: number }) => void;
+  wrapperClass?: string;
+  setParamsDisabled: (isDisabled: boolean) => void;
+}
+
+export interface SearchResultView {
+  // dynamic field names
+  [key: string]: string | number;
+  rank: number;
+  distance: number;
+}
+
+export interface FieldOption extends Option {
+  fieldType: DataType;
+  // used to get metric type, index type and index params for search params
+  // if user doesn't create index, default value is null
+  indexInfo: IndexView | null;
+}
+
+export interface SearchParamInputConfig {
+  label: string;
+  key: searchKeywordsType;
+  min: number;
+  max: number;
+  isInt?: boolean;
+  // no value: empty string
+  value: number | string;
+  handleChange: (value: number) => void;
+}
+
+export interface VectorSearchParam {
+  expr?: string;
+  search_params: { key: string; value: string | number }[];
+  vectors: any;
+  output_fields: string[];
+  vector_type: number | DataTypeEnum;
+}
+
+export interface SearchResult {
+  // dynamic field names
+  [key: string]: string | number;
+  score: number;
+}

+ 422 - 0
client/src/pages/seach/VectorSearch.tsx

@@ -0,0 +1,422 @@
+import { TextField, Typography } from '@material-ui/core';
+import { useTranslation } from 'react-i18next';
+import { useNavigationHook } from '../../hooks/Navigation';
+import { ALL_ROUTER_TYPES } from '../../router/Types';
+import CustomSelector from '../../components/customSelector/CustomSelector';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import SearchParams from './SearchParams';
+import { DEFAULT_METRIC_VALUE_MAP } from '../../consts/Milvus';
+import { FieldOption, SearchResultView, VectorSearchParam } from './Types';
+import MilvusGrid from '../../components/grid/Grid';
+import EmptyCard from '../../components/cards/EmptyCard';
+import icons from '../../components/icons/Icons';
+import { usePaginationHook } from '../../hooks/Pagination';
+import CustomButton from '../../components/customButton/CustomButton';
+import SimpleMenu from '../../components/menu/SimpleMenu';
+import { TOP_K_OPTIONS } from './Constants';
+import { Option } from '../../components/customSelector/Types';
+import { CollectionHttp } from '../../http/Collection';
+import { CollectionData, DataTypeEnum } from '../collections/Types';
+import { IndexHttp } from '../../http/Index';
+import { getVectorSearchStyles } from './Styles';
+import { parseValue } from '../../utils/Insert';
+import {
+  classifyFields,
+  getDefaultIndexType,
+  getEmbeddingType,
+  getNonVectorFieldsForFilter,
+  getVectorFieldOptions,
+  transferSearchResult,
+} from '../../utils/search';
+import { ColDefinitionsType } from '../../components/grid/Types';
+import Filter from '../../components/advancedSearch';
+import { Field } from '../../components/advancedSearch/Types';
+
+const VectorSearch = () => {
+  useNavigationHook(ALL_ROUTER_TYPES.SEARCH);
+  const { t: searchTrans } = useTranslation('search');
+  const { t: btnTrans } = useTranslation('btn');
+  const classes = getVectorSearchStyles();
+
+  // data stored inside the component
+  const [tableLoading, setTableLoading] = useState<boolean>(false);
+  const [collections, setCollections] = useState<CollectionData[]>([]);
+  const [selectedCollection, setSelectedCollection] = useState<string>('');
+  const [fieldOptions, setFieldOptions] = useState<FieldOption[]>([]);
+  // fields for advanced filter
+  const [filterFields, setFilterFields] = useState<Field[]>([]);
+  const [selectedField, setSelectedField] = useState<string>('');
+  // search params form
+  const [searchParam, setSearchParam] = useState<{ [key in string]: number }>(
+    {}
+  );
+  // search params disable state
+  const [paramDisabled, setParamDisabled] = useState<boolean>(true);
+  // use null as init value before search, empty array means no results
+  const [searchResult, setSearchResult] = useState<SearchResultView[] | null>(
+    null
+  );
+  // default topK is 100
+  const [topK, setTopK] = useState<number>(100);
+  const [expression, setExpression] = useState<string>('');
+  const [vectors, setVectors] = useState<string>('');
+
+  const {
+    pageSize,
+    handlePageSize,
+    currentPage,
+    handleCurrentPage,
+    total,
+    data: result,
+  } = usePaginationHook(searchResult || []);
+
+  const searchDisabled = useMemo(() => {
+    /**
+     * before search, user must:
+     * 1. enter vector value
+     * 2. choose collection and field
+     * 3. set extra search params
+     */
+    const isInvalid =
+      vectors === '' ||
+      selectedCollection === '' ||
+      selectedField === '' ||
+      paramDisabled;
+    return isInvalid;
+  }, [paramDisabled, selectedField, selectedCollection, vectors]);
+
+  const collectionOptions: Option[] = useMemo(
+    () =>
+      collections.map(c => ({
+        label: c._name,
+        value: c._name,
+      })),
+    [collections]
+  );
+
+  const outputFields: string[] = useMemo(() => {
+    const fields =
+      collections.find(c => c._name === selectedCollection)?._fields || [];
+    // vector field can't be output fields
+    const invalidTypes = ['BinaryVector', 'FloatVector'];
+    const nonVectorFields = fields.filter(
+      field => !invalidTypes.includes(field._fieldType)
+    );
+    return nonVectorFields.map(f => f._fieldName);
+  }, [selectedCollection, collections]);
+
+  const colDefinitions: ColDefinitionsType[] = useMemo(() => {
+    // filter id and score
+    return searchResult && searchResult.length > 0
+      ? Object.keys(searchResult[0])
+          .filter(item => item !== 'id' && item !== 'score')
+          .map(key => ({
+            id: key,
+            align: 'left',
+            disablePadding: false,
+            label: key,
+          }))
+      : [];
+  }, [searchResult]);
+
+  const { metricType, indexType, indexParams, fieldType, embeddingType } =
+    useMemo(() => {
+      if (selectedField !== '') {
+        // field options must contain selected field, so selectedFieldInfo will never undefined
+        const selectedFieldInfo = fieldOptions.find(
+          f => f.value === selectedField
+        );
+        const index = selectedFieldInfo?.indexInfo;
+        const embeddingType = getEmbeddingType(selectedFieldInfo!.fieldType);
+        const metric =
+          index?._metricType || DEFAULT_METRIC_VALUE_MAP[embeddingType];
+        const indexParams = index?._indexParameterPairs || [];
+
+        return {
+          metricType: metric,
+          indexType: index?._indexType || getDefaultIndexType(embeddingType),
+          indexParams,
+          fieldType: DataTypeEnum[selectedFieldInfo?.fieldType!],
+          embeddingType,
+        };
+      }
+
+      return {
+        metricType: '',
+        indexType: '',
+        indexParams: [],
+        fieldType: 0,
+        embeddingType: DataTypeEnum.FloatVector,
+      };
+    }, [selectedField, fieldOptions]);
+
+  // fetch data
+  const fetchCollections = useCallback(async () => {
+    const collections = await CollectionHttp.getCollections();
+    setCollections(collections);
+  }, []);
+
+  const fetchFieldsWithIndex = useCallback(
+    async (collectionName: string, collections: CollectionData[]) => {
+      const fields =
+        collections.find(c => c._name === collectionName)?._fields || [];
+      const indexes = await IndexHttp.getIndexInfo(collectionName);
+
+      const { vectorFields, nonVectorFields } = classifyFields(fields);
+
+      // only vector type fields can be select
+      const fieldOptions = getVectorFieldOptions(vectorFields, indexes);
+      setFieldOptions(fieldOptions);
+      // only non vector type fields can be advanced filter
+      const filterFields = getNonVectorFieldsForFilter(nonVectorFields);
+      setFilterFields(filterFields);
+    },
+    []
+  );
+
+  useEffect(() => {
+    fetchCollections();
+  }, [fetchCollections]);
+
+  // get field options with index when selected collection changed
+  useEffect(() => {
+    if (selectedCollection !== '') {
+      fetchFieldsWithIndex(selectedCollection, collections);
+    }
+  }, [selectedCollection, collections, fetchFieldsWithIndex]);
+
+  // icons
+  const VectorSearchIcon = icons.vectorSearch;
+  const ResetIcon = icons.refresh;
+  const ArrowIcon = icons.dropdown;
+
+  // methods
+  const handlePageChange = (e: any, page: number) => {
+    handleCurrentPage(page);
+  };
+  const handleReset = () => {
+    /**
+     * reset search includes:
+     * 1. reset vectors
+     * 2. reset selected collection and field
+     * 3. reset search params
+     * 4. reset advanced filter expression
+     * 5. clear search result
+     */
+    setVectors('');
+    setSelectedField('');
+    setSelectedCollection('');
+    setSearchResult(null);
+    setFilterFields([]);
+    setExpression('');
+  };
+  const handleSearch = async (topK: number, expr = expression) => {
+    const searhParamPairs = [
+      // dynamic search params
+      {
+        key: 'params',
+        value: JSON.stringify(searchParam),
+      },
+      {
+        key: 'anns_field',
+        value: selectedField,
+      },
+      {
+        key: 'topk',
+        value: topK,
+      },
+      {
+        key: 'metric_type',
+        value: metricType,
+      },
+    ];
+
+    const params: VectorSearchParam = {
+      output_fields: outputFields,
+      expr,
+      search_params: searhParamPairs,
+      vectors: [parseValue(vectors)],
+      vector_type: fieldType,
+    };
+
+    setTableLoading(true);
+    try {
+      const res = await CollectionHttp.vectorSearchData(
+        selectedCollection,
+        params
+      );
+      setTableLoading(false);
+
+      const result = transferSearchResult(res.results);
+      setSearchResult(result);
+    } catch (err) {
+      setTableLoading(false);
+    }
+  };
+  const handleAdvancedFilterChange = (expression: string) => {
+    setExpression(expression);
+    if (!searchDisabled) {
+      handleSearch(topK, expression);
+    }
+  };
+
+  const handleVectorChange = (value: string) => {
+    setVectors(value);
+  };
+
+  return (
+    <section className="page-wrapper">
+      {/* form section */}
+      <form className={classes.form}>
+        {/* vector value textarea */}
+        <fieldset className="field">
+          <Typography className="text">{searchTrans('firstTip')}</Typography>
+          <TextField
+            className="textarea"
+            InputProps={{
+              classes: {
+                root: 'textfield',
+                multiline: 'multiline',
+              },
+            }}
+            multiline
+            rows={5}
+            placeholder={searchTrans('vectorPlaceholder')}
+            value={vectors}
+            onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
+              handleVectorChange(e.target.value as string);
+            }}
+          />
+        </fieldset>
+        {/* collection and field selectors */}
+        <fieldset className="field field-second">
+          <Typography className="text">{searchTrans('secondTip')}</Typography>
+          <CustomSelector
+            options={collectionOptions}
+            wrapperClass={classes.selector}
+            variant="filled"
+            label={searchTrans('collection')}
+            value={selectedCollection}
+            onChange={(e: { target: { value: unknown } }) => {
+              const collection = e.target.value;
+              setSelectedCollection(collection as string);
+              // every time selected collection changed, reset field
+              setSelectedField('');
+            }}
+          />
+          <CustomSelector
+            options={fieldOptions}
+            readOnly={selectedCollection === ''}
+            wrapperClass={classes.selector}
+            variant="filled"
+            label={searchTrans('field')}
+            value={selectedField}
+            onChange={(e: { target: { value: unknown } }) => {
+              const field = e.target.value;
+              setSelectedField(field as string);
+            }}
+          />
+        </fieldset>
+        {/* search params selectors */}
+        <fieldset className="field">
+          <Typography className="text">{searchTrans('thirdTip')}</Typography>
+          <SearchParams
+            wrapperClass={classes.paramsWrapper}
+            metricType={metricType!}
+            embeddingType={
+              embeddingType as
+                | DataTypeEnum.BinaryVector
+                | DataTypeEnum.FloatVector
+            }
+            indexType={indexType}
+            indexParams={indexParams!}
+            searchParamsForm={searchParam}
+            handleFormChange={setSearchParam}
+            topK={topK}
+            setParamsDisabled={setParamDisabled}
+          />
+        </fieldset>
+      </form>
+
+      {/**
+       * search toolbar section
+       * including topK selector, advanced filter, search and reset btn
+       */}
+      <section className={classes.toolbar}>
+        <div className="left">
+          <Typography variant="h5" className="text">
+            {`${searchTrans('result')}: `}
+          </Typography>
+          {/* topK selector */}
+          <SimpleMenu
+            label={searchTrans('topK', { number: topK })}
+            menuItems={TOP_K_OPTIONS.map(item => ({
+              label: item.toString(),
+              callback: () => {
+                setTopK(item);
+                if (!searchDisabled) {
+                  handleSearch(item);
+                }
+              },
+              wrapperClass: classes.menuItem,
+            }))}
+            buttonProps={{
+              className: classes.menuLabel,
+              endIcon: <ArrowIcon />,
+            }}
+            menuItemWidth="108px"
+          />
+
+          <Filter
+            title="Advanced Filter"
+            fields={filterFields}
+            filterDisabled={selectedField === '' || selectedCollection === ''}
+            onSubmit={handleAdvancedFilterChange}
+          />
+        </div>
+        <div className="right">
+          <CustomButton className="btn" onClick={handleReset}>
+            <ResetIcon classes={{ root: 'icon' }} />
+            {btnTrans('reset')}
+          </CustomButton>
+          <CustomButton
+            variant="contained"
+            disabled={searchDisabled}
+            onClick={() => handleSearch(topK)}
+          >
+            {btnTrans('search')}
+          </CustomButton>
+        </div>
+      </section>
+
+      {/* search result table section */}
+      {(searchResult && searchResult.length > 0) || tableLoading ? (
+        <MilvusGrid
+          toolbarConfigs={[]}
+          colDefinitions={colDefinitions}
+          rows={result}
+          rowCount={total}
+          primaryKey="rank"
+          page={currentPage}
+          onChangePage={handlePageChange}
+          rowsPerPage={pageSize}
+          setRowsPerPage={handlePageSize}
+          openCheckBox={false}
+          isLoading={tableLoading}
+        />
+      ) : (
+        <EmptyCard
+          wrapperClass={`page-empty-card`}
+          icon={<VectorSearchIcon />}
+          text={
+            searchResult !== null
+              ? searchTrans('empty')
+              : searchTrans('startTip')
+          }
+        />
+      )}
+    </section>
+  );
+};
+
+export default VectorSearch;

+ 6 - 0
client/src/router/Config.ts

@@ -2,6 +2,7 @@ import Collection from '../pages/collections/Collection';
 import Collections from '../pages/collections/Collections';
 import Collections from '../pages/collections/Collections';
 import Connect from '../pages/connect/Connect';
 import Connect from '../pages/connect/Connect';
 import Overview from '../pages/overview/Overview';
 import Overview from '../pages/overview/Overview';
+import VectorSearch from '../pages/seach/VectorSearch';
 import { RouterConfigType } from './Types';
 import { RouterConfigType } from './Types';
 
 
 const RouterConfig: RouterConfigType[] = [
 const RouterConfig: RouterConfigType[] = [
@@ -25,6 +26,11 @@ const RouterConfig: RouterConfigType[] = [
     component: Collection,
     component: Collection,
     auth: true,
     auth: true,
   },
   },
+  {
+    path: '/search',
+    component: VectorSearch,
+    auth: true,
+  },
 ];
 ];
 
 
 export default RouterConfig;
 export default RouterConfig;

+ 2 - 2
client/src/router/Types.ts

@@ -5,8 +5,8 @@ export enum ALL_ROUTER_TYPES {
   COLLECTIONS = 'collections',
   COLLECTIONS = 'collections',
   // '/collections/:collectionId'
   // '/collections/:collectionId'
   COLLECTION_DETAIL = 'collection_detail',
   COLLECTION_DETAIL = 'collection_detail',
-  // '/console'
-  CONSOLE = 'console',
+  // 'search'
+  SEARCH = 'search',
 }
 }
 
 
 export type NavInfo = {
 export type NavInfo = {

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

@@ -44,3 +44,20 @@ export const findKeyValue = (
   obj: { key: string; value: string }[],
   obj: { key: string; value: string }[],
   key: string
   key: string
 ) => obj.find(v => v.key === key)?.value;
 ) => obj.find(v => v.key === key)?.value;
+
+export const generateHashCode = (source: string) => {
+  let hash = 0,
+    i,
+    chr;
+  if (source.length === 0) return hash;
+  for (i = 0; i < source.length; i++) {
+    chr = source.charCodeAt(i);
+    hash = (hash << 5) - hash + chr;
+    hash |= 0; // Convert to 32bit integer
+  }
+  return hash.toString();
+};
+
+export const generateIdByHash = (salt?: string) => {
+  return generateHashCode(`${new Date().getTime().toString()}-${salt}`);
+};

+ 18 - 7
client/src/utils/Format.ts

@@ -74,18 +74,29 @@ export const getEnumKeyByValue = (enumObj: any, enumValue: any) => {
   return '--';
   return '--';
 };
 };
 
 
+/**
+ *
+ * @param obj e.g. {name: 'test'}
+ * @returns key value pair, e.g. [{key: 'name', value: 'test'}]
+ */
+export const getKeyValuePairFromObj = (
+  obj: { [key in string]: any }
+): { key: string; value: any }[] => {
+  const pairs: { key: string; value: string }[] = Object.entries(obj).map(
+    ([key, value]) => ({
+      key,
+      value: value as string,
+    })
+  );
+  return pairs;
+};
+
 export const getKeyValueListFromJsonString = (
 export const getKeyValueListFromJsonString = (
   json: string
   json: string
 ): { key: string; value: string }[] => {
 ): { key: string; value: string }[] => {
   try {
   try {
     const obj = JSON.parse(json);
     const obj = JSON.parse(json);
-
-    const pairs: { key: string; value: string }[] = Object.entries(obj).map(
-      ([key, value]) => ({
-        key,
-        value: value as string,
-      })
-    );
+    const pairs = getKeyValuePairFromObj(obj);
 
 
     return pairs;
     return pairs;
   } catch (err) {
   } catch (err) {

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

@@ -26,7 +26,7 @@ const replaceKeysByIndex = (obj: any, newKeys: string[]) => {
   return Object.assign({}, ...keyValues);
   return Object.assign({}, ...keyValues);
 };
 };
 
 
-const parseValue = (value: string) => {
+export const parseValue = (value: string) => {
   try {
   try {
     return JSON.parse(value);
     return JSON.parse(value);
   } catch (err) {
   } catch (err) {

+ 29 - 5
client/src/utils/Validation.ts

@@ -14,15 +14,16 @@ export type ValidType =
   | 'dimension'
   | 'dimension'
   | 'multiple'
   | 'multiple'
   | 'partitionName'
   | 'partitionName'
-  | 'firstCharacter';
+  | 'firstCharacter'
+  | 'specValueOrRange';
 export interface ICheckMapParam {
 export interface ICheckMapParam {
   value: string;
   value: string;
   extraParam?: IExtraParam;
   extraParam?: IExtraParam;
   rule: ValidType;
   rule: ValidType;
 }
 }
 export interface IExtraParam {
 export interface IExtraParam {
-  // used for confirm type
-  compareValue?: string;
+  // used for confirm or any compare type
+  compareValue?: string | number;
   // used for length type
   // used for length type
   min?: number;
   min?: number;
   max?: number;
   max?: number;
@@ -64,13 +65,13 @@ export const checkPasswordStrength = (value: string): boolean => {
 };
 };
 
 
 export const checkRange = (param: {
 export const checkRange = (param: {
-  value: string;
+  value: string | number;
   min?: number;
   min?: number;
   max?: number;
   max?: number;
   type?: 'string' | 'number';
   type?: 'string' | 'number';
 }): boolean => {
 }): boolean => {
   const { value, min = 0, max = 0, type } = param;
   const { value, min = 0, max = 0, type } = param;
-  const length = type === 'number' ? Number(value) : value.length;
+  const length = type === 'number' ? Number(value) : (value as string).length;
 
 
   let result = true;
   let result = true;
   const conditionMap = {
   const conditionMap = {
@@ -181,6 +182,23 @@ export const checkDimension = (param: {
   return checkMultiple({ value, multipleNumber });
   return checkMultiple({ value, multipleNumber });
 };
 };
 
 
+/**
+ * function to check whether value(type: number) is equal to specified value or in valid range
+ * @param param specValue and params checkRange function needed
+ * @returns whether input is valid
+ */
+export const checkSpecValueOrRange = (param: {
+  value: number;
+  min: number;
+  max: number;
+  compareValue: number;
+}): boolean => {
+  const { value, min, max, compareValue } = param;
+  return (
+    value === compareValue || checkRange({ min, max, value, type: 'number' })
+  );
+};
+
 export const getCheckResult = (param: ICheckMapParam): boolean => {
 export const getCheckResult = (param: ICheckMapParam): boolean => {
   const { value, extraParam = {}, rule } = param;
   const { value, extraParam = {}, rule } = param;
   const numberValue = Number(value);
   const numberValue = Number(value);
@@ -215,6 +233,12 @@ export const getCheckResult = (param: ICheckMapParam): boolean => {
       value,
       value,
       invalidTypes: extraParam?.invalidTypes,
       invalidTypes: extraParam?.invalidTypes,
     }),
     }),
+    specValueOrRange: checkSpecValueOrRange({
+      value: Number(value),
+      min: extraParam?.min || 0,
+      max: extraParam?.max || 0,
+      compareValue: Number(extraParam.compareValue) || 0,
+    }),
   };
   };
 
 
   return checkMap[rule];
   return checkMap[rule];

+ 104 - 0
client/src/utils/search.ts

@@ -0,0 +1,104 @@
+import { Field } from '../components/advancedSearch/Types';
+import { DataType, DataTypeEnum } from '../pages/collections/Types';
+import {
+  FieldData,
+  IndexType,
+  IndexView,
+  INDEX_TYPES_ENUM,
+} from '../pages/schema/Types';
+import {
+  FieldOption,
+  SearchResult,
+  SearchResultView,
+} from '../pages/seach/Types';
+
+export const transferSearchResult = (
+  result: SearchResult[]
+): SearchResultView[] => {
+  const resultView = result
+    .sort((a, b) => a.score - b.score)
+    .map((r, index) => ({
+      rank: index + 1,
+      distance: r.score,
+      ...r,
+    }));
+
+  return resultView;
+};
+
+/**
+ * function to get EmbeddingType
+ * @param fieldType only vector type fields: 'BinaryVector' or 'FloatVector'
+ */
+export const getEmbeddingType = (
+  fieldType: DataType
+): DataTypeEnum.BinaryVector | DataTypeEnum.FloatVector => {
+  const type =
+    fieldType === 'BinaryVector'
+      ? DataTypeEnum.BinaryVector
+      : DataTypeEnum.FloatVector;
+  return type;
+};
+
+/**
+ * function to get default index type according to embedding type
+ * use FLAT as default float index type, BIN_FLAT as default binary index type
+ * @param embeddingType float or binary
+ * @returns index type
+ */
+export const getDefaultIndexType = (embeddingType: DataTypeEnum): IndexType => {
+  const defaultIndexType =
+    embeddingType === DataTypeEnum.FloatVector
+      ? INDEX_TYPES_ENUM.FLAT
+      : INDEX_TYPES_ENUM.BIN_FLAT;
+  return defaultIndexType;
+};
+
+/**
+ * funtion to divide fields into two categories: vector or nonVector
+ */
+export const classifyFields = (
+  fields: FieldData[]
+): { vectorFields: FieldData[]; nonVectorFields: FieldData[] } => {
+  const vectorTypes: DataType[] = ['BinaryVector', 'FloatVector'];
+  return fields.reduce(
+    (result, cur) => {
+      const changedFieldType = vectorTypes.includes(cur._fieldType)
+        ? 'vectorFields'
+        : 'nonVectorFields';
+
+      result[changedFieldType].push(cur);
+
+      return result;
+    },
+    { vectorFields: [] as FieldData[], nonVectorFields: [] as FieldData[] }
+  );
+};
+
+export const getVectorFieldOptions = (
+  fields: FieldData[],
+  indexes: IndexView[]
+): FieldOption[] => {
+  const options: FieldOption[] = fields.map(f => {
+    const embeddingType = getEmbeddingType(f._fieldType);
+    const defaultIndex = getDefaultIndexType(embeddingType);
+    const index = indexes.find(i => i._fieldName === f._fieldName);
+
+    return {
+      label: `${f._fieldName} (${index?._indexType || defaultIndex})`,
+      value: f._fieldName,
+      fieldType: f._fieldType,
+      indexInfo: index || null,
+    };
+  });
+
+  return options;
+};
+
+export const getNonVectorFieldsForFilter = (fields: FieldData[]): Field[] => {
+  const intTypes: DataType[] = ['Int8', 'Int16', 'Int32', 'Int64'];
+  return fields.map(f => ({
+    name: f._fieldName,
+    type: intTypes.includes(f._fieldType) ? 'int' : 'float',
+  }));
+};

+ 1 - 1
server/package.json

@@ -32,7 +32,7 @@
     "@nestjs/websockets": "^8.0.4",
     "@nestjs/websockets": "^8.0.4",
     "@types/passport-jwt": "^3.0.5",
     "@types/passport-jwt": "^3.0.5",
     "@types/passport-local": "^1.0.33",
     "@types/passport-local": "^1.0.33",
-    "@zilliz/milvus2-sdk-node": "^1.0.3",
+    "@zilliz/milvus2-sdk-node": "^1.0.4",
     "body-parser": "^1.19.0",
     "body-parser": "^1.19.0",
     "cache-manager": "^3.4.4",
     "cache-manager": "^3.4.4",
     "class-transformer": "^0.4.0",
     "class-transformer": "^0.4.0",

+ 11 - 3
server/src/collections/collections.controller.ts

@@ -29,18 +29,26 @@ import { cacheKeys } from '../cache/config';
 @ApiTags('collections')
 @ApiTags('collections')
 @Controller('collections')
 @Controller('collections')
 export class CollectionsController {
 export class CollectionsController {
-  constructor(private collectionsService: CollectionsService, @Inject(CACHE_MANAGER) private cacheManager: Cache) { }
+  constructor(
+    private collectionsService: CollectionsService,
+    @Inject(CACHE_MANAGER) private cacheManager: Cache,
+  ) {}
 
 
   // manually control cache if logic is complicated
   // manually control cache if logic is complicated
   @Get()
   @Get()
   async getCollections(@Query() data?: ShowCollections) {
   async getCollections(@Query() data?: ShowCollections) {
     if (Number(data.type) === 1) {
     if (Number(data.type) === 1) {
-      let loadedCollections = await this.cacheManager.get(cacheKeys.LOADEDCOLLECTIONS);
+      let loadedCollections = await this.cacheManager.get(
+        cacheKeys.LOADEDCOLLECTIONS,
+      );
       if (loadedCollections) {
       if (loadedCollections) {
         return loadedCollections;
         return loadedCollections;
       }
       }
       loadedCollections = await this.collectionsService.getLoadedColletions();
       loadedCollections = await this.collectionsService.getLoadedColletions();
-      await this.cacheManager.set(cacheKeys.LOADEDCOLLECTIONS, loadedCollections);
+      await this.cacheManager.set(
+        cacheKeys.LOADEDCOLLECTIONS,
+        loadedCollections,
+      );
       return loadedCollections;
       return loadedCollections;
     }
     }
     let allCollections = await this.cacheManager.get(cacheKeys.ALLCOLLECTIONS);
     let allCollections = await this.cacheManager.get(cacheKeys.ALLCOLLECTIONS);

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

@@ -118,6 +118,7 @@ export class CollectionsService {
           rowCount: findKeyValue(collectionStatistics.stats, ROW_COUNT),
           rowCount: findKeyValue(collectionStatistics.stats, ROW_COUNT),
           id: collectionInfo.collectionID,
           id: collectionInfo.collectionID,
           isLoaded: loadedCollections.collection_names.includes(name),
           isLoaded: loadedCollections.collection_names.includes(name),
+          createdTime: collectionInfo.created_utc_timestamp,
         });
         });
       }
       }
     }
     }

+ 4 - 4
server/yarn.lock

@@ -1337,10 +1337,10 @@
   resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
   resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
   integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
   integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
 
 
-"@zilliz/milvus2-sdk-node@^1.0.3":
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/@zilliz/milvus2-sdk-node/-/milvus2-sdk-node-1.0.3.tgz#909f1b44f5a7dedcfc3d075180f0273e8ace90f1"
-  integrity sha512-jAc/l7siEyNcDJ/WJ5tvbD/9bPMKXNWXCXaeUgLWLTitSrmjRLquhHKkf66OUU+rbEiDQw0l408jQyyY3CIZwA==
+"@zilliz/milvus2-sdk-node@^1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@zilliz/milvus2-sdk-node/-/milvus2-sdk-node-1.0.4.tgz#3e32edb5338abbcb710ea7da8b4e2ba2603fcea5"
+  integrity sha512-67KTHnZP1pLAGDct15XPZfUIHJy/4RPGcUQ3rAMepEj/4BmxCAL8rpRqThFL+UPJz4QYcphoqL5HQDNK59IH0A==
   dependencies:
   dependencies:
     "@grpc/grpc-js" "^1.2.12"
     "@grpc/grpc-js" "^1.2.12"
     "@grpc/proto-loader" "^0.6.0"
     "@grpc/proto-loader" "^0.6.0"