import { TextField, Typography } from '@material-ui/core'; import { useTranslation } from 'react-i18next'; import { useNavigationHook } from '../../hooks/Navigation'; import { ALL_ROUTER_TYPES } from '../../router/Types'; import CustomSelector from '../../components/customSelector/CustomSelector'; import { useCallback, useEffect, useMemo, useState } from 'react'; import SearchParams from './SearchParams'; import { DEFAULT_METRIC_VALUE_MAP } from '../../consts/Milvus'; import { FieldOption, SearchResultView, VectorSearchParam } from './Types'; import MilvusGrid from '../../components/grid/Grid'; import EmptyCard from '../../components/cards/EmptyCard'; import icons from '../../components/icons/Icons'; import { usePaginationHook } from '../../hooks/Pagination'; import CustomButton from '../../components/customButton/CustomButton'; import SimpleMenu from '../../components/menu/SimpleMenu'; import { TOP_K_OPTIONS } from './Constants'; import { Option } from '../../components/customSelector/Types'; import { CollectionHttp } from '../../http/Collection'; import { CollectionData, DataTypeEnum } from '../collections/Types'; import { IndexHttp } from '../../http/Index'; import { getVectorSearchStyles } from './Styles'; import { parseValue } from '../../utils/Insert'; import { classifyFields, getDefaultIndexType, getEmbeddingType, getNonVectorFieldsForFilter, getVectorFieldOptions, transferSearchResult, } from '../../utils/search'; import { ColDefinitionsType } from '../../components/grid/Types'; import Filter from '../../components/advancedSearch'; import { Field } from '../../components/advancedSearch/Types'; import { useLocation } from 'react-router-dom'; import { parseLocationSearch } from '../../utils/Format'; const VectorSearch = () => { useNavigationHook(ALL_ROUTER_TYPES.SEARCH); const location = useLocation(); // i18n const { t: searchTrans } = useTranslation('search'); const { t: btnTrans } = useTranslation('btn'); const classes = getVectorSearchStyles(); // data stored inside the component const [tableLoading, setTableLoading] = useState(false); const [collections, setCollections] = useState([]); const [selectedCollection, setSelectedCollection] = useState(''); const [fieldOptions, setFieldOptions] = useState([]); // fields for advanced filter const [filterFields, setFilterFields] = useState([]); const [selectedField, setSelectedField] = useState(''); // search params form const [searchParam, setSearchParam] = useState<{ [key in string]: number }>( {} ); // search params disable state const [paramDisabled, setParamDisabled] = useState(true); // use null as init value before search, empty array means no results const [searchResult, setSearchResult] = useState( null ); // default topK is 100 const [topK, setTopK] = useState(100); const [expression, setExpression] = useState(''); const [vectors, setVectors] = useState(''); const { pageSize, handlePageSize, currentPage, handleCurrentPage, total, data: result, } = usePaginationHook(searchResult || []); const collectionOptions: Option[] = useMemo( () => collections.map(c => ({ label: c._name, value: c._name, })), [collections] ); const outputFields: string[] = useMemo(() => { const fields = collections.find(c => c._name === selectedCollection)?._fields || []; // vector field can't be output fields const invalidTypes = ['BinaryVector', 'FloatVector']; const nonVectorFields = fields.filter( field => !invalidTypes.includes(field._fieldType) ); return nonVectorFields.map(f => f._fieldName); }, [selectedCollection, collections]); const primaryKeyField = useMemo(() => { const selectedCollectionInfo = collections.find( c => c._name === selectedCollection ); const fields = selectedCollectionInfo?._fields || []; return fields.find(f => f._isPrimaryKey)?._fieldName; }, [selectedCollection, collections]); const colDefinitions: ColDefinitionsType[] = useMemo(() => { /** * id represents primary key, score represents distance * since we transfer score to distance in the view, and original field which is primary key has already in the table * we filter 'id' and 'score' to avoid redundant data */ return searchResult && searchResult.length > 0 ? Object.keys(searchResult[0]) .filter(item => { // if primary key field name is id, don't filter it const invalidItems = primaryKeyField === 'id' ? ['score'] : ['id', 'score']; return !invalidItems.includes(item); }) .map(key => ({ id: key, align: 'left', disablePadding: false, label: key, })) : []; }, [searchResult, primaryKeyField]); 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: metric, indexType: index?._indexType || getDefaultIndexType(embeddingType), indexParams, fieldType: DataTypeEnum[selectedFieldInfo?.fieldType!], embeddingType, selectedFieldDimension: dim, }; } 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 () => { const collections = await CollectionHttp.getCollections(); setCollections(collections); }, []); const fetchFieldsWithIndex = useCallback( async (collectionName: string, collections: CollectionData[]) => { const fields = collections.find(c => c._name === collectionName)?._fields || []; const indexes = await IndexHttp.getIndexInfo(collectionName); const { vectorFields, nonVectorFields } = classifyFields(fields); // only vector type fields can be select const fieldOptions = getVectorFieldOptions(vectorFields, indexes); setFieldOptions(fieldOptions); if (fieldOptions.length > 0) { // set first option value as default field value const [{ value: defaultFieldValue }] = fieldOptions; setSelectedField(defaultFieldValue as string); } // only non vector type fields can be advanced filter const filterFields = getNonVectorFieldsForFilter(nonVectorFields); setFilterFields(filterFields); }, [] ); useEffect(() => { fetchCollections(); }, [fetchCollections]); // get field options with index when selected collection changed useEffect(() => { if (selectedCollection !== '') { fetchFieldsWithIndex(selectedCollection, collections); } }, [selectedCollection, collections, fetchFieldsWithIndex]); // set default collection value if is from overview page useEffect(() => { if (location.search && collections.length > 0) { const { collectionName } = parseLocationSearch(location.search); // collection name validation const isNameValid = collections .map(c => c._name) .includes(collectionName); isNameValid && setSelectedCollection(collectionName); } }, [location, collections]); // icons const VectorSearchIcon = icons.vectorSearch; const ResetIcon = icons.refresh; const ArrowIcon = icons.dropdown; // methods const handlePageChange = (e: any, page: number) => { handleCurrentPage(page); }; const handleReset = () => { /** * reset search includes: * 1. reset vectors * 2. reset selected collection and field * 3. reset search params * 4. reset advanced filter expression * 5. clear search result */ setVectors(''); setSelectedField(''); setSelectedCollection(''); setSearchResult(null); setFilterFields([]); setExpression(''); }; const handleSearch = async (topK: number, expr = expression) => { const searhParamPairs = [ // dynamic search params { key: 'params', value: JSON.stringify(searchParam), }, { key: 'anns_field', value: selectedField, }, { key: 'topk', value: topK, }, { key: 'metric_type', value: metricType, }, ]; const params: VectorSearchParam = { output_fields: outputFields, expr, search_params: searhParamPairs, vectors: [parseValue(vectors)], vector_type: fieldType, }; setTableLoading(true); try { const res = await CollectionHttp.vectorSearchData( selectedCollection, params ); setTableLoading(false); const result = transferSearchResult(res.results); setSearchResult(result); } catch (err) { setTableLoading(false); } }; const handleAdvancedFilterChange = (expression: string) => { setExpression(expression); if (!searchDisabled) { handleSearch(topK, expression); } }; const handleVectorChange = (value: string) => { setVectors(value); }; return (
{/* form section */}
{/** * vector value textarea * use field-params class because it also has error msg if invalid */}
{searchTrans('firstTip', { dimensionTip: selectedFieldDimension !== 0 ? `(dimension: ${selectedFieldDimension})` : '', })} ) => { handleVectorChange(e.target.value as string); }} /> {/* validation */} {!vectorValueValid && ( {searchTrans('vectorValueWarning', { dimension: selectedFieldDimension, })} )}
{/* collection and field selectors */}
{searchTrans('secondTip')} { const collection = e.target.value; setSelectedCollection(collection as string); // every time selected collection changed, reset field setSelectedField(''); }} /> { const field = e.target.value; setSelectedField(field as string); }} />
{/* search params selectors */}
{searchTrans('thirdTip')}
{/** * search toolbar section * including topK selector, advanced filter, search and reset btn */}
{`${searchTrans('result')}: `} {/* topK selector */} ({ label: item.toString(), callback: () => { setTopK(item); if (!searchDisabled) { handleSearch(item); } }, wrapperClass: classes.menuItem, }))} buttonProps={{ className: classes.menuLabel, endIcon: , }} menuItemWidth="108px" />
{btnTrans('reset')} handleSearch(topK)} > {btnTrans('search')}
{/* search result table section */} {(searchResult && searchResult.length > 0) || tableLoading ? ( ) : ( } text={ searchResult !== null ? searchTrans('empty') : searchTrans('startTip') } /> )}
); }; export default VectorSearch;