Browse Source

add vector search ui

tumao 4 years ago
parent
commit
ddfaf156a2

+ 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>

+ 12 - 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 RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline';
 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 { ReactComponent as MilvusIcon } from '../../assets/icons/milvus.svg';
 import { ReactComponent as OverviewIcon } from '../../assets/icons/overview.svg';
@@ -29,6 +31,8 @@ import { ReactComponent as ReleaseIcon } from '../../assets/icons/release.svg';
 import { ReactComponent as LoadIcon } from '../../assets/icons/load.svg';
 import { ReactComponent as KeyIcon } from '../../assets/icons/key.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';
 
 const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   search: (props = {}) => <SearchIcon {...props} />,
@@ -50,6 +54,8 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   rightArrow: (props = {}) => <ArrowForwardIosIcon {...props} />,
   remove: (props = {}) => <RemoveCircleOutlineIcon {...props} />,
   dropdown: (props = {}) => <ArrowDropDownIcon {...props} />,
+  refresh: (props = {}) => <CachedIcon {...props} />,
+  filter: (props = {}) => <FilterListIcon {...props} />,
 
   milvus: (props = {}) => (
     <SvgIcon viewBox="0 0 44 31" component={MilvusIcon} {...props} />
@@ -63,6 +69,9 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   navConsole: (props = {}) => (
     <SvgIcon viewBox="0 0 20 20" component={ConsoleIcon} {...props} />
   ),
+  navSearch: (props = {}) => (
+    <SvgIcon viewBox="0 0 20 20" component={VectorSearchIcon} {...props} />
+  ),
   info: (props = {}) => (
     <SvgIcon viewBox="0 0 16 16" component={InfoIcon} {...props} />
   ),
@@ -78,6 +87,9 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   upload: (props = {}) => (
     <SvgIcon viewBox="0 0 16 16" component={UploadIcon} {...props} />
   ),
+  vectorSearch: (props = {}) => (
+    <SvgIcon viewBox="0 0 48 48" component={SearchEmptyIcon} {...props} />
+  ),
 };
 
 export default icons;

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

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

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

