VectorSearch.tsx 17 KB

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