@larkup/vector-stores 0.1.20 → 0.1.22
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/CHANGELOG.md +13 -0
- package/package.json +5 -5
- package/src/adapters/base.ts +24 -22
- package/src/adapters/chroma.ts +49 -73
- package/src/adapters/lancedb.ts +86 -44
- package/src/adapters/pinecone.ts +47 -93
- package/src/factory.ts +45 -25
- package/src/registry.ts +258 -212
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @larkup/vector-stores
|
|
2
|
+
|
|
3
|
+
## 0.1.22
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 08e7029: Fix LanceDB native-module loading in packaged Larkup installations, align marketplace catalog versions with published tools, and make clearing a knowledge base discoverable.
|
|
8
|
+
|
|
9
|
+
## 0.1.21
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 5caaf2f: feat: support persistent S3-compatible LanceDB storage for serverless deployments
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@larkup/vector-stores",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
"./chroma": "./src/adapters/chroma.ts"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@lancedb/lancedb": "^0.
|
|
18
|
-
"@pinecone-database/pinecone": "^
|
|
17
|
+
"@lancedb/lancedb": "^0.31.0",
|
|
18
|
+
"@pinecone-database/pinecone": "^8.1.0"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"@larkup/core": "0.1.20"
|
|
21
|
+
"@larkup/core": ">=0.1.20 <1.0.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"chromadb": "^
|
|
24
|
+
"chromadb": "^3.5.0",
|
|
25
25
|
"typescript": "5.7.3"
|
|
26
26
|
}
|
|
27
27
|
}
|
package/src/adapters/base.ts
CHANGED
|
@@ -7,42 +7,44 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export interface VectorRecord {
|
|
10
|
-
id: string
|
|
11
|
-
vector: number[]
|
|
12
|
-
text: string
|
|
13
|
-
title: string
|
|
14
|
-
url?: string
|
|
15
|
-
source: string
|
|
16
|
-
documentId: string
|
|
17
|
-
chunkIndex: number
|
|
18
|
-
metadata?: Record<string, any
|
|
10
|
+
id: string;
|
|
11
|
+
vector: number[];
|
|
12
|
+
text: string;
|
|
13
|
+
title: string;
|
|
14
|
+
url?: string;
|
|
15
|
+
source: string;
|
|
16
|
+
documentId: string;
|
|
17
|
+
chunkIndex: number;
|
|
18
|
+
metadata?: Record<string, any>;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface QueryHit {
|
|
22
|
-
id: string
|
|
23
|
-
score: number
|
|
24
|
-
text: string
|
|
25
|
-
title: string
|
|
26
|
-
url?: string
|
|
27
|
-
documentId: string
|
|
28
|
-
metadata?: Record<string, any
|
|
22
|
+
id: string;
|
|
23
|
+
score: number;
|
|
24
|
+
text: string;
|
|
25
|
+
title: string;
|
|
26
|
+
url?: string;
|
|
27
|
+
documentId: string;
|
|
28
|
+
metadata?: Record<string, any>;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface VectorStoreAdapter {
|
|
32
32
|
/** Create / connect to the index, sized for `dimensions`. */
|
|
33
|
-
init(dimensions: number): Promise<void
|
|
33
|
+
init(dimensions: number): Promise<void>;
|
|
34
34
|
/** Remove all existing vectors (full re-index). */
|
|
35
|
-
reset(): Promise<void
|
|
35
|
+
reset(): Promise<void>;
|
|
36
36
|
/** Upsert a batch of records. */
|
|
37
|
-
upsert(records: VectorRecord[]): Promise<void
|
|
37
|
+
upsert(records: VectorRecord[]): Promise<void>;
|
|
38
38
|
/** Number of vectors currently stored, if cheaply knowable. */
|
|
39
|
-
count(): Promise<number | null
|
|
39
|
+
count(): Promise<number | null>;
|
|
40
40
|
/**
|
|
41
41
|
* Nearest-neighbour search for a query vector.
|
|
42
42
|
* Pass `queryText` for hybrid/lexical stores so they can also generate a
|
|
43
43
|
* sparse vector and merge dense + sparse results (RRF).
|
|
44
44
|
*/
|
|
45
|
-
query(vector: number[], topK: number, queryText?: string): Promise<QueryHit[]
|
|
45
|
+
query(vector: number[], topK: number, queryText?: string): Promise<QueryHit[]>;
|
|
46
46
|
/** Test connection to the vector store. Throws if invalid. */
|
|
47
|
-
testConnection(dimensions: number): Promise<void
|
|
47
|
+
testConnection(dimensions: number): Promise<void>;
|
|
48
|
+
/** Delete all chunks associated with the given document IDs. */
|
|
49
|
+
deleteByDocumentIds(documentIds: string[]): Promise<void>;
|
|
48
50
|
}
|
package/src/adapters/chroma.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QueryHit, VectorRecord, VectorStoreAdapter } from
|
|
1
|
+
import type { QueryHit, VectorRecord, VectorStoreAdapter } from './base';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Chroma adapter — self-hosted (client-server) or Chroma Cloud.
|
|
@@ -47,7 +47,7 @@ export class ChromaAdapter implements VectorStoreAdapter {
|
|
|
47
47
|
private readonly collectionName: string;
|
|
48
48
|
|
|
49
49
|
constructor(private readonly config: ChromaConfig) {
|
|
50
|
-
this.collectionName = config.collectionName?.trim() ||
|
|
50
|
+
this.collectionName = config.collectionName?.trim() || 'documents';
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
@@ -60,37 +60,37 @@ export class ChromaAdapter implements VectorStoreAdapter {
|
|
|
60
60
|
|
|
61
61
|
let chromadb: any;
|
|
62
62
|
try {
|
|
63
|
-
chromadb = await import(
|
|
63
|
+
chromadb = await import('chromadb');
|
|
64
64
|
} catch {
|
|
65
65
|
throw new Error(
|
|
66
66
|
'The "chromadb" package is not installed. Please install it first via the Configure page or run: pnpm add chromadb --filter @larkup/vector-stores',
|
|
67
67
|
);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
if (this.config.mode ===
|
|
70
|
+
if (this.config.mode === 'cloud') {
|
|
71
71
|
if (!this.config.apiKey) {
|
|
72
|
-
throw new Error(
|
|
72
|
+
throw new Error('Chroma Cloud requires an API key.');
|
|
73
73
|
}
|
|
74
74
|
const CloudClientClass = chromadb.CloudClient ?? chromadb.default?.CloudClient;
|
|
75
75
|
if (!CloudClientClass) {
|
|
76
|
-
throw new Error(
|
|
76
|
+
throw new Error('Could not find CloudClient in the chromadb package.');
|
|
77
77
|
}
|
|
78
78
|
this.client = new CloudClientClass({
|
|
79
79
|
apiKey: this.config.apiKey,
|
|
80
|
-
tenant: this.config.tenant ||
|
|
81
|
-
database: this.config.database ||
|
|
80
|
+
tenant: this.config.tenant || 'default_tenant',
|
|
81
|
+
database: this.config.database || 'default_database',
|
|
82
82
|
});
|
|
83
83
|
} else {
|
|
84
84
|
// Server mode
|
|
85
|
-
const host = this.config.host?.trim() ||
|
|
85
|
+
const host = this.config.host?.trim() || 'http://localhost:8000';
|
|
86
86
|
const ClientClass = chromadb.ChromaClient ?? chromadb.default?.ChromaClient;
|
|
87
87
|
if (!ClientClass) {
|
|
88
|
-
throw new Error(
|
|
88
|
+
throw new Error('Could not find ChromaClient in the chromadb package.');
|
|
89
89
|
}
|
|
90
90
|
const opts: any = { path: host };
|
|
91
91
|
if (this.config.authToken) {
|
|
92
92
|
opts.auth = {
|
|
93
|
-
provider:
|
|
93
|
+
provider: 'token',
|
|
94
94
|
credentials: this.config.authToken,
|
|
95
95
|
};
|
|
96
96
|
}
|
|
@@ -105,7 +105,7 @@ export class ChromaAdapter implements VectorStoreAdapter {
|
|
|
105
105
|
const client = await this.getClient();
|
|
106
106
|
this.collection = await client.getOrCreateCollection({
|
|
107
107
|
name: this.collectionName,
|
|
108
|
-
metadata: {
|
|
108
|
+
metadata: { 'hnsw:space': 'cosine' },
|
|
109
109
|
});
|
|
110
110
|
return this.collection;
|
|
111
111
|
}
|
|
@@ -126,6 +126,16 @@ export class ChromaAdapter implements VectorStoreAdapter {
|
|
|
126
126
|
this.collection = null;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
async deleteByDocumentIds(documentIds: string[]): Promise<void> {
|
|
130
|
+
if (documentIds.length === 0) return;
|
|
131
|
+
try {
|
|
132
|
+
const collection = await this.getCollection();
|
|
133
|
+
await collection.delete({ where: { documentId: { $in: documentIds } } });
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error('Failed to delete vectors by documentIds in Chroma:', err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
129
139
|
// ── Upsert ─────────────────────────────────────────────────────────────────
|
|
130
140
|
|
|
131
141
|
async upsert(records: VectorRecord[]): Promise<void> {
|
|
@@ -145,7 +155,6 @@ export class ChromaAdapter implements VectorStoreAdapter {
|
|
|
145
155
|
metadatas.push(this.buildMetadata(r));
|
|
146
156
|
}
|
|
147
157
|
|
|
148
|
-
// Chroma has a max batch size of ~5000 for upsert, process in chunks
|
|
149
158
|
const BATCH = 4096;
|
|
150
159
|
for (let i = 0; i < ids.length; i += BATCH) {
|
|
151
160
|
await collection.upsert({
|
|
@@ -169,7 +178,7 @@ export class ChromaAdapter implements VectorStoreAdapter {
|
|
|
169
178
|
if (r.metadata) {
|
|
170
179
|
for (const [k, v] of Object.entries(r.metadata)) {
|
|
171
180
|
if (v === null || v === undefined) continue;
|
|
172
|
-
if (typeof v ===
|
|
181
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
173
182
|
meta[k] = v;
|
|
174
183
|
} else {
|
|
175
184
|
meta[k] = JSON.stringify(v);
|
|
@@ -192,14 +201,10 @@ export class ChromaAdapter implements VectorStoreAdapter {
|
|
|
192
201
|
|
|
193
202
|
// ── Query ──────────────────────────────────────────────────────────────────
|
|
194
203
|
|
|
195
|
-
async query(
|
|
196
|
-
vector: number[],
|
|
197
|
-
topK: number,
|
|
198
|
-
queryText?: string,
|
|
199
|
-
): Promise<QueryHit[]> {
|
|
204
|
+
async query(vector: number[], topK: number, queryText?: string): Promise<QueryHit[]> {
|
|
200
205
|
const collection = await this.getCollection();
|
|
201
|
-
const isSemantic = this.config.indexType ===
|
|
202
|
-
const isLexical = this.config.indexType ===
|
|
206
|
+
const isSemantic = this.config.indexType === 'semantic';
|
|
207
|
+
const isLexical = this.config.indexType === 'lexical';
|
|
203
208
|
|
|
204
209
|
// Semantic only
|
|
205
210
|
if (isSemantic || !queryText?.trim()) {
|
|
@@ -226,15 +231,11 @@ export class ChromaAdapter implements VectorStoreAdapter {
|
|
|
226
231
|
return denseHits;
|
|
227
232
|
}
|
|
228
233
|
|
|
229
|
-
private async denseQuery(
|
|
230
|
-
collection: any,
|
|
231
|
-
vector: number[],
|
|
232
|
-
topK: number,
|
|
233
|
-
): Promise<QueryHit[]> {
|
|
234
|
+
private async denseQuery(collection: any, vector: number[], topK: number): Promise<QueryHit[]> {
|
|
234
235
|
const result = await collection.query({
|
|
235
236
|
queryEmbeddings: [vector],
|
|
236
237
|
nResults: topK,
|
|
237
|
-
include: [
|
|
238
|
+
include: ['documents', 'metadatas', 'distances'],
|
|
238
239
|
});
|
|
239
240
|
|
|
240
241
|
return this.parseQueryResult(result);
|
|
@@ -245,11 +246,10 @@ export class ChromaAdapter implements VectorStoreAdapter {
|
|
|
245
246
|
queryText: string,
|
|
246
247
|
topK: number,
|
|
247
248
|
): Promise<QueryHit[]> {
|
|
248
|
-
// Chroma supports substring filtering natively via get() with whereDocument
|
|
249
249
|
const result = await collection.get({
|
|
250
|
-
whereDocument: {
|
|
250
|
+
whereDocument: { $contains: queryText },
|
|
251
251
|
limit: topK,
|
|
252
|
-
include: [
|
|
252
|
+
include: ['documents', 'metadatas'],
|
|
253
253
|
});
|
|
254
254
|
|
|
255
255
|
return this.parseGetResult(result);
|
|
@@ -266,24 +266,16 @@ export class ChromaAdapter implements VectorStoreAdapter {
|
|
|
266
266
|
const meta = (metadatas[i] ?? {}) as Record<string, any>;
|
|
267
267
|
const score = 1.0; // Exact/lexical matches get max base score before RRF
|
|
268
268
|
|
|
269
|
-
const {
|
|
270
|
-
title,
|
|
271
|
-
url,
|
|
272
|
-
source,
|
|
273
|
-
documentId,
|
|
274
|
-
chunkIndex,
|
|
275
|
-
...customMetadata
|
|
276
|
-
} = meta;
|
|
269
|
+
const { title, url, source, documentId, chunkIndex, ...customMetadata } = meta;
|
|
277
270
|
|
|
278
271
|
return {
|
|
279
272
|
id,
|
|
280
273
|
score,
|
|
281
|
-
text: (documents[i] as string) ??
|
|
282
|
-
title: (title as string) ??
|
|
274
|
+
text: (documents[i] as string) ?? '',
|
|
275
|
+
title: (title as string) ?? 'Untitled',
|
|
283
276
|
url: (url as string) || undefined,
|
|
284
|
-
documentId: (documentId as string) ??
|
|
285
|
-
metadata:
|
|
286
|
-
Object.keys(customMetadata).length > 0 ? customMetadata : undefined,
|
|
277
|
+
documentId: (documentId as string) ?? '',
|
|
278
|
+
metadata: Object.keys(customMetadata).length > 0 ? customMetadata : undefined,
|
|
287
279
|
};
|
|
288
280
|
});
|
|
289
281
|
}
|
|
@@ -299,28 +291,19 @@ export class ChromaAdapter implements VectorStoreAdapter {
|
|
|
299
291
|
return ids.map((id, i) => {
|
|
300
292
|
const meta = (metadatas[i] ?? {}) as Record<string, any>;
|
|
301
293
|
const distance = distances[i] ?? 0;
|
|
302
|
-
// Chroma cosine distance: 0 = identical, 2 = opposite
|
|
303
294
|
// Convert to a similarity score
|
|
304
295
|
const score = 1 / (1 + distance);
|
|
305
296
|
|
|
306
|
-
const {
|
|
307
|
-
title,
|
|
308
|
-
url,
|
|
309
|
-
source,
|
|
310
|
-
documentId,
|
|
311
|
-
chunkIndex,
|
|
312
|
-
...customMetadata
|
|
313
|
-
} = meta;
|
|
297
|
+
const { title, url, source, documentId, chunkIndex, ...customMetadata } = meta;
|
|
314
298
|
|
|
315
299
|
return {
|
|
316
300
|
id,
|
|
317
301
|
score,
|
|
318
|
-
text: (documents[i] as string) ??
|
|
319
|
-
title: (title as string) ??
|
|
302
|
+
text: (documents[i] as string) ?? '',
|
|
303
|
+
title: (title as string) ?? 'Untitled',
|
|
320
304
|
url: (url as string) || undefined,
|
|
321
|
-
documentId: (documentId as string) ??
|
|
322
|
-
metadata:
|
|
323
|
-
Object.keys(customMetadata).length > 0 ? customMetadata : undefined,
|
|
305
|
+
documentId: (documentId as string) ?? '',
|
|
306
|
+
metadata: Object.keys(customMetadata).length > 0 ? customMetadata : undefined,
|
|
324
307
|
};
|
|
325
308
|
});
|
|
326
309
|
}
|
|
@@ -339,29 +322,26 @@ export class ChromaAdapter implements VectorStoreAdapter {
|
|
|
339
322
|
try {
|
|
340
323
|
await client.heartbeat();
|
|
341
324
|
} catch (err: any) {
|
|
342
|
-
const msg: string = err.message ??
|
|
325
|
+
const msg: string = err.message ?? '';
|
|
343
326
|
|
|
344
327
|
if (
|
|
345
|
-
msg.includes(
|
|
346
|
-
msg.includes(
|
|
347
|
-
msg.includes(
|
|
348
|
-
msg.includes(
|
|
328
|
+
msg.includes('401') ||
|
|
329
|
+
msg.includes('Unauthorized') ||
|
|
330
|
+
msg.includes('403') ||
|
|
331
|
+
msg.includes('Forbidden')
|
|
349
332
|
) {
|
|
350
|
-
throw new Error(
|
|
351
|
-
"Invalid Chroma credentials. Please check your auth token or API key.",
|
|
352
|
-
);
|
|
333
|
+
throw new Error('Invalid Chroma credentials. Please check your auth token or API key.');
|
|
353
334
|
}
|
|
354
335
|
|
|
355
|
-
if (msg.includes(
|
|
336
|
+
if (msg.includes('ECONNREFUSED') || msg.includes('fetch failed')) {
|
|
356
337
|
throw new Error(
|
|
357
|
-
|
|
338
|
+
'Could not reach the Chroma server. Is it running and accessible at the configured URL?',
|
|
358
339
|
);
|
|
359
340
|
}
|
|
360
341
|
|
|
361
342
|
throw new Error(`Failed to reach Chroma server: ${msg}`);
|
|
362
343
|
}
|
|
363
344
|
|
|
364
|
-
// Verify we can access the specified tenant/database by listing collections
|
|
365
345
|
try {
|
|
366
346
|
await client.listCollections();
|
|
367
347
|
} catch (err: any) {
|
|
@@ -374,11 +354,7 @@ export class ChromaAdapter implements VectorStoreAdapter {
|
|
|
374
354
|
|
|
375
355
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
376
356
|
|
|
377
|
-
function rrfMerge(
|
|
378
|
-
denseHits: QueryHit[],
|
|
379
|
-
sparseHits: QueryHit[],
|
|
380
|
-
topK: number,
|
|
381
|
-
): QueryHit[] {
|
|
357
|
+
function rrfMerge(denseHits: QueryHit[], sparseHits: QueryHit[], topK: number): QueryHit[] {
|
|
382
358
|
const scores = new Map<string, { hit: QueryHit; score: number }>();
|
|
383
359
|
|
|
384
360
|
const addList = (hits: QueryHit[]) =>
|
package/src/adapters/lancedb.ts
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import fs from
|
|
3
|
-
import
|
|
4
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import type { QueryHit, VectorRecord, VectorStoreAdapter } from './base';
|
|
4
|
+
|
|
5
|
+
type LanceDBModule = typeof import('@lancedb/lancedb');
|
|
6
|
+
type LanceConnection = Awaited<ReturnType<LanceDBModule['connect']>>;
|
|
7
|
+
type LanceTable = Awaited<ReturnType<LanceConnection['openTable']>>;
|
|
8
|
+
|
|
9
|
+
let lancedbModule: LanceDBModule | null = null;
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
|
-
* LanceDB
|
|
12
|
+
* Keep LanceDB outside Turbopack's server bundle. Its native binding resolves
|
|
13
|
+
* platform-specific optional packages at runtime, and static bundling turns
|
|
14
|
+
* the package name into a non-resolvable hashed external module.
|
|
15
|
+
*/
|
|
16
|
+
async function getLanceDB(): Promise<LanceDBModule> {
|
|
17
|
+
if (!lancedbModule) {
|
|
18
|
+
lancedbModule = await import(/* turbopackIgnore: true */ '@lancedb/lancedb');
|
|
19
|
+
}
|
|
20
|
+
return lancedbModule;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* LanceDB adapter — embedded/on-disk for local dev, LanceDB Cloud, or an
|
|
25
|
+
* S3-compatible object store (including Cloudflare R2).
|
|
8
26
|
*
|
|
9
27
|
* The table schema is inferred from the first batch of rows we add, so `init`
|
|
10
28
|
* is a no-op connect; the table is (re)created lazily on `reset`/first upsert.
|
|
@@ -15,6 +33,11 @@ interface LanceConfig {
|
|
|
15
33
|
dbPath?: string;
|
|
16
34
|
uri?: string;
|
|
17
35
|
apiKey?: string;
|
|
36
|
+
s3Uri?: string;
|
|
37
|
+
s3Endpoint?: string;
|
|
38
|
+
s3Region?: string;
|
|
39
|
+
s3AccessKeyId?: string;
|
|
40
|
+
s3SecretAccessKey?: string;
|
|
18
41
|
tableName?: string;
|
|
19
42
|
}
|
|
20
43
|
|
|
@@ -31,39 +54,55 @@ interface LanceRow extends Record<string, unknown> {
|
|
|
31
54
|
}
|
|
32
55
|
|
|
33
56
|
export class LanceDBAdapter implements VectorStoreAdapter {
|
|
34
|
-
private conn:
|
|
35
|
-
private table:
|
|
57
|
+
private conn: LanceConnection | null = null;
|
|
58
|
+
private table: LanceTable | null = null;
|
|
36
59
|
private readonly tableName: string;
|
|
37
60
|
|
|
38
61
|
constructor(private readonly config: LanceConfig) {
|
|
39
|
-
this.tableName = config.tableName?.trim() ||
|
|
62
|
+
this.tableName = config.tableName?.trim() || 'documents';
|
|
40
63
|
}
|
|
41
64
|
|
|
42
|
-
private async connect(): Promise<
|
|
65
|
+
private async connect(): Promise<LanceConnection> {
|
|
43
66
|
if (this.conn) return this.conn;
|
|
44
|
-
|
|
67
|
+
const lancedb = await getLanceDB();
|
|
68
|
+
if (this.config.mode === 'cloud') {
|
|
45
69
|
if (!this.config.uri || !this.config.apiKey) {
|
|
46
|
-
throw new Error(
|
|
70
|
+
throw new Error('LanceDB Cloud requires both a URI and an API key.');
|
|
47
71
|
}
|
|
48
|
-
if (!this.config.uri.startsWith(
|
|
49
|
-
throw new Error(
|
|
50
|
-
"LanceDB Cloud URI must start with 'db://' (e.g. db://my-database).",
|
|
51
|
-
);
|
|
72
|
+
if (!this.config.uri.startsWith('db://')) {
|
|
73
|
+
throw new Error("LanceDB Cloud URI must start with 'db://' (e.g. db://my-database).");
|
|
52
74
|
}
|
|
53
75
|
this.conn = await lancedb.connect(this.config.uri, {
|
|
54
76
|
apiKey: this.config.apiKey,
|
|
55
77
|
});
|
|
78
|
+
} else if (this.config.mode === 's3') {
|
|
79
|
+
if (!this.config.s3Uri || !this.config.s3AccessKeyId || !this.config.s3SecretAccessKey) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
'S3-compatible LanceDB requires a URI, access key ID, and secret access key.',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (!this.config.s3Uri.startsWith('s3://')) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"S3-compatible LanceDB URI must start with 's3://' (e.g. s3://my-bucket/lancedb).",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
this.conn = await lancedb.connect(this.config.s3Uri, {
|
|
90
|
+
storageOptions: {
|
|
91
|
+
aws_access_key_id: this.config.s3AccessKeyId,
|
|
92
|
+
aws_secret_access_key: this.config.s3SecretAccessKey,
|
|
93
|
+
...(this.config.s3Endpoint ? { aws_endpoint: this.config.s3Endpoint } : {}),
|
|
94
|
+
...(this.config.s3Region ? { aws_region: this.config.s3Region } : {}),
|
|
95
|
+
},
|
|
96
|
+
});
|
|
56
97
|
} else {
|
|
57
|
-
const dir = this.config.dbPath?.trim() ||
|
|
98
|
+
const dir = this.config.dbPath?.trim() || './.larkup/lancedb';
|
|
58
99
|
let abs = path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir);
|
|
59
100
|
|
|
60
101
|
try {
|
|
61
102
|
await fs.mkdir(abs, { recursive: true });
|
|
62
103
|
} catch (err: any) {
|
|
63
104
|
if (path.isAbsolute(dir)) {
|
|
64
|
-
|
|
65
|
-
// Automatically retry by prepending "." to make it relative to the workspace.
|
|
66
|
-
const fallbackDir = "." + dir;
|
|
105
|
+
const fallbackDir = '.' + dir;
|
|
67
106
|
abs = path.join(process.cwd(), fallbackDir);
|
|
68
107
|
try {
|
|
69
108
|
await fs.mkdir(abs, { recursive: true });
|
|
@@ -73,9 +112,7 @@ export class LanceDBAdapter implements VectorStoreAdapter {
|
|
|
73
112
|
);
|
|
74
113
|
}
|
|
75
114
|
} else {
|
|
76
|
-
throw new Error(
|
|
77
|
-
`Could not create directory for LanceDB at "${abs}": ${err.message}`,
|
|
78
|
-
);
|
|
115
|
+
throw new Error(`Could not create directory for LanceDB at "${abs}": ${err.message}`);
|
|
79
116
|
}
|
|
80
117
|
}
|
|
81
118
|
|
|
@@ -101,13 +138,23 @@ export class LanceDBAdapter implements VectorStoreAdapter {
|
|
|
101
138
|
this.table = null;
|
|
102
139
|
}
|
|
103
140
|
|
|
141
|
+
async deleteByDocumentIds(documentIds: string[]): Promise<void> {
|
|
142
|
+
if (documentIds.length === 0) return;
|
|
143
|
+
if (!this.table) {
|
|
144
|
+
await this.init();
|
|
145
|
+
if (!this.table) return;
|
|
146
|
+
}
|
|
147
|
+
const idsString = documentIds.map((id) => `'${id}'`).join(', ');
|
|
148
|
+
await this.table.delete(`documentId IN (${idsString})`);
|
|
149
|
+
}
|
|
150
|
+
|
|
104
151
|
private toRow(r: VectorRecord): LanceRow {
|
|
105
152
|
return {
|
|
106
153
|
id: r.id,
|
|
107
154
|
vector: r.vector,
|
|
108
155
|
text: r.text,
|
|
109
156
|
title: r.title,
|
|
110
|
-
url: r.url ??
|
|
157
|
+
url: r.url ?? '',
|
|
111
158
|
source: r.source,
|
|
112
159
|
documentId: r.documentId,
|
|
113
160
|
chunkIndex: r.chunkIndex,
|
|
@@ -151,39 +198,35 @@ export class LanceDBAdapter implements VectorStoreAdapter {
|
|
|
151
198
|
await this.init();
|
|
152
199
|
if (!this.table) return [];
|
|
153
200
|
}
|
|
154
|
-
const rows = (await this.table
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
.toArray()) as Array<LanceRow & { _distance?: number }>;
|
|
201
|
+
const rows = (await this.table.search(vector).limit(topK).toArray()) as Array<
|
|
202
|
+
LanceRow & { _distance?: number }
|
|
203
|
+
>;
|
|
158
204
|
|
|
159
205
|
return rows.map((row) => ({
|
|
160
206
|
id: row.id as string,
|
|
161
|
-
|
|
162
|
-
score: typeof row._distance === "number" ? 1 / (1 + row._distance) : 0,
|
|
207
|
+
score: typeof row._distance === 'number' ? 1 / (1 + row._distance) : 0,
|
|
163
208
|
text: row.text as string,
|
|
164
209
|
title: row.title as string,
|
|
165
210
|
url: (row.url as string) || undefined,
|
|
166
211
|
documentId: row.documentId as string,
|
|
167
|
-
metadata:
|
|
168
|
-
typeof row.metadata === "string" ? JSON.parse(row.metadata) : undefined,
|
|
212
|
+
metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : undefined,
|
|
169
213
|
}));
|
|
170
214
|
}
|
|
171
215
|
|
|
172
216
|
async testConnection(dimensions: number): Promise<void> {
|
|
173
|
-
|
|
174
|
-
if (this.config.mode !== "cloud") {
|
|
217
|
+
if (this.config.mode !== 'cloud') {
|
|
175
218
|
try {
|
|
176
219
|
await this.connect();
|
|
177
220
|
return;
|
|
178
221
|
} catch (err: any) {
|
|
179
|
-
|
|
222
|
+
const label = this.config.mode === 's3' ? 'S3-compatible LanceDB' : 'local LanceDB';
|
|
223
|
+
throw new Error(`Failed to connect to ${label}: ${err.message}`);
|
|
180
224
|
}
|
|
181
225
|
}
|
|
182
226
|
|
|
183
227
|
// ── Cloud mode ───────────────────────────────────────────────────────────
|
|
184
|
-
// connect() is lazy (never contacts the server), so we call tableNames()
|
|
185
228
|
// to force a real authenticated HTTP round-trip.
|
|
186
|
-
let conn:
|
|
229
|
+
let conn: LanceConnection;
|
|
187
230
|
try {
|
|
188
231
|
conn = await this.connect();
|
|
189
232
|
} catch (err: any) {
|
|
@@ -193,22 +236,21 @@ export class LanceDBAdapter implements VectorStoreAdapter {
|
|
|
193
236
|
try {
|
|
194
237
|
await conn.tableNames();
|
|
195
238
|
} catch (err: any) {
|
|
196
|
-
const msg: string = err.message ??
|
|
239
|
+
const msg: string = err.message ?? '';
|
|
197
240
|
|
|
198
|
-
// The SDK puts the HTTP status INSIDE the message string (not as a
|
|
199
241
|
// property). Pattern: "Http error: ... 401 Unauthorized: Unauthorized"
|
|
200
242
|
if (
|
|
201
|
-
msg.includes(
|
|
202
|
-
msg.includes(
|
|
203
|
-
msg.includes(
|
|
204
|
-
msg.includes(
|
|
243
|
+
msg.includes('401') ||
|
|
244
|
+
msg.includes('Unauthorized') ||
|
|
245
|
+
msg.includes('403') ||
|
|
246
|
+
msg.includes('Forbidden')
|
|
205
247
|
) {
|
|
206
248
|
throw new Error(
|
|
207
|
-
|
|
249
|
+
'Invalid LanceDB Cloud API key. Please check your credentials in the LanceDB Cloud dashboard.',
|
|
208
250
|
);
|
|
209
251
|
}
|
|
210
252
|
|
|
211
|
-
if (msg.includes(
|
|
253
|
+
if (msg.includes('404') || msg.includes('Not Found')) {
|
|
212
254
|
throw new Error(
|
|
213
255
|
`LanceDB Cloud database not found. Please verify your URI (e.g. "db://my-database").`,
|
|
214
256
|
);
|