@@ -59,9 +59,6 @@ const getStyles = makeStyles((theme: Theme) => ({
     },
   },
 
-  active: {
-    color: theme.palette.primary.main,
-  },
   menuIcon: {
     color: theme.palette.milvusGrey.dark,
   },

+ 19 - 0
client/src/components/layout/Layout.tsx

@@ -17,6 +17,18 @@ const useStyles = makeStyles((theme: Theme) =>
     },
     content: {
       display: 'flex',
+
+      '& .normalSearchIcon': {
+        '& path': {
+          fill: theme.palette.milvusGrey.dark,
+        },
+      },
+
+      '& .activeSearchIcon': {
+        '& path': {
+          fill: theme.palette.primary.main,
+        },
+      },
     },
     body: {
       flex: 1,
@@ -53,6 +65,13 @@ const Layout = (props: any) => {
       label: navTrans('collection'),
       onClick: () => history.push('/collections'),
     },
+    {
+      icon: icons.navSearch,
+      label: navTrans('search'),
+      onClick: () => history.push('/search'),
+      iconActiveClass: 'activeSearchIcon',
+      iconNormalClass: 'normalSearchIcon',
+    },
   ];
 
   return (

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

@@ -13,7 +13,7 @@ const getStyles = makeStyles((theme: Theme) => ({
     borderRadius: '4px',
   },
   menuItem: {
-    minWidth: '160px',
+    minWidth: (props: { minWidth: string }) => props.minWidth,
     padding: theme.spacing(1),
 
     '&:hover': {
@@ -23,10 +23,16 @@ const getStyles = makeStyles((theme: Theme) => ({
 }));
 
 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 classes = getStyles();
+  const classes = getStyles({ minWidth: menuItemWidth });
   const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
     setAnchorEl(event.currentTarget);
   };

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

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

+ 48 - 4
client/src/consts/Milvus.tsx

@@ -65,10 +65,10 @@ export const INDEX_CONFIG: {
     create: ['nlist', 'm'],
     search: ['nprobe'],
   },
-  // IVF_SQ8: {
-  //   create: ['nlist'],
-  //   search: ['nprobe'],
-  // },
+  IVF_SQ8: {
+    create: ['nlist'],
+    search: ['nprobe'],
+  },
   // IVF_SQ8_HYBRID: {
   //   create: ['nlist'],
   //   search: ['nprobe'],
@@ -115,3 +115,47 @@ export enum EmbeddingTypeEnum {
   float = 'FLOAT_POINT',
   binary = 'BINARY',
 }
+
+export const METRIC_OPTIONS_MAP = {
+  [EmbeddingTypeEnum.float]: [
+    {
+      value: METRIC_TYPES_VALUES.L2,
+      label: 'L2',
+    },
+    {
+      value: METRIC_TYPES_VALUES.IP,
+      label: 'IP',
+    },
+  ],
+  [EmbeddingTypeEnum.binary]: [
+    {
+      value: METRIC_TYPES_VALUES.SUBSTRUCTURE,
+      label: 'Substructure',
+    },
+    {
+      value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
+      label: 'Superstructure',
+    },
+    {
+      value: METRIC_TYPES_VALUES.HAMMING,
+      label: 'Hamming',
+    },
+    {
+      value: METRIC_TYPES_VALUES.JACCARD,
+      label: 'Jaccard',
+    },
+    {
+      value: METRIC_TYPES_VALUES.TANIMOTO,
+      label: 'Tanimoto',
+    },
+  ],
+};
+
+/**
+ * use L2 as float default metric type
+ * use Hamming as binary default metric type
+ */
+export const DEFAULT_METRIC_VALUE_MAP = {
+  [EmbeddingTypeEnum.float]: METRIC_TYPES_VALUES.L2,
+  [EmbeddingTypeEnum.binary]: METRIC_TYPES_VALUES.HAMMING,
+};

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

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

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

@@ -68,4 +68,8 @@ export class IndexHttp extends BaseModel implements IndexView {
   get _fieldName() {
     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',
   collection: 'Collection',
   console: 'Search Console',
+  search: 'Vector Search',
 };
 
 export default navTrans;

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

@@ -0,0 +1,13 @@
+const searchTrans = {
+  firstTip: '1. Enter vector value and choose targeted field',
+  secondTip: '2. Set search parameters',
+  vectorPlaceholder: 'Please input your vector value here',
+  collection: 'Choose Collection',
+  field: 'Choose Field',
+  empty: 'start your vector search',
+  result: 'Search Results',
+  topK: 'TopK {{number}}',
+  filter: 'Advanced Filter',
+};
+
+export default searchTrans;

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

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

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

@@ -0,0 +1,13 @@
+const searchTrans = {
+  firstTip: '1. Enter vector value and choose targeted field',
+  secondTip: '2. Set search parameters',
+  vectorPlaceholder: 'Please input your vector value here',
+  collection: 'Choose Collection',
+  field: 'Choose Field',
+  empty: 'start your vector search',
+  result: 'Search Results',
+  topK: 'TopK {{number}}',
+  filter: 'Advanced Filter',
+};
+
+export default searchTrans;

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

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

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

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

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

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

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

@@ -0,0 +1,48 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { FC, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import CustomSelector from '../../components/customSelector/CustomSelector';
+import { Option } from '../../components/customSelector/Types';
+import { METRIC_OPTIONS_MAP } from '../../consts/Milvus';
+import { SearchParamsProps } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  selector: {
+    // minWidth: '218px',
+    flexBasis: '32%',
+  },
+}));
+
+const SearchParams: FC<SearchParamsProps> = ({
+  indexType,
+  searchParamsForm,
+  handleFormChange,
+  embeddingType,
+  metricType,
+  wrapperClass = '',
+}) => {
+  const { t: indexTrans } = useTranslation('index');
+  const classes = getStyles();
+
+  const metricOptions: Option[] = METRIC_OPTIONS_MAP[embeddingType];
+
+  return (
+    <div className={wrapperClass}>
+      <CustomSelector
+        options={metricOptions}
+        value={metricType}
+        label={indexTrans('metric')}
+        wrapperClass={classes.selector}
+        variant="filled"
+        onChange={(e: { target: { value: unknown } }) => {
+          const metric = e.target.value;
+          console.log('metric', metric);
+        }}
+        // not selectable now
+        readOnly={true}
+      />
+    </div>
+  );
+};
+
+export default SearchParams;

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

@@ -0,0 +1,32 @@
+import { Option } from '../../components/customSelector/Types';
+import { EmbeddingTypeEnum } from '../../consts/Milvus';
+import { DataType } from '../collections/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: EmbeddingTypeEnum;
+  // default index type is FLAT
+  indexType: string;
+  searchParamsForm: {
+    [key in string]: number;
+  };
+  handleFormChange: (form: { [key in string]: number }) => void;
+  wrapperClass?: string;
+}
+
+export interface SearchResultView {
+  // dynamic field names
+  [key: string]: string | number;
+  _rank: number;
+  _distance: number;
+}
+
+export interface FieldOption extends Option {
+  fieldType: DataType;
+  // if user doesn't create index, use empty string as metric type
+  metricType: string;
+  indexType: string;
+}

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

@@ -0,0 +1,445 @@
+import {
+  Chip,
+  makeStyles,
+  TextField,
+  Theme,
+  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,
+  EmbeddingTypeEnum,
+} from '../../consts/Milvus';
+import { FieldOption, SearchResultView } 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, DataType } from '../collections/Types';
+import { IndexHttp } from '../../http/Index';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  form: {
+    display: 'flex',
+
+    '& .field': {
+      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),
+        margin: theme.spacing(2, 0),
+        minWidth: '364px',
+      },
+
+      // 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,
+      marginLeft: theme.spacing(1),
+    },
+
+    '& .text': {
+      color: theme.palette.milvusGrey.dark,
+      fontWeight: 500,
+    },
+  },
+  selectors: {
+    display: 'flex',
+    justifyContent: 'space-between',
+
+    '& .selector': {
+      flexBasis: '45%',
+      minWidth: '183px',
+    },
+  },
+  paramsWrapper: {
+    display: 'flex',
+    justifyContent: 'space-between',
+    paddingTop: theme.spacing(2),
+  },
+  toolbar: {
+    display: 'flex',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+
+    padding: theme.spacing(2, 0),
+
+    '& .left': {
+      display: 'flex',
+      alignItems: 'center',
+
+      '& .text': {
+        color: theme.palette.milvusGrey.main,
+      },
+
+      '& .button': {
+        marginLeft: theme.spacing(1),
+        fontSize: '16px',
+        lineHeight: '24px',
+      },
+    },
+    '& .right': {
+      '& .btn': {
+        marginRight: theme.spacing(1),
+      },
+      '& .icon': {
+        fontSize: '16px',
+      },
+    },
+  },
+  menuLabel: {
+    minWidth: '108px',
+
+    padding: theme.spacing(0, 1),
+    marginLeft: theme.spacing(1),
+
+    backgroundColor: '#fff',
+    color: theme.palette.milvusGrey.dark,
+  },
+  menuItem: {
+    fontWeight: 500,
+    fontSize: '12px',
+    lineHeight: '16px',
+    color: theme.palette.milvusGrey.dark,
+  },
+  chip: {
+    display: 'flex',
+    alignItems: 'center',
+    padding: theme.spacing(0, 0.5, 0, 1),
+    marginLeft: theme.spacing(1),
+  },
+  chipLabel: {
+    color: theme.palette.primary.main,
+    paddingLeft: 0,
+    paddingRight: theme.spacing(1),
+    fontSize: '12px',
+    lineHeight: '16px',
+  },
+}));
+
+const VectorSearch = () => {
+  useNavigationHook(ALL_ROUTER_TYPES.SEARCH);
+  const { t: searchTrans } = useTranslation('search');
+  const { t: btnTrans } = useTranslation('btn');
+  const classes = getStyles();
+
+  // TODO: set real loading
+  const [tableLoading, setTableLoading] = useState<boolean>(false);
+  const [collections, setCollections] = useState<CollectionData[]>([]);
+  const [selectedCollection, setSelectedCollection] = useState<string>('');
+  const [fieldOptions, setFieldOptions] = useState<FieldOption[]>([]);
+  const [selectedField, setSelectedField] = useState<string>('');
+  const [searchParam, setSearchParam] = useState<{ [key in string]: number }>(
+    {}
+  );
+  const [searchResult, setSearchResult] = useState<SearchResultView[]>([]);
+  // default topK is 100
+  const [topK, setTopK] = useState<number>(100);
+  const [vectors, setVectors] = useState<string>('');
+
+  const {
+    pageSize,
+    handlePageSize,
+    currentPage,
+    handleCurrentPage,
+    total,
+    data: result,
+  } = usePaginationHook(searchResult);
+
+  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 || [];
+    return fields.map(f => f._fieldName);
+  }, [selectedCollection, collections]);
+
+  const { metricType, indexType } = useMemo(() => {
+    if (selectedField !== '') {
+      // field options must contain selected field, so selectedFieldInfo will never undefined
+      const selectedFieldInfo = fieldOptions.find(
+        f => f.value === selectedField
+      );
+
+      const embeddingType =
+        selectedFieldInfo!.fieldType === 'BinaryVector'
+          ? EmbeddingTypeEnum.binary
+          : EmbeddingTypeEnum.float;
+
+      const metric =
+        selectedFieldInfo!.metricType ||
+        DEFAULT_METRIC_VALUE_MAP[embeddingType];
+
+      return {
+        metricType: metric,
+        indexType: selectedFieldInfo!.indexType,
+      };
+    }
+
+    return { metricType: '', indexType: '' };
+  }, [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 vectorTypes: DataType[] = ['BinaryVector', 'FloatVector'];
+      const indexes = await IndexHttp.getIndexInfo(collectionName);
+      console.log('----- 226 indexes', indexes);
+
+      const fieldOptions = fields
+        // only vector type field can be select
+        .filter(field => vectorTypes.includes(field._fieldType))
+        // use FLAT as default index type
+        .map(f => {
+          const index = indexes.find(i => i._fieldName === f._fieldName);
+          return {
+            label: `${f._fieldName} (${index?._indexType || 'FLAT'})`,
+            value: f._fieldName,
+            fieldType: f._fieldType,
+            metricType: index?._metricType || '',
+            indexType: index?._indexType || 'FLAT',
+          };
+        });
+      setFieldOptions(fieldOptions);
+    },
+    []
+  );
+
+  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;
+  const FilterIcon = icons.filter;
+  const ClearIcon = icons.clear;
+
+  // methods
+  const handlePageChange = (e: any, page: number) => {
+    handleCurrentPage(page);
+  };
+  const handleReset = () => {};
+  const handleSearch = () => {};
+  const handleFilter = () => {};
+  const handleClearFilter = () => {};
+  const handleVectorChange = (value: string) => {
+    setVectors(value);
+  };
+
+  return (
+    <section className="page-wrapper">
+      {/* form section */}
+      <form className={classes.form}>
+        {/* vector value, collection and field */}
+        <fieldset className="field">
+          <Typography className="text">{searchTrans('firstTip')}</Typography>
+          <TextField
+            className="textarea"
+            InputProps={{
+              classes: {
+                root: 'textfield',
+                multiline: 'multiline',
+              },
+            }}
+            multiline
+            rows={2}
+            placeholder={searchTrans('vectorPlaceholder')}
+            onBlur={(e: React.ChangeEvent<{ value: unknown }>) => {
+              handleVectorChange(e.target.value as string);
+            }}
+          />
+          <div className={classes.selectors}>
+            <CustomSelector
+              options={collectionOptions}
+              wrapperClass="selector"
+              variant="filled"
+              label={searchTrans('collection')}
+              value={selectedCollection}
+              onChange={(e: { target: { value: unknown } }) => {
+                const collection = e.target.value;
+                setSelectedCollection(collection as string);
+                // everytime selected collection changed, reset field
+                setSelectedField('');
+              }}
+            />
+            <CustomSelector
+              options={fieldOptions}
+              readOnly={selectedCollection === ''}
+              wrapperClass="selector"
+              variant="filled"
+              label={searchTrans('field')}
+              value={selectedField}
+              onChange={(e: { target: { value: unknown } }) => {
+                const field = e.target.value;
+                setSelectedField(field as string);
+                console.log('selected field', field);
+              }}
+            />
+          </div>
+        </fieldset>
+        {/* search params */}
+        <fieldset className="field field-second">
+          <Typography className="text">{searchTrans('secondTip')}</Typography>
+          <SearchParams
+            wrapperClass={classes.paramsWrapper}
+            metricType={metricType!}
+            embeddingType={EmbeddingTypeEnum.float}
+            indexType={indexType}
+            searchParamsForm={searchParam}
+            handleFormChange={setSearchParam}
+          />
+        </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),
+              wrapperClass: classes.menuItem,
+            }))}
+            buttonProps={{
+              className: classes.menuLabel,
+              endIcon: <ArrowIcon />,
+            }}
+            menuItemWidth="108px"
+          />
+          <CustomButton
+            className="button"
+            disabled={selectedField === '' || selectedCollection === ''}
+            onClick={handleFilter}
+          >
+            <FilterIcon />
+            {searchTrans('filter')}
+          </CustomButton>
+          {/* advanced filter number chip */}
+          <Chip
+            variant="outlined"
+            size="small"
+            // TODO: need to replace mock data
+            label={'3'}
+            classes={{ root: classes.chip, label: classes.chipLabel }}
+            deleteIcon={<ClearIcon />}
+            onDelete={handleClearFilter}
+          />
+        </div>
+        <div className="right">
+          <CustomButton className="btn" onClick={handleReset}>
+            <ResetIcon classes={{ root: 'icon' }} />
+            {btnTrans('reset')}
+          </CustomButton>
+          <CustomButton variant="contained" onClick={handleSearch}>
+            {btnTrans('search')}
+          </CustomButton>
+        </div>
+      </section>
+
+      {/* search result table section */}
+      {searchResult.length > 0 || tableLoading ? (
+        <MilvusGrid
+          toolbarConfigs={[]}
+          colDefinitions={[]}
+          rows={result}
+          rowCount={total}
+          primaryKey="_row"
+          page={currentPage}
+          onChangePage={handlePageChange}
+          rowsPerPage={pageSize}
+          setRowsPerPage={handlePageSize}
+          openCheckBox={false}
+          isLoading={tableLoading}
+        />
+      ) : (
+        <EmptyCard
+          wrapperClass={`page-empty-card`}
+          icon={<VectorSearchIcon />}
+          text={searchTrans('empty')}
+        />
+      )}
+    </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 Connect from '../pages/connect/Connect';
 import Overview from '../pages/overview/Overview';
+import VectorSearch from '../pages/seach/VectorSearch';
 import { RouterConfigType } from './Types';
 
 const RouterConfig: RouterConfigType[] = [
@@ -25,6 +26,11 @@ const RouterConfig: RouterConfigType[] = [
     component: Collection,
     auth: true,
   },
+  {
+    path: '/search',
+    component: VectorSearch,
+    auth: true,
+  },
 ];
 
 export default RouterConfig;

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

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