Skip to content

Commit d5f2a30

Browse files
authored
feat(compass-indexes): display vector search type in search indexes table COMPASS-7509 (#5413)
1 parent 1423539 commit d5f2a30

File tree

9 files changed

+179
-30
lines changed

9 files changed

+179
-30
lines changed

packages/compass-indexes/src/components/regular-indexes-table/badge-with-icon-link.spec.tsx renamed to packages/compass-indexes/src/components/indexes-table/badge-with-icon-link.spec.tsx

File renamed without changes.

packages/compass-indexes/src/components/regular-indexes-table/badge-with-icon-link.tsx renamed to packages/compass-indexes/src/components/indexes-table/badge-with-icon-link.tsx

File renamed without changes.

packages/compass-indexes/src/components/regular-indexes-table/property-field.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
useDarkMode,
1212
} from '@mongodb-js/compass-components';
1313
import type { RegularIndex } from '../../modules/regular-indexes';
14-
import BadgeWithIconLink from './badge-with-icon-link';
14+
import BadgeWithIconLink from '../indexes-table/badge-with-icon-link';
1515

1616
const containerStyles = css({
1717
display: 'flex',

packages/compass-indexes/src/components/regular-indexes-table/type-field.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import getIndexHelpLink from '../../utils/index-link-helper';
33
import { Tooltip, Body } from '@mongodb-js/compass-components';
44

55
import type { RegularIndex } from '../../modules/regular-indexes';
6-
import BadgeWithIconLink from './badge-with-icon-link';
6+
import BadgeWithIconLink from '../indexes-table/badge-with-icon-link';
77

88
export const canRenderTooltip = (type: RegularIndex['type']) => {
99
return ['text', 'wildcard', 'columnstore'].indexOf(type ?? '') !== -1;

packages/compass-indexes/src/components/search-indexes-table/search-indexes-table.spec.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import type { Document } from 'mongodb';
1414

1515
import { POLLING_INTERVAL, SearchIndexesTable } from './search-indexes-table';
1616
import { SearchIndexesStatuses } from '../../modules/search-indexes';
17-
import { searchIndexes as indexes } from './../../../test/fixtures/search-indexes';
17+
import {
18+
searchIndexes as indexes,
19+
vectorSearchIndexes,
20+
} from './../../../test/fixtures/search-indexes';
1821

1922
const renderIndexList = (
2023
props: Partial<React.ComponentProps<typeof SearchIndexesTable>> = {}
@@ -186,6 +189,30 @@ describe('SearchIndexesTable Component', function () {
186189
});
187190
});
188191

192+
context('vector search index', function () {
193+
it('renders the vector search index details when expanded', function () {
194+
renderIndexList({
195+
indexes: vectorSearchIndexes,
196+
});
197+
198+
const indexRow = screen.getByTestId(
199+
`search-indexes-row-vectorSearching123`
200+
);
201+
const expandButton = within(indexRow).getByLabelText('Expand row');
202+
expect(expandButton).to.exist;
203+
fireEvent.click(expandButton);
204+
205+
const details = screen.getByTestId(
206+
`search-indexes-details-vectorSearching123`
207+
);
208+
expect(details).to.exist;
209+
210+
for (const path of ['plot_embedding', 'genres']) {
211+
expect(within(details).getAllByText(path)).to.exist;
212+
}
213+
});
214+
});
215+
189216
describe('connectivity', function () {
190217
it('does poll the index for changes in online mode', async function () {
191218
const onPollIndexesSpy = sinon.spy();

packages/compass-indexes/src/components/search-indexes-table/search-indexes-table.tsx

Lines changed: 83 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import type { Document } from 'mongodb';
44
import type { SearchIndex, SearchIndexStatus } from 'mongodb-data-service';
55
import { withPreferences } from 'compass-preferences-model/provider';
66
import { useOpenWorkspace } from '@mongodb-js/compass-workspaces/provider';
7-
import { BadgeVariant } from '@mongodb-js/compass-components';
7+
import { BadgeVariant, Disclaimer } from '@mongodb-js/compass-components';
88
import {
99
EmptyContent,
1010
Button,
1111
Link,
1212
Badge,
13+
Tooltip,
1314
css,
1415
spacing,
1516
} from '@mongodb-js/compass-components';
@@ -18,6 +19,7 @@ import {
1819
SearchIndexesStatuses,
1920
dropSearchIndex,
2021
getInitialSearchIndexPipeline,
22+
getInitialVectorSearchIndexPipelineText,
2123
pollSearchIndexes,
2224
showCreateModal,
2325
showUpdateModal,
@@ -28,6 +30,7 @@ import type { SortDirection, RootState } from '../../modules';
2830
import { IndexesTable } from '../indexes-table';
2931
import IndexActions from './search-index-actions';
3032
import { ZeroGraphic } from './zero-graphic';
33+
import BadgeWithIconLink from '../indexes-table/badge-with-icon-link';
3134

3235
export const POLLING_INTERVAL = 5000;
3336

@@ -107,6 +110,10 @@ function IndexStatus({
107110
);
108111
}
109112

113+
function SearchIndexType({ type, link }: { type: string; link: string }) {
114+
return <BadgeWithIconLink text={type} link={link} />;
115+
}
116+
110117
const searchIndexDetailsStyles = css({
111118
display: 'inline-flex',
112119
gap: spacing[1],
@@ -118,14 +125,35 @@ const searchIndexFieldStyles = css({
118125
gap: spacing[1],
119126
});
120127

121-
function SearchIndexDetails({
122-
indexName,
123-
definition,
124-
}: {
125-
indexName: string;
126-
definition: Document;
127-
}) {
128+
function VectorSearchIndexDetails({ definition }: { definition: Document }) {
129+
return (
130+
<>
131+
{!definition.fields || definition.fields.length === 0 ? (
132+
<Disclaimer>No fields in the index definition.</Disclaimer>
133+
) : (
134+
definition.fields.map((field: { path: string }) => (
135+
<Tooltip
136+
align="top"
137+
key={field.path}
138+
justify="middle"
139+
trigger={({ children, ...props }) => (
140+
<Badge {...props} className={searchIndexFieldStyles}>
141+
{children}
142+
{field.path}
143+
</Badge>
144+
)}
145+
>
146+
{JSON.stringify(field, null, 2)}
147+
</Tooltip>
148+
))
149+
)}
150+
</>
151+
);
152+
}
153+
154+
function SearchIndexDetails({ definition }: { definition: Document }) {
128155
const badges: { name: string; className?: string }[] = [];
156+
129157
if (definition.mappings?.dynamic) {
130158
badges.push({
131159
name: 'Dynamic Mappings',
@@ -142,22 +170,21 @@ function SearchIndexDetails({
142170
);
143171
}
144172
return (
145-
<div
146-
className={searchIndexDetailsStyles}
147-
data-testid={`search-indexes-details-${indexName}`}
148-
>
149-
{badges.length === 0
150-
? '[empty]'
151-
: badges.map((badge) => (
152-
<Badge key={badge.name} className={badge.className}>
153-
{badge.name}
154-
</Badge>
155-
))}
156-
</div>
173+
<>
174+
{badges.length === 0 ? (
175+
<Disclaimer>No mappings in the index definition.</Disclaimer>
176+
) : (
177+
badges.map((badge) => (
178+
<Badge key={badge.name} className={badge.className}>
179+
{badge.name}
180+
</Badge>
181+
))
182+
)}
183+
</>
157184
);
158185
}
159186

160-
const COLUMNS = ['Name and Fields', 'Status'] as const;
187+
const COLUMNS = ['Name and Fields', 'Type', 'Status'] as const;
161188

162189
export const SearchIndexesTable: React.FunctionComponent<
163190
SearchIndexesTableProps
@@ -184,6 +211,7 @@ export const SearchIndexesTable: React.FunctionComponent<
184211

185212
const data = useMemo(() => {
186213
return indexes.map((index) => {
214+
const isVectorSearchIndex = index.type === 'vectorSearch';
187215
return {
188216
key: index.name,
189217
'data-testid': `row-${index.name}`,
@@ -195,6 +223,22 @@ export const SearchIndexesTable: React.FunctionComponent<
195223
},
196224
children: index.name,
197225
},
226+
{
227+
'data-testid': 'type-field',
228+
style: {
229+
width: '20%',
230+
},
231+
children: (
232+
<SearchIndexType
233+
type={isVectorSearchIndex ? 'Vector Search' : 'Search'}
234+
link={
235+
isVectorSearchIndex
236+
? 'https://www.mongodb.com/docs/atlas/atlas-vector-search/create-index/'
237+
: 'https://www.mongodb.com/docs/atlas/atlas-search/create-index/'
238+
}
239+
/>
240+
),
241+
},
198242
{
199243
'data-testid': 'status-field',
200244
style: {
@@ -215,18 +259,30 @@ export const SearchIndexesTable: React.FunctionComponent<
215259
onEditIndex={onEditIndex}
216260
onRunAggregateIndex={(name) => {
217261
openCollectionWorkspace(namespace, {
218-
initialPipeline: getInitialSearchIndexPipeline(name),
219262
newTab: true,
263+
...(isVectorSearchIndex
264+
? {
265+
initialPipelineText:
266+
getInitialVectorSearchIndexPipelineText(name),
267+
}
268+
: {
269+
initialPipeline: getInitialSearchIndexPipeline(name),
270+
}),
220271
});
221272
}}
222273
/>
223274
),
224-
// TODO(COMPASS-7206): details for the nested row
225275
details: (
226-
<SearchIndexDetails
227-
indexName={index.name}
228-
definition={index.latestDefinition}
229-
/>
276+
<div
277+
className={searchIndexDetailsStyles}
278+
data-testid={`search-indexes-details-${index.name}`}
279+
>
280+
{isVectorSearchIndex ? (
281+
<VectorSearchIndexDetails definition={index.latestDefinition} />
282+
) : (
283+
<SearchIndexDetails definition={index.latestDefinition} />
284+
)}
285+
</div>
230286
),
231287
};
232288
});

packages/compass-indexes/src/modules/search-indexes.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,3 +331,13 @@ describe('search-indexes module', function () {
331331
});
332332
});
333333
});
334+
335+
describe('#getInitialVectorSearchIndexPipelineText', function () {
336+
it('returns pipeline text with the index name', function () {
337+
expect(
338+
searchIndexesSlice.getInitialVectorSearchIndexPipelineText('pineapple')
339+
).to.include(`$vectorSearch: {
340+
// Name of the Atlas Vector Search index to use.
341+
index: "pineapple",`);
342+
});
343+
});

packages/compass-indexes/src/modules/search-indexes.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type SearchSortColumn = keyof typeof sortColumnToProps;
2121

2222
const sortColumnToProps = {
2323
'Name and Fields': 'name',
24+
Type: 'type',
2425
Status: 'status',
2526
} as const;
2627

@@ -669,6 +670,29 @@ export function getInitialSearchIndexPipeline(name: string) {
669670
];
670671
}
671672

673+
export function getInitialVectorSearchIndexPipelineText(name: string) {
674+
return `[
675+
{
676+
$vectorSearch: {
677+
// Name of the Atlas Vector Search index to use.
678+
index: ${JSON.stringify(name)},
679+
// Indexed vectorEmbedding type field to search.
680+
"path": "<field-to-search>",
681+
// Array of numbers that represent the query vector.
682+
// The array size must match the number of vector dimensions specified in the index definition for the field.
683+
"queryVector": [],
684+
// Number of nearest neighbors to use during the search.
685+
// Value must be less than or equal to (<=) 10000.
686+
"numCandidates": 50,
687+
"limit": 10,
688+
// Any MQL match expression that compares an indexed field with a boolean,
689+
// number (not decimals), or string to use as a prefilter.
690+
"filter": {}
691+
},
692+
},
693+
]`;
694+
}
695+
672696
function _sortIndexes(
673697
indexes: SearchIndex[],
674698
column: SearchSortColumn,

packages/compass-indexes/test/fixtures/search-indexes.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,35 @@ export const searchIndexes: SearchIndex[] = [
1919
latestDefinition: {},
2020
},
2121
];
22+
23+
export const vectorSearchIndexes: SearchIndex[] = [
24+
{
25+
id: '1',
26+
name: 'vectorSearching123',
27+
status: 'READY',
28+
type: 'vectorSearch',
29+
queryable: true,
30+
latestDefinition: {
31+
fields: [
32+
{
33+
type: 'vector',
34+
path: 'plot_embedding',
35+
numDimensions: 1536,
36+
similarity: 'euclidean',
37+
},
38+
{
39+
type: 'filter',
40+
path: 'genres',
41+
},
42+
],
43+
},
44+
},
45+
{
46+
id: '2',
47+
name: 'pineapple',
48+
status: 'FAILED',
49+
type: 'vectorSearch',
50+
queryable: false,
51+
latestDefinition: {},
52+
},
53+
];

0 commit comments

Comments
 (0)