Browse Source

new search interface with download search result (#348)

Signed-off-by: ruiyi.jiang <ruiyi.jiang@zilliz.com>
ryjiang 1 year ago
parent
commit
8ddba0f223

+ 54 - 46
client/src/components/grid/Table.tsx

@@ -52,6 +52,10 @@ const useStyles = makeStyles(theme => ({
     background: theme.palette.common.white,
     padding: `${theme.spacing(1.5)} ${theme.spacing(2)}`,
   },
+  cellContainer: {
+    display: 'flex',
+    whiteSpace: 'nowrap'
+  },
   hoverActionCell: {
     transition: '0.2s all',
     padding: 0,
@@ -261,53 +265,57 @@ const EnhancedTable: FC<TableType> = props => {
                             className={`${classes.cell} ${classes.tableCell}`}
                             style={cellStyle}
                           >
-                            {row[colDef.id] &&
-                            typeof row[colDef.id] === 'string' ? (
-                              <Typography title={row[colDef.id]}>
-                                {colDef.onClick ? (
-                                  <Button
-                                    color="primary"
-                                    data-data={row[colDef.id]}
-                                    data-index={index}
-                                    size="small"
-                                    onClick={e => {
-                                      colDef.onClick && colDef.onClick(e, row);
-                                    }}
-                                  >
-                                    {row[colDef.id]}
-                                  </Button>
-                                ) : (
-                                  row[colDef.id]
-                                )}
-                              </Typography>
-                            ) : (
-                              <>
-                                {colDef.onClick ? (
-                                  <Button
-                                    color="primary"
-                                    data-data={row[colDef.id]}
-                                    data-index={index}
-                                    size="small"
-                                    onClick={e => {
-                                      colDef.onClick && colDef.onClick(e, row);
-                                    }}
-                                  >
-                                    {row[colDef.id]}
-                                  </Button>
-                                ) : (
-                                  row[colDef.id]
-                                )}
-                              </>
-                            )}
+                            <div className={classes.cellContainer}>
+                              {row[colDef.id] &&
+                              typeof row[colDef.id] === 'string' ? (
+                                <Typography title={row[colDef.id]}>
+                                  {colDef.onClick ? (
+                                    <Button
+                                      color="primary"
+                                      data-data={row[colDef.id]}
+                                      data-index={index}
+                                      size="small"
+                                      onClick={e => {
+                                        colDef.onClick &&
+                                          colDef.onClick(e, row);
+                                      }}
+                                    >
+                                      {row[colDef.id]}
+                                    </Button>
+                                  ) : (
+                                    row[colDef.id]
+                                  )}
+                                </Typography>
+                              ) : (
+                                <>
+                                  {colDef.onClick ? (
+                                    <Button
+                                      color="primary"
+                                      data-data={row[colDef.id]}
+                                      data-index={index}
+                                      size="small"
+                                      onClick={e => {
+                                        colDef.onClick &&
+                                          colDef.onClick(e, row);
+                                      }}
+                                    >
+                                      {row[colDef.id]}
+                                    </Button>
+                                  ) : (
+                                    row[colDef.id]
+                                  )}
+                                </>
+                              )}
 
-                            {needCopy && row[colDef.id] && (
-                              <CopyButton
-                                label={copyTrans.label}
-                                value={row[colDef.id]}
-                                size="small"
-                                className={classes.copyBtn}
-                              />
-                            )}
+                              {needCopy && row[colDef.id] && (
+                                <CopyButton
+                                  label={copyTrans.label}
+                                  value={row[colDef.id]}
+                                  size="small"
+                                  className={classes.copyBtn}
+                                />
+                              )}
+                            </div>
                           </TableCell>
                         );
                       })}

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

@@ -25,6 +25,7 @@ const btnTrans = {
   example: '生成随机向量',
   rename: '重命名',
   duplicate: '复制',
+  export: '导出',
 };
 
 export default btnTrans;

+ 3 - 3
client/src/i18n/cn/search.ts

@@ -1,11 +1,11 @@
 const searchTrans = {
   firstTip: '2. 输入搜索向量 {{dimensionTip}}',
   secondTip: '1. 选择Collection和字段',
-  thirdTip: '3. 设置搜索参数',
+  thirdTip: '搜索参数',
   vectorPlaceholder: '请在此输入您的向量值,例如 [1, 2, 3, 4]',
-  collection: '选择已加载的Collection',
+  collection: '已加载的Collection',
   noCollection: '没有已加载的Collection',
-  field: '选择向量字段',
+  field: '向量字段',
   startTip: '开始您的向量搜索',
   empty: '无结果',
   result: '搜索结果',

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

@@ -25,6 +25,7 @@ const btnTrans = {
   example: 'Generate random vector',
   rename: 'Rename',
   duplicate: 'Duplicate',
+  export: 'Export'
 };
 
 export default btnTrans;

+ 3 - 3
client/src/i18n/en/search.ts

@@ -1,11 +1,11 @@
 const searchTrans = {
   firstTip: '2. Enter search vector {{dimensionTip}}',
   secondTip: '1. Choose collection and field',
-  thirdTip: '3. Set search parameters',
+  thirdTip: 'Search parameters',
   vectorPlaceholder: 'Please input your vector value here, e.g. [1, 2, 3, 4]',
-  collection: 'Choose loaded collection',
+  collection: 'loaded collection',
   noCollection: 'No loaded collection',
-  field: 'Choose vector field',
+  field: 'Vector field',
   startTip: 'Start your vector search',
   empty: 'No result',
   result: 'Search Results',

+ 2 - 13
client/src/pages/query/Query.tsx

@@ -1,11 +1,10 @@
 import { FC, useEffect, useState, useRef, useContext } from 'react';
 import { TextField } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
-import { saveAs } from 'file-saver';
-import { Parser } from '@json2csv/plainjs';
 import { rootContext } from '@/context';
 import { Collection, DataService } from '@/http';
 import { usePaginationHook, useSearchResult } from '@/hooks';
+import { saveCsvAs } from '@/utils';
 import EmptyCard from '@/components/cards/EmptyCard';
 import icons from '@/components/icons/Icons';
 import CustomButton from '@/components/customButton/CustomButton';
@@ -182,17 +181,7 @@ const Query: FC<{
     {
       type: 'iconBtn',
       onClick: () => {
-        try {
-          const opts = {};
-          const parser = new Parser(opts);
-          const csv = parser.parse(queryResult);
-          const csvData = new Blob([csv], {
-            type: 'text/csv;charset=utf-8',
-          });
-          saveAs(csvData, 'milvus_query_result.csv');
-        } catch (err) {
-          console.error(err);
-        }
+        saveCsvAs(queryResult, 'milvus_query_result.csv');
       },
       label: '',
       icon: 'download',

+ 5 - 5
client/src/pages/search/SearchParams.tsx

@@ -26,12 +26,12 @@ const getStyles = makeStyles((theme: Theme) => ({
     marginBottom: theme.spacing(2),
   },
   inlineInput: {
-    width: '48%',
-  },
-  inlineInputWrapper: {
-    display: 'flex',
-    justifyContent: 'space-between',
+    width: 160,
+    '&:nth-child(odd)': {
+      marginRight: theme.spacing(1),
+    },
   },
+  inlineInputWrapper: {},
 }));
 
 const SearchParams: FC<SearchParamsProps> = ({

+ 27 - 16
client/src/pages/search/Styles.ts

@@ -3,18 +3,22 @@ import { makeStyles, Theme } from '@material-ui/core';
 export const getVectorSearchStyles = makeStyles((theme: Theme) => ({
   pageContainer: {
     display: 'flex',
-    flexDirection: 'column',
+    flexDirection: 'row',
+    gap: theme.spacing(2),
   },
   form: {
     display: 'flex',
-    flexDirection: 'row',
+    flexDirection: 'column',
     gap: theme.spacing(0),
+    width: 360,
+    flexShrink: 0,
 
     '& textarea': {
       border: `1px solid ${theme.palette.attuGrey.main}`,
       borderRadius: theme.spacing(0.5),
       padding: theme.spacing(0.5, 1),
       marginTop: theme.spacing(0),
+      marginBottom: theme.spacing(1),
       overflow: 'scroll',
       height: '130px',
       maxWidth: '100%',
@@ -23,31 +27,33 @@ export const getVectorSearchStyles = makeStyles((theme: Theme) => ({
       boxSizing: 'border-box',
     },
     '& .text': {
-      marginBottom: theme.spacing(2),
+      marginBottom: theme.spacing(1),
+      fontWeight: '600',
     },
-    height: '210px',
     overflow: 'hidden',
   },
   s1: {
+    '& .wrapper': {
+      display: 'flex',
+      flexDirection: 'row',
+      gap: theme.spacing(2),
+    },
+
     '& .MuiSelect-root': {
-      minWidth: '240px',
+      minWidth: '116px',
     },
   },
   s2: {
-    minWidth: '600px',
     position: 'relative',
+    textAlign: 'right',
   },
-  s3: {
-    minWidth: '260px',
-  },
+  s3: {},
   selector: {
     display: 'block',
-    marginBottom: theme.spacing(2),
+    marginBottom: theme.spacing(0),
   },
   exampleBtn: {
-    right: theme.spacing(2),
-    top: theme.spacing(1.5),
-    position: 'absolute',
+    marginRight: theme.spacing(1),
   },
   paramsWrapper: {
     display: 'flex',
@@ -57,15 +63,15 @@ export const getVectorSearchStyles = makeStyles((theme: Theme) => ({
   resultsWrapper: {
     display: 'flex',
     flexDirection: 'column',
-    flexGrow: 1,
+    flexGrow: 0,
+    width: 'calc(100vw - 500px)', // replace 300px with the actual width of your form
+    height: `calc(100vh - 108px)`,
   },
   toolbar: {
     display: 'flex',
     justifyContent: 'space-between',
     alignItems: 'center',
 
-    padding: theme.spacing(2, 0),
-
     '& .left': {
       display: 'flex',
       alignItems: 'center',
@@ -101,4 +107,9 @@ export const getVectorSearchStyles = makeStyles((theme: Theme) => ({
   error: {
     color: theme.palette.error.main,
   },
+
+  vectorTableCell: {
+    display: 'flex',
+    whiteSpace: 'nowrap'
+  },
 }));

+ 65 - 64
client/src/pages/search/VectorSearch.tsx

@@ -5,6 +5,7 @@ import { useLocation } from 'react-router-dom';
 import { ALL_ROUTER_TYPES } from '@/router/Types';
 import { useNavigationHook, useSearchResult, usePaginationHook } from '@/hooks';
 import { dataContext } from '@/context';
+import { saveCsvAs } from '@/utils';
 import CustomSelector from '@/components/customSelector/CustomSelector';
 import { ColDefinitionsType } from '@/components/grid/Types';
 import AttuGrid from '@/components/grid/Grid';
@@ -310,6 +311,7 @@ const VectorSearch = () => {
   const VectorSearchIcon = icons.vectorSearch;
   const ResetIcon = icons.refresh;
   const ArrowIcon = icons.dropdown;
+  const ExportIcon = icons.download;
 
   // methods
   const handlePageChange = (e: any, page: number) => {
@@ -384,66 +386,40 @@ const VectorSearch = () => {
     <section className={`page-wrapper ${classes.pageContainer}`}>
       <Card className={classes.form}>
         <CardContent className={classes.s1}>
-          <Typography className="text">{searchTrans('secondTip')}</Typography>
-          <CustomSelector
-            options={collectionOptions}
-            wrapperClass={classes.selector}
-            variant="filled"
-            label={searchTrans(
-              collectionOptions.length === 0 ? 'noCollection' : 'collection'
-            )}
-            disabled={collectionOptions.length === 0}
-            value={selectedCollection}
-            onChange={(e: { target: { value: unknown } }) => {
-              const collection = e.target.value as string;
-
-              setSelectedCollection(collection);
-              // every time selected collection changed, reset field
-              setSelectedField('');
-              setSearchResult([]);
-            }}
-          />
-          <CustomSelector
-            options={fieldOptions}
-            // readOnly can't avoid all events, so we use disabled instead
-            disabled={selectedCollection === ''}
-            wrapperClass={classes.selector}
-            variant="filled"
-            label={searchTrans('field')}
-            value={selectedField}
-            onChange={(e: { target: { value: unknown } }) => {
-              const field = e.target.value as string;
-              setSelectedField(field);
-            }}
-          />
+          <div className="wrapper">
+            <CustomSelector
+              options={collectionOptions}
+              wrapperClass={classes.selector}
+              variant="filled"
+              label={searchTrans('collection')}
+              disabled={collectionOptions.length === 0}
+              value={selectedCollection}
+              onChange={(e: { target: { value: unknown } }) => {
+                const collection = e.target.value as string;
+
+                setSelectedCollection(collection);
+                // every time selected collection changed, reset field
+                setSelectedField('');
+                setSearchResult([]);
+              }}
+            />
+            <CustomSelector
+              options={fieldOptions}
+              // readOnly can't avoid all events, so we use disabled instead
+              disabled={selectedCollection === ''}
+              wrapperClass={classes.selector}
+              variant="filled"
+              label={searchTrans('field')}
+              value={selectedField}
+              onChange={(e: { target: { value: unknown } }) => {
+                const field = e.target.value as string;
+                setSelectedField(field);
+              }}
+            />
+          </div>
         </CardContent>
 
         <CardContent className={classes.s2}>
-          <Typography className="text">
-            {searchTrans('firstTip', {
-              dimensionTip:
-                selectedFieldDimension !== 0
-                  ? `(dimension: ${selectedFieldDimension})`
-                  : '',
-            })}
-            {selectedFieldDimension !== 0 ? (
-              <Button
-                className={classes.exampleBtn}
-                variant="outlined"
-                size="small"
-                onClick={() => {
-                  const dim =
-                    fieldType === DataTypeEnum.BinaryVector
-                      ? selectedFieldDimension / 8
-                      : selectedFieldDimension;
-                  fillWithExampleVector(dim);
-                }}
-              >
-                {btnTrans('example')}
-              </Button>
-            ) : null}
-          </Typography>
-
           <textarea
             className="textarea"
             placeholder={searchTrans('vectorPlaceholder')}
@@ -452,6 +428,27 @@ const VectorSearch = () => {
               handleVectorChange(e.target.value as string);
             }}
           />
+          {selectedFieldDimension !== 0 ? (
+            <Button
+              className={classes.exampleBtn}
+              onClick={() => {
+                const dim =
+                  fieldType === DataTypeEnum.BinaryVector
+                    ? selectedFieldDimension / 8
+                    : selectedFieldDimension;
+                fillWithExampleVector(dim);
+              }}
+            >
+              {btnTrans('example')}
+            </Button>
+          ) : null}
+          <CustomButton
+            variant="contained"
+            disabled={searchDisabled}
+            onClick={() => handleSearch(topK)}
+          >
+            {btnTrans('search')}
+          </CustomButton>
           {/* validation */}
           {!vectorValueValid && (
             <Typography variant="caption" className={classes.error}>
@@ -518,19 +515,23 @@ const VectorSearch = () => {
               filterDisabled={selectedField === '' || selectedCollection === ''}
               onSubmit={handleAdvancedFilterChange}
             />
+            <CustomButton
+              className="btn"
+              disabled={result.length === 0}
+              onClick={() => {
+                console.log(searchResult)
+                saveCsvAs(searchResult, `search_result_${selectedCollection}`);
+              }}
+            >
+              <ExportIcon classes={{ root: 'icon' }} />
+              {btnTrans('export')}
+            </CustomButton>
           </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>
 

+ 18 - 1
client/src/utils/Common.ts

@@ -1,3 +1,6 @@
+import { saveAs } from 'file-saver';
+import { Parser } from '@json2csv/plainjs';
+
 export const copyToCommand = (
   value: string,
   classSelector?: string,
@@ -63,7 +66,7 @@ export const generateIdByHash = (salt?: string) => {
 };
 
 export const generateVector = (dim: number) => {
-  return Array.from({ length: dim }).map(() => (Math.random()));
+  return Array.from({ length: dim }).map(() => Math.random());
 };
 
 export const cloneObj = (obj: any) => {
@@ -83,3 +86,17 @@ export const detectItemType = (item: unknown) => {
     return 'unknown';
   }
 };
+
+export const saveCsvAs = (csvObj: any, as: string) => {
+  try {
+    const opts = {};
+    const parser = new Parser(opts);
+    const csv = parser.parse(csvObj);
+    const csvData = new Blob([csv], {
+      type: 'text/csv;charset=utf-8',
+    });
+    saveAs(csvData, as);
+  } catch (err) {
+    console.error(err);
+  }
+};