VectorSearch.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. import { TextField, Typography, Button } from '@material-ui/core';
  2. import { useTranslation } from 'react-i18next';
  3. import { useNavigationHook } from '../../hooks/Navigation';
  4. import { ALL_ROUTER_TYPES } from '../../router/Types';
  5. import CustomSelector from '../../components/customSelector/CustomSelector';
  6. import { useCallback, useEffect, useMemo, useState } from 'react';
  7. import SearchParams from './SearchParams';
  8. import { DEFAULT_METRIC_VALUE_MAP } from '../../consts/Milvus';
  9. import { FieldOption, SearchResultView, VectorSearchParam } from './Types';
  10. import AttuGrid from '../../components/grid/Grid';
  11. import EmptyCard from '../../components/cards/EmptyCard';
  12. import icons from '../../components/icons/Icons';
  13. import { usePaginationHook } from '../../hooks/Pagination';
  14. import CustomButton from '../../components/customButton/CustomButton';
  15. import SimpleMenu from '../../components/menu/SimpleMenu';
  16. import { TOP_K_OPTIONS } from './Constants';
  17. import { Option } from '../../components/customSelector/Types';
  18. import { CollectionHttp } from '../../http/Collection';
  19. import { CollectionData, DataTypeEnum } from '../collections/Types';
  20. import { IndexHttp } from '../../http/Index';
  21. import { getVectorSearchStyles } from './Styles';
  22. import { parseValue } from '../../utils/Insert';
  23. import {
  24. classifyFields,
  25. getDefaultIndexType,
  26. getEmbeddingType,
  27. getNonVectorFieldsForFilter,
  28. getVectorFieldOptions,
  29. transferSearchResult,
  30. } from '../../utils/search';
  31. import { ColDefinitionsType } from '../../components/grid/Types';
  32. import Filter from '../../components/advancedSearch';
  33. import { Field } from '../../components/advancedSearch/Types';
  34. import { useLocation } from 'react-router-dom';
  35. import { parseLocationSearch } from '../../utils/Format';
  36. import { cloneObj, generateVector } from '../../utils/Common';
  37. import { CustomDatePicker } from '../../components/customDatePicker/CustomDatePicker';
  38. import { useTimeTravelHook } from '../../hooks/TimeTravel';
  39. import { LOADING_STATE } from '../../consts/Milvus';
  40. const VectorSearch = () => {
  41. useNavigationHook(ALL_ROUTER_TYPES.SEARCH);
  42. const location = useLocation();
  43. // i18n
  44. const { t: searchTrans } = useTranslation('search');
  45. const { t: btnTrans } = useTranslation('btn');
  46. const classes = getVectorSearchStyles();
  47. // data stored inside the component
  48. const [tableLoading, setTableLoading] = useState<boolean>(false);
  49. const [collections, setCollections] = useState<CollectionData[]>([]);
  50. const [selectedCollection, setSelectedCollection] = useState<string>('');
  51. const [fieldOptions, setFieldOptions] = useState<FieldOption[]>([]);
  52. // fields for advanced filter
  53. const [filterFields, setFilterFields] = useState<Field[]>([]);
  54. const [selectedField, setSelectedField] = useState<string>('');
  55. // search params form
  56. const [searchParam, setSearchParam] = useState<{ [key in string]: number }>(
  57. {}
  58. );
  59. // search params disable state
  60. const [paramDisabled, setParamDisabled] = useState<boolean>(true);
  61. // use null as init value before search, empty array means no results
  62. const [searchResult, setSearchResult] = useState<SearchResultView[] | null>(
  63. null
  64. );
  65. // default topK is 100
  66. const [topK, setTopK] = useState<number>(100);
  67. const [expression, setExpression] = useState<string>('');
  68. const [vectors, setVectors] = useState<string>('');
  69. const {
  70. pageSize,
  71. handlePageSize,
  72. currentPage,
  73. handleCurrentPage,
  74. total,
  75. data: result,
  76. order,
  77. orderBy,
  78. handleGridSort,
  79. } = usePaginationHook(searchResult || []);
  80. const { timeTravel, setTimeTravel, timeTravelInfo, handleDateTimeChange } =
  81. useTimeTravelHook();
  82. const collectionOptions: Option[] = useMemo(
  83. () =>
  84. collections.map(c => ({
  85. label: c._name,
  86. value: c._name,
  87. })),
  88. [collections]
  89. );
  90. const outputFields: string[] = useMemo(() => {
  91. const fields =
  92. collections.find(c => c._name === selectedCollection)?._fields || [];
  93. // vector field can't be output fields
  94. const invalidTypes = ['BinaryVector', 'FloatVector'];
  95. const nonVectorFields = fields.filter(
  96. field => !invalidTypes.includes(field._fieldType)
  97. );
  98. return nonVectorFields.map(f => f._fieldName);
  99. }, [selectedCollection, collections]);
  100. const primaryKeyField = useMemo(() => {
  101. const selectedCollectionInfo = collections.find(
  102. c => c._name === selectedCollection
  103. );
  104. const fields = selectedCollectionInfo?._fields || [];
  105. return fields.find(f => f._isPrimaryKey)?._fieldName;
  106. }, [selectedCollection, collections]);
  107. const colDefinitions: ColDefinitionsType[] = useMemo(() => {
  108. /**
  109. * id represents primary key, score represents distance
  110. * since we transfer score to distance in the view, and original field which is primary key has already in the table
  111. * we filter 'id' and 'score' to avoid redundant data
  112. */
  113. return searchResult && searchResult.length > 0
  114. ? Object.keys(searchResult[0])
  115. .filter(item => {
  116. // if primary key field name is id, don't filter it
  117. const invalidItems =
  118. primaryKeyField === 'id' ? ['score'] : ['id', 'score'];
  119. return !invalidItems.includes(item);
  120. })
  121. .map(key => ({
  122. id: key,
  123. align: 'left',
  124. disablePadding: false,
  125. label: key,
  126. needCopy: primaryKeyField === key
  127. }))
  128. : [];
  129. }, [searchResult, primaryKeyField]);
  130. const [selectedMetricType, setSelectedMetricType] = useState<string>('');
  131. const {
  132. indexType,
  133. indexParams,
  134. fieldType,
  135. embeddingType,
  136. selectedFieldDimension,
  137. } = useMemo(() => {
  138. if (selectedField !== '') {
  139. // field options must contain selected field, so selectedFieldInfo will never undefined
  140. const selectedFieldInfo = fieldOptions.find(
  141. f => f.value === selectedField
  142. );
  143. const index = selectedFieldInfo?.indexInfo;
  144. const embeddingType = getEmbeddingType(selectedFieldInfo!.fieldType);
  145. const metric =
  146. index?._metricType || DEFAULT_METRIC_VALUE_MAP[embeddingType];
  147. const indexParams = index?._indexParameterPairs || [];
  148. const dim = selectedFieldInfo?.dimension || 0;
  149. setSelectedMetricType(metric);
  150. return {
  151. metricType: metric,
  152. indexType: index?._indexType || getDefaultIndexType(embeddingType),
  153. indexParams,
  154. fieldType: DataTypeEnum[selectedFieldInfo?.fieldType!],
  155. embeddingType,
  156. selectedFieldDimension: dim,
  157. };
  158. }
  159. return {
  160. indexType: '',
  161. indexParams: [],
  162. fieldType: 0,
  163. embeddingType: DataTypeEnum.FloatVector,
  164. selectedFieldDimension: 0,
  165. };
  166. }, [selectedField, fieldOptions]);
  167. /**
  168. * vector value validation
  169. * @return whether is valid
  170. */
  171. const vectorValueValid = useMemo(() => {
  172. // if user hasn't input value or not select field, don't trigger validation check
  173. if (vectors === '' || selectedFieldDimension === 0) {
  174. return true;
  175. }
  176. const dim =
  177. fieldType === DataTypeEnum.BinaryVector
  178. ? selectedFieldDimension / 8
  179. : selectedFieldDimension;
  180. const value = parseValue(vectors);
  181. const isArray = Array.isArray(value);
  182. return isArray && value.length === dim;
  183. }, [vectors, selectedFieldDimension, fieldType]);
  184. const searchDisabled = useMemo(() => {
  185. /**
  186. * before search, user must:
  187. * 1. enter vector value, it should be an array and length should be equal to selected field dimension
  188. * 2. choose collection and field
  189. * 3. set extra search params
  190. */
  191. const isInvalid =
  192. vectors === '' ||
  193. selectedCollection === '' ||
  194. selectedField === '' ||
  195. paramDisabled ||
  196. !vectorValueValid;
  197. return isInvalid;
  198. }, [
  199. paramDisabled,
  200. selectedField,
  201. selectedCollection,
  202. vectors,
  203. vectorValueValid,
  204. ]);
  205. // fetch data
  206. const fetchCollections = useCallback(async () => {
  207. const collections = await CollectionHttp.getCollections();
  208. setCollections(collections.filter(c => c._status === LOADING_STATE.LOADED));
  209. }, []);
  210. const fetchFieldsWithIndex = useCallback(
  211. async (collectionName: string, collections: CollectionData[]) => {
  212. const fields =
  213. collections.find(c => c._name === collectionName)?._fields || [];
  214. const indexes = await IndexHttp.getIndexInfo(collectionName);
  215. const { vectorFields, nonVectorFields } = classifyFields(fields);
  216. // only vector type fields can be select
  217. const fieldOptions = getVectorFieldOptions(vectorFields, indexes);
  218. setFieldOptions(fieldOptions);
  219. if (fieldOptions.length > 0) {
  220. // set first option value as default field value
  221. const [{ value: defaultFieldValue }] = fieldOptions;
  222. setSelectedField(defaultFieldValue as string);
  223. }
  224. // only non vector type fields can be advanced filter
  225. const filterFields = getNonVectorFieldsForFilter(nonVectorFields);
  226. setFilterFields(filterFields);
  227. },
  228. []
  229. );
  230. useEffect(() => {
  231. fetchCollections();
  232. }, [fetchCollections]);
  233. // get field options with index when selected collection changed
  234. useEffect(() => {
  235. if (selectedCollection !== '') {
  236. fetchFieldsWithIndex(selectedCollection, collections);
  237. }
  238. }, [selectedCollection, collections, fetchFieldsWithIndex]);
  239. // set default collection value if is from overview page
  240. useEffect(() => {
  241. if (location.search && collections.length > 0) {
  242. const { collectionName } = parseLocationSearch(location.search);
  243. // collection name validation
  244. const isNameValid = collections
  245. .map(c => c._name)
  246. .includes(collectionName);
  247. isNameValid && setSelectedCollection(collectionName);
  248. }
  249. }, [location, collections]);
  250. // icons
  251. const VectorSearchIcon = icons.vectorSearch;
  252. const ResetIcon = icons.refresh;
  253. const ArrowIcon = icons.dropdown;
  254. // methods
  255. const handlePageChange = (e: any, page: number) => {
  256. handleCurrentPage(page);
  257. };
  258. const handleReset = () => {
  259. /**
  260. * reset search includes:
  261. * 1. reset vectors
  262. * 2. reset selected collection and field
  263. * 3. reset search params
  264. * 4. reset advanced filter expression
  265. * 5. clear search result
  266. */
  267. setVectors('');
  268. setSelectedField('');
  269. setSelectedCollection('');
  270. setSearchResult(null);
  271. setFilterFields([]);
  272. setExpression('');
  273. setTimeTravel(null);
  274. };
  275. const handleSearch = async (topK: number, expr = expression) => {
  276. const clonedSearchParams = cloneObj(searchParam);
  277. delete clonedSearchParams.round_decimal;
  278. const searhParamPairs = {
  279. params: JSON.stringify(clonedSearchParams),
  280. anns_field: selectedField,
  281. topk: topK,
  282. metric_type: selectedMetricType,
  283. round_decimal: searchParam.round_decimal,
  284. };
  285. const params: VectorSearchParam = {
  286. output_fields: outputFields,
  287. expr,
  288. search_params: searhParamPairs,
  289. vectors: [parseValue(vectors)],
  290. vector_type: fieldType,
  291. travel_timestamp: timeTravelInfo.timestamp,
  292. };
  293. setTableLoading(true);
  294. try {
  295. const res = await CollectionHttp.vectorSearchData(
  296. selectedCollection,
  297. params
  298. );
  299. setTableLoading(false);
  300. const result = transferSearchResult(res.results);
  301. setSearchResult(result);
  302. } catch (err) {
  303. setTableLoading(false);
  304. }
  305. };
  306. const handleAdvancedFilterChange = (expression: string) => {
  307. setExpression(expression);
  308. if (!searchDisabled) {
  309. handleSearch(topK, expression);
  310. }
  311. };
  312. const handleVectorChange = (value: string) => {
  313. setVectors(value);
  314. };
  315. const fillWithExampleVector = (selectedFieldDimension: number) => {
  316. const v = generateVector(selectedFieldDimension);
  317. setVectors(`[${v}]`);
  318. };
  319. return (
  320. <section className="page-wrapper">
  321. {/* form section */}
  322. <form className={classes.form}>
  323. {/* collection and field selectors */}
  324. <fieldset className="field">
  325. <Typography className="text">{searchTrans('secondTip')}</Typography>
  326. <CustomSelector
  327. options={collectionOptions}
  328. wrapperClass={classes.selector}
  329. variant="filled"
  330. label={searchTrans(
  331. collectionOptions.length === 0 ? 'noCollection' : 'collection'
  332. )}
  333. disabled={collectionOptions.length === 0}
  334. value={selectedCollection}
  335. onChange={(e: { target: { value: unknown } }) => {
  336. const collection = e.target.value;
  337. setSelectedCollection(collection as string);
  338. // every time selected collection changed, reset field
  339. setSelectedField('');
  340. setSearchResult([]);
  341. }}
  342. />
  343. <CustomSelector
  344. options={fieldOptions}
  345. // readOnly can't avoid all events, so we use disabled instead
  346. disabled={selectedCollection === ''}
  347. wrapperClass={classes.selector}
  348. variant="filled"
  349. label={searchTrans('field')}
  350. value={selectedField}
  351. onChange={(e: { target: { value: unknown } }) => {
  352. const field = e.target.value;
  353. setSelectedField(field as string);
  354. }}
  355. />
  356. </fieldset>
  357. {/**
  358. * vector value textarea
  359. * use field-params class because it also has error msg if invalid
  360. */}
  361. <fieldset className="field field-params field-second">
  362. <Typography className="text">
  363. {searchTrans('firstTip', {
  364. dimensionTip:
  365. selectedFieldDimension !== 0
  366. ? `(dimension: ${selectedFieldDimension})`
  367. : '',
  368. })}
  369. {selectedFieldDimension !== 0 ? (
  370. <Button
  371. variant="outlined"
  372. size="small"
  373. onClick={() => {
  374. const dim =
  375. fieldType === DataTypeEnum.BinaryVector
  376. ? selectedFieldDimension / 8
  377. : selectedFieldDimension;
  378. fillWithExampleVector(dim);
  379. }}
  380. >
  381. {btnTrans('example')}
  382. </Button>
  383. ) : null}
  384. </Typography>
  385. <TextField
  386. className="textarea"
  387. InputProps={{
  388. classes: {
  389. root: 'textfield',
  390. multiline: 'multiline',
  391. },
  392. }}
  393. multiline
  394. rows={5}
  395. placeholder={searchTrans('vectorPlaceholder')}
  396. value={vectors}
  397. onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
  398. handleVectorChange(e.target.value as string);
  399. }}
  400. />
  401. {/* validation */}
  402. {!vectorValueValid && (
  403. <Typography variant="caption" className={classes.error}>
  404. {searchTrans('vectorValueWarning', {
  405. dimension:
  406. fieldType === DataTypeEnum.BinaryVector
  407. ? selectedFieldDimension / 8
  408. : selectedFieldDimension,
  409. })}
  410. </Typography>
  411. )}
  412. </fieldset>
  413. {/* search params selectors */}
  414. <fieldset className="field field-params">
  415. <Typography className="text">{searchTrans('thirdTip')}</Typography>
  416. <SearchParams
  417. wrapperClass={classes.paramsWrapper}
  418. metricType={selectedMetricType}
  419. embeddingType={
  420. embeddingType as
  421. | DataTypeEnum.BinaryVector
  422. | DataTypeEnum.FloatVector
  423. }
  424. indexType={indexType}
  425. indexParams={indexParams!}
  426. searchParamsForm={searchParam}
  427. handleFormChange={setSearchParam}
  428. handleMetricTypeChange={setSelectedMetricType}
  429. topK={topK}
  430. setParamsDisabled={setParamDisabled}
  431. />
  432. </fieldset>
  433. </form>
  434. {/**
  435. * search toolbar section
  436. * including topK selector, advanced filter, search and reset btn
  437. */}
  438. <section className={classes.toolbar}>
  439. <div className="left">
  440. <Typography variant="h5" className="text">
  441. {`${searchTrans('result')}: `}
  442. </Typography>
  443. {/* topK selector */}
  444. <SimpleMenu
  445. label={searchTrans('topK', { number: topK })}
  446. menuItems={TOP_K_OPTIONS.map(item => ({
  447. label: item.toString(),
  448. callback: () => {
  449. setTopK(item);
  450. if (!searchDisabled) {
  451. handleSearch(item);
  452. }
  453. },
  454. wrapperClass: classes.menuItem,
  455. }))}
  456. buttonProps={{
  457. className: classes.menuLabel,
  458. endIcon: <ArrowIcon />,
  459. }}
  460. menuItemWidth="108px"
  461. />
  462. <Filter
  463. title="Advanced Filter"
  464. fields={filterFields}
  465. filterDisabled={selectedField === '' || selectedCollection === ''}
  466. onSubmit={handleAdvancedFilterChange}
  467. />
  468. <CustomDatePicker
  469. label={timeTravelInfo.label}
  470. onChange={handleDateTimeChange}
  471. date={timeTravel}
  472. setDate={setTimeTravel}
  473. />
  474. </div>
  475. <div className="right">
  476. <CustomButton className="btn" onClick={handleReset}>
  477. <ResetIcon classes={{ root: 'icon' }} />
  478. {btnTrans('reset')}
  479. </CustomButton>
  480. <CustomButton
  481. variant="contained"
  482. disabled={searchDisabled}
  483. onClick={() => handleSearch(topK)}
  484. >
  485. {btnTrans('search')}
  486. </CustomButton>
  487. </div>
  488. </section>
  489. {/* search result table section */}
  490. {(searchResult && searchResult.length > 0) || tableLoading ? (
  491. <AttuGrid
  492. toolbarConfigs={[]}
  493. colDefinitions={colDefinitions}
  494. rows={result}
  495. rowCount={total}
  496. primaryKey="rank"
  497. page={currentPage}
  498. onChangePage={handlePageChange}
  499. rowsPerPage={pageSize}
  500. setRowsPerPage={handlePageSize}
  501. openCheckBox={false}
  502. isLoading={tableLoading}
  503. orderBy={orderBy}
  504. order={order}
  505. handleSort={handleGridSort}
  506. />
  507. ) : (
  508. <EmptyCard
  509. wrapperClass={`page-empty-card`}
  510. icon={<VectorSearchIcon />}
  511. text={
  512. searchResult !== null
  513. ? searchTrans('empty')
  514. : searchTrans('startTip')
  515. }
  516. />
  517. )}
  518. </section>
  519. );
  520. };
  521. export default VectorSearch;