@mastra/couchbase 0.0.2-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +23 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE.md +46 -0
- package/README.md +267 -0
- package/dist/_tsup-dts-rollup.d.cts +39 -0
- package/dist/_tsup-dts-rollup.d.ts +39 -0
- package/dist/index.cjs +243 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +240 -0
- package/docker-compose.yaml +21 -0
- package/eslint.config.js +6 -0
- package/package.json +46 -0
- package/scripts/start-docker.js +14 -0
- package/scripts/stop-docker.js +7 -0
- package/src/index.ts +1 -0
- package/src/vector/index.integration.test.ts +558 -0
- package/src/vector/index.ts +276 -0
- package/src/vector/index.unit.test.ts +737 -0
- package/tsconfig.json +5 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, afterEach, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { CouchbaseVector, DISTANCE_MAPPING } from './index';
|
|
3
|
+
|
|
4
|
+
const dimension = vi.hoisted(() => 3);
|
|
5
|
+
const test_bucketName = vi.hoisted(() => 'test-bucket');
|
|
6
|
+
const test_scopeName = vi.hoisted(() => 'test-scope');
|
|
7
|
+
const test_collectionName = vi.hoisted(() => 'test-collection');
|
|
8
|
+
const test_indexName = vi.hoisted(() => 'test-index');
|
|
9
|
+
const similarity = vi.hoisted(() => 'l2_norm');
|
|
10
|
+
|
|
11
|
+
const mockUpsertIndexFn = vi.hoisted(() => vi.fn().mockResolvedValue({}));
|
|
12
|
+
const mockGetAllIndexesFn = vi.hoisted(() =>
|
|
13
|
+
vi
|
|
14
|
+
.fn()
|
|
15
|
+
.mockResolvedValue([
|
|
16
|
+
{ name: `${test_indexName}` },
|
|
17
|
+
{ name: `${test_indexName}_1` },
|
|
18
|
+
{ name: `${test_indexName}_2` },
|
|
19
|
+
]),
|
|
20
|
+
);
|
|
21
|
+
const mockGetIndexFn = vi.hoisted(() =>
|
|
22
|
+
vi.fn().mockResolvedValue({
|
|
23
|
+
name: test_indexName,
|
|
24
|
+
sourceName: test_bucketName,
|
|
25
|
+
type: 'fulltext-index',
|
|
26
|
+
params: {
|
|
27
|
+
mapping: {
|
|
28
|
+
types: {
|
|
29
|
+
[`${test_scopeName}.${test_collectionName}`]: {
|
|
30
|
+
properties: {
|
|
31
|
+
embedding: {
|
|
32
|
+
fields: [
|
|
33
|
+
{
|
|
34
|
+
dims: dimension,
|
|
35
|
+
similarity: similarity,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
sourceUuid: 'test-source-uuid',
|
|
45
|
+
sourceParams: {},
|
|
46
|
+
sourceType: 'test-source-type',
|
|
47
|
+
planParams: {},
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
const mockDropIndexFn = vi.hoisted(() => vi.fn().mockResolvedValue({}));
|
|
51
|
+
const mockScopeSearchIndexesFn = vi.hoisted(() =>
|
|
52
|
+
vi.fn().mockReturnValue({
|
|
53
|
+
upsertIndex: mockUpsertIndexFn,
|
|
54
|
+
getAllIndexes: mockGetAllIndexesFn,
|
|
55
|
+
getIndex: mockGetIndexFn,
|
|
56
|
+
dropIndex: mockDropIndexFn,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
const mockScopeSearchFn = vi.hoisted(() =>
|
|
60
|
+
vi.fn().mockResolvedValue({
|
|
61
|
+
rows: [
|
|
62
|
+
{
|
|
63
|
+
id: 'test_restult_id_1',
|
|
64
|
+
score: 0.5,
|
|
65
|
+
fields: {
|
|
66
|
+
label: 'test-label',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'test_restult_id_2',
|
|
71
|
+
score: 0.5,
|
|
72
|
+
fields: {
|
|
73
|
+
label: 'test-label',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'test_restult_id_3',
|
|
78
|
+
score: 0.5,
|
|
79
|
+
fields: {
|
|
80
|
+
label: 'test-label',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const mockCollectionUpsertFn = vi.hoisted(() => vi.fn().mockResolvedValue({}));
|
|
88
|
+
const getMockCollection = vi.hoisted(() =>
|
|
89
|
+
vi.fn().mockReturnValue({
|
|
90
|
+
collection_name: test_collectionName,
|
|
91
|
+
upsert: mockCollectionUpsertFn,
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
const getMockScope = vi.hoisted(() =>
|
|
95
|
+
vi.fn().mockReturnValue({
|
|
96
|
+
scope_name: test_scopeName,
|
|
97
|
+
collection: getMockCollection,
|
|
98
|
+
searchIndexes: mockScopeSearchIndexesFn,
|
|
99
|
+
search: mockScopeSearchFn,
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
const getMockBucket = vi.hoisted(() =>
|
|
103
|
+
vi.fn().mockReturnValue({
|
|
104
|
+
bucket_name: test_bucketName,
|
|
105
|
+
scope: getMockScope,
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
const mockCluster = vi.hoisted(() => ({
|
|
109
|
+
bucket: getMockBucket,
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
const mockCouchbaseConnectFn = vi.hoisted(() => vi.fn().mockResolvedValue(mockCluster));
|
|
113
|
+
const mockSearchRequestCreateFn = vi.hoisted(() => vi.fn().mockReturnValue('mockRequest'));
|
|
114
|
+
const mockVectorSearchFn = vi.hoisted(() => vi.fn().mockReturnValue('mockVectorSearch'));
|
|
115
|
+
const mockNumCandidatesFn = vi.hoisted(() => vi.fn().mockReturnValue('mockVectorQuery'));
|
|
116
|
+
const mockVectorQueryCreateFn = vi.hoisted(() =>
|
|
117
|
+
vi.fn().mockReturnValue({
|
|
118
|
+
numCandidates: mockNumCandidatesFn,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
vi.mock('couchbase', () => {
|
|
123
|
+
return {
|
|
124
|
+
connect: mockCouchbaseConnectFn,
|
|
125
|
+
SearchRequest: {
|
|
126
|
+
create: mockSearchRequestCreateFn,
|
|
127
|
+
},
|
|
128
|
+
VectorSearch: {
|
|
129
|
+
fromVectorQuery: mockVectorSearchFn,
|
|
130
|
+
},
|
|
131
|
+
VectorQuery: {
|
|
132
|
+
create: mockVectorQueryCreateFn,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
function clearAllMocks() {
|
|
138
|
+
mockCollectionUpsertFn.mockClear();
|
|
139
|
+
getMockCollection.mockClear();
|
|
140
|
+
getMockScope.mockClear();
|
|
141
|
+
getMockBucket.mockClear();
|
|
142
|
+
mockUpsertIndexFn.mockClear();
|
|
143
|
+
mockGetAllIndexesFn.mockClear();
|
|
144
|
+
mockGetIndexFn.mockClear();
|
|
145
|
+
mockDropIndexFn.mockClear();
|
|
146
|
+
mockScopeSearchIndexesFn.mockClear();
|
|
147
|
+
mockScopeSearchFn.mockClear();
|
|
148
|
+
mockCouchbaseConnectFn.mockClear();
|
|
149
|
+
mockSearchRequestCreateFn.mockClear();
|
|
150
|
+
mockVectorSearchFn.mockClear();
|
|
151
|
+
mockVectorQueryCreateFn.mockClear();
|
|
152
|
+
mockNumCandidatesFn.mockClear();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
describe('Unit Testing CouchbaseVector', () => {
|
|
156
|
+
let couchbase_client: CouchbaseVector;
|
|
157
|
+
|
|
158
|
+
describe('Connection', () => {
|
|
159
|
+
beforeAll(async () => {
|
|
160
|
+
clearAllMocks();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
afterAll(async () => {
|
|
164
|
+
clearAllMocks();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should connect to couchbase', async () => {
|
|
168
|
+
couchbase_client = new CouchbaseVector(
|
|
169
|
+
'COUCHBASE_CONNECTION_STRING',
|
|
170
|
+
'COUCHBASE_USERNAME',
|
|
171
|
+
'COUCHBASE_PASSWORD',
|
|
172
|
+
test_bucketName,
|
|
173
|
+
test_scopeName,
|
|
174
|
+
test_collectionName,
|
|
175
|
+
);
|
|
176
|
+
expect(mockCouchbaseConnectFn).toHaveBeenCalledTimes(1);
|
|
177
|
+
expect(mockCouchbaseConnectFn).toHaveBeenCalledWith('COUCHBASE_CONNECTION_STRING', {
|
|
178
|
+
username: 'COUCHBASE_USERNAME',
|
|
179
|
+
password: 'COUCHBASE_PASSWORD',
|
|
180
|
+
configProfile: 'wanDevelopment',
|
|
181
|
+
});
|
|
182
|
+
}, 50000);
|
|
183
|
+
|
|
184
|
+
it('should get collection', async () => {
|
|
185
|
+
await couchbase_client.getCollection();
|
|
186
|
+
|
|
187
|
+
expect(mockCouchbaseConnectFn).toHaveResolved();
|
|
188
|
+
|
|
189
|
+
expect(getMockBucket).toHaveBeenCalledTimes(1);
|
|
190
|
+
expect(getMockBucket).toHaveBeenCalledWith(test_bucketName);
|
|
191
|
+
|
|
192
|
+
expect(getMockScope).toHaveBeenCalledTimes(1);
|
|
193
|
+
expect(getMockScope).toHaveBeenCalledWith(test_scopeName);
|
|
194
|
+
|
|
195
|
+
expect(getMockCollection).toHaveBeenCalledTimes(1);
|
|
196
|
+
expect(getMockCollection).toHaveBeenCalledWith(test_collectionName);
|
|
197
|
+
}, 50000);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('Index Operations', () => {
|
|
201
|
+
beforeAll(async () => {
|
|
202
|
+
clearAllMocks();
|
|
203
|
+
couchbase_client = new CouchbaseVector(
|
|
204
|
+
'COUCHBASE_CONNECTION_STRING',
|
|
205
|
+
'COUCHBASE_USERNAME',
|
|
206
|
+
'COUCHBASE_PASSWORD',
|
|
207
|
+
test_bucketName,
|
|
208
|
+
test_scopeName,
|
|
209
|
+
test_collectionName,
|
|
210
|
+
);
|
|
211
|
+
await couchbase_client.getCollection();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
afterEach(async () => {
|
|
215
|
+
clearAllMocks();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
afterAll(async () => {
|
|
219
|
+
clearAllMocks();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should create index', async () => {
|
|
223
|
+
await couchbase_client.createIndex({ indexName: test_indexName, dimension });
|
|
224
|
+
|
|
225
|
+
expect(mockScopeSearchIndexesFn).toHaveBeenCalledTimes(1);
|
|
226
|
+
expect(mockUpsertIndexFn).toHaveBeenCalledTimes(1);
|
|
227
|
+
expect(mockUpsertIndexFn).toHaveBeenCalledWith({
|
|
228
|
+
name: test_indexName,
|
|
229
|
+
sourceName: test_bucketName,
|
|
230
|
+
type: 'fulltext-index',
|
|
231
|
+
params: {
|
|
232
|
+
doc_config: {
|
|
233
|
+
docid_prefix_delim: '',
|
|
234
|
+
docid_regexp: '',
|
|
235
|
+
mode: 'scope.collection.type_field',
|
|
236
|
+
type_field: 'type',
|
|
237
|
+
},
|
|
238
|
+
mapping: {
|
|
239
|
+
default_analyzer: 'standard',
|
|
240
|
+
default_datetime_parser: 'dateTimeOptional',
|
|
241
|
+
default_field: '_all',
|
|
242
|
+
default_mapping: {
|
|
243
|
+
dynamic: true,
|
|
244
|
+
enabled: false,
|
|
245
|
+
},
|
|
246
|
+
default_type: '_default',
|
|
247
|
+
docvalues_dynamic: true,
|
|
248
|
+
index_dynamic: true,
|
|
249
|
+
store_dynamic: true,
|
|
250
|
+
type_field: '_type',
|
|
251
|
+
types: {
|
|
252
|
+
[`${test_scopeName}.${test_collectionName}`]: {
|
|
253
|
+
dynamic: true,
|
|
254
|
+
enabled: true,
|
|
255
|
+
properties: {
|
|
256
|
+
embedding: {
|
|
257
|
+
enabled: true,
|
|
258
|
+
fields: [
|
|
259
|
+
{
|
|
260
|
+
dims: dimension,
|
|
261
|
+
index: true,
|
|
262
|
+
name: 'embedding',
|
|
263
|
+
similarity: 'dot_product',
|
|
264
|
+
type: 'vector',
|
|
265
|
+
vector_index_optimized_for: 'recall',
|
|
266
|
+
store: true,
|
|
267
|
+
docvalues: true,
|
|
268
|
+
include_term_vectors: true,
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
content: {
|
|
273
|
+
enabled: true,
|
|
274
|
+
fields: [
|
|
275
|
+
{
|
|
276
|
+
index: true,
|
|
277
|
+
name: 'content',
|
|
278
|
+
store: true,
|
|
279
|
+
type: 'text',
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
store: {
|
|
288
|
+
indexType: 'scorch',
|
|
289
|
+
segmentVersion: 16,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
sourceUuid: '',
|
|
293
|
+
sourceParams: {},
|
|
294
|
+
sourceType: 'gocbcore',
|
|
295
|
+
planParams: {
|
|
296
|
+
maxPartitionsPerPIndex: 64,
|
|
297
|
+
indexPartitions: 16,
|
|
298
|
+
numReplicas: 0,
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
expect(mockUpsertIndexFn).toHaveResolved();
|
|
302
|
+
}, 50000);
|
|
303
|
+
|
|
304
|
+
it('should list indexes', async () => {
|
|
305
|
+
const indexes = await couchbase_client.listIndexes();
|
|
306
|
+
|
|
307
|
+
expect(mockScopeSearchIndexesFn).toHaveBeenCalledTimes(1);
|
|
308
|
+
expect(mockGetAllIndexesFn).toHaveBeenCalledTimes(1);
|
|
309
|
+
expect(mockGetAllIndexesFn).toHaveResolved();
|
|
310
|
+
expect(indexes).toEqual([`${test_indexName}`, `${test_indexName}_1`, `${test_indexName}_2`]);
|
|
311
|
+
}, 50000);
|
|
312
|
+
|
|
313
|
+
it('should describe index', async () => {
|
|
314
|
+
const stats = await couchbase_client.describeIndex(test_indexName);
|
|
315
|
+
|
|
316
|
+
expect(mockScopeSearchIndexesFn).toHaveBeenCalledTimes(2);
|
|
317
|
+
|
|
318
|
+
expect(mockGetAllIndexesFn).toHaveBeenCalledTimes(1);
|
|
319
|
+
expect(mockGetAllIndexesFn).toHaveResolved();
|
|
320
|
+
|
|
321
|
+
expect(mockGetIndexFn).toHaveBeenCalledTimes(1);
|
|
322
|
+
expect(mockGetIndexFn).toHaveBeenCalledWith(test_indexName);
|
|
323
|
+
expect(mockGetIndexFn).toHaveResolved();
|
|
324
|
+
|
|
325
|
+
expect(stats.dimension).toBe(dimension);
|
|
326
|
+
expect(stats.metric).toBe('euclidean'); // similiarity(=="l2_norm") is mapped to euclidean in couchbase
|
|
327
|
+
expect(typeof stats.count).toBe('number');
|
|
328
|
+
}, 50000);
|
|
329
|
+
|
|
330
|
+
it('should delete index', async () => {
|
|
331
|
+
await couchbase_client.deleteIndex(test_indexName);
|
|
332
|
+
|
|
333
|
+
expect(mockScopeSearchIndexesFn).toHaveBeenCalledTimes(2);
|
|
334
|
+
|
|
335
|
+
expect(mockGetAllIndexesFn).toHaveBeenCalledTimes(1);
|
|
336
|
+
expect(mockGetAllIndexesFn).toHaveResolved();
|
|
337
|
+
|
|
338
|
+
expect(mockDropIndexFn).toHaveBeenCalledTimes(1);
|
|
339
|
+
expect(mockDropIndexFn).toHaveBeenCalledWith(test_indexName);
|
|
340
|
+
expect(mockDropIndexFn).toHaveResolved();
|
|
341
|
+
}, 50000);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('Vector Operations', () => {
|
|
345
|
+
beforeAll(async () => {
|
|
346
|
+
couchbase_client = new CouchbaseVector(
|
|
347
|
+
'COUCHBASE_CONNECTION_STRING',
|
|
348
|
+
'COUCHBASE_USERNAME',
|
|
349
|
+
'COUCHBASE_PASSWORD',
|
|
350
|
+
test_bucketName,
|
|
351
|
+
test_scopeName,
|
|
352
|
+
test_collectionName,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
await couchbase_client.getCollection();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
afterEach(async () => {
|
|
359
|
+
clearAllMocks();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
afterAll(async () => {
|
|
363
|
+
clearAllMocks();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const testVectors = [
|
|
367
|
+
[1.0, 0.0, 0.0],
|
|
368
|
+
[0.0, 1.0, 0.0],
|
|
369
|
+
[0.0, 0.0, 1.0],
|
|
370
|
+
];
|
|
371
|
+
const testMetadata = [
|
|
372
|
+
{ label: 'x-axis' },
|
|
373
|
+
{
|
|
374
|
+
label: 'y-axis',
|
|
375
|
+
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
|
376
|
+
},
|
|
377
|
+
{ label: 'z-axis' },
|
|
378
|
+
];
|
|
379
|
+
let testVectorIds: string[] = ['test_id_1', 'test_id_2', 'test_id_3'];
|
|
380
|
+
|
|
381
|
+
it('should upsert vectors with metadata', async () => {
|
|
382
|
+
const vectorIds = await couchbase_client.upsert({
|
|
383
|
+
indexName: test_indexName,
|
|
384
|
+
vectors: testVectors,
|
|
385
|
+
metadata: testMetadata,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
expect(mockCollectionUpsertFn).toHaveBeenCalledTimes(3);
|
|
389
|
+
expect(mockCollectionUpsertFn).toHaveBeenNthCalledWith(1, vectorIds[0], {
|
|
390
|
+
embedding: [1.0, 0.0, 0.0],
|
|
391
|
+
metadata: { label: 'x-axis' },
|
|
392
|
+
});
|
|
393
|
+
expect(mockCollectionUpsertFn).toHaveBeenNthCalledWith(2, vectorIds[1], {
|
|
394
|
+
embedding: [0.0, 1.0, 0.0],
|
|
395
|
+
metadata: {
|
|
396
|
+
label: 'y-axis',
|
|
397
|
+
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
|
398
|
+
},
|
|
399
|
+
content:
|
|
400
|
+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
|
401
|
+
});
|
|
402
|
+
expect(mockCollectionUpsertFn).toHaveBeenNthCalledWith(3, vectorIds[2], {
|
|
403
|
+
embedding: [0.0, 0.0, 1.0],
|
|
404
|
+
metadata: { label: 'z-axis' },
|
|
405
|
+
});
|
|
406
|
+
expect(mockCollectionUpsertFn).toHaveResolvedTimes(3);
|
|
407
|
+
expect(vectorIds).toHaveLength(3);
|
|
408
|
+
expect(vectorIds[0]).toBeDefined();
|
|
409
|
+
expect(vectorIds[1]).toBeDefined();
|
|
410
|
+
expect(vectorIds[2]).toBeDefined();
|
|
411
|
+
}, 50000);
|
|
412
|
+
|
|
413
|
+
it('should query vectors and return nearest neighbors', async () => {
|
|
414
|
+
const queryVector = [1.0, 0.1, 0.1];
|
|
415
|
+
const topK = 3;
|
|
416
|
+
const results = await couchbase_client.query({ indexName: test_indexName, queryVector, topK });
|
|
417
|
+
|
|
418
|
+
expect(mockScopeSearchIndexesFn).toHaveBeenCalledTimes(2);
|
|
419
|
+
|
|
420
|
+
expect(mockGetAllIndexesFn).toHaveBeenCalledTimes(1);
|
|
421
|
+
expect(mockGetAllIndexesFn).toHaveResolved();
|
|
422
|
+
|
|
423
|
+
expect(mockGetIndexFn).toHaveBeenCalledTimes(1);
|
|
424
|
+
expect(mockGetIndexFn).toHaveBeenCalledWith(test_indexName);
|
|
425
|
+
expect(mockGetIndexFn).toHaveResolved();
|
|
426
|
+
|
|
427
|
+
expect(mockSearchRequestCreateFn).toHaveBeenCalledTimes(1);
|
|
428
|
+
expect(mockSearchRequestCreateFn).toHaveBeenCalledWith('mockVectorSearch');
|
|
429
|
+
|
|
430
|
+
expect(mockVectorSearchFn).toHaveBeenCalledTimes(1);
|
|
431
|
+
expect(mockVectorSearchFn).toHaveBeenCalledWith('mockVectorQuery');
|
|
432
|
+
|
|
433
|
+
expect(mockVectorQueryCreateFn).toHaveBeenCalledTimes(1);
|
|
434
|
+
expect(mockVectorQueryCreateFn).toHaveBeenCalledWith('embedding', queryVector);
|
|
435
|
+
|
|
436
|
+
expect(mockNumCandidatesFn).toHaveBeenCalledTimes(1);
|
|
437
|
+
expect(mockNumCandidatesFn).toHaveBeenCalledWith(topK);
|
|
438
|
+
|
|
439
|
+
expect(mockScopeSearchFn).toHaveBeenCalledTimes(1);
|
|
440
|
+
expect(mockScopeSearchFn).toHaveBeenCalledWith(test_indexName, 'mockRequest', {
|
|
441
|
+
fields: ['*'],
|
|
442
|
+
});
|
|
443
|
+
expect(mockScopeSearchFn).toHaveResolved();
|
|
444
|
+
|
|
445
|
+
expect(results).toHaveLength(3);
|
|
446
|
+
expect(results).toEqual([
|
|
447
|
+
{
|
|
448
|
+
id: 'test_restult_id_1',
|
|
449
|
+
score: 0.5,
|
|
450
|
+
metadata: { label: 'test-label' },
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
id: 'test_restult_id_2',
|
|
454
|
+
score: 0.5,
|
|
455
|
+
metadata: { label: 'test-label' },
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
id: 'test_restult_id_3',
|
|
459
|
+
score: 0.5,
|
|
460
|
+
metadata: { label: 'test-label' },
|
|
461
|
+
},
|
|
462
|
+
]);
|
|
463
|
+
}, 50000);
|
|
464
|
+
|
|
465
|
+
it('should update the vector by id', async () => {
|
|
466
|
+
const vectorIds = await couchbase_client.upsert({
|
|
467
|
+
indexName: test_indexName,
|
|
468
|
+
vectors: testVectors,
|
|
469
|
+
metadata: testMetadata,
|
|
470
|
+
ids: testVectorIds,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(mockCollectionUpsertFn).toHaveBeenCalledTimes(3);
|
|
474
|
+
expect(mockCollectionUpsertFn).toHaveBeenNthCalledWith(1, testVectorIds[0], {
|
|
475
|
+
embedding: [1.0, 0.0, 0.0],
|
|
476
|
+
metadata: { label: 'x-axis' },
|
|
477
|
+
});
|
|
478
|
+
expect(mockCollectionUpsertFn).toHaveBeenNthCalledWith(2, testVectorIds[1], {
|
|
479
|
+
embedding: [0.0, 1.0, 0.0],
|
|
480
|
+
metadata: {
|
|
481
|
+
label: 'y-axis',
|
|
482
|
+
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
|
483
|
+
},
|
|
484
|
+
content:
|
|
485
|
+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
|
486
|
+
});
|
|
487
|
+
expect(mockCollectionUpsertFn).toHaveBeenNthCalledWith(3, testVectorIds[2], {
|
|
488
|
+
embedding: [0.0, 0.0, 1.0],
|
|
489
|
+
metadata: { label: 'z-axis' },
|
|
490
|
+
});
|
|
491
|
+
expect(mockCollectionUpsertFn).toHaveResolvedTimes(3);
|
|
492
|
+
expect(vectorIds).toHaveLength(3);
|
|
493
|
+
expect(vectorIds).toEqual(testVectorIds);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('should throw error for invalid vector dimension', async () => {
|
|
497
|
+
await couchbase_client.createIndex({ indexName: test_indexName, dimension: 4 });
|
|
498
|
+
clearAllMocks();
|
|
499
|
+
|
|
500
|
+
await expect(
|
|
501
|
+
couchbase_client.upsert({
|
|
502
|
+
indexName: test_indexName,
|
|
503
|
+
vectors: [[1, 2, 3]],
|
|
504
|
+
metadata: [{ test: 'initial' }],
|
|
505
|
+
}),
|
|
506
|
+
).rejects.toThrow();
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
describe('Error Cases and Edge Cases', () => {
|
|
511
|
+
beforeAll(async () => {
|
|
512
|
+
clearAllMocks();
|
|
513
|
+
couchbase_client = new CouchbaseVector(
|
|
514
|
+
'COUCHBASE_CONNECTION_STRING',
|
|
515
|
+
'COUCHBASE_USERNAME',
|
|
516
|
+
'COUCHBASE_PASSWORD',
|
|
517
|
+
test_bucketName,
|
|
518
|
+
test_scopeName,
|
|
519
|
+
test_collectionName,
|
|
520
|
+
);
|
|
521
|
+
await couchbase_client.getCollection();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
afterEach(async () => {
|
|
525
|
+
clearAllMocks();
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
afterAll(async () => {
|
|
529
|
+
clearAllMocks();
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('should throw error for negative dimension in createIndex', async () => {
|
|
533
|
+
await expect(couchbase_client.createIndex({ indexName: test_indexName, dimension: -1 })).rejects.toThrow(
|
|
534
|
+
'Dimension must be a positive integer',
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('should throw error for zero dimension in createIndex', async () => {
|
|
539
|
+
await expect(couchbase_client.createIndex({ indexName: test_indexName, dimension: 0 })).rejects.toThrow(
|
|
540
|
+
'Dimension must be a positive integer',
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should throw error when describing a non-existent index', async () => {
|
|
545
|
+
await expect(couchbase_client.describeIndex('non_existent_index')).rejects.toThrow(
|
|
546
|
+
`Index non_existent_index does not exist`,
|
|
547
|
+
);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('should throw error when deleting a non-existent index', async () => {
|
|
551
|
+
await expect(couchbase_client.deleteIndex('non_existent_index')).rejects.toThrow(
|
|
552
|
+
`Index non_existent_index does not exist`,
|
|
553
|
+
);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('should throw error when includeVector is true in query', async () => {
|
|
557
|
+
await expect(
|
|
558
|
+
couchbase_client.query({
|
|
559
|
+
indexName: test_indexName,
|
|
560
|
+
queryVector: [1.0, 2.0, 3.0],
|
|
561
|
+
includeVector: true,
|
|
562
|
+
}),
|
|
563
|
+
).rejects.toThrow('Including vectors in search results is not yet supported by the Couchbase vector store');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('should throw error for empty vectors array in upsert', async () => {
|
|
567
|
+
await expect(
|
|
568
|
+
couchbase_client.upsert({
|
|
569
|
+
indexName: test_indexName,
|
|
570
|
+
vectors: [],
|
|
571
|
+
metadata: [],
|
|
572
|
+
}),
|
|
573
|
+
).rejects.toThrow('No vectors provided');
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
describe('Vector Dimension Tracking', () => {
|
|
578
|
+
beforeEach(async () => {
|
|
579
|
+
clearAllMocks();
|
|
580
|
+
couchbase_client = new CouchbaseVector(
|
|
581
|
+
'COUCHBASE_CONNECTION_STRING',
|
|
582
|
+
'COUCHBASE_USERNAME',
|
|
583
|
+
'COUCHBASE_PASSWORD',
|
|
584
|
+
test_bucketName,
|
|
585
|
+
test_scopeName,
|
|
586
|
+
test_collectionName,
|
|
587
|
+
);
|
|
588
|
+
await couchbase_client.getCollection();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
afterEach(async () => {
|
|
592
|
+
clearAllMocks();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
afterAll(async () => {
|
|
596
|
+
clearAllMocks();
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('should track vector dimension after creating an index', async () => {
|
|
600
|
+
// Initial vector_dimension should be null
|
|
601
|
+
expect((couchbase_client as any).vector_dimension).toBeNull();
|
|
602
|
+
|
|
603
|
+
// After creating index, vector_dimension should be set
|
|
604
|
+
await couchbase_client.createIndex({ indexName: test_indexName, dimension: 5 });
|
|
605
|
+
expect((couchbase_client as any).vector_dimension).toBe(5);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('should validate vector dimensions against tracked dimension during upsert', async () => {
|
|
609
|
+
// Set up dimension tracking
|
|
610
|
+
await couchbase_client.createIndex({ indexName: test_indexName, dimension: 3 });
|
|
611
|
+
clearAllMocks();
|
|
612
|
+
|
|
613
|
+
// Should succeed with correct dimensions
|
|
614
|
+
await couchbase_client.upsert({
|
|
615
|
+
indexName: test_indexName,
|
|
616
|
+
vectors: [
|
|
617
|
+
[1, 2, 3],
|
|
618
|
+
[4, 5, 6],
|
|
619
|
+
],
|
|
620
|
+
metadata: [{}, {}],
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Should fail with incorrect dimensions
|
|
624
|
+
await expect(
|
|
625
|
+
couchbase_client.upsert({
|
|
626
|
+
indexName: test_indexName,
|
|
627
|
+
vectors: [[1, 2, 3, 4]], // 4 dimensions instead of 3
|
|
628
|
+
metadata: [{}],
|
|
629
|
+
}),
|
|
630
|
+
).rejects.toThrow('Vector dimension mismatch');
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('should reset vector_dimension when deleting an index', async () => {
|
|
634
|
+
// Setup - create index and set dimension
|
|
635
|
+
await couchbase_client.createIndex({ indexName: test_indexName, dimension: 3 });
|
|
636
|
+
expect((couchbase_client as any).vector_dimension).toBe(3);
|
|
637
|
+
clearAllMocks();
|
|
638
|
+
|
|
639
|
+
// Delete the index
|
|
640
|
+
await couchbase_client.deleteIndex(test_indexName);
|
|
641
|
+
|
|
642
|
+
// Verify dimension is reset
|
|
643
|
+
expect((couchbase_client as any).vector_dimension).toBeNull();
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
describe('Implementation Details', () => {
|
|
648
|
+
beforeEach(async () => {
|
|
649
|
+
clearAllMocks();
|
|
650
|
+
couchbase_client = new CouchbaseVector(
|
|
651
|
+
'COUCHBASE_CONNECTION_STRING',
|
|
652
|
+
'COUCHBASE_USERNAME',
|
|
653
|
+
'COUCHBASE_PASSWORD',
|
|
654
|
+
test_bucketName,
|
|
655
|
+
test_scopeName,
|
|
656
|
+
test_collectionName,
|
|
657
|
+
);
|
|
658
|
+
await couchbase_client.getCollection();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
afterEach(async () => {
|
|
662
|
+
clearAllMocks();
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
afterAll(async () => {
|
|
666
|
+
clearAllMocks();
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('should handle metric mapping correctly', async () => {
|
|
670
|
+
// Test each possible metric mapping from the imported DISTANCE_MAPPING constant
|
|
671
|
+
const metricsToTest = Object.keys(DISTANCE_MAPPING) as Array<keyof typeof DISTANCE_MAPPING>;
|
|
672
|
+
|
|
673
|
+
for (const mastraMetric of metricsToTest) {
|
|
674
|
+
clearAllMocks();
|
|
675
|
+
const couchbaseMetric = DISTANCE_MAPPING[mastraMetric];
|
|
676
|
+
|
|
677
|
+
// Test createIndex maps Mastra metric to Couchbase metric
|
|
678
|
+
await couchbase_client.createIndex({
|
|
679
|
+
indexName: test_indexName,
|
|
680
|
+
dimension: 3,
|
|
681
|
+
metric: mastraMetric,
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// Verify the upsertIndex was called with the correct Couchbase metric
|
|
685
|
+
expect(mockUpsertIndexFn).toHaveBeenCalledTimes(1);
|
|
686
|
+
const callArgs = mockUpsertIndexFn.mock.calls[0][0];
|
|
687
|
+
expect(callArgs.name).toBe(test_indexName);
|
|
688
|
+
|
|
689
|
+
// Extract the similarity parameter from the deeply nested params
|
|
690
|
+
const similarityParam =
|
|
691
|
+
callArgs.params.mapping.types[`${test_scopeName}.${test_collectionName}`].properties.embedding.fields[0]
|
|
692
|
+
.similarity;
|
|
693
|
+
expect(similarityParam).toBe(couchbaseMetric);
|
|
694
|
+
|
|
695
|
+
mockGetIndexFn.mockResolvedValueOnce({
|
|
696
|
+
params: {
|
|
697
|
+
mapping: {
|
|
698
|
+
types: {
|
|
699
|
+
[`${test_scopeName}.${test_collectionName}`]: {
|
|
700
|
+
properties: {
|
|
701
|
+
embedding: {
|
|
702
|
+
fields: [
|
|
703
|
+
{
|
|
704
|
+
dims: dimension,
|
|
705
|
+
similarity: couchbaseMetric,
|
|
706
|
+
},
|
|
707
|
+
],
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
const stats = await couchbase_client.describeIndex(test_indexName);
|
|
717
|
+
expect(stats.metric).toBe(mastraMetric);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it('should cache collection object for multiple operations', async () => {
|
|
722
|
+
// First call should get the collection
|
|
723
|
+
await couchbase_client.getCollection();
|
|
724
|
+
expect(getMockBucket).toHaveBeenCalledTimes(1);
|
|
725
|
+
expect(getMockScope).toHaveBeenCalledTimes(1);
|
|
726
|
+
expect(getMockCollection).toHaveBeenCalledTimes(1);
|
|
727
|
+
|
|
728
|
+
clearAllMocks();
|
|
729
|
+
|
|
730
|
+
// Second call should not get the collection again
|
|
731
|
+
await couchbase_client.getCollection();
|
|
732
|
+
expect(getMockBucket).not.toHaveBeenCalled();
|
|
733
|
+
expect(getMockScope).not.toHaveBeenCalled();
|
|
734
|
+
expect(getMockCollection).not.toHaveBeenCalled();
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
});
|