@mastra/couchbase 0.10.3 → 0.11.0-alpha.1
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 +7 -7
- package/CHANGELOG.md +26 -0
- package/dist/index.cjs +272 -129
- package/dist/index.js +270 -127
- package/package.json +5 -5
- package/src/vector/index.ts +284 -141
- package/src/vector/index.unit.test.ts +1 -1
package/src/vector/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
|
|
1
2
|
import { MastraVector } from '@mastra/core/vector';
|
|
2
3
|
import type {
|
|
3
4
|
QueryResult,
|
|
@@ -44,28 +45,47 @@ export class CouchbaseVector extends MastraVector {
|
|
|
44
45
|
constructor({ connectionString, username, password, bucketName, scopeName, collectionName }: CouchbaseVectorParams) {
|
|
45
46
|
super();
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
try {
|
|
49
|
+
const baseClusterPromise = connect(connectionString, {
|
|
50
|
+
username,
|
|
51
|
+
password,
|
|
52
|
+
configProfile: 'wanDevelopment',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const telemetry = this.__getTelemetry();
|
|
56
|
+
this.clusterPromise =
|
|
57
|
+
telemetry?.traceClass(baseClusterPromise, {
|
|
58
|
+
spanNamePrefix: 'couchbase-vector',
|
|
59
|
+
attributes: {
|
|
60
|
+
'vector.type': 'couchbase',
|
|
61
|
+
},
|
|
62
|
+
}) ?? baseClusterPromise;
|
|
63
|
+
this.cluster = null as unknown as Cluster;
|
|
64
|
+
this.bucketName = bucketName;
|
|
65
|
+
this.collectionName = collectionName;
|
|
66
|
+
this.scopeName = scopeName;
|
|
67
|
+
this.collection = null as unknown as Collection;
|
|
68
|
+
this.bucket = null as unknown as Bucket;
|
|
69
|
+
this.scope = null as unknown as Scope;
|
|
70
|
+
this.vector_dimension = null as unknown as number;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new MastraError(
|
|
73
|
+
{
|
|
74
|
+
id: 'COUCHBASE_VECTOR_INITIALIZE_FAILED',
|
|
75
|
+
domain: ErrorDomain.STORAGE,
|
|
76
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
77
|
+
details: {
|
|
78
|
+
connectionString,
|
|
79
|
+
username,
|
|
80
|
+
password,
|
|
81
|
+
bucketName,
|
|
82
|
+
scopeName,
|
|
83
|
+
collectionName,
|
|
84
|
+
},
|
|
59
85
|
},
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this.collectionName = collectionName;
|
|
64
|
-
this.scopeName = scopeName;
|
|
65
|
-
this.collection = null as unknown as Collection;
|
|
66
|
-
this.bucket = null as unknown as Bucket;
|
|
67
|
-
this.scope = null as unknown as Scope;
|
|
68
|
-
this.vector_dimension = null as unknown as number;
|
|
86
|
+
error,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
69
89
|
}
|
|
70
90
|
|
|
71
91
|
async getCollection() {
|
|
@@ -83,13 +103,13 @@ export class CouchbaseVector extends MastraVector {
|
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
async createIndex({ indexName, dimension, metric = 'dotproduct' as MastraMetric }: CreateIndexParams): Promise<void> {
|
|
86
|
-
|
|
106
|
+
try {
|
|
107
|
+
await this.getCollection();
|
|
87
108
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
109
|
+
if (!Number.isInteger(dimension) || dimension <= 0) {
|
|
110
|
+
throw new Error('Dimension must be a positive integer');
|
|
111
|
+
}
|
|
91
112
|
|
|
92
|
-
try {
|
|
93
113
|
await this.scope.searchIndexes().upsertIndex({
|
|
94
114
|
name: indexName,
|
|
95
115
|
sourceName: this.bucketName,
|
|
@@ -173,88 +193,139 @@ export class CouchbaseVector extends MastraVector {
|
|
|
173
193
|
await this.validateExistingIndex(indexName, dimension, metric);
|
|
174
194
|
return;
|
|
175
195
|
}
|
|
176
|
-
throw
|
|
196
|
+
throw new MastraError(
|
|
197
|
+
{
|
|
198
|
+
id: 'COUCHBASE_VECTOR_CREATE_INDEX_FAILED',
|
|
199
|
+
domain: ErrorDomain.STORAGE,
|
|
200
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
201
|
+
details: {
|
|
202
|
+
indexName,
|
|
203
|
+
dimension,
|
|
204
|
+
metric,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
error,
|
|
208
|
+
);
|
|
177
209
|
}
|
|
178
210
|
}
|
|
179
211
|
|
|
180
212
|
async upsert({ vectors, metadata, ids }: UpsertVectorParams): Promise<string[]> {
|
|
181
|
-
|
|
213
|
+
try {
|
|
214
|
+
await this.getCollection();
|
|
182
215
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
if (!vectors || vectors.length === 0) {
|
|
217
|
+
throw new Error('No vectors provided');
|
|
218
|
+
}
|
|
219
|
+
if (this.vector_dimension) {
|
|
220
|
+
for (const vector of vectors) {
|
|
221
|
+
if (!vector || this.vector_dimension !== vector.length) {
|
|
222
|
+
throw new Error('Vector dimension mismatch');
|
|
223
|
+
}
|
|
190
224
|
}
|
|
191
225
|
}
|
|
192
|
-
}
|
|
193
226
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
227
|
+
const pointIds = ids || vectors.map(() => crypto.randomUUID());
|
|
228
|
+
const records = vectors.map((vector, i) => {
|
|
229
|
+
const metadataObj = metadata?.[i] || {};
|
|
230
|
+
const record: Record<string, any> = {
|
|
231
|
+
embedding: vector,
|
|
232
|
+
metadata: metadataObj,
|
|
233
|
+
};
|
|
234
|
+
// If metadata has a text field, save it as content
|
|
235
|
+
if (metadataObj.text) {
|
|
236
|
+
record.content = metadataObj.text;
|
|
237
|
+
}
|
|
238
|
+
return record;
|
|
239
|
+
});
|
|
207
240
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
241
|
+
const allPromises = [];
|
|
242
|
+
for (let i = 0; i < records.length; i++) {
|
|
243
|
+
allPromises.push(this.collection.upsert(pointIds[i]!, records[i]));
|
|
244
|
+
}
|
|
245
|
+
await Promise.all(allPromises);
|
|
246
|
+
|
|
247
|
+
return pointIds;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
throw new MastraError(
|
|
250
|
+
{
|
|
251
|
+
id: 'COUCHBASE_VECTOR_UPSERT_FAILED',
|
|
252
|
+
domain: ErrorDomain.STORAGE,
|
|
253
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
254
|
+
},
|
|
255
|
+
error,
|
|
256
|
+
);
|
|
211
257
|
}
|
|
212
|
-
await Promise.all(allPromises);
|
|
213
|
-
|
|
214
|
-
return pointIds;
|
|
215
258
|
}
|
|
216
259
|
|
|
217
260
|
async query({ indexName, queryVector, topK = 10, includeVector = false }: QueryVectorParams): Promise<QueryResult[]> {
|
|
218
|
-
|
|
261
|
+
try {
|
|
262
|
+
await this.getCollection();
|
|
219
263
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
264
|
+
const index_stats = await this.describeIndex({ indexName });
|
|
265
|
+
if (queryVector.length !== index_stats.dimension) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`Query vector dimension mismatch. Expected ${index_stats.dimension}, got ${queryVector.length}`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
224
270
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
271
|
+
let request = SearchRequest.create(
|
|
272
|
+
VectorSearch.fromVectorQuery(VectorQuery.create('embedding', queryVector).numCandidates(topK)),
|
|
273
|
+
);
|
|
274
|
+
const results = await this.scope.search(indexName, request, {
|
|
275
|
+
fields: ['*'],
|
|
276
|
+
});
|
|
231
277
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
278
|
+
if (includeVector) {
|
|
279
|
+
throw new Error('Including vectors in search results is not yet supported by the Couchbase vector store');
|
|
280
|
+
}
|
|
281
|
+
const output = [];
|
|
282
|
+
for (const match of results.rows) {
|
|
283
|
+
const cleanedMetadata: Record<string, any> = {};
|
|
284
|
+
const fields = (match.fields as Record<string, any>) || {}; // Ensure fields is an object
|
|
285
|
+
for (const key in fields) {
|
|
286
|
+
if (Object.prototype.hasOwnProperty.call(fields, key)) {
|
|
287
|
+
const newKey = key.startsWith('metadata.') ? key.substring('metadata.'.length) : key;
|
|
288
|
+
cleanedMetadata[newKey] = fields[key];
|
|
289
|
+
}
|
|
243
290
|
}
|
|
291
|
+
output.push({
|
|
292
|
+
id: match.id as string,
|
|
293
|
+
score: (match.score as number) || 0,
|
|
294
|
+
metadata: cleanedMetadata, // Use the cleaned metadata object
|
|
295
|
+
});
|
|
244
296
|
}
|
|
245
|
-
output
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
297
|
+
return output;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
throw new MastraError(
|
|
300
|
+
{
|
|
301
|
+
id: 'COUCHBASE_VECTOR_QUERY_FAILED',
|
|
302
|
+
domain: ErrorDomain.STORAGE,
|
|
303
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
304
|
+
details: {
|
|
305
|
+
indexName,
|
|
306
|
+
topK,
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
error,
|
|
310
|
+
);
|
|
250
311
|
}
|
|
251
|
-
return output;
|
|
252
312
|
}
|
|
253
313
|
|
|
254
314
|
async listIndexes(): Promise<string[]> {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
315
|
+
try {
|
|
316
|
+
await this.getCollection();
|
|
317
|
+
const indexes = await this.scope.searchIndexes().getAllIndexes();
|
|
318
|
+
return indexes?.map(index => index.name) || [];
|
|
319
|
+
} catch (error) {
|
|
320
|
+
throw new MastraError(
|
|
321
|
+
{
|
|
322
|
+
id: 'COUCHBASE_VECTOR_LIST_INDEXES_FAILED',
|
|
323
|
+
domain: ErrorDomain.STORAGE,
|
|
324
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
325
|
+
},
|
|
326
|
+
error,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
258
329
|
}
|
|
259
330
|
|
|
260
331
|
/**
|
|
@@ -264,33 +335,64 @@ export class CouchbaseVector extends MastraVector {
|
|
|
264
335
|
* @returns A promise that resolves to the index statistics including dimension, count and metric
|
|
265
336
|
*/
|
|
266
337
|
async describeIndex({ indexName }: DescribeIndexParams): Promise<IndexStats> {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
338
|
+
try {
|
|
339
|
+
await this.getCollection();
|
|
340
|
+
if (!(await this.listIndexes()).includes(indexName)) {
|
|
341
|
+
throw new Error(`Index ${indexName} does not exist`);
|
|
342
|
+
}
|
|
343
|
+
const index = await this.scope.searchIndexes().getIndex(indexName);
|
|
344
|
+
const dimensions =
|
|
345
|
+
index.params.mapping?.types?.[`${this.scopeName}.${this.collectionName}`]?.properties?.embedding?.fields?.[0]
|
|
346
|
+
?.dims;
|
|
347
|
+
const count = -1; // Not added support yet for adding a count of documents covered by an index
|
|
348
|
+
const metric = index.params.mapping?.types?.[`${this.scopeName}.${this.collectionName}`]?.properties?.embedding
|
|
349
|
+
?.fields?.[0]?.similarity as CouchbaseMetric;
|
|
350
|
+
return {
|
|
351
|
+
dimension: dimensions,
|
|
352
|
+
count: count,
|
|
353
|
+
metric: Object.keys(DISTANCE_MAPPING).find(
|
|
354
|
+
key => DISTANCE_MAPPING[key as MastraMetric] === metric,
|
|
355
|
+
) as MastraMetric,
|
|
356
|
+
};
|
|
357
|
+
} catch (error) {
|
|
358
|
+
throw new MastraError(
|
|
359
|
+
{
|
|
360
|
+
id: 'COUCHBASE_VECTOR_DESCRIBE_INDEX_FAILED',
|
|
361
|
+
domain: ErrorDomain.STORAGE,
|
|
362
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
363
|
+
details: {
|
|
364
|
+
indexName,
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
error,
|
|
368
|
+
);
|
|
270
369
|
}
|
|
271
|
-
const index = await this.scope.searchIndexes().getIndex(indexName);
|
|
272
|
-
const dimensions =
|
|
273
|
-
index.params.mapping?.types?.[`${this.scopeName}.${this.collectionName}`]?.properties?.embedding?.fields?.[0]
|
|
274
|
-
?.dims;
|
|
275
|
-
const count = -1; // Not added support yet for adding a count of documents covered by an index
|
|
276
|
-
const metric = index.params.mapping?.types?.[`${this.scopeName}.${this.collectionName}`]?.properties?.embedding
|
|
277
|
-
?.fields?.[0]?.similarity as CouchbaseMetric;
|
|
278
|
-
return {
|
|
279
|
-
dimension: dimensions,
|
|
280
|
-
count: count,
|
|
281
|
-
metric: Object.keys(DISTANCE_MAPPING).find(
|
|
282
|
-
key => DISTANCE_MAPPING[key as MastraMetric] === metric,
|
|
283
|
-
) as MastraMetric,
|
|
284
|
-
};
|
|
285
370
|
}
|
|
286
371
|
|
|
287
372
|
async deleteIndex({ indexName }: DeleteIndexParams): Promise<void> {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
373
|
+
try {
|
|
374
|
+
await this.getCollection();
|
|
375
|
+
if (!(await this.listIndexes()).includes(indexName)) {
|
|
376
|
+
throw new Error(`Index ${indexName} does not exist`);
|
|
377
|
+
}
|
|
378
|
+
await this.scope.searchIndexes().dropIndex(indexName);
|
|
379
|
+
this.vector_dimension = null as unknown as number;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (error instanceof MastraError) {
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
throw new MastraError(
|
|
385
|
+
{
|
|
386
|
+
id: 'COUCHBASE_VECTOR_DELETE_INDEX_FAILED',
|
|
387
|
+
domain: ErrorDomain.STORAGE,
|
|
388
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
389
|
+
details: {
|
|
390
|
+
indexName,
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
error,
|
|
394
|
+
);
|
|
291
395
|
}
|
|
292
|
-
await this.scope.searchIndexes().dropIndex(indexName);
|
|
293
|
-
this.vector_dimension = null as unknown as number;
|
|
294
396
|
}
|
|
295
397
|
|
|
296
398
|
/**
|
|
@@ -304,29 +406,45 @@ export class CouchbaseVector extends MastraVector {
|
|
|
304
406
|
* @throws Will throw an error if no updates are provided or if the update operation fails.
|
|
305
407
|
*/
|
|
306
408
|
async updateVector({ id, update }: UpdateVectorParams): Promise<void> {
|
|
307
|
-
if (!update.vector && !update.metadata) {
|
|
308
|
-
throw new Error('No updates provided');
|
|
309
|
-
}
|
|
310
|
-
if (update.vector && this.vector_dimension && update.vector.length !== this.vector_dimension) {
|
|
311
|
-
throw new Error('Vector dimension mismatch');
|
|
312
|
-
}
|
|
313
|
-
const collection = await this.getCollection();
|
|
314
|
-
|
|
315
|
-
// Check if document exists
|
|
316
409
|
try {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
410
|
+
if (!update.vector && !update.metadata) {
|
|
411
|
+
throw new Error('No updates provided');
|
|
412
|
+
}
|
|
413
|
+
if (update.vector && this.vector_dimension && update.vector.length !== this.vector_dimension) {
|
|
414
|
+
throw new Error('Vector dimension mismatch');
|
|
415
|
+
}
|
|
416
|
+
const collection = await this.getCollection();
|
|
417
|
+
|
|
418
|
+
// Check if document exists
|
|
419
|
+
try {
|
|
420
|
+
await collection.get(id);
|
|
421
|
+
} catch (err: any) {
|
|
422
|
+
if (err.code === 13 || err.message?.includes('document not found')) {
|
|
423
|
+
throw new Error(`Vector with id ${id} does not exist`);
|
|
424
|
+
}
|
|
425
|
+
throw err;
|
|
321
426
|
}
|
|
322
|
-
throw err;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const specs: MutateInSpec[] = [];
|
|
326
|
-
if (update.vector) specs.push(MutateInSpec.replace('embedding', update.vector));
|
|
327
|
-
if (update.metadata) specs.push(MutateInSpec.replace('metadata', update.metadata));
|
|
328
427
|
|
|
329
|
-
|
|
428
|
+
const specs: MutateInSpec[] = [];
|
|
429
|
+
if (update.vector) specs.push(MutateInSpec.replace('embedding', update.vector));
|
|
430
|
+
if (update.metadata) specs.push(MutateInSpec.replace('metadata', update.metadata));
|
|
431
|
+
|
|
432
|
+
await collection.mutateIn(id, specs);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
throw new MastraError(
|
|
435
|
+
{
|
|
436
|
+
id: 'COUCHBASE_VECTOR_UPDATE_FAILED',
|
|
437
|
+
domain: ErrorDomain.STORAGE,
|
|
438
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
439
|
+
details: {
|
|
440
|
+
id,
|
|
441
|
+
hasVectorUpdate: !!update.vector,
|
|
442
|
+
hasMetadataUpdate: !!update.metadata,
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
error,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
330
448
|
}
|
|
331
449
|
|
|
332
450
|
/**
|
|
@@ -337,25 +455,50 @@ export class CouchbaseVector extends MastraVector {
|
|
|
337
455
|
* @throws Will throw an error if the deletion operation fails.
|
|
338
456
|
*/
|
|
339
457
|
async deleteVector({ id }: DeleteVectorParams): Promise<void> {
|
|
340
|
-
const collection = await this.getCollection();
|
|
341
|
-
|
|
342
|
-
// Check if document exists
|
|
343
458
|
try {
|
|
344
|
-
await
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
459
|
+
const collection = await this.getCollection();
|
|
460
|
+
|
|
461
|
+
// Check if document exists
|
|
462
|
+
try {
|
|
463
|
+
await collection.get(id);
|
|
464
|
+
} catch (err: any) {
|
|
465
|
+
if (err.code === 13 || err.message?.includes('document not found')) {
|
|
466
|
+
throw new Error(`Vector with id ${id} does not exist`);
|
|
467
|
+
}
|
|
468
|
+
throw err;
|
|
348
469
|
}
|
|
349
|
-
throw err;
|
|
350
|
-
}
|
|
351
470
|
|
|
352
|
-
|
|
471
|
+
await collection.remove(id);
|
|
472
|
+
} catch (error) {
|
|
473
|
+
throw new MastraError(
|
|
474
|
+
{
|
|
475
|
+
id: 'COUCHBASE_VECTOR_DELETE_FAILED',
|
|
476
|
+
domain: ErrorDomain.STORAGE,
|
|
477
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
478
|
+
details: {
|
|
479
|
+
id,
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
error,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
353
485
|
}
|
|
354
486
|
|
|
355
487
|
async disconnect() {
|
|
356
|
-
|
|
357
|
-
|
|
488
|
+
try {
|
|
489
|
+
if (!this.cluster) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
await this.cluster.close();
|
|
493
|
+
} catch (error) {
|
|
494
|
+
throw new MastraError(
|
|
495
|
+
{
|
|
496
|
+
id: 'COUCHBASE_VECTOR_DISCONNECT_FAILED',
|
|
497
|
+
domain: ErrorDomain.STORAGE,
|
|
498
|
+
category: ErrorCategory.THIRD_PARTY,
|
|
499
|
+
},
|
|
500
|
+
error,
|
|
501
|
+
);
|
|
358
502
|
}
|
|
359
|
-
await this.cluster.close();
|
|
360
503
|
}
|
|
361
504
|
}
|
|
@@ -323,7 +323,7 @@ describe('Unit Testing CouchbaseVector', () => {
|
|
|
323
323
|
expect(mockGetIndexFn).toHaveResolved();
|
|
324
324
|
|
|
325
325
|
expect(stats.dimension).toBe(dimension);
|
|
326
|
-
expect(stats.metric).toBe('euclidean'); //
|
|
326
|
+
expect(stats.metric).toBe('euclidean'); // similarity(=="l2_norm") is mapped to euclidean in couchbase
|
|
327
327
|
expect(typeof stats.count).toBe('number');
|
|
328
328
|
}, 50000);
|
|
329
329
|
|