@mastra/lance 1.4.0 → 1.5.0-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.
@@ -3,7 +3,7 @@ name: mastra-lance
3
3
  description: Documentation for @mastra/lance. Use when working with @mastra/lance APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/lance"
6
- version: "1.4.0"
6
+ version: "1.5.0-alpha.0"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.4.0",
2
+ "version": "1.5.0-alpha.0",
3
3
  "package": "@mastra/lance",
4
4
  "exports": {},
5
5
  "modules": {}
@@ -49,15 +49,17 @@ const s3Store = await LanceVectorStore.create('s3://bucket/db', {
49
49
 
50
50
  #### `LanceIndexConfig`
51
51
 
52
- **type** (`'ivfflat' | 'hnsw'`): Index type (Default: `hnsw`)
52
+ **type** (`'ivfflat' | 'ivfpq' | 'hnsw'`): Index type (Default: `hnsw`)
53
53
 
54
- **type.ivfflat** (`ivfflat`): Clusters vectors into lists for approximate search.
54
+ **type.ivfflat** (`ivfflat`): Clusters vectors into partitions and stores full vectors. Searches compute exact distances only within the probed partitions, so closer vectors in other partitions can be missed.
55
+
56
+ **type.ivfpq** (`ivfpq`): IVF index with product quantization. Uses less memory than ivfflat with lower recall.
55
57
 
56
58
  **type.hnsw** (`hnsw`): Graph-based index offering fast search times and high recall.
57
59
 
58
60
  **numPartitions** (`number`): Number of partitions for IVF indexes (Default: `128`)
59
61
 
60
- **numSubVectors** (`number`): Number of sub-vectors for product quantization (Default: `16`)
62
+ **numSubVectors** (`number`): Number of sub-vectors for product quantization. Only applies to ivfpq (Default: `16`)
61
63
 
62
64
  **hnsw** (`HNSWConfig`): HNSW configuration
63
65
 
@@ -137,16 +139,60 @@ interface IndexStats {
137
139
  dimension: number
138
140
  count: number
139
141
  metric: 'cosine' | 'euclidean' | 'dotproduct'
140
- type: 'ivfflat' | 'hnsw'
141
- config: {
142
- m?: number
143
- efConstruction?: number
144
- numPartitions?: number
145
- numSubVectors?: number
146
- }
147
142
  }
148
143
  ```
149
144
 
145
+ ### `getIndexCoverage()`
146
+
147
+ **tableName** (`string`): Table to inspect. Defaults to indexName when omitted.
148
+
149
+ **indexName** (`string`): Used as the table name when tableName is omitted, matching upsert() and query().
150
+
151
+ Returns one entry per index on the table:
152
+
153
+ ```typescript
154
+ interface LanceIndexCoverage {
155
+ indexName: string
156
+ indexType: string
157
+ columns: string[]
158
+ numIndexedRows: number
159
+ numUnindexedRows: number
160
+ }
161
+ ```
162
+
163
+ ### `optimize()`
164
+
165
+ Runs LanceDB table maintenance. It compacts data files, adds rows written since the index was built to the existing index, and removes old table versions. Mastra never calls it automatically. Concurrent calls for the same table in one process share a single run.
166
+
167
+ **tableName** (`string`): Table to optimize. Defaults to indexName when omitted.
168
+
169
+ **indexName** (`string`): Used as the table name when tableName is omitted.
170
+
171
+ **cleanupOlderThan** (`Date`): Remove table versions older than this date. When omitted, LanceDB keeps versions from the last 7 days.
172
+
173
+ **deleteUnverified** (`boolean`): Also delete unverified files newer than 7 days. Only set this when no other process writes to the table, or the table can be corrupted. (Default: `false`)
174
+
175
+ Returns LanceDB `OptimizeStats` (compaction and pruning counts).
176
+
177
+ #### When to optimize
178
+
179
+ `upsert()` writes rows outside the existing vector index. `query()` still returns those rows by scanning them without the index, so results stay complete, but queries get slower as unindexed rows grow. LanceDB's `fastSearch()` skips unindexed rows entirely.
180
+
181
+ `optimize()` adds new rows to the existing index without retraining it. If your data distribution has changed a lot, rebuild the index with `deleteIndex()` and `createIndex()` instead.
182
+
183
+ LanceDB suggests optimizing after roughly 100,000 modified rows or 20 write operations. Use `getIndexCoverage()` to build your own policy:
184
+
185
+ ```typescript
186
+ const [coverage] = await store.getIndexCoverage({ indexName: 'docs' })
187
+ const total = coverage.numIndexedRows + coverage.numUnindexedRows
188
+
189
+ if (coverage.numUnindexedRows >= 1_000 && coverage.numUnindexedRows / total >= 0.1) {
190
+ await store.optimize({ indexName: 'docs' })
191
+ }
192
+ ```
193
+
194
+ Managed LanceDB Cloud and Enterprise maintain tables automatically, so you don't need to call `optimize()` there.
195
+
150
196
  ### `deleteIndex()`
151
197
 
152
198
  **indexName** (`string`): Name of the index to delete
package/dist/index.cjs CHANGED
@@ -2190,8 +2190,11 @@ var LanceFilterTranslator = class extends _mastra_core_vector_filter.BaseFilterT
2190
2190
  };
2191
2191
  //#endregion
2192
2192
  //#region src/vector/index.ts
2193
+ const LANCE_INDEX_FINGERPRINT_KEY = "mastra.index.fingerprint";
2194
+ const LANCE_INDEX_UUID_KEY = "mastra.index.uuid";
2193
2195
  var LanceVectorStore = class LanceVectorStore extends _mastra_core_vector.MastraVector {
2194
2196
  lanceClient;
2197
+ optimizeInFlight = /* @__PURE__ */ new Map();
2195
2198
  /**
2196
2199
  * Creates a new instance of LanceVectorStore
2197
2200
  * @param uri The URI to connect to LanceDB
@@ -2576,6 +2579,7 @@ var LanceVectorStore = class LanceVectorStore extends _mastra_core_vector.Mastra
2576
2579
  }
2577
2580
  }, err);
2578
2581
  }
2582
+ if (indexConfig.type === "ivfflat" && indexConfig.numSubVectors !== void 0) this.logger.warn(`numSubVectors is ignored for 'ivfflat' indexes. Use type 'ivfpq' for product-quantized IVF indexes.`);
2579
2583
  try {
2580
2584
  const tables = await this.lanceClient.tableNames();
2581
2585
  let table;
@@ -2601,19 +2605,51 @@ var LanceVectorStore = class LanceVectorStore extends _mastra_core_vector.Mastra
2601
2605
  this.logger.warn(`Table ${resolvedTableName} has ${rowCount} rows, which is below the 256 row minimum for index creation. Skipping index creation.`);
2602
2606
  return;
2603
2607
  }
2604
- if (indexConfig.type === "ivfflat") await table.createIndex(columnToIndex, { config: _lancedb_lancedb.Index.ivfPq({
2605
- numPartitions: indexConfig.numPartitions || 128,
2606
- numSubVectors: indexConfig.numSubVectors || 16,
2608
+ const numPartitions = indexConfig.numPartitions || 128;
2609
+ const buildParams = indexConfig.type === "ivfflat" ? { numPartitions } : indexConfig.type === "ivfpq" ? {
2610
+ numPartitions,
2611
+ numSubVectors: indexConfig.numSubVectors || 16
2612
+ } : {
2613
+ m: indexConfig?.hnsw?.m || 16,
2614
+ efConstruction: indexConfig?.hnsw?.efConstruction || 100
2615
+ };
2616
+ const fingerprintType = indexConfig.type === "ivfflat" ? "ivf_flat" : indexConfig.type === "ivfpq" ? "ivf_pq" : "hnsw_pq";
2617
+ const fingerprint = JSON.stringify({
2618
+ type: fingerprintType,
2619
+ metric: metricType,
2620
+ ...buildParams
2621
+ });
2622
+ const existingIndex = (await table.listIndices()).find((index) => index.columns.includes(columnToIndex));
2623
+ if (existingIndex?.indexUuid) {
2624
+ const field = (await table.schema()).fields.find((f) => f.name === columnToIndex);
2625
+ if (field?.metadata.get(LANCE_INDEX_FINGERPRINT_KEY) === fingerprint && field.metadata.get(LANCE_INDEX_UUID_KEY) === existingIndex.indexUuid) {
2626
+ this.logger.debug(`Index ${existingIndex.name} on ${resolvedTableName} is unchanged. Skipping rebuild.`);
2627
+ return;
2628
+ }
2629
+ }
2630
+ if (indexConfig.type === "ivfflat") await table.createIndex(columnToIndex, { config: _lancedb_lancedb.Index.ivfFlat({
2631
+ numPartitions,
2632
+ distanceType: metricType
2633
+ }) });
2634
+ else if (indexConfig.type === "ivfpq") await table.createIndex(columnToIndex, { config: _lancedb_lancedb.Index.ivfPq({
2635
+ ...buildParams,
2607
2636
  distanceType: metricType
2608
2637
  }) });
2609
2638
  else {
2610
2639
  this.logger.debug("Creating HNSW PQ index with config:", indexConfig);
2611
2640
  await table.createIndex(columnToIndex, { config: _lancedb_lancedb.Index.hnswPq({
2612
- m: indexConfig?.hnsw?.m || 16,
2613
- efConstruction: indexConfig?.hnsw?.efConstruction || 100,
2641
+ ...buildParams,
2614
2642
  distanceType: metricType
2615
2643
  }) });
2616
2644
  }
2645
+ const builtUuid = (await table.listIndices()).find((index) => index.columns.includes(columnToIndex))?.indexUuid;
2646
+ if (builtUuid && builtUuid !== existingIndex?.indexUuid) await table.updateFieldMetadata([{
2647
+ path: columnToIndex,
2648
+ metadata: {
2649
+ [LANCE_INDEX_FINGERPRINT_KEY]: fingerprint,
2650
+ [LANCE_INDEX_UUID_KEY]: builtUuid
2651
+ }
2652
+ }]);
2617
2653
  } catch (error) {
2618
2654
  throw new _mastra_core_error.MastraError({
2619
2655
  id: (0, _mastra_core_storage.createVectorErrorId)("LANCE", "CREATE_INDEX", "FAILED"),
@@ -2722,6 +2758,118 @@ var LanceVectorStore = class LanceVectorStore extends _mastra_core_vector.Mastra
2722
2758
  }, error);
2723
2759
  }
2724
2760
  }
2761
+ async openExistingTable({ tableName, indexName }, operation) {
2762
+ const resolvedTableName = tableName ?? indexName;
2763
+ let validationError;
2764
+ if (!this.lanceClient) validationError = "LanceDB client not initialized. Use LanceVectorStore.create() to create an instance";
2765
+ else if (!resolvedTableName) validationError = "tableName or indexName is required";
2766
+ if (validationError || !resolvedTableName || !this.lanceClient) throw new _mastra_core_error.MastraError({
2767
+ id: (0, _mastra_core_storage.createVectorErrorId)("LANCE", operation, "INVALID_ARGS"),
2768
+ text: validationError,
2769
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
2770
+ category: _mastra_core_error.ErrorCategory.USER,
2771
+ details: { tableName: resolvedTableName ?? "" }
2772
+ });
2773
+ let tableExists;
2774
+ try {
2775
+ tableExists = (await this.lanceClient.tableNames()).includes(resolvedTableName);
2776
+ if (tableExists) return {
2777
+ table: await this.lanceClient.openTable(resolvedTableName),
2778
+ resolvedTableName
2779
+ };
2780
+ } catch (error) {
2781
+ throw new _mastra_core_error.MastraError({
2782
+ id: (0, _mastra_core_storage.createVectorErrorId)("LANCE", operation, "FAILED"),
2783
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
2784
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
2785
+ details: { tableName: resolvedTableName }
2786
+ }, error);
2787
+ }
2788
+ throw new _mastra_core_error.MastraError({
2789
+ id: (0, _mastra_core_storage.createVectorErrorId)("LANCE", operation, "INVALID_ARGS"),
2790
+ text: `Table ${resolvedTableName} does not exist`,
2791
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
2792
+ category: _mastra_core_error.ErrorCategory.USER,
2793
+ details: { tableName: resolvedTableName }
2794
+ });
2795
+ }
2796
+ /**
2797
+ * Runs LanceDB table maintenance: compacts fragments, adds unindexed rows to existing indexes
2798
+ * (without retraining them), and prunes old table versions.
2799
+ *
2800
+ * Never runs implicitly. Concurrent calls for the same table share a single in-flight run.
2801
+ */
2802
+ async optimize(params) {
2803
+ const key = params.tableName ?? params.indexName;
2804
+ const inFlight = key ? this.optimizeInFlight.get(key) : void 0;
2805
+ if (inFlight) return inFlight;
2806
+ const run = (async () => {
2807
+ try {
2808
+ const { table, resolvedTableName } = await this.openExistingTable(params, "OPTIMIZE");
2809
+ try {
2810
+ const schema = await table.schema();
2811
+ const tracked = (await table.listIndices()).filter((index) => index.columns.some((column) => schema.fields.find((f) => f.name === column)?.metadata.get(LANCE_INDEX_UUID_KEY) === index.indexUuid));
2812
+ const stats = await table.optimize({
2813
+ ...params.cleanupOlderThan ? { cleanupOlderThan: params.cleanupOlderThan } : {},
2814
+ deleteUnverified: params.deleteUnverified ?? false
2815
+ });
2816
+ if (tracked.length) {
2817
+ const after = await table.listIndices();
2818
+ const updates = tracked.flatMap((before) => {
2819
+ const current = after.find((index) => index.name === before.name);
2820
+ return current?.indexUuid && current.indexUuid !== before.indexUuid ? before.columns.map((path) => ({
2821
+ path,
2822
+ metadata: { [LANCE_INDEX_UUID_KEY]: current.indexUuid }
2823
+ })) : [];
2824
+ });
2825
+ if (updates.length) await table.updateFieldMetadata(updates);
2826
+ }
2827
+ return stats;
2828
+ } catch (error) {
2829
+ throw new _mastra_core_error.MastraError({
2830
+ id: (0, _mastra_core_storage.createVectorErrorId)("LANCE", "OPTIMIZE", "FAILED"),
2831
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
2832
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
2833
+ details: { tableName: resolvedTableName }
2834
+ }, error);
2835
+ }
2836
+ } finally {
2837
+ if (key) this.optimizeInFlight.delete(key);
2838
+ }
2839
+ })();
2840
+ if (key) this.optimizeInFlight.set(key, run);
2841
+ return run;
2842
+ }
2843
+ /**
2844
+ * Reports how many rows each index on a table covers. Rows written after an index was built
2845
+ * stay unindexed until `optimize()` runs; use this to decide when to optimize.
2846
+ */
2847
+ async getIndexCoverage(params) {
2848
+ const { table, resolvedTableName } = await this.openExistingTable(params, "GET_INDEX_COVERAGE");
2849
+ try {
2850
+ const indices = await table.listIndices();
2851
+ const coverage = [];
2852
+ for (const index of indices) {
2853
+ const stats = await table.indexStats(index.name);
2854
+ if (!stats) continue;
2855
+ coverage.push({
2856
+ indexName: index.name,
2857
+ indexType: stats.indexType,
2858
+ columns: index.columns,
2859
+ numIndexedRows: stats.numIndexedRows,
2860
+ numUnindexedRows: stats.numUnindexedRows
2861
+ });
2862
+ }
2863
+ return coverage;
2864
+ } catch (error) {
2865
+ throw new _mastra_core_error.MastraError({
2866
+ id: (0, _mastra_core_storage.createVectorErrorId)("LANCE", "GET_INDEX_COVERAGE", "FAILED"),
2867
+ domain: _mastra_core_error.ErrorDomain.STORAGE,
2868
+ category: _mastra_core_error.ErrorCategory.THIRD_PARTY,
2869
+ details: { tableName: resolvedTableName }
2870
+ }, error);
2871
+ }
2872
+ }
2725
2873
  /**
2726
2874
  * Deletes all tables in the database
2727
2875
  */