Browse Source

update search params default value

tumao 4 years ago
parent
commit
3f9137a533

+ 14 - 0
client/src/consts/Milvus.tsx

@@ -159,3 +159,17 @@ export const DEFAULT_METRIC_VALUE_MAP = {
   [EmbeddingTypeEnum.float]: METRIC_TYPES_VALUES.L2,
   [EmbeddingTypeEnum.binary]: METRIC_TYPES_VALUES.HAMMING,
 };
+
+// search params default value map
+export const DEFAULT_SEARCH_PARAM_VALUE_MAP: {
+  [key in searchKeywordsType]: number;
+} = {
+  // range: [top_k, 32768]
+  ef: 50,
+  // range: [1, nlist]
+  nprobe: 1,
+  // range: {-1} ∪ [top_k, n × n_trees]
+  search_k: 50,
+  // range: [10, 300]
+  search_length: 10,
+};

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

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

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

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

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

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

+ 216 - 13
client/src/pages/seach/SearchParams.tsx

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

+ 1 - 1
client/src/pages/seach/Styles.ts

@@ -75,7 +75,7 @@ export const getVectorSearchStyles = makeStyles((theme: Theme) => ({
   },
   paramsWrapper: {
     display: 'flex',
-    justifyContent: 'space-between',
+    flexDirection: 'column',
   },
   toolbar: {
     display: 'flex',

+ 20 - 4
client/src/pages/seach/Types.ts

@@ -1,6 +1,7 @@
 import { Option } from '../../components/customSelector/Types';
-import { EmbeddingTypeEnum } from '../../consts/Milvus';
+import { EmbeddingTypeEnum, searchKeywordsType } from '../../consts/Milvus';
 import { DataType } from '../collections/Types';
+import { IndexView } from '../schema/Types';
 
 export interface SearchParamsProps {
   // if user created index, pass metric type choosed when creating
@@ -10,11 +11,15 @@ export interface SearchParamsProps {
   embeddingType: EmbeddingTypeEnum;
   // default index type is FLAT
   indexType: string;
+  // index extra params, e.g. nlist
+  indexParams: { key: string; value: string }[];
   searchParamsForm: {
     [key in string]: number;
   };
+  topK: number;
   handleFormChange: (form: { [key in string]: number }) => void;
   wrapperClass?: string;
+  setParamsDisabled: (isDisabled: boolean) => void;
 }
 
 export interface SearchResultView {
@@ -26,7 +31,18 @@ export interface SearchResultView {
 
 export interface FieldOption extends Option {
   fieldType: DataType;
-  // if user doesn't create index, use empty string as metric type
-  metricType: string;
-  indexType: string;
+  // used to get metric type, index type and index params for search params
+  // if user doesn't create index, default value is null
+  indexInfo: IndexView | null;
+}
+
+export interface SearchParamInputConfig {
+  label: string;
+  key: searchKeywordsType;
+  min: number;
+  max: number;
+  isInt?: boolean;
+  // no value: empty string
+  value: number | string;
+  handleChange: (value: number) => void;
 }

+ 17 - 13
client/src/pages/seach/VectorSearch.tsx

@@ -1,8 +1,4 @@
-import {
-  Chip,
-  TextField,
-  Typography,
-} from '@material-ui/core';
+import { Chip, TextField, Typography } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
 import { useNavigationHook } from '../../hooks/Navigation';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
@@ -40,9 +36,12 @@ const VectorSearch = () => {
   const [selectedCollection, setSelectedCollection] = useState<string>('');
   const [fieldOptions, setFieldOptions] = useState<FieldOption[]>([]);
   const [selectedField, setSelectedField] = useState<string>('');
+  // search params form
   const [searchParam, setSearchParam] = useState<{ [key in string]: number }>(
     {}
   );
+  // search params disable state
+  const [paramDisabled, setParamDisabled] = useState<boolean>(true);
   const [searchResult, setSearchResult] = useState<SearchResultView[]>([]);
   // default topK is 100
   const [topK, setTopK] = useState<number>(100);
@@ -72,29 +71,33 @@ const VectorSearch = () => {
     return fields.map(f => f._fieldName);
   }, [selectedCollection, collections]);
 
-  const { metricType, indexType } = useMemo(() => {
+  const { metricType, indexType, indexParams } = 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 =
         selectedFieldInfo!.fieldType === 'BinaryVector'
           ? EmbeddingTypeEnum.binary
           : EmbeddingTypeEnum.float;
 
       const metric =
-        selectedFieldInfo!.metricType ||
-        DEFAULT_METRIC_VALUE_MAP[embeddingType];
+        index!._metricType || DEFAULT_METRIC_VALUE_MAP[embeddingType];
+
+      const indexParams = index?._indexParameterPairs || [];
 
       return {
         metricType: metric,
-        indexType: selectedFieldInfo!.indexType,
+        indexType: index!._indexType,
+        indexParams,
       };
     }
 
-    return { metricType: '', indexType: '' };
+    return { metricType: '', indexType: '', indexParams: [] };
   }, [selectedField, fieldOptions]);
 
   // fetch data
@@ -109,7 +112,6 @@ const VectorSearch = () => {
         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
@@ -121,8 +123,7 @@ const VectorSearch = () => {
             label: `${f._fieldName} (${index?._indexType || 'FLAT'})`,
             value: f._fieldName,
             fieldType: f._fieldType,
-            metricType: index?._metricType || '',
-            indexType: index?._indexType || 'FLAT',
+            indexInfo: index || null,
           };
         });
       setFieldOptions(fieldOptions);
@@ -221,8 +222,11 @@ const VectorSearch = () => {
             metricType={metricType!}
             embeddingType={EmbeddingTypeEnum.float}
             indexType={indexType}
+            indexParams={indexParams!}
             searchParamsForm={searchParam}
             handleFormChange={setSearchParam}
+            topK={topK}
+            setParamsDisabled={setParamDisabled}
           />
         </fieldset>
       </form>

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

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