Procházet zdrojové kódy

update expression filter

tumao před 4 roky
rodič
revize
bc44069ef6

+ 17 - 4
client/src/components/advancedSearch/Filter.tsx

@@ -1,9 +1,8 @@
-import React, { useState, useEffect } from 'react';
+import { useState, useEffect } from 'react';
 import {
 import {
   makeStyles,
   makeStyles,
   Theme,
   Theme,
   createStyles,
   createStyles,
-  Button,
   Chip,
   Chip,
   Tooltip,
   Tooltip,
 } from '@material-ui/core';
 } from '@material-ui/core';
@@ -11,12 +10,14 @@ import FilterListIcon from '@material-ui/icons/FilterList';
 import AdvancedDialog from './Dialog';
 import AdvancedDialog from './Dialog';
 import { FilterProps, ConditionData } from './Types';
 import { FilterProps, ConditionData } from './Types';
 import { generateIdByHash } from '../../utils/Common';
 import { generateIdByHash } from '../../utils/Common';
+import CustomButton from '../customButton/CustomButton';
 
 
 const Filter = function Filter(props: FilterProps) {
 const Filter = function Filter(props: FilterProps) {
   const {
   const {
     title = 'title',
     title = 'title',
     showTitle = true,
     showTitle = true,
     className = '',
     className = '',
+    filterDisabled = false,
     tooltipPlacement = 'top',
     tooltipPlacement = 'top',
     onSubmit,
     onSubmit,
     fields = [],
     fields = [],
@@ -31,6 +32,14 @@ const Filter = function Filter(props: FilterProps) {
   const [isConditionsLegal, setIsConditionsLegal] = useState(false);
   const [isConditionsLegal, setIsConditionsLegal] = useState(false);
   const [filterExpression, setFilterExpression] = useState('');
   const [filterExpression, setFilterExpression] = useState('');
 
 
+  // if fields if empty array, reset all conditions
+  useEffect(() => {
+    if (fields.length === 0) {
+      setFlatConditions([]);
+      setInitConditions([]);
+    }
+  }, [fields]);
+
   // Check all conditions are all correct.
   // Check all conditions are all correct.
   useEffect(() => {
   useEffect(() => {
     // Calc the sum of conditions.
     // Calc the sum of conditions.
@@ -256,10 +265,14 @@ const Filter = function Filter(props: FilterProps) {
   return (
   return (
     <>
     <>
       <div className={`${classes.wrapper} ${className}`} {...others}>
       <div className={`${classes.wrapper} ${className}`} {...others}>
-        <Button className={`${classes.afBtn} af-btn`} onClick={handleClickOpen}>
+        <CustomButton
+          disabled={filterDisabled}
+          className={`${classes.afBtn} af-btn`}
+          onClick={handleClickOpen}
+        >
           <FilterListIcon />
           <FilterListIcon />
           {showTitle ? title : ''}
           {showTitle ? title : ''}
-        </Button>
+        </CustomButton>
         {conditionSum > 0 && (
         {conditionSum > 0 && (
           <Tooltip
           <Tooltip
             arrow
             arrow

+ 1 - 0
client/src/components/advancedSearch/Types.ts

@@ -68,6 +68,7 @@ export interface FilterProps {
   className?: string;
   className?: string;
   title: string;
   title: string;
   showTitle?: boolean;
   showTitle?: boolean;
+  filterDisabled?: boolean;
   others?: object;
   others?: object;
   onSubmit: (data: any) => void;
   onSubmit: (data: any) => void;
   tooltipPlacement?: 'left' | 'right' | 'bottom' | 'top';
   tooltipPlacement?: 'left' | 'right' | 'bottom' | 'top';

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

@@ -5,7 +5,8 @@ const searchTrans = {
   vectorPlaceholder: 'Please input your vector value here, e.g. [1, 2, 3, 4]',
   vectorPlaceholder: 'Please input your vector value here, e.g. [1, 2, 3, 4]',
   collection: 'Choose Collection',
   collection: 'Choose Collection',
   field: 'Choose Field',
   field: 'Choose Field',
-  empty: 'start your vector search',
+  startTip: 'start your vector search',
+  empty: 'no result',
   result: 'Search Results',
   result: 'Search Results',
   topK: 'TopK {{number}}',
   topK: 'TopK {{number}}',
   filter: 'Advanced Filter',
   filter: 'Advanced Filter',

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

@@ -5,7 +5,8 @@ const searchTrans = {
   vectorPlaceholder: 'Please input your vector value here, e.g. [1, 2, 3, 4]',
   vectorPlaceholder: 'Please input your vector value here, e.g. [1, 2, 3, 4]',
   collection: 'Choose Collection',
   collection: 'Choose Collection',
   field: 'Choose Field',
   field: 'Choose Field',
-  empty: 'start your vector search',
+  startTip: 'start your vector search',
+  empty: 'no result',
   result: 'Search Results',
   result: 'Search Results',
   topK: 'TopK {{number}}',
   topK: 'TopK {{number}}',
   filter: 'Advanced Filter',
   filter: 'Advanced Filter',

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

@@ -91,12 +91,6 @@ export const getVectorSearchStyles = makeStyles((theme: Theme) => ({
       '& .text': {
       '& .text': {
         color: theme.palette.milvusGrey.main,
         color: theme.palette.milvusGrey.main,
       },
       },
-
-      '& .button': {
-        marginLeft: theme.spacing(1),
-        fontSize: '16px',
-        lineHeight: '24px',
-      },
     },
     },
     '& .right': {
     '& .right': {
       '& .btn': {
       '& .btn': {
@@ -111,7 +105,7 @@ export const getVectorSearchStyles = makeStyles((theme: Theme) => ({
     minWidth: '108px',
     minWidth: '108px',
 
 
     padding: theme.spacing(0, 1),
     padding: theme.spacing(0, 1),
-    marginLeft: theme.spacing(1),
+    margin: theme.spacing(0, 1),
 
 
     backgroundColor: '#fff',
     backgroundColor: '#fff',
     color: theme.palette.milvusGrey.dark,
     color: theme.palette.milvusGrey.dark,
@@ -122,17 +116,4 @@ export const getVectorSearchStyles = makeStyles((theme: Theme) => ({
     lineHeight: '16px',
     lineHeight: '16px',
     color: theme.palette.milvusGrey.dark,
     color: theme.palette.milvusGrey.dark,
   },
   },
-  chip: {
-    display: 'flex',
-    alignItems: 'center',
-    padding: theme.spacing(0, 0.5, 0, 1),
-    marginLeft: theme.spacing(1),
-  },
-  chipLabel: {
-    color: theme.palette.primary.main,
-    paddingLeft: 0,
-    paddingRight: theme.spacing(1),
-    fontSize: '12px',
-    lineHeight: '16px',
-  },
 }));
 }));

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

@@ -1,4 +1,4 @@
-import { Chip, TextField, Typography } from '@material-ui/core';
+import { TextField, Typography } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { useNavigationHook } from '../../hooks/Navigation';
 import { useNavigationHook } from '../../hooks/Navigation';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
@@ -19,16 +19,21 @@ import SimpleMenu from '../../components/menu/SimpleMenu';
 import { TOP_K_OPTIONS } from './Constants';
 import { TOP_K_OPTIONS } from './Constants';
 import { Option } from '../../components/customSelector/Types';
 import { Option } from '../../components/customSelector/Types';
 import { CollectionHttp } from '../../http/Collection';
 import { CollectionHttp } from '../../http/Collection';
-import { CollectionData, DataType, DataTypeEnum } from '../collections/Types';
+import { CollectionData, DataTypeEnum } from '../collections/Types';
 import { IndexHttp } from '../../http/Index';
 import { IndexHttp } from '../../http/Index';
 import { getVectorSearchStyles } from './Styles';
 import { getVectorSearchStyles } from './Styles';
 import { parseValue } from '../../utils/Insert';
 import { parseValue } from '../../utils/Insert';
 import {
 import {
+  classifyFields,
   getDefaultIndexType,
   getDefaultIndexType,
   getEmbeddingType,
   getEmbeddingType,
+  getNonVectorFieldsForFilter,
+  getVectorFieldOptions,
   transferSearchResult,
   transferSearchResult,
 } from '../../utils/search';
 } from '../../utils/search';
 import { ColDefinitionsType } from '../../components/grid/Types';
 import { ColDefinitionsType } from '../../components/grid/Types';
+import Filter from '../../components/advancedSearch';
+import { Field } from '../../components/advancedSearch/Types';
 
 
 const VectorSearch = () => {
 const VectorSearch = () => {
   useNavigationHook(ALL_ROUTER_TYPES.SEARCH);
   useNavigationHook(ALL_ROUTER_TYPES.SEARCH);
@@ -41,6 +46,8 @@ const VectorSearch = () => {
   const [collections, setCollections] = useState<CollectionData[]>([]);
   const [collections, setCollections] = useState<CollectionData[]>([]);
   const [selectedCollection, setSelectedCollection] = useState<string>('');
   const [selectedCollection, setSelectedCollection] = useState<string>('');
   const [fieldOptions, setFieldOptions] = useState<FieldOption[]>([]);
   const [fieldOptions, setFieldOptions] = useState<FieldOption[]>([]);
+  // fields for advanced filter
+  const [filterFields, setFilterFields] = useState<Field[]>([]);
   const [selectedField, setSelectedField] = useState<string>('');
   const [selectedField, setSelectedField] = useState<string>('');
   // search params form
   // search params form
   const [searchParam, setSearchParam] = useState<{ [key in string]: number }>(
   const [searchParam, setSearchParam] = useState<{ [key in string]: number }>(
@@ -48,9 +55,13 @@ const VectorSearch = () => {
   );
   );
   // search params disable state
   // search params disable state
   const [paramDisabled, setParamDisabled] = useState<boolean>(true);
   const [paramDisabled, setParamDisabled] = useState<boolean>(true);
-  const [searchResult, setSearchResult] = useState<SearchResultView[]>([]);
+  // use null as init value before search, empty array means no results
+  const [searchResult, setSearchResult] = useState<SearchResultView[] | null>(
+    null
+  );
   // default topK is 100
   // default topK is 100
   const [topK, setTopK] = useState<number>(100);
   const [topK, setTopK] = useState<number>(100);
+  const [expression, setExpression] = useState<string>('');
   const [vectors, setVectors] = useState<string>('');
   const [vectors, setVectors] = useState<string>('');
 
 
   const {
   const {
@@ -60,7 +71,7 @@ const VectorSearch = () => {
     handleCurrentPage,
     handleCurrentPage,
     total,
     total,
     data: result,
     data: result,
-  } = usePaginationHook(searchResult);
+  } = usePaginationHook(searchResult || []);
 
 
   const searchDisabled = useMemo(() => {
   const searchDisabled = useMemo(() => {
     /**
     /**
@@ -99,7 +110,7 @@ const VectorSearch = () => {
 
 
   const colDefinitions: ColDefinitionsType[] = useMemo(() => {
   const colDefinitions: ColDefinitionsType[] = useMemo(() => {
     // filter id and score
     // filter id and score
-    return searchResult.length > 0
+    return searchResult && searchResult.length > 0
       ? Object.keys(searchResult[0])
       ? Object.keys(searchResult[0])
           .filter(item => item !== 'id' && item !== 'score')
           .filter(item => item !== 'id' && item !== 'score')
           .map(key => ({
           .map(key => ({
@@ -152,25 +163,16 @@ const VectorSearch = () => {
     async (collectionName: string, collections: CollectionData[]) => {
     async (collectionName: string, collections: CollectionData[]) => {
       const fields =
       const fields =
         collections.find(c => c._name === collectionName)?._fields || [];
         collections.find(c => c._name === collectionName)?._fields || [];
-      const vectorTypes: DataType[] = ['BinaryVector', 'FloatVector'];
       const indexes = await IndexHttp.getIndexInfo(collectionName);
       const indexes = await IndexHttp.getIndexInfo(collectionName);
 
 
-      const fieldOptions = fields
-        // only vector type field can be select
-        .filter(field => vectorTypes.includes(field._fieldType))
-        .map(f => {
-          const embeddingType = getEmbeddingType(f._fieldType);
-          const defaultIndex = getDefaultIndexType(embeddingType);
-          const index = indexes.find(i => i._fieldName === f._fieldName);
+      const { vectorFields, nonVectorFields } = classifyFields(fields);
 
 
-          return {
-            label: `${f._fieldName} (${index?._indexType || defaultIndex})`,
-            value: f._fieldName,
-            fieldType: f._fieldType,
-            indexInfo: index || null,
-          };
-        });
+      // only vector type fields can be select
+      const fieldOptions = getVectorFieldOptions(vectorFields, indexes);
       setFieldOptions(fieldOptions);
       setFieldOptions(fieldOptions);
+      // only non vector type fields can be advanced filter
+      const filterFields = getNonVectorFieldsForFilter(nonVectorFields);
+      setFilterFields(filterFields);
     },
     },
     []
     []
   );
   );
@@ -190,8 +192,6 @@ const VectorSearch = () => {
   const VectorSearchIcon = icons.vectorSearch;
   const VectorSearchIcon = icons.vectorSearch;
   const ResetIcon = icons.refresh;
   const ResetIcon = icons.refresh;
   const ArrowIcon = icons.dropdown;
   const ArrowIcon = icons.dropdown;
-  const FilterIcon = icons.filter;
-  const ClearIcon = icons.clear;
 
 
   // methods
   // methods
   const handlePageChange = (e: any, page: number) => {
   const handlePageChange = (e: any, page: number) => {
@@ -203,15 +203,17 @@ const VectorSearch = () => {
      * 1. reset vectors
      * 1. reset vectors
      * 2. reset selected collection and field
      * 2. reset selected collection and field
      * 3. reset search params
      * 3. reset search params
-     * 4. reset advanced filter
+     * 4. reset advanced filter expression
      * 5. clear search result
      * 5. clear search result
      */
      */
     setVectors('');
     setVectors('');
     setSelectedField('');
     setSelectedField('');
     setSelectedCollection('');
     setSelectedCollection('');
-    setSearchResult([]);
+    setSearchResult(null);
+    setFilterFields([]);
+    setExpression('');
   };
   };
-  const handleSearch = async (topK: number) => {
+  const handleSearch = async (topK: number, expr = expression) => {
     const searhParamPairs = [
     const searhParamPairs = [
       // dynamic search params
       // dynamic search params
       {
       {
@@ -234,7 +236,7 @@ const VectorSearch = () => {
 
 
     const params: VectorSearchParam = {
     const params: VectorSearchParam = {
       output_fields: outputFields,
       output_fields: outputFields,
-      expr: '',
+      expr,
       search_params: searhParamPairs,
       search_params: searhParamPairs,
       vectors: [parseValue(vectors)],
       vectors: [parseValue(vectors)],
       vector_type: fieldType,
       vector_type: fieldType,
@@ -254,8 +256,11 @@ const VectorSearch = () => {
       setTableLoading(false);
       setTableLoading(false);
     }
     }
   };
   };
-  const handleFilter = () => {};
-  const handleClearFilter = () => {};
+  const handleAdvancedFilterChange = (expression: string) => {
+    setExpression(expression);
+    handleSearch(topK, expression);
+  };
+
   const handleVectorChange = (value: string) => {
   const handleVectorChange = (value: string) => {
     setVectors(value);
     setVectors(value);
   };
   };
@@ -358,23 +363,12 @@ const VectorSearch = () => {
             }}
             }}
             menuItemWidth="108px"
             menuItemWidth="108px"
           />
           />
-          <CustomButton
-            className="button"
-            disabled={selectedField === '' || selectedCollection === ''}
-            onClick={handleFilter}
-          >
-            <FilterIcon />
-            {searchTrans('filter')}
-          </CustomButton>
-          {/* advanced filter number chip */}
-          <Chip
-            variant="outlined"
-            size="small"
-            // TODO: need to replace mock data
-            label={'3'}
-            classes={{ root: classes.chip, label: classes.chipLabel }}
-            deleteIcon={<ClearIcon />}
-            onDelete={handleClearFilter}
+
+          <Filter
+            title="Advanced Filter"
+            fields={filterFields}
+            filterDisabled={selectedField === '' || selectedCollection === ''}
+            onSubmit={handleAdvancedFilterChange}
           />
           />
         </div>
         </div>
         <div className="right">
         <div className="right">
@@ -393,7 +387,7 @@ const VectorSearch = () => {
       </section>
       </section>
 
 
       {/* search result table section */}
       {/* search result table section */}
-      {searchResult.length > 0 || tableLoading ? (
+      {(searchResult && searchResult.length > 0) || tableLoading ? (
         <MilvusGrid
         <MilvusGrid
           toolbarConfigs={[]}
           toolbarConfigs={[]}
           colDefinitions={colDefinitions}
           colDefinitions={colDefinitions}
@@ -411,7 +405,11 @@ const VectorSearch = () => {
         <EmptyCard
         <EmptyCard
           wrapperClass={`page-empty-card`}
           wrapperClass={`page-empty-card`}
           icon={<VectorSearchIcon />}
           icon={<VectorSearchIcon />}
-          text={searchTrans('empty')}
+          text={
+            searchResult !== null
+              ? searchTrans('empty')
+              : searchTrans('startTip')
+          }
         />
         />
       )}
       )}
     </section>
     </section>

+ 61 - 2
client/src/utils/search.ts

@@ -1,7 +1,17 @@
+import { Field } from '../components/advancedSearch/Types';
 import { EmbeddingTypeEnum } from '../consts/Milvus';
 import { EmbeddingTypeEnum } from '../consts/Milvus';
 import { DataType } from '../pages/collections/Types';
 import { DataType } from '../pages/collections/Types';
-import { IndexType, INDEX_TYPES_ENUM } from '../pages/schema/Types';
-import { SearchResult, SearchResultView } from '../pages/seach/Types';
+import {
+  FieldData,
+  IndexType,
+  IndexView,
+  INDEX_TYPES_ENUM,
+} from '../pages/schema/Types';
+import {
+  FieldOption,
+  SearchResult,
+  SearchResultView,
+} from '../pages/seach/Types';
 
 
 export const transferSearchResult = (
 export const transferSearchResult = (
   result: SearchResult[]
   result: SearchResult[]
@@ -45,3 +55,52 @@ export const getDefaultIndexType = (
       : INDEX_TYPES_ENUM.BIN_FLAT;
       : INDEX_TYPES_ENUM.BIN_FLAT;
   return defaultIndexType;
   return defaultIndexType;
 };
 };
+
+/**
+ * funtion to divide fields into two categories: vector or nonVector
+ */
+export const classifyFields = (
+  fields: FieldData[]
+): { vectorFields: FieldData[]; nonVectorFields: FieldData[] } => {
+  const vectorTypes: DataType[] = ['BinaryVector', 'FloatVector'];
+  return fields.reduce(
+    (result, cur) => {
+      const changedFieldType = vectorTypes.includes(cur._fieldType)
+        ? 'vectorFields'
+        : 'nonVectorFields';
+
+      result[changedFieldType].push(cur);
+
+      return result;
+    },
+    { vectorFields: [] as FieldData[], nonVectorFields: [] as FieldData[] }
+  );
+};
+
+export const getVectorFieldOptions = (
+  fields: FieldData[],
+  indexes: IndexView[]
+): FieldOption[] => {
+  const options: FieldOption[] = fields.map(f => {
+    const embeddingType = getEmbeddingType(f._fieldType);
+    const defaultIndex = getDefaultIndexType(embeddingType);
+    const index = indexes.find(i => i._fieldName === f._fieldName);
+
+    return {
+      label: `${f._fieldName} (${index?._indexType || defaultIndex})`,
+      value: f._fieldName,
+      fieldType: f._fieldType,
+      indexInfo: index || null,
+    };
+  });
+
+  return options;
+};
+
+export const getNonVectorFieldsForFilter = (fields: FieldData[]): Field[] => {
+  const intTypes: DataType[] = ['Int8', 'Int16', 'Int32', 'Int64'];
+  return fields.map(f => ({
+    name: f._fieldName,
+    type: intTypes.includes(f._fieldType) ? 'int' : 'float',
+  }));
+};