@larkup/vector-stores 0.1.20 → 0.1.21
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 +7 -0
- package/package.json +2 -2
- package/src/adapters/base.ts +24 -22
- package/src/adapters/chroma.ts +49 -73
- package/src/adapters/lancedb.ts +64 -40
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@larkup/vector-stores",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@pinecone-database/pinecone": "^7.2.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
24
|
"chromadb": "^1.10.5",
|
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,11 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import fs from
|
|
3
|
-
import * as lancedb from
|
|
4
|
-
import type { QueryHit, VectorRecord, VectorStoreAdapter } from
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import * as lancedb from '@lancedb/lancedb';
|
|
4
|
+
import type { QueryHit, VectorRecord, VectorStoreAdapter } from './base';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* LanceDB adapter — embedded/on-disk for local dev,
|
|
7
|
+
* LanceDB adapter — embedded/on-disk for local dev, LanceDB Cloud, or an
|
|
8
|
+
* S3-compatible object store (including Cloudflare R2).
|
|
8
9
|
*
|
|
9
10
|
* The table schema is inferred from the first batch of rows we add, so `init`
|
|
10
11
|
* is a no-op connect; the table is (re)created lazily on `reset`/first upsert.
|
|
@@ -15,6 +16,11 @@ interface LanceConfig {
|
|
|
15
16
|
dbPath?: string;
|
|
16
17
|
uri?: string;
|
|
17
18
|
apiKey?: string;
|
|
19
|
+
s3Uri?: string;
|
|
20
|
+
s3Endpoint?: string;
|
|
21
|
+
s3Region?: string;
|
|
22
|
+
s3AccessKeyId?: string;
|
|
23
|
+
s3SecretAccessKey?: string;
|
|
18
24
|
tableName?: string;
|
|
19
25
|
}
|
|
20
26
|
|
|
@@ -36,34 +42,49 @@ export class LanceDBAdapter implements VectorStoreAdapter {
|
|
|
36
42
|
private readonly tableName: string;
|
|
37
43
|
|
|
38
44
|
constructor(private readonly config: LanceConfig) {
|
|
39
|
-
this.tableName = config.tableName?.trim() ||
|
|
45
|
+
this.tableName = config.tableName?.trim() || 'documents';
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
private async connect(): Promise<lancedb.Connection> {
|
|
43
49
|
if (this.conn) return this.conn;
|
|
44
|
-
if (this.config.mode ===
|
|
50
|
+
if (this.config.mode === 'cloud') {
|
|
45
51
|
if (!this.config.uri || !this.config.apiKey) {
|
|
46
|
-
throw new Error(
|
|
52
|
+
throw new Error('LanceDB Cloud requires both a URI and an API key.');
|
|
47
53
|
}
|
|
48
|
-
if (!this.config.uri.startsWith(
|
|
49
|
-
throw new Error(
|
|
50
|
-
"LanceDB Cloud URI must start with 'db://' (e.g. db://my-database).",
|
|
51
|
-
);
|
|
54
|
+
if (!this.config.uri.startsWith('db://')) {
|
|
55
|
+
throw new Error("LanceDB Cloud URI must start with 'db://' (e.g. db://my-database).");
|
|
52
56
|
}
|
|
53
57
|
this.conn = await lancedb.connect(this.config.uri, {
|
|
54
58
|
apiKey: this.config.apiKey,
|
|
55
59
|
});
|
|
60
|
+
} else if (this.config.mode === 's3') {
|
|
61
|
+
if (!this.config.s3Uri || !this.config.s3AccessKeyId || !this.config.s3SecretAccessKey) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
'S3-compatible LanceDB requires a URI, access key ID, and secret access key.',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (!this.config.s3Uri.startsWith('s3://')) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"S3-compatible LanceDB URI must start with 's3://' (e.g. s3://my-bucket/lancedb).",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
this.conn = await lancedb.connect(this.config.s3Uri, {
|
|
72
|
+
storageOptions: {
|
|
73
|
+
aws_access_key_id: this.config.s3AccessKeyId,
|
|
74
|
+
aws_secret_access_key: this.config.s3SecretAccessKey,
|
|
75
|
+
...(this.config.s3Endpoint ? { aws_endpoint: this.config.s3Endpoint } : {}),
|
|
76
|
+
...(this.config.s3Region ? { aws_region: this.config.s3Region } : {}),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
56
79
|
} else {
|
|
57
|
-
const dir = this.config.dbPath?.trim() ||
|
|
80
|
+
const dir = this.config.dbPath?.trim() || './.larkup/lancedb';
|
|
58
81
|
let abs = path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir);
|
|
59
82
|
|
|
60
83
|
try {
|
|
61
84
|
await fs.mkdir(abs, { recursive: true });
|
|
62
85
|
} catch (err: any) {
|
|
63
86
|
if (path.isAbsolute(dir)) {
|
|
64
|
-
|
|
65
|
-
// Automatically retry by prepending "." to make it relative to the workspace.
|
|
66
|
-
const fallbackDir = "." + dir;
|
|
87
|
+
const fallbackDir = '.' + dir;
|
|
67
88
|
abs = path.join(process.cwd(), fallbackDir);
|
|
68
89
|
try {
|
|
69
90
|
await fs.mkdir(abs, { recursive: true });
|
|
@@ -73,9 +94,7 @@ export class LanceDBAdapter implements VectorStoreAdapter {
|
|
|
73
94
|
);
|
|
74
95
|
}
|
|
75
96
|
} else {
|
|
76
|
-
throw new Error(
|
|
77
|
-
`Could not create directory for LanceDB at "${abs}": ${err.message}`,
|
|
78
|
-
);
|
|
97
|
+
throw new Error(`Could not create directory for LanceDB at "${abs}": ${err.message}`);
|
|
79
98
|
}
|
|
80
99
|
}
|
|
81
100
|
|
|
@@ -101,13 +120,23 @@ export class LanceDBAdapter implements VectorStoreAdapter {
|
|
|
101
120
|
this.table = null;
|
|
102
121
|
}
|
|
103
122
|
|
|
123
|
+
async deleteByDocumentIds(documentIds: string[]): Promise<void> {
|
|
124
|
+
if (documentIds.length === 0) return;
|
|
125
|
+
if (!this.table) {
|
|
126
|
+
await this.init();
|
|
127
|
+
if (!this.table) return;
|
|
128
|
+
}
|
|
129
|
+
const idsString = documentIds.map((id) => `'${id}'`).join(', ');
|
|
130
|
+
await this.table.delete(`documentId IN (${idsString})`);
|
|
131
|
+
}
|
|
132
|
+
|
|
104
133
|
private toRow(r: VectorRecord): LanceRow {
|
|
105
134
|
return {
|
|
106
135
|
id: r.id,
|
|
107
136
|
vector: r.vector,
|
|
108
137
|
text: r.text,
|
|
109
138
|
title: r.title,
|
|
110
|
-
url: r.url ??
|
|
139
|
+
url: r.url ?? '',
|
|
111
140
|
source: r.source,
|
|
112
141
|
documentId: r.documentId,
|
|
113
142
|
chunkIndex: r.chunkIndex,
|
|
@@ -151,37 +180,33 @@ export class LanceDBAdapter implements VectorStoreAdapter {
|
|
|
151
180
|
await this.init();
|
|
152
181
|
if (!this.table) return [];
|
|
153
182
|
}
|
|
154
|
-
const rows = (await this.table
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
.toArray()) as Array<LanceRow & { _distance?: number }>;
|
|
183
|
+
const rows = (await this.table.search(vector).limit(topK).toArray()) as Array<
|
|
184
|
+
LanceRow & { _distance?: number }
|
|
185
|
+
>;
|
|
158
186
|
|
|
159
187
|
return rows.map((row) => ({
|
|
160
188
|
id: row.id as string,
|
|
161
|
-
|
|
162
|
-
score: typeof row._distance === "number" ? 1 / (1 + row._distance) : 0,
|
|
189
|
+
score: typeof row._distance === 'number' ? 1 / (1 + row._distance) : 0,
|
|
163
190
|
text: row.text as string,
|
|
164
191
|
title: row.title as string,
|
|
165
192
|
url: (row.url as string) || undefined,
|
|
166
193
|
documentId: row.documentId as string,
|
|
167
|
-
metadata:
|
|
168
|
-
typeof row.metadata === "string" ? JSON.parse(row.metadata) : undefined,
|
|
194
|
+
metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : undefined,
|
|
169
195
|
}));
|
|
170
196
|
}
|
|
171
197
|
|
|
172
198
|
async testConnection(dimensions: number): Promise<void> {
|
|
173
|
-
|
|
174
|
-
if (this.config.mode !== "cloud") {
|
|
199
|
+
if (this.config.mode !== 'cloud') {
|
|
175
200
|
try {
|
|
176
201
|
await this.connect();
|
|
177
202
|
return;
|
|
178
203
|
} catch (err: any) {
|
|
179
|
-
|
|
204
|
+
const label = this.config.mode === 's3' ? 'S3-compatible LanceDB' : 'local LanceDB';
|
|
205
|
+
throw new Error(`Failed to connect to ${label}: ${err.message}`);
|
|
180
206
|
}
|
|
181
207
|
}
|
|
182
208
|
|
|
183
209
|
// ── Cloud mode ───────────────────────────────────────────────────────────
|
|
184
|
-
// connect() is lazy (never contacts the server), so we call tableNames()
|
|
185
210
|
// to force a real authenticated HTTP round-trip.
|
|
186
211
|
let conn: lancedb.Connection;
|
|
187
212
|
try {
|
|
@@ -193,22 +218,21 @@ export class LanceDBAdapter implements VectorStoreAdapter {
|
|
|
193
218
|
try {
|
|
194
219
|
await conn.tableNames();
|
|
195
220
|
} catch (err: any) {
|
|
196
|
-
const msg: string = err.message ??
|
|
221
|
+
const msg: string = err.message ?? '';
|
|
197
222
|
|
|
198
|
-
// The SDK puts the HTTP status INSIDE the message string (not as a
|
|
199
223
|
// property). Pattern: "Http error: ... 401 Unauthorized: Unauthorized"
|
|
200
224
|
if (
|
|
201
|
-
msg.includes(
|
|
202
|
-
msg.includes(
|
|
203
|
-
msg.includes(
|
|
204
|
-
msg.includes(
|
|
225
|
+
msg.includes('401') ||
|
|
226
|
+
msg.includes('Unauthorized') ||
|
|
227
|
+
msg.includes('403') ||
|
|
228
|
+
msg.includes('Forbidden')
|
|
205
229
|
) {
|
|
206
230
|
throw new Error(
|
|
207
|
-
|
|
231
|
+
'Invalid LanceDB Cloud API key. Please check your credentials in the LanceDB Cloud dashboard.',
|
|
208
232
|
);
|
|
209
233
|
}
|
|
210
234
|
|
|
211
|
-
if (msg.includes(
|
|
235
|
+
if (msg.includes('404') || msg.includes('Not Found')) {
|
|
212
236
|
throw new Error(
|
|
213
237
|
`LanceDB Cloud database not found. Please verify your URI (e.g. "db://my-database").`,
|
|
214
238
|
);
|