@mastra/lance 1.4.0 → 1.5.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/dist/index.js CHANGED
@@ -2189,8 +2189,11 @@ var LanceFilterTranslator = class extends BaseFilterTranslator {
2189
2189
  };
2190
2190
  //#endregion
2191
2191
  //#region src/vector/index.ts
2192
+ const LANCE_INDEX_FINGERPRINT_KEY = "mastra.index.fingerprint";
2193
+ const LANCE_INDEX_UUID_KEY = "mastra.index.uuid";
2192
2194
  var LanceVectorStore = class LanceVectorStore extends MastraVector {
2193
2195
  lanceClient;
2196
+ optimizeInFlight = /* @__PURE__ */ new Map();
2194
2197
  /**
2195
2198
  * Creates a new instance of LanceVectorStore
2196
2199
  * @param uri The URI to connect to LanceDB
@@ -2575,6 +2578,7 @@ var LanceVectorStore = class LanceVectorStore extends MastraVector {
2575
2578
  }
2576
2579
  }, err);
2577
2580
  }
2581
+ 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.`);
2578
2582
  try {
2579
2583
  const tables = await this.lanceClient.tableNames();
2580
2584
  let table;
@@ -2600,19 +2604,51 @@ var LanceVectorStore = class LanceVectorStore extends MastraVector {
2600
2604
  this.logger.warn(`Table ${resolvedTableName} has ${rowCount} rows, which is below the 256 row minimum for index creation. Skipping index creation.`);
2601
2605
  return;
2602
2606
  }
2603
- if (indexConfig.type === "ivfflat") await table.createIndex(columnToIndex, { config: Index.ivfPq({
2604
- numPartitions: indexConfig.numPartitions || 128,
2605
- numSubVectors: indexConfig.numSubVectors || 16,
2607
+ const numPartitions = indexConfig.numPartitions || 128;
2608
+ const buildParams = indexConfig.type === "ivfflat" ? { numPartitions } : indexConfig.type === "ivfpq" ? {
2609
+ numPartitions,
2610
+ numSubVectors: indexConfig.numSubVectors || 16
2611
+ } : {
2612
+ m: indexConfig?.hnsw?.m || 16,
2613
+ efConstruction: indexConfig?.hnsw?.efConstruction || 100
2614
+ };
2615
+ const fingerprintType = indexConfig.type === "ivfflat" ? "ivf_flat" : indexConfig.type === "ivfpq" ? "ivf_pq" : "hnsw_pq";
2616
+ const fingerprint = JSON.stringify({
2617
+ type: fingerprintType,
2618
+ metric: metricType,
2619
+ ...buildParams
2620
+ });
2621
+ const existingIndex = (await table.listIndices()).find((index) => index.columns.includes(columnToIndex));
2622
+ if (existingIndex?.indexUuid) {
2623
+ const field = (await table.schema()).fields.find((f) => f.name === columnToIndex);
2624
+ if (field?.metadata.get(LANCE_INDEX_FINGERPRINT_KEY) === fingerprint && field.metadata.get(LANCE_INDEX_UUID_KEY) === existingIndex.indexUuid) {
2625
+ this.logger.debug(`Index ${existingIndex.name} on ${resolvedTableName} is unchanged. Skipping rebuild.`);
2626
+ return;
2627
+ }
2628
+ }
2629
+ if (indexConfig.type === "ivfflat") await table.createIndex(columnToIndex, { config: Index.ivfFlat({
2630
+ numPartitions,
2631
+ distanceType: metricType
2632
+ }) });
2633
+ else if (indexConfig.type === "ivfpq") await table.createIndex(columnToIndex, { config: Index.ivfPq({
2634
+ ...buildParams,
2606
2635
  distanceType: metricType
2607
2636
  }) });
2608
2637
  else {
2609
2638
  this.logger.debug("Creating HNSW PQ index with config:", indexConfig);
2610
2639
  await table.createIndex(columnToIndex, { config: Index.hnswPq({
2611
- m: indexConfig?.hnsw?.m || 16,
2612
- efConstruction: indexConfig?.hnsw?.efConstruction || 100,
2640
+ ...buildParams,
2613
2641
  distanceType: metricType
2614
2642
  }) });
2615
2643
  }
2644
+ const builtUuid = (await table.listIndices()).find((index) => index.columns.includes(columnToIndex))?.indexUuid;
2645
+ if (builtUuid && builtUuid !== existingIndex?.indexUuid) await table.updateFieldMetadata([{
2646
+ path: columnToIndex,
2647
+ metadata: {
2648
+ [LANCE_INDEX_FINGERPRINT_KEY]: fingerprint,
2649
+ [LANCE_INDEX_UUID_KEY]: builtUuid
2650
+ }
2651
+ }]);
2616
2652
  } catch (error) {
2617
2653
  throw new MastraError({
2618
2654
  id: createVectorErrorId("LANCE", "CREATE_INDEX", "FAILED"),
@@ -2721,6 +2757,118 @@ var LanceVectorStore = class LanceVectorStore extends MastraVector {
2721
2757
  }, error);
2722
2758
  }
2723
2759
  }
2760
+ async openExistingTable({ tableName, indexName }, operation) {
2761
+ const resolvedTableName = tableName ?? indexName;
2762
+ let validationError;
2763
+ if (!this.lanceClient) validationError = "LanceDB client not initialized. Use LanceVectorStore.create() to create an instance";
2764
+ else if (!resolvedTableName) validationError = "tableName or indexName is required";
2765
+ if (validationError || !resolvedTableName || !this.lanceClient) throw new MastraError({
2766
+ id: createVectorErrorId("LANCE", operation, "INVALID_ARGS"),
2767
+ text: validationError,
2768
+ domain: ErrorDomain.STORAGE,
2769
+ category: ErrorCategory.USER,
2770
+ details: { tableName: resolvedTableName ?? "" }
2771
+ });
2772
+ let tableExists;
2773
+ try {
2774
+ tableExists = (await this.lanceClient.tableNames()).includes(resolvedTableName);
2775
+ if (tableExists) return {
2776
+ table: await this.lanceClient.openTable(resolvedTableName),
2777
+ resolvedTableName
2778
+ };
2779
+ } catch (error) {
2780
+ throw new MastraError({
2781
+ id: createVectorErrorId("LANCE", operation, "FAILED"),
2782
+ domain: ErrorDomain.STORAGE,
2783
+ category: ErrorCategory.THIRD_PARTY,
2784
+ details: { tableName: resolvedTableName }
2785
+ }, error);
2786
+ }
2787
+ throw new MastraError({
2788
+ id: createVectorErrorId("LANCE", operation, "INVALID_ARGS"),
2789
+ text: `Table ${resolvedTableName} does not exist`,
2790
+ domain: ErrorDomain.STORAGE,
2791
+ category: ErrorCategory.USER,
2792
+ details: { tableName: resolvedTableName }
2793
+ });
2794
+ }
2795
+ /**
2796
+ * Runs LanceDB table maintenance: compacts fragments, adds unindexed rows to existing indexes
2797
+ * (without retraining them), and prunes old table versions.
2798
+ *
2799
+ * Never runs implicitly. Concurrent calls for the same table share a single in-flight run.
2800
+ */
2801
+ async optimize(params) {
2802
+ const key = params.tableName ?? params.indexName;
2803
+ const inFlight = key ? this.optimizeInFlight.get(key) : void 0;
2804
+ if (inFlight) return inFlight;
2805
+ const run = (async () => {
2806
+ try {
2807
+ const { table, resolvedTableName } = await this.openExistingTable(params, "OPTIMIZE");
2808
+ try {
2809
+ const schema = await table.schema();
2810
+ 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));
2811
+ const stats = await table.optimize({
2812
+ ...params.cleanupOlderThan ? { cleanupOlderThan: params.cleanupOlderThan } : {},
2813
+ deleteUnverified: params.deleteUnverified ?? false
2814
+ });
2815
+ if (tracked.length) {
2816
+ const after = await table.listIndices();
2817
+ const updates = tracked.flatMap((before) => {
2818
+ const current = after.find((index) => index.name === before.name);
2819
+ return current?.indexUuid && current.indexUuid !== before.indexUuid ? before.columns.map((path) => ({
2820
+ path,
2821
+ metadata: { [LANCE_INDEX_UUID_KEY]: current.indexUuid }
2822
+ })) : [];
2823
+ });
2824
+ if (updates.length) await table.updateFieldMetadata(updates);
2825
+ }
2826
+ return stats;
2827
+ } catch (error) {
2828
+ throw new MastraError({
2829
+ id: createVectorErrorId("LANCE", "OPTIMIZE", "FAILED"),
2830
+ domain: ErrorDomain.STORAGE,
2831
+ category: ErrorCategory.THIRD_PARTY,
2832
+ details: { tableName: resolvedTableName }
2833
+ }, error);
2834
+ }
2835
+ } finally {
2836
+ if (key) this.optimizeInFlight.delete(key);
2837
+ }
2838
+ })();
2839
+ if (key) this.optimizeInFlight.set(key, run);
2840
+ return run;
2841
+ }
2842
+ /**
2843
+ * Reports how many rows each index on a table covers. Rows written after an index was built
2844
+ * stay unindexed until `optimize()` runs; use this to decide when to optimize.
2845
+ */
2846
+ async getIndexCoverage(params) {
2847
+ const { table, resolvedTableName } = await this.openExistingTable(params, "GET_INDEX_COVERAGE");
2848
+ try {
2849
+ const indices = await table.listIndices();
2850
+ const coverage = [];
2851
+ for (const index of indices) {
2852
+ const stats = await table.indexStats(index.name);
2853
+ if (!stats) continue;
2854
+ coverage.push({
2855
+ indexName: index.name,
2856
+ indexType: stats.indexType,
2857
+ columns: index.columns,
2858
+ numIndexedRows: stats.numIndexedRows,
2859
+ numUnindexedRows: stats.numUnindexedRows
2860
+ });
2861
+ }
2862
+ return coverage;
2863
+ } catch (error) {
2864
+ throw new MastraError({
2865
+ id: createVectorErrorId("LANCE", "GET_INDEX_COVERAGE", "FAILED"),
2866
+ domain: ErrorDomain.STORAGE,
2867
+ category: ErrorCategory.THIRD_PARTY,
2868
+ details: { tableName: resolvedTableName }
2869
+ }, error);
2870
+ }
2871
+ }
2724
2872
  /**
2725
2873
  * Deletes all tables in the database
2726
2874
  */