@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
package/dist/index.d.cts
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { MastraVector } from '@mastra/core/vector';
|
|
2
|
+
import { connect, SearchRequest, VectorSearch, VectorQuery } from 'couchbase';
|
|
3
|
+
|
|
4
|
+
// src/vector/index.ts
|
|
5
|
+
var DISTANCE_MAPPING = {
|
|
6
|
+
cosine: "cosine",
|
|
7
|
+
euclidean: "l2_norm",
|
|
8
|
+
dotproduct: "dot_product"
|
|
9
|
+
};
|
|
10
|
+
var CouchbaseVector = class extends MastraVector {
|
|
11
|
+
clusterPromise;
|
|
12
|
+
cluster;
|
|
13
|
+
bucketName;
|
|
14
|
+
collectionName;
|
|
15
|
+
scopeName;
|
|
16
|
+
collection;
|
|
17
|
+
bucket;
|
|
18
|
+
scope;
|
|
19
|
+
vector_dimension;
|
|
20
|
+
constructor(cnn_string, username, password, bucketName, scopeName, collectionName) {
|
|
21
|
+
super();
|
|
22
|
+
const baseClusterPromise = connect(cnn_string, {
|
|
23
|
+
username,
|
|
24
|
+
password,
|
|
25
|
+
configProfile: "wanDevelopment"
|
|
26
|
+
});
|
|
27
|
+
const telemetry = this.__getTelemetry();
|
|
28
|
+
this.clusterPromise = telemetry?.traceClass(baseClusterPromise, {
|
|
29
|
+
spanNamePrefix: "couchbase-vector",
|
|
30
|
+
attributes: {
|
|
31
|
+
"vector.type": "couchbase"
|
|
32
|
+
}
|
|
33
|
+
}) ?? baseClusterPromise;
|
|
34
|
+
this.cluster = null;
|
|
35
|
+
this.bucketName = bucketName;
|
|
36
|
+
this.collectionName = collectionName;
|
|
37
|
+
this.scopeName = scopeName;
|
|
38
|
+
this.collection = null;
|
|
39
|
+
this.bucket = null;
|
|
40
|
+
this.scope = null;
|
|
41
|
+
this.vector_dimension = null;
|
|
42
|
+
}
|
|
43
|
+
async getCollection() {
|
|
44
|
+
if (!this.cluster) {
|
|
45
|
+
this.cluster = await this.clusterPromise;
|
|
46
|
+
}
|
|
47
|
+
if (!this.collection) {
|
|
48
|
+
this.bucket = this.cluster.bucket(this.bucketName);
|
|
49
|
+
this.scope = this.bucket.scope(this.scopeName);
|
|
50
|
+
this.collection = this.scope.collection(this.collectionName);
|
|
51
|
+
}
|
|
52
|
+
return this.collection;
|
|
53
|
+
}
|
|
54
|
+
async createIndex(params) {
|
|
55
|
+
const { indexName, dimension, metric = "dotproduct" } = params;
|
|
56
|
+
await this.getCollection();
|
|
57
|
+
if (!Number.isInteger(dimension) || dimension <= 0) {
|
|
58
|
+
throw new Error("Dimension must be a positive integer");
|
|
59
|
+
}
|
|
60
|
+
await this.scope.searchIndexes().upsertIndex({
|
|
61
|
+
name: indexName,
|
|
62
|
+
sourceName: this.bucketName,
|
|
63
|
+
type: "fulltext-index",
|
|
64
|
+
params: {
|
|
65
|
+
doc_config: {
|
|
66
|
+
docid_prefix_delim: "",
|
|
67
|
+
docid_regexp: "",
|
|
68
|
+
mode: "scope.collection.type_field",
|
|
69
|
+
type_field: "type"
|
|
70
|
+
},
|
|
71
|
+
mapping: {
|
|
72
|
+
default_analyzer: "standard",
|
|
73
|
+
default_datetime_parser: "dateTimeOptional",
|
|
74
|
+
default_field: "_all",
|
|
75
|
+
default_mapping: {
|
|
76
|
+
dynamic: true,
|
|
77
|
+
enabled: false
|
|
78
|
+
},
|
|
79
|
+
default_type: "_default",
|
|
80
|
+
docvalues_dynamic: true,
|
|
81
|
+
// [Doc](https://docs.couchbase.com/server/current/search/search-index-params.html#params) mentions this attribute is required for vector search to return the indexed field
|
|
82
|
+
index_dynamic: true,
|
|
83
|
+
store_dynamic: true,
|
|
84
|
+
// [Doc](https://docs.couchbase.com/server/current/search/search-index-params.html#params) mentions this attribute is required for vector search to return the indexed field
|
|
85
|
+
type_field: "_type",
|
|
86
|
+
types: {
|
|
87
|
+
[`${this.scopeName}.${this.collectionName}`]: {
|
|
88
|
+
dynamic: true,
|
|
89
|
+
enabled: true,
|
|
90
|
+
properties: {
|
|
91
|
+
embedding: {
|
|
92
|
+
enabled: true,
|
|
93
|
+
fields: [
|
|
94
|
+
{
|
|
95
|
+
dims: dimension,
|
|
96
|
+
index: true,
|
|
97
|
+
name: "embedding",
|
|
98
|
+
similarity: DISTANCE_MAPPING[metric],
|
|
99
|
+
type: "vector",
|
|
100
|
+
vector_index_optimized_for: "recall",
|
|
101
|
+
store: true,
|
|
102
|
+
// CHANGED due to https://docs.couchbase.com/server/current/search/search-index-params.html#fields
|
|
103
|
+
docvalues: true,
|
|
104
|
+
// CHANGED due to https://docs.couchbase.com/server/current/search/search-index-params.html#fields
|
|
105
|
+
include_term_vectors: true
|
|
106
|
+
// CHANGED due to https://docs.couchbase.com/server/current/search/search-index-params.html#fields
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
content: {
|
|
111
|
+
enabled: true,
|
|
112
|
+
fields: [
|
|
113
|
+
{
|
|
114
|
+
index: true,
|
|
115
|
+
name: "content",
|
|
116
|
+
store: true,
|
|
117
|
+
type: "text"
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
store: {
|
|
126
|
+
indexType: "scorch",
|
|
127
|
+
segmentVersion: 16
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
sourceUuid: "",
|
|
131
|
+
sourceParams: {},
|
|
132
|
+
sourceType: "gocbcore",
|
|
133
|
+
planParams: {
|
|
134
|
+
maxPartitionsPerPIndex: 64,
|
|
135
|
+
indexPartitions: 16,
|
|
136
|
+
numReplicas: 0
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
this.vector_dimension = dimension;
|
|
140
|
+
}
|
|
141
|
+
async upsert(params) {
|
|
142
|
+
const { vectors, metadata, ids } = params;
|
|
143
|
+
await this.getCollection();
|
|
144
|
+
if (!vectors || vectors.length === 0) {
|
|
145
|
+
throw new Error("No vectors provided");
|
|
146
|
+
}
|
|
147
|
+
if (this.vector_dimension) {
|
|
148
|
+
for (const vector of vectors) {
|
|
149
|
+
if (!vector || this.vector_dimension !== vector.length) {
|
|
150
|
+
throw new Error("Vector dimension mismatch");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const pointIds = ids || vectors.map(() => crypto.randomUUID());
|
|
155
|
+
const records = vectors.map((vector, i) => {
|
|
156
|
+
const metadataObj = metadata?.[i] || {};
|
|
157
|
+
const record = {
|
|
158
|
+
embedding: vector,
|
|
159
|
+
metadata: metadataObj
|
|
160
|
+
};
|
|
161
|
+
if (metadataObj.text) {
|
|
162
|
+
record.content = metadataObj.text;
|
|
163
|
+
}
|
|
164
|
+
return record;
|
|
165
|
+
});
|
|
166
|
+
const allPromises = [];
|
|
167
|
+
for (let i = 0; i < records.length; i++) {
|
|
168
|
+
allPromises.push(this.collection.upsert(pointIds[i], records[i]));
|
|
169
|
+
}
|
|
170
|
+
await Promise.all(allPromises);
|
|
171
|
+
return pointIds;
|
|
172
|
+
}
|
|
173
|
+
async query(params) {
|
|
174
|
+
const { indexName, queryVector, topK = 10, includeVector = false } = params;
|
|
175
|
+
await this.getCollection();
|
|
176
|
+
const index_stats = await this.describeIndex(indexName);
|
|
177
|
+
if (queryVector.length !== index_stats.dimension) {
|
|
178
|
+
throw new Error(`Query vector dimension mismatch. Expected ${index_stats.dimension}, got ${queryVector.length}`);
|
|
179
|
+
}
|
|
180
|
+
let request = SearchRequest.create(
|
|
181
|
+
VectorSearch.fromVectorQuery(VectorQuery.create("embedding", queryVector).numCandidates(topK))
|
|
182
|
+
);
|
|
183
|
+
const results = await this.scope.search(indexName, request, {
|
|
184
|
+
fields: ["*"]
|
|
185
|
+
});
|
|
186
|
+
if (includeVector) {
|
|
187
|
+
throw new Error("Including vectors in search results is not yet supported by the Couchbase vector store");
|
|
188
|
+
}
|
|
189
|
+
const output = [];
|
|
190
|
+
for (const match of results.rows) {
|
|
191
|
+
const cleanedMetadata = {};
|
|
192
|
+
const fields = match.fields || {};
|
|
193
|
+
for (const key in fields) {
|
|
194
|
+
if (Object.prototype.hasOwnProperty.call(fields, key)) {
|
|
195
|
+
const newKey = key.startsWith("metadata.") ? key.substring("metadata.".length) : key;
|
|
196
|
+
cleanedMetadata[newKey] = fields[key];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
output.push({
|
|
200
|
+
id: match.id,
|
|
201
|
+
score: match.score || 0,
|
|
202
|
+
metadata: cleanedMetadata
|
|
203
|
+
// Use the cleaned metadata object
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return output;
|
|
207
|
+
}
|
|
208
|
+
async listIndexes() {
|
|
209
|
+
await this.getCollection();
|
|
210
|
+
const indexes = await this.scope.searchIndexes().getAllIndexes();
|
|
211
|
+
return indexes?.map((index) => index.name) || [];
|
|
212
|
+
}
|
|
213
|
+
async describeIndex(indexName) {
|
|
214
|
+
await this.getCollection();
|
|
215
|
+
if (!(await this.listIndexes()).includes(indexName)) {
|
|
216
|
+
throw new Error(`Index ${indexName} does not exist`);
|
|
217
|
+
}
|
|
218
|
+
const index = await this.scope.searchIndexes().getIndex(indexName);
|
|
219
|
+
const dimensions = index.params.mapping?.types?.[`${this.scopeName}.${this.collectionName}`]?.properties?.embedding?.fields?.[0]?.dims;
|
|
220
|
+
const count = -1;
|
|
221
|
+
const metric = index.params.mapping?.types?.[`${this.scopeName}.${this.collectionName}`]?.properties?.embedding?.fields?.[0]?.similarity;
|
|
222
|
+
return {
|
|
223
|
+
dimension: dimensions,
|
|
224
|
+
count,
|
|
225
|
+
metric: Object.keys(DISTANCE_MAPPING).find(
|
|
226
|
+
(key) => DISTANCE_MAPPING[key] === metric
|
|
227
|
+
)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
async deleteIndex(indexName) {
|
|
231
|
+
await this.getCollection();
|
|
232
|
+
if (!(await this.listIndexes()).includes(indexName)) {
|
|
233
|
+
throw new Error(`Index ${indexName} does not exist`);
|
|
234
|
+
}
|
|
235
|
+
await this.scope.searchIndexes().dropIndex(indexName);
|
|
236
|
+
this.vector_dimension = null;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export { CouchbaseVector, DISTANCE_MAPPING };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
services:
|
|
2
|
+
mastra_couchbase_testing:
|
|
3
|
+
image: couchbase:enterprise-7.6.4
|
|
4
|
+
ports:
|
|
5
|
+
- "8091-8095:8091-8095"
|
|
6
|
+
- "11210:11210"
|
|
7
|
+
- "9102:9102"
|
|
8
|
+
expose:
|
|
9
|
+
- "8091"
|
|
10
|
+
- "8092"
|
|
11
|
+
- "8093"
|
|
12
|
+
- "8094"
|
|
13
|
+
- "8095"
|
|
14
|
+
- "9102"
|
|
15
|
+
- "11210"
|
|
16
|
+
healthcheck: # checks couchbase server is up
|
|
17
|
+
test: ["CMD", "curl", "-v", "http://localhost:8091/pools"]
|
|
18
|
+
interval: 20s
|
|
19
|
+
timeout: 20s
|
|
20
|
+
retries: 5
|
|
21
|
+
container_name: mastra_couchbase_testing
|
package/eslint.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mastra/couchbase",
|
|
3
|
+
"version": "0.0.2-alpha.0",
|
|
4
|
+
"description": "Couchbase vector store provider for Mastra",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.cts",
|
|
16
|
+
"default": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"couchbase": "^4.4.5",
|
|
23
|
+
"@mastra/core": "^0.9.2-alpha.5"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@microsoft/api-extractor": "^7.52.1",
|
|
27
|
+
"@types/node": "^20.17.27",
|
|
28
|
+
"@vitest/coverage-v8": "3.0.9",
|
|
29
|
+
"@vitest/ui": "3.0.9",
|
|
30
|
+
"axios": "^1.8.4",
|
|
31
|
+
"eslint": "^9.23.0",
|
|
32
|
+
"tsup": "^8.4.0",
|
|
33
|
+
"typescript": "^5.8.2",
|
|
34
|
+
"vitest": "^3.0.9",
|
|
35
|
+
"@internal/lint": "0.0.2"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting",
|
|
39
|
+
"build:watch": "pnpm build --watch",
|
|
40
|
+
"lint": "eslint .",
|
|
41
|
+
"coverage": "vitest run --coverage",
|
|
42
|
+
"pretest": "node scripts/start-docker.js",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"posttest": "node scripts/stop-docker.js"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
try {
|
|
4
|
+
execSync('docker compose -f "./docker-compose.yaml" ps --quiet');
|
|
5
|
+
console.log('Container already running, bringing it down first...');
|
|
6
|
+
execSync('docker compose -f "./docker-compose.yaml" down --volumes', { stdio: 'inherit' });
|
|
7
|
+
} catch (error) {
|
|
8
|
+
console.error('No existing container found', error);
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
execSync('docker compose -f "./docker-compose.yaml" up --wait', { stdio: 'inherit' });
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error('Failed to start container', error);
|
|
14
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './vector';
|