@mastra/lance 0.1.1-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.
@@ -0,0 +1,700 @@
1
+ import { connect, Index } from '@lancedb/lancedb';
2
+ import type { Connection, ConnectionOptions, CreateTableOptions, Table, TableLike } from '@lancedb/lancedb';
3
+
4
+ import type {
5
+ CreateIndexParams,
6
+ DeleteIndexParams,
7
+ DeleteVectorParams,
8
+ DescribeIndexParams,
9
+ IndexStats,
10
+ QueryResult,
11
+ QueryVectorParams,
12
+ UpdateVectorParams,
13
+ UpsertVectorParams,
14
+ } from '@mastra/core';
15
+
16
+ import { MastraVector } from '@mastra/core/vector';
17
+ import type { VectorFilter } from '@mastra/core/vector/filter';
18
+ import { LanceFilterTranslator } from './filter';
19
+ import type { IndexConfig } from './types';
20
+
21
+ interface LanceCreateIndexParams extends CreateIndexParams {
22
+ indexConfig?: LanceIndexConfig;
23
+ tableName?: string;
24
+ }
25
+
26
+ interface LanceIndexConfig extends IndexConfig {
27
+ numPartitions?: number;
28
+ numSubVectors?: number;
29
+ }
30
+
31
+ interface LanceUpsertVectorParams extends UpsertVectorParams {
32
+ tableName: string;
33
+ }
34
+
35
+ interface LanceQueryVectorParams extends QueryVectorParams {
36
+ tableName: string;
37
+ columns?: string[];
38
+ includeAllColumns?: boolean;
39
+ }
40
+
41
+ export class LanceVectorStore extends MastraVector {
42
+ private lanceClient!: Connection;
43
+
44
+ /**
45
+ * Creates a new instance of LanceVectorStore
46
+ * @param uri The URI to connect to LanceDB
47
+ * @param options connection options
48
+ *
49
+ * Usage:
50
+ *
51
+ * Connect to a local database
52
+ * ```ts
53
+ * const store = await LanceVectorStore.create('/path/to/db');
54
+ * ```
55
+ *
56
+ * Connect to a LanceDB cloud database
57
+ * ```ts
58
+ * const store = await LanceVectorStore.create('db://host:port');
59
+ * ```
60
+ *
61
+ * Connect to a cloud database
62
+ * ```ts
63
+ * const store = await LanceVectorStore.create('s3://bucket/db', { storageOptions: { timeout: '60s' } });
64
+ * ```
65
+ */
66
+ public static async create(uri: string, options?: ConnectionOptions): Promise<LanceVectorStore> {
67
+ const instance = new LanceVectorStore();
68
+ try {
69
+ instance.lanceClient = await connect(uri, options);
70
+ return instance;
71
+ } catch (e) {
72
+ throw new Error(`Failed to connect to LanceDB: ${e}`);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * @internal
78
+ * Private constructor to enforce using the create factory method
79
+ */
80
+ private constructor() {
81
+ super();
82
+ }
83
+
84
+ close() {
85
+ if (this.lanceClient) {
86
+ this.lanceClient.close();
87
+ }
88
+ }
89
+
90
+ async query({
91
+ tableName,
92
+ queryVector,
93
+ filter,
94
+ includeVector = false,
95
+ topK = 10,
96
+ columns = [],
97
+ includeAllColumns = false,
98
+ }: LanceQueryVectorParams): Promise<QueryResult[]> {
99
+ if (!this.lanceClient) {
100
+ throw new Error('LanceDB client not initialized. Use LanceVectorStore.create() to create an instance');
101
+ }
102
+
103
+ if (!tableName) {
104
+ throw new Error('tableName is required');
105
+ }
106
+
107
+ if (!queryVector) {
108
+ throw new Error('queryVector is required');
109
+ }
110
+
111
+ try {
112
+ // Open the table
113
+ const table = await this.lanceClient.openTable(tableName);
114
+
115
+ // Prepare the list of columns to select
116
+ const selectColumns = [...columns];
117
+ if (!selectColumns.includes('id')) {
118
+ selectColumns.push('id');
119
+ }
120
+
121
+ // Create the query builder
122
+ let query = table.search(queryVector);
123
+
124
+ // Add filter if provided
125
+ if (filter && Object.keys(filter).length > 0) {
126
+ const whereClause = this.filterTranslator(filter);
127
+ this.logger.debug(`Where clause generated: ${whereClause}`);
128
+ query = query.where(whereClause);
129
+ }
130
+
131
+ // Apply column selection and limit
132
+ if (!includeAllColumns && selectColumns.length > 0) {
133
+ query = query.select(selectColumns);
134
+ }
135
+ query = query.limit(topK);
136
+
137
+ // Execute the query
138
+ const results = await query.toArray();
139
+
140
+ return results.map(result => {
141
+ // Collect all metadata_ prefixed fields
142
+ const flatMetadata: Record<string, any> = {};
143
+
144
+ // Get all keys from the result object
145
+ Object.keys(result).forEach(key => {
146
+ // Skip reserved keys (id, score, and the vector column)
147
+ if (key !== 'id' && key !== 'score' && key !== 'vector' && key !== '_distance') {
148
+ if (key.startsWith('metadata_')) {
149
+ // Remove the prefix and add to flat metadata
150
+ const metadataKey = key.substring('metadata_'.length);
151
+ flatMetadata[metadataKey] = result[key];
152
+ }
153
+ }
154
+ });
155
+
156
+ // Reconstruct nested metadata object
157
+ const metadata = this.unflattenObject(flatMetadata);
158
+
159
+ return {
160
+ id: String(result.id || ''),
161
+ metadata,
162
+ vector:
163
+ includeVector && result.vector
164
+ ? Array.isArray(result.vector)
165
+ ? result.vector
166
+ : Array.from(result.vector as any[])
167
+ : undefined,
168
+ document: result.document,
169
+ score: result._distance,
170
+ };
171
+ });
172
+ } catch (error: any) {
173
+ throw new Error(`Failed to query vectors: ${error.message}`);
174
+ }
175
+ }
176
+
177
+ private filterTranslator(filter: VectorFilter): string {
178
+ // Add metadata_ prefix to filter keys if they don't already have it
179
+ const processFilterKeys = (filterObj: Record<string, any>): Record<string, any> => {
180
+ const result: Record<string, any> = {};
181
+
182
+ Object.entries(filterObj).forEach(([key, value]) => {
183
+ // Don't add prefix to logical operators
184
+ if (key === '$or' || key === '$and' || key === '$not' || key === '$in') {
185
+ // For logical operators, process their array contents
186
+ if (Array.isArray(value)) {
187
+ result[key] = value.map(item =>
188
+ typeof item === 'object' && item !== null ? processFilterKeys(item as Record<string, any>) : item,
189
+ );
190
+ } else {
191
+ result[key] = value;
192
+ }
193
+ }
194
+ // Don't add prefix if it already has metadata_ prefix
195
+ else if (key.startsWith('metadata_')) {
196
+ result[key] = value;
197
+ }
198
+ // Add metadata_ prefix to regular field keys
199
+ else {
200
+ // Convert dot notation to underscore notation for nested fields
201
+ if (key.includes('.')) {
202
+ const convertedKey = `metadata_${key.replace(/\./g, '_')}`;
203
+ result[convertedKey] = value;
204
+ } else {
205
+ result[`metadata_${key}`] = value;
206
+ }
207
+ }
208
+ });
209
+
210
+ return result;
211
+ };
212
+
213
+ const prefixedFilter = filter && typeof filter === 'object' ? processFilterKeys(filter as Record<string, any>) : {};
214
+
215
+ const translator = new LanceFilterTranslator();
216
+ return translator.translate(prefixedFilter);
217
+ }
218
+
219
+ async upsert({ tableName, vectors, metadata = [], ids = [] }: LanceUpsertVectorParams): Promise<string[]> {
220
+ if (!this.lanceClient) {
221
+ throw new Error('LanceDB client not initialized. Use LanceVectorStore.create() to create an instance');
222
+ }
223
+
224
+ if (!tableName) {
225
+ throw new Error('tableName is required');
226
+ }
227
+
228
+ if (!vectors || !Array.isArray(vectors) || vectors.length === 0) {
229
+ throw new Error('vectors array is required and must not be empty');
230
+ }
231
+
232
+ try {
233
+ const tables = await this.lanceClient.tableNames();
234
+ if (!tables.includes(tableName)) {
235
+ throw new Error(`Table ${tableName} does not exist`);
236
+ }
237
+
238
+ const table = await this.lanceClient.openTable(tableName);
239
+
240
+ // Generate IDs if not provided
241
+ const vectorIds = ids.length === vectors.length ? ids : vectors.map((_, i) => ids[i] || crypto.randomUUID());
242
+
243
+ // Create data with metadata fields expanded at the top level
244
+ const data = vectors.map((vector, i) => {
245
+ const id = String(vectorIds[i]);
246
+ const metadataItem = metadata[i] || {};
247
+
248
+ // Create the base object with id and vector
249
+ const rowData: Record<string, any> = {
250
+ id,
251
+ vector: vector,
252
+ };
253
+
254
+ // Flatten the metadata object and prefix all keys with 'metadata_'
255
+ if (Object.keys(metadataItem).length > 0) {
256
+ const flattenedMetadata = this.flattenObject(metadataItem, 'metadata');
257
+ // Add all flattened metadata properties to the row data object
258
+ Object.entries(flattenedMetadata).forEach(([key, value]) => {
259
+ rowData[key] = value;
260
+ });
261
+ }
262
+
263
+ return rowData;
264
+ });
265
+
266
+ await table.add(data, { mode: 'overwrite' });
267
+
268
+ return vectorIds;
269
+ } catch (error: any) {
270
+ throw new Error(`Failed to upsert vectors: ${error.message}`);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Flattens a nested object, creating new keys with underscores for nested properties.
276
+ * Example: { metadata: { text: 'test' } } → { metadata_text: 'test' }
277
+ */
278
+ private flattenObject(obj: Record<string, unknown>, prefix = ''): Record<string, unknown> {
279
+ return Object.keys(obj).reduce((acc: Record<string, unknown>, k: string) => {
280
+ const pre = prefix.length ? `${prefix}_` : '';
281
+ if (typeof obj[k] === 'object' && obj[k] !== null && !Array.isArray(obj[k])) {
282
+ Object.assign(acc, this.flattenObject(obj[k] as Record<string, unknown>, pre + k));
283
+ } else {
284
+ acc[pre + k] = obj[k];
285
+ }
286
+ return acc;
287
+ }, {});
288
+ }
289
+
290
+ async createTable(
291
+ tableName: string,
292
+ data: Record<string, unknown>[] | TableLike,
293
+ options?: Partial<CreateTableOptions>,
294
+ ): Promise<Table> {
295
+ if (!this.lanceClient) {
296
+ throw new Error('LanceDB client not initialized. Use LanceVectorStore.create() to create an instance');
297
+ }
298
+
299
+ try {
300
+ // Flatten nested objects if data is an array of records
301
+ if (Array.isArray(data)) {
302
+ data = data.map(record => this.flattenObject(record));
303
+ }
304
+
305
+ return await this.lanceClient.createTable(tableName, data, options);
306
+ } catch (error: any) {
307
+ throw new Error(`Failed to create table: ${error.message}`);
308
+ }
309
+ }
310
+
311
+ async listTables(): Promise<string[]> {
312
+ if (!this.lanceClient) {
313
+ throw new Error('LanceDB client not initialized. Use LanceVectorStore.create() to create an instance');
314
+ }
315
+ return await this.lanceClient.tableNames();
316
+ }
317
+
318
+ async getTableSchema(tableName: string): Promise<any> {
319
+ if (!this.lanceClient) {
320
+ throw new Error('LanceDB client not initialized. Use LanceVectorStore.create() to create an instance');
321
+ }
322
+ const table = await this.lanceClient.openTable(tableName);
323
+ return await table.schema();
324
+ }
325
+
326
+ /**
327
+ * indexName is actually a column name in a table in lanceDB
328
+ */
329
+ async createIndex({
330
+ tableName,
331
+ indexName,
332
+ dimension,
333
+ metric = 'cosine',
334
+ indexConfig = {},
335
+ }: LanceCreateIndexParams): Promise<void> {
336
+ if (!this.lanceClient) {
337
+ throw new Error('LanceDB client not initialized. Use LanceVectorStore.create() to create an instance');
338
+ }
339
+
340
+ try {
341
+ if (!tableName) {
342
+ throw new Error('tableName is required');
343
+ }
344
+
345
+ if (!indexName) {
346
+ throw new Error('indexName is required');
347
+ }
348
+
349
+ if (typeof dimension !== 'number' || dimension <= 0) {
350
+ throw new Error('dimension must be a positive number');
351
+ }
352
+
353
+ const tables = await this.lanceClient.tableNames();
354
+ if (!tables.includes(tableName)) {
355
+ throw new Error(
356
+ `Table ${tableName} does not exist. Please create the table first by calling createTable() method.`,
357
+ );
358
+ }
359
+
360
+ const table = await this.lanceClient.openTable(tableName);
361
+
362
+ // Convert metric to LanceDB metric
363
+ type LanceMetric = 'cosine' | 'l2' | 'dot';
364
+ let metricType: LanceMetric | undefined;
365
+ if (metric === 'euclidean') {
366
+ metricType = 'l2';
367
+ } else if (metric === 'dotproduct') {
368
+ metricType = 'dot';
369
+ } else if (metric === 'cosine') {
370
+ metricType = 'cosine';
371
+ }
372
+
373
+ if (indexConfig.type === 'ivfflat') {
374
+ await table.createIndex(indexName, {
375
+ config: Index.ivfPq({
376
+ numPartitions: indexConfig.numPartitions || 128,
377
+ numSubVectors: indexConfig.numSubVectors || 16,
378
+ distanceType: metricType,
379
+ }),
380
+ });
381
+ } else {
382
+ // Default to HNSW PQ index
383
+ this.logger.debug('Creating HNSW PQ index with config:', indexConfig);
384
+ await table.createIndex(indexName, {
385
+ config: Index.hnswPq({
386
+ m: indexConfig?.hnsw?.m || 16,
387
+ efConstruction: indexConfig?.hnsw?.efConstruction || 100,
388
+ distanceType: metricType,
389
+ }),
390
+ });
391
+ }
392
+ } catch (error: any) {
393
+ throw new Error(`Failed to create index: ${error.message}`);
394
+ }
395
+ }
396
+
397
+ async listIndexes(): Promise<string[]> {
398
+ if (!this.lanceClient) {
399
+ throw new Error('LanceDB client not initialized. Use LanceVectorStore.create() to create an instance');
400
+ }
401
+
402
+ try {
403
+ const tables = await this.lanceClient.tableNames();
404
+ const allIndices: string[] = [];
405
+
406
+ for (const tableName of tables) {
407
+ const table = await this.lanceClient.openTable(tableName);
408
+ const tableIndices = await table.listIndices();
409
+ allIndices.push(...tableIndices.map(index => index.name));
410
+ }
411
+
412
+ return allIndices;
413
+ } catch (error: any) {
414
+ throw new Error(`Failed to list indexes: ${error.message}`);
415
+ }
416
+ }
417
+
418
+ async describeIndex({ indexName }: DescribeIndexParams): Promise<IndexStats> {
419
+ if (!this.lanceClient) {
420
+ throw new Error('LanceDB client not initialized. Use LanceVectorStore.create() to create an instance');
421
+ }
422
+
423
+ if (!indexName) {
424
+ throw new Error('indexName is required');
425
+ }
426
+
427
+ try {
428
+ const tables = await this.lanceClient.tableNames();
429
+
430
+ for (const tableName of tables) {
431
+ this.logger.debug('Checking table:' + tableName);
432
+ const table = await this.lanceClient.openTable(tableName);
433
+ const tableIndices = await table.listIndices();
434
+ const foundIndex = tableIndices.find(index => index.name === indexName);
435
+
436
+ if (foundIndex) {
437
+ const stats = await table.indexStats(foundIndex.name);
438
+
439
+ if (!stats) {
440
+ throw new Error(`Index stats not found for index: ${indexName}`);
441
+ }
442
+
443
+ const schema = await table.schema();
444
+ const vectorCol = foundIndex.columns[0] || 'vector';
445
+
446
+ // Find the vector column in the schema
447
+ const vectorField = schema.fields.find(field => field.name === vectorCol);
448
+ const dimension = vectorField?.type?.['listSize'] || 0;
449
+
450
+ return {
451
+ dimension: dimension,
452
+ metric: stats.distanceType as 'cosine' | 'euclidean' | 'dotproduct' | undefined,
453
+ count: stats.numIndexedRows,
454
+ };
455
+ }
456
+ }
457
+
458
+ throw new Error(`IndexName: ${indexName} not found`);
459
+ } catch (error: any) {
460
+ throw new Error(`Failed to describe index: ${error.message}`);
461
+ }
462
+ }
463
+
464
+ async deleteIndex({ indexName }: DeleteIndexParams): Promise<void> {
465
+ if (!this.lanceClient) {
466
+ throw new Error('LanceDB client not initialized. Use LanceVectorStore.create() to create an instance');
467
+ }
468
+
469
+ if (!indexName) {
470
+ throw new Error('indexName is required');
471
+ }
472
+
473
+ try {
474
+ const tables = await this.lanceClient.tableNames();
475
+
476
+ for (const tableName of tables) {
477
+ const table = await this.lanceClient.openTable(tableName);
478
+ const tableIndices = await table.listIndices();
479
+ const foundIndex = tableIndices.find(index => index.name === indexName);
480
+
481
+ if (foundIndex) {
482
+ await table.dropIndex(indexName);
483
+ return;
484
+ }
485
+ }
486
+
487
+ throw new Error(`Index ${indexName} not found`);
488
+ } catch (error: any) {
489
+ throw new Error(`Failed to delete index: ${error.message}`);
490
+ }
491
+ }
492
+
493
+ /**
494
+ * Deletes all tables in the database
495
+ */
496
+ async deleteAllTables(): Promise<void> {
497
+ if (!this.lanceClient) {
498
+ throw new Error('LanceDB client not initialized. Use LanceVectorStore.create() to create an instance');
499
+ }
500
+
501
+ try {
502
+ await this.lanceClient.dropAllTables();
503
+ } catch (error: any) {
504
+ throw new Error(`Failed to delete tables: ${error.message}`);
505
+ }
506
+ }
507
+
508
+ async deleteTable(tableName: string): Promise<void> {
509
+ if (!this.lanceClient) {
510
+ throw new Error('LanceDB client not initialized. Use LanceVectorStore.create() to create an instance');
511
+ }
512
+
513
+ try {
514
+ await this.lanceClient.dropTable(tableName);
515
+ } catch (error: any) {
516
+ throw new Error(`Failed to delete tables: ${error.message}`);
517
+ }
518
+ }
519
+
520
+ async updateVector({ indexName, id, update }: UpdateVectorParams): Promise<void> {
521
+ if (!this.lanceClient) {
522
+ throw new Error('LanceDB client not initialized. Use LanceVectorStore.create() to create an instance');
523
+ }
524
+
525
+ if (!indexName) {
526
+ throw new Error('indexName is required');
527
+ }
528
+
529
+ if (!id) {
530
+ throw new Error('id is required');
531
+ }
532
+
533
+ try {
534
+ // In LanceDB, the indexName is actually a column name in a table
535
+ // We need to find which table has this column as an index
536
+ const tables = await this.lanceClient.tableNames();
537
+
538
+ for (const tableName of tables) {
539
+ this.logger.debug('Checking table:' + tableName);
540
+ const table = await this.lanceClient.openTable(tableName);
541
+
542
+ try {
543
+ const schema = await table.schema();
544
+ const hasColumn = schema.fields.some(field => field.name === indexName);
545
+
546
+ if (hasColumn) {
547
+ this.logger.debug(`Found column ${indexName} in table ${tableName}`);
548
+
549
+ // First, query the existing record to preserve values that aren't being updated
550
+ const existingRecord = await table
551
+ .query()
552
+ .where(`id = '${id}'`)
553
+ .select(schema.fields.map(field => field.name))
554
+ .limit(1)
555
+ .toArray();
556
+
557
+ if (existingRecord.length === 0) {
558
+ throw new Error(`Record with id '${id}' not found in table ${tableName}`);
559
+ }
560
+
561
+ // Create a clean data object for update
562
+ const rowData: Record<string, any> = {
563
+ id,
564
+ };
565
+
566
+ // Copy all existing field values except special fields
567
+ Object.entries(existingRecord[0]).forEach(([key, value]) => {
568
+ // Skip special fields
569
+ if (key !== 'id' && key !== '_distance') {
570
+ // Handle vector field specially to avoid nested properties
571
+ if (key === indexName) {
572
+ // If we're about to update this vector anyway, skip copying
573
+ if (!update.vector) {
574
+ // Ensure vector is a plain array
575
+ if (Array.isArray(value)) {
576
+ rowData[key] = [...value];
577
+ } else if (typeof value === 'object' && value !== null) {
578
+ // Handle vector objects by converting to array if needed
579
+ rowData[key] = Array.from(value as any[]);
580
+ } else {
581
+ rowData[key] = value;
582
+ }
583
+ }
584
+ } else {
585
+ rowData[key] = value;
586
+ }
587
+ }
588
+ });
589
+
590
+ // Apply the vector update if provided
591
+ if (update.vector) {
592
+ rowData[indexName] = update.vector;
593
+ }
594
+
595
+ // Apply metadata updates if provided
596
+ if (update.metadata) {
597
+ Object.entries(update.metadata).forEach(([key, value]) => {
598
+ rowData[`metadata_${key}`] = value;
599
+ });
600
+ }
601
+
602
+ // Update the record
603
+ await table.add([rowData], { mode: 'overwrite' });
604
+ return;
605
+ }
606
+ } catch (err) {
607
+ this.logger.error(`Error checking schema for table ${tableName}:` + err);
608
+ // Continue to the next table if there's an error
609
+ continue;
610
+ }
611
+ }
612
+
613
+ throw new Error(`No table found with column/index '${indexName}'`);
614
+ } catch (error: any) {
615
+ throw new Error(`Failed to update index: ${error.message}`);
616
+ }
617
+ }
618
+
619
+ async deleteVector({ indexName, id }: DeleteVectorParams): Promise<void> {
620
+ if (!this.lanceClient) {
621
+ throw new Error('LanceDB client not initialized. Use LanceVectorStore.create() to create an instance');
622
+ }
623
+
624
+ if (!indexName) {
625
+ throw new Error('indexName is required');
626
+ }
627
+
628
+ if (!id) {
629
+ throw new Error('id is required');
630
+ }
631
+
632
+ try {
633
+ // In LanceDB, the indexName is actually a column name in a table
634
+ // We need to find which table has this column as an index
635
+ const tables = await this.lanceClient.tableNames();
636
+
637
+ for (const tableName of tables) {
638
+ this.logger.debug('Checking table:' + tableName);
639
+ const table = await this.lanceClient.openTable(tableName);
640
+
641
+ try {
642
+ // Try to get the schema to check if this table has the column we're looking for
643
+ const schema = await table.schema();
644
+ const hasColumn = schema.fields.some(field => field.name === indexName);
645
+
646
+ if (hasColumn) {
647
+ this.logger.debug(`Found column ${indexName} in table ${tableName}`);
648
+ await table.delete(`id = '${id}'`);
649
+ return;
650
+ }
651
+ } catch (err) {
652
+ this.logger.error(`Error checking schema for table ${tableName}:` + err);
653
+ // Continue to the next table if there's an error
654
+ continue;
655
+ }
656
+ }
657
+
658
+ throw new Error(`No table found with column/index '${indexName}'`);
659
+ } catch (error: any) {
660
+ throw new Error(`Failed to delete index: ${error.message}`);
661
+ }
662
+ }
663
+
664
+ /**
665
+ * Converts a flattened object with keys using underscore notation back to a nested object.
666
+ * Example: { name: 'test', details_text: 'test' } → { name: 'test', details: { text: 'test' } }
667
+ */
668
+ private unflattenObject(obj: Record<string, any>): Record<string, any> {
669
+ const result: Record<string, any> = {};
670
+
671
+ Object.keys(obj).forEach(key => {
672
+ const value = obj[key];
673
+ const parts = key.split('_');
674
+
675
+ // Start with the result object
676
+ let current = result;
677
+
678
+ // Process all parts except the last one
679
+ for (let i = 0; i < parts.length - 1; i++) {
680
+ const part = parts[i];
681
+ // Skip empty parts
682
+ if (!part) continue;
683
+
684
+ // Create nested object if it doesn't exist
685
+ if (!current[part] || typeof current[part] !== 'object') {
686
+ current[part] = {};
687
+ }
688
+ current = current[part];
689
+ }
690
+
691
+ // Set the value at the last part
692
+ const lastPart = parts[parts.length - 1];
693
+ if (lastPart) {
694
+ current[lastPart] = value;
695
+ }
696
+ });
697
+
698
+ return result;
699
+ }
700
+ }
@@ -0,0 +1,16 @@
1
+ export type IndexType = 'ivfflat' | 'hnsw';
2
+
3
+ interface IVFConfig {
4
+ lists?: number;
5
+ }
6
+
7
+ interface HNSWConfig {
8
+ m?: number; // Max number of connections (default: 16)
9
+ efConstruction?: number; // Build-time complexity (default: 64)
10
+ }
11
+
12
+ export interface IndexConfig {
13
+ type?: IndexType;
14
+ ivf?: IVFConfig;
15
+ hnsw?: HNSWConfig;
16
+ }