Browse Source

Merge branch 'main' of github.com:milvus-io/milvus-insight into main

nameczz 4 years ago
parent
commit
172f0e9b0f

BIN
.github/images/screenshot.png


+ 1 - 1
client/src/components/menu/NavMenu.tsx

@@ -146,7 +146,7 @@ const useStyles = makeStyles((theme: Theme) =>
 const NavMenu: FC<NavMenuType> = props => {
   const { width, data, defaultActive = '' } = props;
   const classes = useStyles({ width });
-  const [expanded, setExpanded] = useState<boolean>(true);
+  const [expanded, setExpanded] = useState<boolean>(false);
   const [active, setActive] = useState<string>(defaultActive);
 
   const { t } = useTranslation();

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

@@ -1,5 +1,5 @@
 const searchTrans = {
-  firstTip: '1. Enter vector value',
+  firstTip: '1. Enter vector value {{dimensionTip}}',
   secondTip: '2. Choose collection and field',
   thirdTip: '3. Set search parameters',
   vectorPlaceholder: 'Please input your vector value here, e.g. [1, 2, 3, 4]',
@@ -11,6 +11,8 @@ const searchTrans = {
   result: 'Search Results',
   topK: 'TopK {{number}}',
   filter: 'Advanced Filter',
+  vectorValueWarning:
+    'Vector value should be an array of length {{dimension}}(dimension)',
 };
 
 export default searchTrans;

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

@@ -1,5 +1,5 @@
 const searchTrans = {
-  firstTip: '1. Enter vector value',
+  firstTip: '1. Enter vector value {{dimensionTip}}',
   secondTip: '2. Choose collection and field',
   thirdTip: '3. Set search parameters',
   vectorPlaceholder: 'Please input your vector value here, e.g. [1, 2, 3, 4]',
@@ -11,6 +11,7 @@ const searchTrans = {
   result: 'Search Results',
   topK: 'TopK {{number}}',
   filter: 'Advanced Filter',
+  vectorValueWarning: 'Vector value should be an array of length {{dimension}}',
 };
 
 export default searchTrans;

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

@@ -122,4 +122,8 @@ export const getVectorSearchStyles = makeStyles((theme: Theme) => ({
     lineHeight: '16px',
     color: theme.palette.milvusGrey.dark,
   },
+  error: {
+    marginTop: theme.spacing(1),
+    color: theme.palette.error.main,
+  },
 }));

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

@@ -34,6 +34,8 @@ export interface FieldOption extends Option {
   // used to get metric type, index type and index params for search params
   // if user doesn't create index, default value is null
   indexInfo: IndexView | null;
+  // used for check vector input validation
+  dimension: number;
 }
 
 export interface SearchParamInputConfig {

+ 93 - 45
client/src/pages/seach/VectorSearch.tsx

@@ -75,21 +75,6 @@ const VectorSearch = () => {
     data: result,
   } = usePaginationHook(searchResult || []);
 
-  const searchDisabled = useMemo(() => {
-    /**
-     * before search, user must:
-     * 1. enter vector value
-     * 2. choose collection and field
-     * 3. set extra search params
-     */
-    const isInvalid =
-      vectors === '' ||
-      selectedCollection === '' ||
-      selectedField === '' ||
-      paramDisabled;
-    return isInvalid;
-  }, [paramDisabled, selectedField, selectedCollection, vectors]);
-
   const collectionOptions: Option[] = useMemo(
     () =>
       collections.map(c => ({
@@ -124,36 +109,81 @@ const VectorSearch = () => {
       : [];
   }, [searchResult]);
 
-  const { metricType, indexType, indexParams, fieldType, embeddingType } =
-    useMemo(() => {
-      if (selectedField !== '') {
-        // field options must contain selected field, so selectedFieldInfo will never undefined
-        const selectedFieldInfo = fieldOptions.find(
-          f => f.value === selectedField
-        );
-        const index = selectedFieldInfo?.indexInfo;
-        const embeddingType = getEmbeddingType(selectedFieldInfo!.fieldType);
-        const metric =
-          index?._metricType || DEFAULT_METRIC_VALUE_MAP[embeddingType];
-        const indexParams = index?._indexParameterPairs || [];
-
-        return {
-          metricType: metric,
-          indexType: index?._indexType || getDefaultIndexType(embeddingType),
-          indexParams,
-          fieldType: DataTypeEnum[selectedFieldInfo?.fieldType!],
-          embeddingType,
-        };
-      }
+  const {
+    metricType,
+    indexType,
+    indexParams,
+    fieldType,
+    embeddingType,
+    selectedFieldDimension,
+  } = useMemo(() => {
+    if (selectedField !== '') {
+      // field options must contain selected field, so selectedFieldInfo will never undefined
+      const selectedFieldInfo = fieldOptions.find(
+        f => f.value === selectedField
+      );
+      const index = selectedFieldInfo?.indexInfo;
+      const embeddingType = getEmbeddingType(selectedFieldInfo!.fieldType);
+      const metric =
+        index?._metricType || DEFAULT_METRIC_VALUE_MAP[embeddingType];
+      const indexParams = index?._indexParameterPairs || [];
+      const dim = selectedFieldInfo?.dimension || 0;
 
       return {
-        metricType: '',
-        indexType: '',
-        indexParams: [],
-        fieldType: 0,
-        embeddingType: DataTypeEnum.FloatVector,
+        metricType: metric,
+        indexType: index?._indexType || getDefaultIndexType(embeddingType),
+        indexParams,
+        fieldType: DataTypeEnum[selectedFieldInfo?.fieldType!],
+        embeddingType,
+        selectedFieldDimension: dim,
       };
-    }, [selectedField, fieldOptions]);
+    }
+
+    return {
+      metricType: '',
+      indexType: '',
+      indexParams: [],
+      fieldType: 0,
+      embeddingType: DataTypeEnum.FloatVector,
+      selectedFieldDimension: 0,
+    };
+  }, [selectedField, fieldOptions]);
+
+  /**
+   * vector value validation
+   * @return whether is valid
+   */
+  const vectorValueValid = useMemo(() => {
+    // if user hasn't input value or not select field, don't trigger validation check
+    if (vectors === '' || selectedFieldDimension === 0) {
+      return true;
+    }
+    const value = parseValue(vectors);
+    const isArray = Array.isArray(value);
+    return isArray && value.length === selectedFieldDimension;
+  }, [vectors, selectedFieldDimension]);
+
+  const searchDisabled = useMemo(() => {
+    /**
+     * before search, user must:
+     * 1. enter vector value, it should be an array and length should be equal to selected field dimension
+     * 2. choose collection and field
+     * 3. set extra search params
+     */
+    const isInvalid =
+      vectors === '' ||
+      selectedCollection === '' ||
+      selectedField === '' ||
+      paramDisabled ||
+      !vectorValueValid;
+    return isInvalid;
+  }, [
+    paramDisabled,
+    selectedField,
+    selectedCollection,
+    vectors,
+    vectorValueValid,
+  ]);
 
   // fetch data
   const fetchCollections = useCallback(async () => {
@@ -285,9 +315,19 @@ const VectorSearch = () => {
     <section className="page-wrapper">
       {/* form section */}
       <form className={classes.form}>
-        {/* vector value textarea */}
-        <fieldset className="field">
-          <Typography className="text">{searchTrans('firstTip')}</Typography>
+        {/**
+         * vector value textarea
+         * use field-params class because it also has error msg if invalid
+         */}
+        <fieldset className="field field-params">
+          <Typography className="text">
+            {searchTrans('firstTip', {
+              dimensionTip:
+                selectedFieldDimension !== 0
+                  ? `(dimension: ${selectedFieldDimension})`
+                  : '',
+            })}
+          </Typography>
           <TextField
             className="textarea"
             InputProps={{
@@ -304,6 +344,14 @@ const VectorSearch = () => {
               handleVectorChange(e.target.value as string);
             }}
           />
+          {/* validation */}
+          {!vectorValueValid && (
+            <Typography variant="caption" className={classes.error}>
+              {searchTrans('vectorValueWarning', {
+                dimension: selectedFieldDimension,
+              })}
+            </Typography>
+          )}
         </fieldset>
         {/* collection and field selectors */}
         <fieldset className="field field-second">

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

@@ -89,6 +89,7 @@ export const getVectorFieldOptions = (
       value: f._fieldName,
       fieldType: f._fieldType,
       indexInfo: index || null,
+      dimension: Number(f._dimension),
     };
   });
 

+ 3 - 3
server/generate-csv.ts

@@ -18,9 +18,9 @@ const generateVector = (dimension) => {
   return JSON.stringify(vectors);
 };
 
-while (records.length < 50000) {
-  const value = generateVector(4);
-  records.push({ vector: value, age: 10 });
+while (records.length < 5000) {
+  const value = generateVector(128);
+  records.push({ vector: value });
 }
 
 csvWriter

+ 2 - 0
server/package.json

@@ -37,6 +37,8 @@
     "cache-manager": "^3.4.4",
     "class-transformer": "^0.4.0",
     "class-validator": "^0.13.1",
+    "helmet": "^4.6.0",
+    "hyperlinker": "^1.0.0",
     "passport": "^0.4.1",
     "passport-jwt": "^4.0.0",
     "passport-local": "^1.0.0",

+ 32 - 7
server/src/main.ts

@@ -1,26 +1,51 @@
+import * as helmet from 'helmet';
 import { NestFactory } from '@nestjs/core';
-import { Logger } from '@nestjs/common';
-
 import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
 import { AppModule } from './app.module';
 import { json } from 'body-parser';
+const hyperlinker = require('hyperlinker');
 
+/*
+  Milvus insight API server bootstrap function
+*/
 async function bootstrap() {
+  // by default the server will be listening on port 3000
   const port = 3000;
-  const app = await NestFactory.create(AppModule, {
-    cors: true,
-  });
+  // create the nest application with Cross-origin resource sharing
+  const app = await NestFactory.create(AppModule, { cors: true });
+  // security patches
+  app.use(helmet());
+  // set upload file size limit
+  app.use(json({ limit: '150mb' }));
+  // add an API prefix
   app.setGlobalPrefix('/api/v1');
 
+  // prepare swagger config
   const config = new DocumentBuilder()
     .setTitle('Milvus insight')
     .setVersion('1.0')
     .build();
+  // create swagger document
   const document = SwaggerModule.createDocument(app, config);
+  // set up API
   SwaggerModule.setup('api', app, document);
-  app.use(json({ limit: '150mb' }));
 
+  // start listening
   await app.listen(port);
-  Logger.log(`Milvus insight API server is running on port ${port}`);
+
+  // output server info
+  require('dns').lookup(require('os').hostname(), (err, add, fam) => {
+    // get link
+    // add = `127.0.0.1`;
+    const link = `http://${add}:${port}/api`;
+    const blue = `\x1b[34m%s\x1b[0m`;
+    const light = '\x1b[1m%s\x1b[0m';
+    console.log(blue, '\n    Milvus insight server started.');
+    console.log(
+      light,
+      `    View the API docs on ${hyperlinker(link, link)} \n`,
+    );
+  });
 }
+// Start the server
 bootstrap();

+ 10 - 0
server/yarn.lock

@@ -3132,6 +3132,11 @@ has@^1.0.3:
   dependencies:
     function-bind "^1.1.1"
 
+helmet@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.npmjs.org/helmet/-/helmet-4.6.0.tgz#579971196ba93c5978eb019e4e8ec0e50076b4df"
+  integrity sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==
+
 hosted-git-info@^2.1.4:
   version "2.8.9"
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
@@ -3193,6 +3198,11 @@ human-signals@^1.1.1:
   resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
   integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
 
+hyperlinker@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmjs.org/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e"
+  integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ==
+
 iconv-lite@0.4.24, iconv-lite@^0.4.24:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"