Browse Source

New data page (#382)

* part2

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

* finish query part3

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

* query part4

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

* finish query

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

* remove console

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

* update tip text

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

* add more tooltip

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

---------

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
ryjiang 1 year ago
parent
commit
46513a2a94

+ 4 - 4
client/src/assets/icons/upload.svg

@@ -1,5 +1,5 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M14 6.66669C13.6318 6.66669 13.3333 6.36821 13.3333 6.00002L13.3333 3.33335C13.3333 3.15654 13.2631 2.98697 13.1381 2.86195C13.0131 2.73692 12.8435 2.66669 12.6667 2.66669L3.33334 2.66669C3.15653 2.66669 2.98696 2.73693 2.86193 2.86195C2.73691 2.98697 2.66667 3.15654 2.66667 3.33335L2.66667 6.00002C2.66667 6.36821 2.36819 6.66669 2 6.66669C1.63181 6.66669 1.33334 6.36821 1.33334 6.00002L1.33334 3.33335C1.33334 2.80292 1.54405 2.29421 1.91912 1.91914C2.2942 1.54407 2.8029 1.33335 3.33334 1.33335L12.6667 1.33335C13.1971 1.33335 13.7058 1.54407 14.0809 1.91914C14.456 2.29421 14.6667 2.80292 14.6667 3.33335L14.6667 6.00002C14.6667 6.36821 14.3682 6.66669 14 6.66669Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M11.8047 9.80474C11.5444 10.0651 11.1223 10.0651 10.8619 9.80474L8 6.94281L5.13807 9.80474C4.87772 10.0651 4.45561 10.0651 4.19526 9.80474C3.93491 9.54439 3.93491 9.12228 4.19526 8.86193L7.5286 5.5286C7.78894 5.26825 8.21105 5.26825 8.4714 5.5286L11.8047 8.86193C12.0651 9.12228 12.0651 9.54439 11.8047 9.80474Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00001 14.6667C7.63182 14.6667 7.33334 14.3682 7.33334 14L7.33334 6.00002C7.33334 5.63183 7.63181 5.33335 8 5.33335C8.36819 5.33335 8.66667 5.63183 8.66667 6.00002L8.66667 14C8.66667 14.3682 8.36819 14.6667 8.00001 14.6667Z" fill="white"/>
+<svg width="16" height="16" viewBox="0 0 16 16"  xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M14 6.66669C13.6318 6.66669 13.3333 6.36821 13.3333 6.00002L13.3333 3.33335C13.3333 3.15654 13.2631 2.98697 13.1381 2.86195C13.0131 2.73692 12.8435 2.66669 12.6667 2.66669L3.33334 2.66669C3.15653 2.66669 2.98696 2.73693 2.86193 2.86195C2.73691 2.98697 2.66667 3.15654 2.66667 3.33335L2.66667 6.00002C2.66667 6.36821 2.36819 6.66669 2 6.66669C1.63181 6.66669 1.33334 6.36821 1.33334 6.00002L1.33334 3.33335C1.33334 2.80292 1.54405 2.29421 1.91912 1.91914C2.2942 1.54407 2.8029 1.33335 3.33334 1.33335L12.6667 1.33335C13.1971 1.33335 13.7058 1.54407 14.0809 1.91914C14.456 2.29421 14.6667 2.80292 14.6667 3.33335L14.6667 6.00002C14.6667 6.36821 14.3682 6.66669 14 6.66669Z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.8047 9.80474C11.5444 10.0651 11.1223 10.0651 10.8619 9.80474L8 6.94281L5.13807 9.80474C4.87772 10.0651 4.45561 10.0651 4.19526 9.80474C3.93491 9.54439 3.93491 9.12228 4.19526 8.86193L7.5286 5.5286C7.78894 5.26825 8.21105 5.26825 8.4714 5.5286L11.8047 8.86193C12.0651 9.12228 12.0651 9.54439 11.8047 9.80474Z"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00001 14.6667C7.63182 14.6667 7.33334 14.3682 7.33334 14L7.33334 6.00002C7.33334 5.63183 7.63181 5.33335 8 5.33335C8.36819 5.33335 8.66667 5.63183 8.66667 6.00002L8.66667 14C8.66667 14.3682 8.36819 14.6667 8.00001 14.6667Z"/>
 </svg>

+ 1 - 6
client/src/components/icons/Icons.tsx

@@ -118,12 +118,7 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
     </svg>
   ),
   upload: (props = {}) => (
-    <SvgIcon
-      viewBox="0 0 16 16"
-      component={UploadIcon}
-      {...props}
-      fill="#000"
-    />
+    <SvgIcon viewBox="0 0 20 20" component={UploadIcon} {...props} fill="currentColor" />
   ),
   vectorSearch: (props = {}) => (
     <SvgIcon viewBox="0 0 48 48" component={SearchEmptyIcon} {...props} />

+ 6 - 0
client/src/consts/Milvus.ts

@@ -267,6 +267,12 @@ export enum LOADING_STATE {
   LOADING,
   UNLOADED,
 }
+export enum LOAD_STATE {
+  LoadStateNotExist = 'LoadStateNotExist',
+  LoadStateNotLoad = 'LoadStateNotLoad',
+  LoadStateLoading = 'LoadStateLoading',
+  LoadStateLoaded = 'LoadStateLoaded',
+}
 
 export const DEFAULT_VECTORS = 100000;
 export const DEFAULT_SEFMENT_FILE_SIZE = 1024;

+ 224 - 0
client/src/hooks/Query.ts

@@ -0,0 +1,224 @@
+import { useState, useRef, useEffect } from 'react';
+import { DataTypeStringEnum, DYNAMIC_FIELD, LOAD_STATE } from '@/consts';
+import { Collection } from '@/http';
+
+export const useQuery = (params: {
+  onQueryStart: Function;
+  onQueryEnd?: Function;
+  onQueryFinally?: Function;
+  collectionName: string;
+}) => {
+  // state
+  const [collection, setCollection] = useState<any>({
+    fields: [],
+    consistencyLevel: '',
+    primaryKey: { value: '', type: DataTypeStringEnum.Int64 },
+    loaded: false,
+  });
+  const [currentPage, setCurrentPage] = useState<number>(0);
+  const [pageSize, setPageSize] = useState<number>(0);
+  const [total, setTotal] = useState<number>(0);
+  const [expr, setExpr] = useState<string>('');
+  const [queryResult, setQueryResult] = useState<any>({ data: [], latency: 0 });
+
+  // build local cache for pk ids
+  const pageCache = useRef(new Map());
+
+  // get next/previous expression
+  const getPageExpr = (page: number) => {
+    const cache = pageCache.current.get(
+      page > currentPage ? currentPage : page
+    );
+    const primaryKey = collection.primaryKey;
+
+    const formatPKValue = (pkId: string | number) =>
+      primaryKey.type === DataTypeStringEnum.VarChar ? `'${pkId}'` : pkId;
+
+    // If cache does not exist, return expression based on primaryKey type
+    let condition = '';
+    if (!cache) {
+      const defaultValue =
+        primaryKey.type === DataTypeStringEnum.VarChar ? "''" : '0';
+      condition = `${primaryKey.value} > ${defaultValue}`;
+    } else {
+      const { firstPKId, lastPKId } = cache;
+      const lastPKValue = formatPKValue(lastPKId);
+      const firstPKValue = formatPKValue(firstPKId);
+
+      condition =
+        page > currentPage
+          ? `(${primaryKey.value} > ${lastPKValue})`
+          : `((${primaryKey.value} <= ${lastPKValue}) && (${primaryKey.value} >= ${firstPKValue}))`;
+    }
+
+    return expr ? `${expr} && ${condition}` : condition;
+  };
+
+  // query function
+  const query = async (
+    page: number = currentPage,
+    consistency_level = collection.consistencyLevel
+  ) => {
+    if (!collection.primaryKey.value || !collection.loaded) {
+      //   console.info('[skip running query]: no key yet');
+      return;
+    }
+    const _expr = getPageExpr(page);
+    // console.log('query expr', _expr);
+    params.onQueryStart(_expr);
+
+    try {
+      // execute query
+      const res = await Collection.queryData(params.collectionName, {
+        expr: _expr,
+        output_fields: collection.fields.map((i: any) => i.name),
+        limit: pageSize || 10,
+        consistency_level,
+        // travel_timestamp: timeTravelInfo.timestamp,
+      });
+
+      // get last item of the data
+      const lastItem = res.data[res.data.length - 1];
+      // get first item of the data
+      const firstItem = res.data[0];
+      // get last pk id
+      const lastPKId: string | number =
+        lastItem && lastItem[collection.primaryKey.value];
+      // get first pk id
+      const firstPKId: string | number =
+        firstItem && firstItem[collection.primaryKey.value];
+
+      // store pk id in the cache with the page number
+      if (lastItem) {
+        pageCache.current.set(page, {
+          firstPKId,
+          lastPKId,
+        });
+      }
+
+      // console.log('query result page', page, pageCache.current);
+
+      // update query result
+      setQueryResult(res);
+
+      params.onQueryEnd?.(res);
+    } catch (e: any) {
+      reset();
+    } finally {
+      params.onQueryFinally?.();
+    }
+  };
+
+  // get collection info
+  const prepare = async (collectionName: string) => {
+    const collection = await Collection.getCollectionInfo(collectionName);
+    const schemaList = collection.fields;
+
+    const nameList = schemaList.map(v => ({
+      name: v.name,
+      type: v.fieldType,
+    }));
+
+    // if the dynamic field is enabled, we add $meta column in the grid
+    if (collection.enableDynamicField) {
+      nameList.push({
+        name: DYNAMIC_FIELD,
+        type: DataTypeStringEnum.JSON,
+      });
+    }
+    const primaryKey = schemaList.find(v => v.isPrimaryKey === true)!;
+    setCollection({
+      fields: nameList as any[],
+      consistencyLevel: collection.consistency_level,
+      primaryKey: { value: primaryKey['name'], type: primaryKey['fieldType'] },
+      loaded: collection.state === LOAD_STATE.LoadStateLoaded,
+    });
+  };
+
+  const count = async (consistency_level = collection.consistency_level) => {
+    if (!collection.primaryKey.value || !collection.loaded) {
+      return;
+    }
+    const count = 'count(*)';
+    const res = await Collection.queryData(params.collectionName, {
+      expr: expr,
+      output_fields: [count],
+      consistency_level,
+      // travel_timestamp: timeTravelInfo.timestamp,
+    });
+    setTotal(Number(res.data[0][count]));
+  };
+
+  // reset
+  const reset = () => {
+    setCurrentPage(0);
+    setQueryResult({ data: [] });
+    pageCache.current.clear();
+  };
+
+  // Get fields at first or collection name changed.
+  useEffect(() => {
+    params.collectionName && prepare(params.collectionName);
+  }, [params.collectionName]);
+
+  // query if expr is changed
+  useEffect(() => {
+    // reset
+    reset();
+    // get count;
+    count();
+    // do the query
+    query();
+  }, [expr]);
+
+  // query if collection is changed
+  useEffect(() => {
+    // reset
+    reset();
+    // get count;
+    count();
+    // do the query
+    query();
+  }, [collection]);
+
+  // query if page size is changed
+  useEffect(() => {
+    // reset
+    reset();
+    // do the query
+    query();
+  }, [pageSize]);
+
+  return {
+    // collection info(primaryKey, consistency level, fields)
+    collection,
+    // total query count
+    total,
+    // page size
+    pageSize,
+    // update page size
+    setPageSize,
+    // update consistency level
+    setConsistencyLevel: (level: string) => {
+      setCollection({ ...collection, consistencyLevel: level });
+    },
+    // current page
+    currentPage,
+    // query current data page
+    setCurrentPage,
+    // expression to query
+    expr,
+    // expression updater
+    setExpr,
+    // query result
+    queryResult,
+    // query
+    query,
+    // reset
+    reset,
+    // count
+    count,
+    // get expression
+    getPageExpr,
+  };
+};

+ 1 - 0
client/src/hooks/index.tsx

@@ -4,4 +4,5 @@ export * from './Navigation';
 export * from './Pagination';
 export * from './Result';
 export * from './SystemView';
+export * from './Query';
 export * from './TimeTravel';

+ 3 - 2
client/src/http/Collection.ts

@@ -4,7 +4,7 @@ import { VectorSearchParam } from '@/types/SearchTypes';
 import { QueryParam } from '@/pages/query/Types';
 import { formatNumber } from '@/utils/Common';
 import BaseModel from './BaseModel';
-import { LOADING_STATE } from '@/consts';
+import { LOADING_STATE, LOAD_STATE } from '@/consts';
 import {
   IndexDescription,
   ShowCollectionsType,
@@ -27,6 +27,7 @@ export class Collection extends BaseModel implements CollectionData {
   public index_descriptions!: IndexDescription[];
   public schema!: CollectionSchema;
   public replicas!: ReplicaInfo[];
+  public state!: LOAD_STATE;
 
   static COLLECTIONS_URL = '/collections';
   static COLLECTIONS_STATISTICS_URL = '/collections/statistics';
@@ -147,7 +148,7 @@ export class Collection extends BaseModel implements CollectionData {
     return formatNumber(Number(this.rowCount));
   }
 
-  // load status
+  // TODO: deprecated
   get status() {
     // If not load, insight server will return '-1'. Otherwise milvus will return percentage
     return this.loadedPercentage === '-1'

+ 13 - 3
client/src/i18n/cn/button.ts

@@ -12,23 +12,33 @@ const btnTrans = {
   drop: 'drop',
   release: '释放',
   load: '加载',
-  insert: '导入数据',
+  insert: '插入数据',
+  importFile: '导入文件',
   refresh: '刷新',
   next: '下一步',
   previous: '上一步',
   done: '完成',
   vectorSearch: '向量搜索',
   query: '查询',
-  importSampleData: '入样本数据',
+  importSampleData: '入样本数据',
   loading: '加载中...',
   importing: '导入中...',
   example: '生成随机向量',
   rename: '重命名',
   duplicate: '复制',
-  export: '导出',
+  export: '导出数据',
   empty: '清空数据',
   flush: '落盘(Flush)',
   compact: '压缩(Compact)',
+  copyJson: '复制为JSON',
+
+  // tips
+  importFileTooltip: '导入JSON或者CSV文件',
+  importSampleDataTooltip: '导入样例数据',
+  exportTooltip: '将选择的查询结果导出到CSV文件',
+  copyJsonTooltip: '复制所选的数据为JSON格式',
+  emptyTooltip: '清空所有数据',
+  deleteTooltip: '删除所选的数据',
 };
 
 export default btnTrans;

+ 1 - 5
client/src/i18n/cn/collection.ts

@@ -16,10 +16,7 @@ const collectionTrans = {
   alias: '别名',
   aliasTooltip: '请选择一个Collection创建别名',
   download: '下载',
-  downloadTooltip: '将所有查询结果导出到CSV文件',
-  downloadDisabledTooltip: '请在导出前查询数据',
   empty: '清空数据',
-  emptyDataDisableTooltip: '请选择一个已加载的Collection进行清空数据操作',
 
   collection: 'Collection',
   entities: 'Entities',
@@ -97,11 +94,10 @@ const collectionTrans = {
   // collection tabs
   partitionTab: '分区',
   schemaTab: 'Schema',
-  queryTab: '数据查询',
+  queryTab: '数据',
   previewTab: '数据预览',
   segmentsTab: '数据段(Segments)',
   startTip: '开始你的数据查询',
-  dataQuerylimits: '请注意,你的数据查询的结果数量最大为16384。',
   exprPlaceHolder: '请输入你的数据查询,例如 id > 0',
 
   // alias dialog

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

@@ -15,6 +15,7 @@ const searchTrans = {
   timeTravel: '时间旅行',
   timeTravelPrefix: '之前的数据',
   dynamicFields: '动态字段',
+  collectionNotLoaded: 'Collection 没有加载',
 };
 
 export default searchTrans;

+ 12 - 2
client/src/i18n/en/button.ts

@@ -12,14 +12,15 @@ const btnTrans = {
   drop: 'Drop',
   release: 'Release',
   load: 'Load',
-  insert: 'Import Data',
+  insert: 'Add Data',
+  importFile: 'Import File',
   refresh: 'Refresh',
   next: 'Next',
   previous: 'Previous',
   done: 'Done',
   vectorSearch: 'Vector Search',
   query: 'Query',
-  importSampleData: 'Import Sample data',
+  importSampleData: 'Add Sample Data',
   loading: 'Loading...',
   importing: 'Importing...',
   example: 'Generate Random Vector',
@@ -29,6 +30,15 @@ const btnTrans = {
   empty: 'Empty',
   flush: 'Flush',
   compact: 'Compact',
+  copyJson: 'Copy as JSON',
+
+  // tips
+  importFileTooltip: 'Import JSON or CSV file',
+  importSampleDataTooltip: 'Import sample data into the current collection',
+  exportTooltip: 'Export selected data to csv',
+  copyJsonTooltip: 'Copy selected data as JSON format',
+  emptyTooltip: 'Empty all data in the collection',
+  deleteTooltip: 'Delete selected data',
 };
 
 export default btnTrans;

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

@@ -16,8 +16,7 @@ const collectionTrans = {
   alias: 'Alias',
   aliasTooltip: 'Please select one collection to create alias',
   download: 'Download',
-  downloadTooltip: 'Export all query results to CSV file',
-  downloadDisabledTooltip: 'Please query data before exporting',
+  downloadDisabledTooltip: 'Please select data before exporting',
   empty: 'empty data',
   emptyDataDisableTooltip: 'Please select one loaded collection to empty data',
 
@@ -99,12 +98,10 @@ const collectionTrans = {
   // collection tabs
   partitionTab: 'Partitions',
   schemaTab: 'Schema',
-  queryTab: 'Data Query',
+  queryTab: 'Data',
   previewTab: 'Data Preview',
   segmentsTab: 'Segments',
   startTip: 'Start Your Data Query',
-  dataQuerylimits:
-    ' Please note that the maximum number of results for your data query is 16384.',
   exprPlaceHolder: 'Please enter your data query, for example id > 0',
 
   // alias dialog

+ 1 - 1
client/src/i18n/en/insert.ts

@@ -30,7 +30,7 @@ const insertTrans = {
   statusSuccess: 'Import Data Successfully!',
   statusError: 'Import Data Failed!',
 
-  importSampleData: 'Import sample data into {{collection}}',
+  importSampleData: 'Add sample data into {{collection}}',
   sampleDataSize: 'Choose sample data size',
   importSampleDataDesc: `This function imports randomly generated data matching the collection schema. Useful for testing and development. Click the download button to get the data.`,
   downloadSampleDataCSV: `Download Sample CSV Data`,

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

@@ -15,6 +15,7 @@ const searchTrans = {
   timeTravel: 'Time Travel',
   timeTravelPrefix: 'Data before',
   dynamicFields: 'Dynamic Fields',
+  collectionNotLoaded: 'Collection not loaded',
 };
 
 export default searchTrans;

+ 6 - 11
client/src/pages/collections/Collection.tsx

@@ -10,7 +10,6 @@ import { ITab } from '@/components/customTabList/Types';
 import Partitions from '../partitions/Partitions';
 import Schema from '../schema/Schema';
 import Query from '../query/Query';
-import Preview from '../preview/Preview';
 import Segments from '../segments/Segments';
 
 const useStyles = makeStyles((theme: Theme) => ({
@@ -46,6 +45,11 @@ const Collection = () => {
   const { t: collectionTrans } = useTranslation('collection');
 
   const tabs: ITab[] = [
+    {
+      label: collectionTrans('queryTab'),
+      component: <Query />,
+      path: `query`,
+    },
     {
       label: collectionTrans('schemaTab'),
       component: <Schema />,
@@ -56,16 +60,7 @@ const Collection = () => {
       component: <Partitions />,
       path: `partitions`,
     },
-    {
-      label: collectionTrans('previewTab'),
-      component: <Preview />,
-      path: `preview`,
-    },
-    {
-      label: collectionTrans('queryTab'),
-      component: <Query />,
-      path: `query`,
-    },
+
     {
       label: collectionTrans('segmentsTab'),
       component: <Segments />,

+ 5 - 65
client/src/pages/collections/Collections.tsx

@@ -9,12 +9,11 @@ import {
   dataContext,
   webSocketContext,
 } from '@/context';
-import { Collection, MilvusService, DataService, MilvusIndex } from '@/http';
+import { Collection, MilvusService, MilvusIndex } from '@/http';
 import { useNavigationHook, usePaginationHook } from '@/hooks';
 import { ALL_ROUTER_TYPES } from '@/router/Types';
 import AttuGrid from '@/components/grid/Grid';
 import CustomToolBar from '@/components/grid/ToolBar';
-import { InsertDataParam } from './Types';
 import { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types';
 import icons from '@/components/icons/Icons';
 import EmptyCard from '@/components/cards/EmptyCard';
@@ -26,7 +25,6 @@ import ReleaseCollectionDialog from '../dialogs/ReleaseCollectionDialog';
 import DropCollectionDialog from '../dialogs/DropCollectionDialog';
 import RenameCollectionDialog from '../dialogs/RenameCollectionDialog';
 import DuplicateCollectionDialog from '../dialogs/DuplicateCollectionDailog';
-import EmptyDataDialog from '../dialogs/EmptyDataDialog';
 import InsertDialog from '../dialogs/insert/Dialog';
 import ImportSampleDialog from '../dialogs/ImportSampleDialog';
 import { LOADING_STATE } from '@/consts';
@@ -151,7 +149,7 @@ const Collections = () => {
       Object.assign(v, {
         nameElement: (
           <Link
-            to={`/collections/${v.collectionName}/schema`}
+            to={`/collections/${v.collectionName}/data`}
             className={classes.link}
             title={v.collectionName}
           >
@@ -303,7 +301,7 @@ const Collections = () => {
       type: 'button',
       btnVariant: 'text',
       btnColor: 'secondary',
-      label: btnTrans('insert'),
+      label: btnTrans('importFile'),
       onClick: () => {
         setDialog({
           open: true,
@@ -319,30 +317,7 @@ const Collections = () => {
                 }
                 // user can't select partition on collection page, so default value is ''
                 defaultSelectedPartition={''}
-                handleInsert={async (
-                  collectionName: string,
-                  partitionName: string,
-                  fieldData: any[]
-                ): Promise<{ result: boolean; msg: string }> => {
-                  const param: InsertDataParam = {
-                    partition_name: partitionName,
-                    fields_data: fieldData,
-                  };
-                  try {
-                    await DataService.insertData(collectionName, param);
-                    await DataService.flush(collectionName);
-                    // update collections
-                    fetchData();
-                    return { result: true, msg: '' };
-                  } catch (err: any) {
-                    const {
-                      response: {
-                        data: { message },
-                      },
-                    } = err;
-                    return { result: false, msg: message || '' };
-                  }
-                }}
+                onInsert={() => {}}
               />
             ),
           },
@@ -420,42 +395,7 @@ const Collections = () => {
       disabledTooltip: collectionTrans('duplicateTooltip'),
       disabled: data => data.length !== 1,
     },
-    {
-      icon: 'deleteOutline',
-      type: 'button',
-      btnVariant: 'text',
-      onClick: () => {
-        setDialog({
-          open: true,
-          type: 'custom',
-          params: {
-            component: (
-              <EmptyDataDialog
-                cb={async () => {
-                  openSnackBar(
-                    successTrans('empty', {
-                      name: collectionTrans('collection'),
-                    })
-                  );
-                  setSelectedCollections([]);
-                  await fetchData();
-                }}
-                collectionName={selectedCollections[0].collectionName}
-              />
-            ),
-          },
-        });
-      },
-      label: btnTrans('empty'),
-      disabledTooltip: collectionTrans('emptyDataDisableTooltip'),
-      disabled: (data: any) => {
-        if (data.length === 0 || data.length > 1) {
-          return true;
-        } else {
-          return Number(data[0].loadedPercentage) !== 100;
-        }
-      },
-    },
+
     {
       icon: 'delete',
       type: 'button',

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

@@ -15,7 +15,7 @@ const EmptyDataDialog: FC<EmptyDataProps> = props => {
 
   const handleDelete = async () => {
     // duplicate
-    DataService.emptyData(collectionName);
+    await DataService.emptyData(collectionName);
     // close dialog
     handleCloseDialog();
     cb && cb();

+ 4 - 1
client/src/pages/dialogs/ImportSampleDialog.tsx

@@ -76,7 +76,7 @@ const sizeOptions = [
   },
 ];
 
-const ImportSampleDialog: FC<{ collection: string }> = props => {
+const ImportSampleDialog: FC<{ collection: string; cb?: Function }> = props => {
   const classes = getStyles();
   const { collection } = props;
   const [size, setSize] = useState<string>(sizeOptions[0].value);
@@ -118,6 +118,9 @@ const ImportSampleDialog: FC<{ collection: string }> = props => {
         return { result: res.sampleFile, msg: '' };
       }
       await DataService.flush(collectionName);
+      if (props.cb) {
+        props.cb();
+      }
       return { result: true, msg: '' };
     } catch (err: any) {
       const {

+ 21 - 10
client/src/pages/dialogs/insert/Dialog.tsx

@@ -25,6 +25,8 @@ import {
   InsertStatusEnum,
   InsertStepperEnum,
 } from './Types';
+import { InsertDataParam } from '@/pages/collections/Types';
+import { DataService } from '@/http';
 
 const getStyles = makeStyles((theme: Theme) => ({
   icon: {
@@ -44,7 +46,7 @@ const InsertContainer: FC<InsertContentProps> = ({
 
   partitions,
   schema,
-  handleInsert,
+  onInsert,
 }) => {
   const classes = getStyles();
 
@@ -330,17 +332,26 @@ const InsertContainer: FC<InsertContentProps> = ({
             isContainFieldNames ? csvData.slice(1) : csvData
           );
 
-    const { result, msg } = await handleInsert(
-      collectionValue,
-      partitionValue,
-      data
-    );
+    const param: InsertDataParam = {
+      partition_name: partitionValue,
+      fields_data: data,
+    };
 
-    if (!result) {
-      setInsertFailMsg(msg);
+    try {
+      await DataService.insertData(collectionValue, param);
+      await DataService.flush(collectionValue);
+      // update collections
+      onInsert();
+      setInsertStatus(InsertStatusEnum.success);
+    } catch (err: any) {
+      const {
+        response: {
+          data: { message },
+        },
+      } = err;
+      setInsertFailMsg(message);
+      setInsertStatus(InsertStatusEnum.error);
     }
-    const status = result ? InsertStatusEnum.success : InsertStatusEnum.error;
-    setInsertStatus(status);
   };
 
   const handleCollectionChange = (name: string) => {

+ 1 - 5
client/src/pages/dialogs/insert/Types.ts

@@ -19,11 +19,7 @@ export interface InsertContentProps {
   // if default value is not '', partitions not selectable
   defaultSelectedPartition: string;
 
-  handleInsert: (
-    collectionName: string,
-    partitionName: string,
-    fieldData: any[]
-  ) => Promise<{ result: boolean; msg: string }>;
+  onInsert: Function;
 }
 
 export enum InsertStepperEnum {

+ 1 - 1
client/src/pages/overview/collectionCard/CollectionCard.tsx

@@ -154,7 +154,7 @@ const CollectionCard: FC<CollectionCardProps> = ({
         <div>
           <Status status={status} percentage={loadedPercentage} />
         </div>
-        <Link className="link" to={`/collections/${collectionName}/schema`}>
+        <Link className="link" to={`/collections/${collectionName}/data`}>
           {collectionName}
           <RightArrowIcon classes={{ root: classes.icon }} />
         </Link>

+ 237 - 148
client/src/pages/query/Query.tsx

@@ -1,10 +1,10 @@
-import { useEffect, useState, useRef, useContext } from 'react';
+import { useState, useEffect, useRef, useContext } from 'react';
 import { TextField } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
 import { useParams } from 'react-router-dom';
 import { rootContext } from '@/context';
-import { Collection, DataService } from '@/http';
-import { usePaginationHook, useSearchResult } from '@/hooks';
+import { DataService } from '@/http';
+import { useQuery, useSearchResult } from '@/hooks';
 import { saveCsvAs } from '@/utils';
 import EmptyCard from '@/components/cards/EmptyCard';
 import icons from '@/components/icons/Icons';
@@ -14,152 +14,233 @@ import { ToolBarConfig } from '@/components/grid/Types';
 import Filter from '@/components/advancedSearch';
 import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
 import CustomToolBar from '@/components/grid/ToolBar';
+import InsertDialog from '../dialogs/insert/Dialog';
 import { getLabelDisplayedRows } from '../search/Utils';
 import { getQueryStyles } from './Styles';
 import {
   DYNAMIC_FIELD,
   DataTypeStringEnum,
   CONSISTENCY_LEVEL_OPTIONS,
+  ConsistencyLevelEnum,
 } from '@/consts';
 import CustomSelector from '@/components/customSelector/CustomSelector';
+import EmptyDataDialog from '../dialogs/EmptyDataDialog';
+import ImportSampleDialog from '../dialogs/ImportSampleDialog';
 
 const Query = () => {
-  const { collectionName } = useParams<{ collectionName: string }>();
-  const [fields, setFields] = useState<any[]>([]);
-  const [expression, setExpression] = useState('');
-  const [tableLoading, setTableLoading] = useState<any>();
-  const [queryResult, setQueryResult] = useState<any>();
+  // get collection name from url
+  const { collectionName = '' } = useParams<{ collectionName: string }>();
+  // UI state
+  const [tableLoading, setTableLoading] = useState<boolean>();
   const [selectedData, setSelectedData] = useState<any[]>([]);
-  const [primaryKey, setPrimaryKey] = useState<{ value: string; type: string }>(
-    { value: '', type: DataTypeStringEnum.Int64 }
-  );
-  const [consistency_level, setConsistency_level] = useState<string>('');
-
-  // latency
-  const [latency, setLatency] = useState<number>(0);
-
+  const [expression, setExpression] = useState<string>('');
+  // UI functions
   const { setDialog, handleCloseDialog, openSnackBar } =
     useContext(rootContext);
+  // icons
   const VectorSearchIcon = icons.vectorSearch;
   const ResetIcon = icons.refresh;
-
+  // translations
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: successTrans } = useTranslation('success');
   const { t: searchTrans } = useTranslation('search');
   const { t: collectionTrans } = useTranslation('collection');
   const { t: btnTrans } = useTranslation('btn');
-
+  // classes
   const classes = getQueryStyles();
 
-  // Format result list
-  const queryResultMemo = useSearchResult(queryResult);
-
-  const {
-    pageSize,
-    handlePageSize,
-    currentPage,
-    handleCurrentPage,
-    total,
-    data: result,
-    order,
-    orderBy,
-    handleGridSort,
-  } = usePaginationHook(queryResultMemo || []);
-
-  const handlePageChange = (e: any, page: number) => {
-    handleCurrentPage(page);
-  };
-
-  const getFields = async (collectionName: string) => {
-    const collection = await Collection.getCollectionInfo(collectionName);
-    const schemaList = collection.fields;
-
-    const nameList = schemaList.map(v => ({
-      name: v.name,
-      type: v.fieldType,
-    }));
-
-    // if the dynamic field is enabled, we add $meta column in the grid
-    if (collection.enableDynamicField) {
-      nameList.push({
-        name: DYNAMIC_FIELD,
-        type: DataTypeStringEnum.JSON,
-      });
-    }
-
-    const primaryKey = schemaList.find(v => v.isPrimaryKey === true)!;
-    setPrimaryKey({ value: primaryKey['name'], type: primaryKey['fieldType'] });
-    setConsistency_level(collection.consistency_level);
-
-    setFields(nameList);
-  };
-
-  // Get fields at first or collection name changed.
-  useEffect(() => {
-    collectionName && getFields(collectionName);
-  }, [collectionName]);
-
+  // UI ref
   const filterRef = useRef();
+  const inputRef = useRef<HTMLInputElement>();
 
-  const handleFilterReset = () => {
+  // UI event handlers
+  const handleFilterReset = async () => {
+    // reset advanced filter
     const currentFilter: any = filterRef.current;
     currentFilter?.getReset();
+    // update UI expression
     setExpression('');
-    setTableLoading(null);
-    setQueryResult(null);
-    handleCurrentPage(0);
+    // reset query
+    reset();
+    // ensure not loading
+    setTableLoading(false);
   };
-
-  const handleFilterSubmit = (expression: string) => {
+  const handleFilterSubmit = async (expression: string) => {
+    // update UI expression
     setExpression(expression);
-    handleQuery(expression);
+    // update expression
+    setExpr(expression);
   };
-
-  const handleQuery = async (expr: string = '') => {
-    setTableLoading(true);
-    if (expr === '') {
-      handleFilterReset();
-      return;
-    }
-    try {
-      const res = await Collection.queryData(collectionName!, {
-        expr: expr,
-        output_fields: fields.map(i => i.name),
-        offset: 0,
-        limit: 16384,
-        consistency_level: consistency_level,
-        // travel_timestamp: timeTravelInfo.timestamp,
-      });
-      const result = res.data;
-      setQueryResult(result);
-      setLatency(res.latency);
-    } catch (err) {
-      setQueryResult([]);
-    } finally {
-      setTableLoading(false);
-    }
+  const handlePageChange = async (e: any, page: number) => {
+    // do the query
+    await query(page);
+    // update page number
+    setCurrentPage(page);
   };
-
-  const handleSelectChange = (value: any) => {
+  const onSelectChange = (value: any) => {
     setSelectedData(value);
   };
-
+  const onDelete = async () => {
+    // reset query
+    reset();
+    count(ConsistencyLevelEnum.Strong);
+    await query(0, ConsistencyLevelEnum.Strong);
+  };
   const handleDelete = async () => {
-    await DataService.deleteEntities(collectionName!, {
-      expr: `${primaryKey.value} in [${selectedData
+    // call delete api
+    await DataService.deleteEntities(collectionName, {
+      expr: `${collection.primaryKey.value} in [${selectedData
         .map(v =>
-          primaryKey.type === DataTypeStringEnum.VarChar
-            ? `"${v[primaryKey.value]}"`
-            : v[primaryKey.value]
+          collection.primaryKey.type === DataTypeStringEnum.VarChar
+            ? `"${v[collection.primaryKey.value]}"`
+            : v[collection.primaryKey.value]
         )
         .join(',')}]`,
     });
     handleCloseDialog();
     openSnackBar(successTrans('delete', { name: collectionTrans('entities') }));
-    handleQuery(expression);
+    setSelectedData([]);
+    await onDelete();
   };
 
+  // Query hook
+  const {
+    collection,
+    currentPage,
+    total,
+    pageSize,
+    expr,
+    queryResult,
+    setPageSize,
+    setConsistencyLevel,
+    setCurrentPage,
+    setExpr,
+    query,
+    reset,
+    count,
+  } = useQuery({
+    collectionName,
+    onQueryStart: (expr: string = '') => {
+      setTableLoading(true);
+      if (expr === '') {
+        handleFilterReset();
+        return;
+      }
+    },
+    onQueryFinally: () => {
+      setTableLoading(false);
+    },
+  });
+
+  // Format result list
+  const queryResultMemo = useSearchResult(queryResult.data);
+
+  // Toolbar settings
   const toolbarConfigs: ToolBarConfig[] = [
+    {
+      icon: 'uploadFile',
+      type: 'button',
+      btnVariant: 'text',
+      btnColor: 'secondary',
+      label: btnTrans('importFile'),
+      tooltip: btnTrans('importFileTooltip'),
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <InsertDialog
+                defaultSelectedCollection={collectionName}
+                // user can't select partition on collection page, so default value is ''
+                defaultSelectedPartition={''}
+                onInsert={() => {}}
+              />
+            ),
+          },
+        });
+      },
+    },
+    {
+      type: 'button',
+      btnVariant: 'text',
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <ImportSampleDialog collection={collectionName} cb={onDelete} />
+            ),
+          },
+        });
+      },
+      tooltip: btnTrans('importSampleDataTooltip'),
+      label: btnTrans('importSampleData'),
+      icon: 'add',
+      // tooltip: collectionTrans('deleteTooltip'),
+    },
+    {
+      icon: 'deleteOutline',
+      type: 'button',
+      btnVariant: 'text',
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <EmptyDataDialog
+                cb={async () => {
+                  openSnackBar(
+                    successTrans('empty', {
+                      name: collectionTrans('collection'),
+                    })
+                  );
+                  await onDelete();
+                }}
+                collectionName={collectionName}
+              />
+            ),
+          },
+        });
+      },
+      disabled: () => total == 0,
+      label: btnTrans('empty'),
+      tooltip: btnTrans('emptyTooltip'),
+      disabledTooltip: collectionTrans('emptyDataDisableTooltip'),
+    },
+    {
+      type: 'button',
+      btnVariant: 'text',
+      onClick: () => {
+        saveCsvAs(selectedData, `${collectionName}.query.csv`);
+      },
+      label: btnTrans('export'),
+      icon: 'download',
+      tooltip: btnTrans('exportTooltip'),
+      disabledTooltip: collectionTrans('downloadDisabledTooltip'),
+      disabled: () => !selectedData?.length,
+    },
+    {
+      type: 'button',
+      btnVariant: 'text',
+      onClick: async () => {
+        const json = JSON.stringify(selectedData);
+        try {
+          await navigator.clipboard.writeText(json);
+          alert(`${selectedData.length} rows copied to clipboard`);
+        } catch (err) {
+          console.error('Failed to copy text: ', err);
+        }
+      },
+      label: btnTrans('copyJson'),
+      icon: 'copy',
+      tooltip: btnTrans('copyJsonTooltip'),
+      disabledTooltip: collectionTrans('downloadDisabledTooltip'),
+      disabled: () => !selectedData?.length,
+    },
+
     {
       type: 'button',
       btnVariant: 'text',
@@ -183,27 +264,21 @@ const Query = () => {
       },
       label: btnTrans('delete'),
       icon: 'delete',
-      // tooltip: collectionTrans('deleteTooltip'),
+      tooltip: btnTrans('deleteTooltip'),
       disabledTooltip: collectionTrans('deleteTooltip'),
       disabled: () => selectedData.length === 0,
     },
-    {
-      type: 'button',
-      btnVariant: 'text',
-      onClick: () => {
-        saveCsvAs(queryResult, 'milvus_query_result.csv');
-      },
-      label: btnTrans('export'),
-      icon: 'download',
-      tooltip: collectionTrans('downloadTooltip'),
-      disabledTooltip: collectionTrans('downloadDisabledTooltip'),
-      disabled: () => !queryResult?.length,
-    },
   ];
 
+  useEffect(() => {
+    if (inputRef.current) {
+      inputRef.current.focus();
+    }
+  }, []);
+
   return (
     <div className={classes.root}>
-      <CustomToolBar toolbarConfigs={toolbarConfigs} />
+      <CustomToolBar toolbarConfigs={toolbarConfigs} hideOnDisable={true} />
       <div className={classes.toolbar}>
         <div className="left">
           <TextField
@@ -214,28 +289,37 @@ const Query = () => {
                 multiline: 'multiline',
               },
             }}
-            placeholder={collectionTrans('exprPlaceHolder')}
             value={expression}
             onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
               setExpression(e.target.value as string);
             }}
+            disabled={!collection.loaded}
+            InputLabelProps={{ shrink: true }}
+            label={collectionTrans('exprPlaceHolder')}
             onKeyDown={e => {
               if (e.key === 'Enter') {
-                // Do code here
-                handleQuery(expression);
+                // reset page
+                setCurrentPage(0);
+                if (expr !== expression) {
+                  setExpr(expression);
+                } else {
+                  // ensure query
+                  query();
+                }
                 e.preventDefault();
               }
             }}
+            inputRef={inputRef}
           />
           <Filter
             ref={filterRef}
             title="Advanced Filter"
-            fields={fields.filter(
-              i =>
+            fields={collection.fields.filter(
+              (i: any) =>
                 i.type !== DataTypeStringEnum.FloatVector &&
                 i.type !== DataTypeStringEnum.BinaryVector
             )}
-            filterDisabled={false}
+            filterDisabled={!collection.loaded}
             onSubmit={handleFilterSubmit}
             showTitle={false}
             showTooltip={false}
@@ -243,13 +327,14 @@ const Query = () => {
           {/* </div> */}
           <CustomSelector
             options={CONSISTENCY_LEVEL_OPTIONS}
-            value={consistency_level}
-            label={collectionTrans('consistencyLevel')}
+            value={collection.consistencyLevel}
+            label={collectionTrans('consistency')}
             wrapperClass={classes.selector}
+            disabled={!collection.loaded}
             variant="filled"
             onChange={(e: { target: { value: unknown } }) => {
               const consistency = e.target.value as string;
-              setConsistency_level(consistency);
+              setConsistencyLevel(consistency);
             }}
           />
         </div>
@@ -257,24 +342,32 @@ const Query = () => {
           <CustomButton
             className="btn"
             onClick={handleFilterReset}
-            disabled={!expression}
+            disabled={!collection.loaded}
           >
             <ResetIcon classes={{ root: 'icon' }} />
             {btnTrans('reset')}
           </CustomButton>
           <CustomButton
             variant="contained"
-            disabled={!expression}
-            onClick={() => handleQuery(expression)}
+            onClick={() => {
+              setCurrentPage(0);
+              if (expr !== expression) {
+                setExpr(expression);
+              } else {
+                // ensure query
+                query();
+              }
+            }}
+            disabled={!collection.loaded}
           >
             {btnTrans('query')}
           </CustomButton>
         </div>
       </div>
-      {tableLoading || queryResult?.length ? (
+      {tableLoading || queryResultMemo?.length ? (
         <AttuGrid
           toolbarConfigs={[]}
-          colDefinitions={fields.map(i => ({
+          colDefinitions={collection.fields.map((i: any) => ({
             id: i.name,
             align: 'left',
             disablePadding: false,
@@ -282,32 +375,28 @@ const Query = () => {
             label:
               i.name === DYNAMIC_FIELD ? searchTrans('dynamicFields') : i.name,
           }))}
-          primaryKey={primaryKey.value}
+          primaryKey={collection.primaryKey.value}
           openCheckBox={true}
           isLoading={!!tableLoading}
-          rows={result}
+          rows={queryResultMemo}
           rowCount={total}
           selected={selectedData}
-          setSelected={handleSelectChange}
+          setSelected={onSelectChange}
           page={currentPage}
           onPageChange={handlePageChange}
+          setRowsPerPage={setPageSize}
           rowsPerPage={pageSize}
-          setRowsPerPage={handlePageSize}
-          orderBy={orderBy}
-          order={order}
-          handleSort={handleGridSort}
-          labelDisplayedRows={getLabelDisplayedRows(`(${latency} ms)`)}
+          labelDisplayedRows={getLabelDisplayedRows(
+            `(${queryResult.latency || ''} ms)`
+          )}
         />
       ) : (
         <EmptyCard
           wrapperClass={`page-empty-card ${classes.emptyCard}`}
           icon={<VectorSearchIcon />}
-          text={
-            queryResult?.length === 0
-              ? searchTrans('empty')
-              : collectionTrans('startTip')
-          }
-          subText={collectionTrans('dataQuerylimits')}
+          text={searchTrans(
+            `${collection.loaded ? 'empty' : 'collectionNotLoaded'}`
+          )}
         />
       )}
     </div>

+ 12 - 5
server/src/collections/collections.controller.ts

@@ -203,14 +203,21 @@ export class CollectionController {
 
   async getCollectionInfo(req: Request, res: Response, next: NextFunction) {
     const name = req.params?.name;
+    const params = {
+      collection_name: name,
+    };
     try {
       const result = await this.collectionsService.describeCollection(
         req.clientId,
-        {
-          collection_name: name,
-        }
+        params
       );
-      res.send(result);
+
+      const loadState = await this.collectionsService.getLoadState(
+        req.clientId,
+        params
+      );
+
+      res.send({ ...result, state: loadState.state });
     } catch (error) {
       next(error);
     }
@@ -346,7 +353,7 @@ export class CollectionController {
     const resultPage: any = req.query?.page;
 
     try {
-      const limit = isNaN(resultLimit) ? 100 : parseInt(resultLimit, 10);
+      const limit = parseInt(resultLimit, 10);
       const page = isNaN(resultPage) ? 0 : parseInt(resultPage, 10);
       // TODO: add page and limit to node SDK
       // Here may raise "Error: 8 RESOURCE_EXHAUSTED: Received message larger than max"

+ 31 - 23
server/src/collections/collections.service.ts

@@ -21,6 +21,7 @@ import {
   HasCollectionReq,
   CountReq,
   FieldSchema,
+  GetLoadStateReq,
 } from '@zilliz/milvus2-sdk-node';
 import { Parser } from '@json2csv/plainjs';
 import {
@@ -43,49 +44,49 @@ export class CollectionsService {
   }
 
   async getCollections(clientId: string, data?: ShowCollectionsReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.showCollections(data);
     throwErrorFromSDK(res.status);
     return res;
   }
 
   async createCollection(clientId: string, data: CreateCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.createCollection(data);
     throwErrorFromSDK(res);
     return res;
   }
 
   async describeCollection(clientId: string, data: DescribeCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.describeCollection(data);
     throwErrorFromSDK(res.status);
     return res;
   }
 
   async renameCollection(clientId: string, data: RenameCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.renameCollection(data);
     throwErrorFromSDK(res);
     return res;
   }
 
   async dropCollection(clientId: string, data: DropCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.dropCollection(data);
     throwErrorFromSDK(res);
     return res;
   }
 
   async loadCollection(clientId: string, data: LoadCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.loadCollection(data);
     throwErrorFromSDK(res);
     return res;
   }
 
   async releaseCollection(clientId: string, data: ReleaseLoadCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.releaseCollection(data);
     throwErrorFromSDK(res);
     return res;
@@ -95,14 +96,21 @@ export class CollectionsService {
     clientId: string,
     data: GetCollectionStatisticsReq
   ) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getCollectionStatistics(data);
     throwErrorFromSDK(res.status);
     return res;
   }
 
+  async getLoadState(clientId: string, data: GetLoadStateReq) {
+    const { milvusClient } = clientCache.get(clientId);
+    const res = await milvusClient.getLoadState(data);
+    throwErrorFromSDK(res.status);
+    return res;
+  }
+
   async count(clientId: string, data: CountReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     let count = 0;
     try {
       const countRes = await milvusClient.count(data);
@@ -118,21 +126,21 @@ export class CollectionsService {
   }
 
   async insert(clientId: string, data: InsertReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.insert(data);
     throwErrorFromSDK(res.status);
     return res;
   }
 
   async deleteEntities(clientId: string, data: DeleteEntitiesReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.deleteEntities(data);
     throwErrorFromSDK(res.status);
     return res;
   }
 
   async vectorSearch(clientId: string, data: SearchReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const now = Date.now();
     const res = await milvusClient.search(data);
     const after = Date.now();
@@ -143,28 +151,28 @@ export class CollectionsService {
   }
 
   async createAlias(clientId: string, data: CreateAliasReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.createAlias(data);
     throwErrorFromSDK(res);
     return res;
   }
 
   async alterAlias(clientId: string, data: AlterAliasReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.alterAlias(data);
     throwErrorFromSDK(res);
     return res;
   }
 
   async dropAlias(clientId: string, data: DropAliasReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.dropAlias(data);
     throwErrorFromSDK(res);
     return res;
   }
 
   async getReplicas(clientId: string, data: GetReplicasDto) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getReplicas(data);
     return res;
   }
@@ -175,7 +183,7 @@ export class CollectionsService {
       collection_name: string;
     } & QueryDto
   ) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const now = Date.now();
     const res = await milvusClient.query(data);
 
@@ -370,14 +378,14 @@ export class CollectionsService {
   }
 
   async getCompactionState(clientId: string, data: GetCompactionStateReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getCompactionState(data);
     throwErrorFromSDK(res.status);
     return res;
   }
 
   async getQuerySegmentInfo(clientId: string, data: GetQuerySegmentInfoReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getQuerySegmentInfo(data);
     throwErrorFromSDK(res.status);
     return res;
@@ -387,21 +395,21 @@ export class CollectionsService {
     clientId: string,
     data: GePersistentSegmentInfoReq
   ) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getPersistentSegmentInfo(data);
     throwErrorFromSDK(res.status);
     return res;
   }
 
   async compact(clientId: string, data: CompactReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.compact(data);
     throwErrorFromSDK(res.status);
     return res;
   }
 
   async hasCollection(clientId: string, data: HasCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.hasCollection(data);
     throwErrorFromSDK(res.status);
     return res;
@@ -430,7 +438,7 @@ export class CollectionsService {
   }
 
   async emptyCollection(clientId: string, data: HasCollectionReq) {
-        const { milvusClient } = clientCache.get(clientId);
+    const { milvusClient } = clientCache.get(clientId);
     const pkField = await milvusClient.getPkFieldName(data);
     const pkType = await milvusClient.getPkFieldType(data);