@mastra/pg 0.1.4 → 0.1.5-alpha.1

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.
@@ -1,18 +1,18 @@
1
1
 
2
- > @mastra/pg@0.1.4 build /home/runner/work/mastra/mastra/stores/pg
2
+ > @mastra/pg@0.1.5-alpha.1 build /home/runner/work/mastra/mastra/stores/pg
3
3
  > tsup src/index.ts --format esm --experimental-dts --clean --treeshake
4
4
 
5
5
  CLI Building entry: src/index.ts
6
6
  CLI Using tsconfig: tsconfig.json
7
7
  CLI tsup v8.3.6
8
8
  TSC Build start
9
- TSC ⚡️ Build success in 8503ms
9
+ TSC ⚡️ Build success in 9745ms
10
10
  DTS Build start
11
11
  CLI Target: es2022
12
12
  Analysis will use the bundled TypeScript version 5.7.3
13
13
  Writing package typings: /home/runner/work/mastra/mastra/stores/pg/dist/_tsup-dts-rollup.d.ts
14
- DTS ⚡️ Build success in 6464ms
14
+ DTS ⚡️ Build success in 6324ms
15
15
  CLI Cleaning output folder
16
16
  ESM Build start
17
- ESM dist/index.js 30.26 KB
18
- ESM ⚡️ Build success in 584ms
17
+ ESM dist/index.js 32.67 KB
18
+ ESM ⚡️ Build success in 1005ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @mastra/pg
2
2
 
3
+ ## 0.1.5-alpha.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 82197f8: Update PG vector to allow for multiple index types
8
+ - Updated dependencies [6cb63e0]
9
+ - @mastra/core@0.4.2-alpha.1
10
+
11
+ ## 0.1.5-alpha.0
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [7fceae1]
16
+ - Updated dependencies [f626fbb]
17
+ - @mastra/core@0.4.2-alpha.0
18
+
3
19
  ## 0.1.4
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -56,7 +56,7 @@ const store = new PostgresStore({
56
56
  port: 5432,
57
57
  database: 'mastra',
58
58
  user: 'postgres',
59
- password: 'postgres'
59
+ password: 'postgres',
60
60
  });
61
61
 
62
62
  // Create a thread
@@ -64,17 +64,19 @@ await store.saveThread({
64
64
  id: 'thread-123',
65
65
  resourceId: 'resource-456',
66
66
  title: 'My Thread',
67
- metadata: { key: 'value' }
67
+ metadata: { key: 'value' },
68
68
  });
69
69
 
70
70
  // Add messages to thread
71
- await store.saveMessages([{
72
- id: 'msg-789',
73
- threadId: 'thread-123',
74
- role: 'user',
75
- type: 'text',
76
- content: [{ type: 'text', text: 'Hello' }]
77
- }]);
71
+ await store.saveMessages([
72
+ {
73
+ id: 'msg-789',
74
+ threadId: 'thread-123',
75
+ role: 'user',
76
+ type: 'text',
77
+ content: [{ type: 'text', text: 'Hello' }],
78
+ },
79
+ ]);
78
80
 
79
81
  // Query threads and messages
80
82
  const savedThread = await store.getThread('thread-123');
@@ -97,14 +99,18 @@ Connection pool settings:
97
99
  ## Features
98
100
 
99
101
  ### Vector Store Features
102
+
100
103
  - Vector similarity search with cosine, euclidean, and dot product metrics
101
104
  - Advanced metadata filtering with MongoDB-like query syntax
102
105
  - Minimum score threshold for queries
103
106
  - Automatic UUID generation for vectors
104
107
  - Table management (create, list, describe, delete, truncate)
105
- - Uses pgvector's IVFFLAT indexing with 100 lists
108
+ - Uses pgvector's IVFFLAT indexing with 100 lists by default
109
+ - Supports HNSW indexing with configurable parameters
110
+ - Supports flat indexing
106
111
 
107
112
  ### Storage Features
113
+
108
114
  - Thread and message storage with JSON support
109
115
  - Atomic transactions for data consistency
110
116
  - Efficient batch operations
@@ -19,8 +19,47 @@ import { StorageThreadType } from '@mastra/core/memory';
19
19
  import { TABLE_NAMES } from '@mastra/core/storage';
20
20
  import { WorkflowRunState } from '@mastra/core/workflows';
21
21
 
22
+ export declare const baseTestConfigs: {
23
+ smokeTests: {
24
+ dimension: number;
25
+ size: number;
26
+ k: number;
27
+ queryCount: number;
28
+ }[];
29
+ '64': {
30
+ dimension: number;
31
+ size: number;
32
+ k: number;
33
+ queryCount: number;
34
+ }[];
35
+ '384': {
36
+ dimension: number;
37
+ size: number;
38
+ k: number;
39
+ queryCount: number;
40
+ }[];
41
+ '1024': {
42
+ dimension: number;
43
+ size: number;
44
+ k: number;
45
+ queryCount: number;
46
+ }[];
47
+ stressTests: {
48
+ dimension: number;
49
+ size: number;
50
+ k: number;
51
+ queryCount: number;
52
+ }[];
53
+ };
54
+
22
55
  export declare function buildFilterQuery(filter: Filter, minScore: number): FilterResult;
23
56
 
57
+ export declare const calculateRecall: (actual: number[], expected: number[], k: number) => number;
58
+
59
+ export declare const calculateTimeout: (dimension: number, size: number, k: number) => number;
60
+
61
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
62
+
24
63
  export declare const FILTER_OPERATORS: Record<string, OperatorFn>;
25
64
 
26
65
  declare type FilterOperator = {
@@ -34,8 +73,60 @@ export declare interface FilterResult {
34
73
  values: any[];
35
74
  }
36
75
 
76
+ export declare const findNearestBruteForce: (query: number[], vectors: number[][], k: number) => number[];
77
+
78
+ export declare const formatTable: (data: any[], columns: string[]) => string;
79
+
80
+ export declare const generateClusteredVectors: (count: number, dim: number, numClusters?: number) => number[][];
81
+
82
+ export declare const generateRandomVectors: (count: number, dim: number) => number[][];
83
+
84
+ export declare const generateSkewedVectors: (count: number, dim: number) => number[][];
85
+
86
+ export declare const getHNSWConfig: (indexConfig: IndexConfig) => {
87
+ m: number;
88
+ efConstruction: number;
89
+ };
90
+
91
+ export declare function getIndexDescription({ type, hnsw, }: {
92
+ type: IndexType;
93
+ hnsw: {
94
+ m: number;
95
+ efConstruction: number;
96
+ };
97
+ }): string;
98
+
99
+ export declare const getListCount: (indexConfig: IndexConfig, size: number) => number | undefined;
100
+
101
+ export declare function getSearchEf(k: number, m: number): {
102
+ default: number;
103
+ lower: number;
104
+ higher: number;
105
+ };
106
+
107
+ export declare const groupBy: <T, K extends keyof T>(array: T[], key: K | ((item: T) => string), reducer?: (group: T[]) => any) => Record<string, any>;
108
+
37
109
  export declare const handleKey: (key: string) => string;
38
110
 
111
+ declare interface HNSWConfig {
112
+ m?: number;
113
+ efConstruction?: number;
114
+ }
115
+
116
+ export declare interface IndexConfig {
117
+ type?: IndexType;
118
+ ivf?: IVFConfig;
119
+ hnsw?: HNSWConfig;
120
+ }
121
+
122
+ export declare type IndexType = 'ivfflat' | 'hnsw' | 'flat';
123
+
124
+ declare interface IVFConfig {
125
+ lists?: number;
126
+ }
127
+
128
+ export declare function measureLatency<T>(fn: () => Promise<T>): Promise<[number, T]>;
129
+
39
130
  declare type OperatorFn = (key: string, paramIndex: number, value?: any) => FilterOperator;
40
131
 
41
132
  export declare type OperatorType = BasicOperator | NumericOperator | ArrayOperator | ElementOperator | LogicalOperator | '$contains' | Exclude<RegexOperator, '$options'>;
@@ -57,15 +148,34 @@ export declare class PGFilterTranslator extends BaseFilterTranslator {
57
148
  private translateRegexPattern;
58
149
  }
59
150
 
151
+ declare interface PGIndexStats extends IndexStats {
152
+ type: IndexType;
153
+ config: {
154
+ m?: number;
155
+ efConstruction?: number;
156
+ lists?: number;
157
+ probes?: number;
158
+ };
159
+ }
160
+ export { PGIndexStats }
161
+ export { PGIndexStats as PGIndexStats_alias_1 }
162
+
60
163
  declare class PgVector extends MastraVector {
61
164
  private pool;
165
+ private indexCache;
62
166
  constructor(connectionString: string);
63
167
  transformFilter(filter?: Filter): Filter;
64
- query(indexName: string, queryVector: number[], topK?: number, filter?: Filter, includeVector?: boolean, minScore?: number): Promise<QueryResult[]>;
168
+ getIndexInfo(indexName: string): Promise<PGIndexStats>;
169
+ query(indexName: string, queryVector: number[], topK?: number, filter?: Filter, includeVector?: boolean, minScore?: number, // Optional minimum score threshold
170
+ options?: {
171
+ ef?: number;
172
+ probes?: number;
173
+ }): Promise<QueryResult[]>;
65
174
  upsert(indexName: string, vectors: number[][], metadata?: Record<string, any>[], ids?: string[]): Promise<string[]>;
66
- createIndex(indexName: string, dimension: number, metric?: 'cosine' | 'euclidean' | 'dotproduct'): Promise<void>;
175
+ createIndex(indexName: string, dimension: number, metric?: 'cosine' | 'euclidean' | 'dotproduct', indexConfig?: IndexConfig, defineIndex?: boolean): Promise<void>;
176
+ defineIndex(indexName: string, metric: "cosine" | "euclidean" | "dotproduct" | undefined, indexConfig: IndexConfig): Promise<void>;
67
177
  listIndexes(): Promise<string[]>;
68
- describeIndex(indexName: string): Promise<IndexStats>;
178
+ describeIndex(indexName: string): Promise<PGIndexStats>;
69
179
  deleteIndex(indexName: string): Promise<void>;
70
180
  truncateIndex(indexName: string): Promise<void>;
71
181
  disconnect(): Promise<void>;
@@ -151,4 +261,40 @@ declare class PostgresStore extends MastraStorage {
151
261
  export { PostgresStore }
152
262
  export { PostgresStore as PostgresStore_alias_1 }
153
263
 
264
+ export declare interface TestConfig {
265
+ dimension: number;
266
+ size: number;
267
+ k: number;
268
+ queryCount: number;
269
+ }
270
+
271
+ export declare interface TestResult {
272
+ distribution: string;
273
+ dimension: number;
274
+ type: IndexType;
275
+ size: number;
276
+ k?: number;
277
+ metrics: {
278
+ recall?: number;
279
+ minRecall?: number;
280
+ maxRecall?: number;
281
+ latency?: {
282
+ p50: number;
283
+ p95: number;
284
+ lists?: number;
285
+ vectorsPerList?: number;
286
+ m?: number;
287
+ ef?: number;
288
+ };
289
+ clustering?: {
290
+ numLists?: number;
291
+ avgVectorsPerList?: number;
292
+ recommendedLists?: number;
293
+ distribution?: string;
294
+ };
295
+ };
296
+ }
297
+
298
+ export declare function warmupQuery(vectorDB: PgVector, indexName: string, dimension: number, k: number): Promise<void>;
299
+
154
300
  export { }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { PGIndexStats } from './_tsup-dts-rollup.js';
1
2
  export { PgVector } from './_tsup-dts-rollup.js';
2
3
  export { PostgresConfig } from './_tsup-dts-rollup.js';
3
4
  export { PostgresStore } from './_tsup-dts-rollup.js';
package/dist/index.js CHANGED
@@ -286,6 +286,7 @@ function buildFilterQuery(filter, minScore) {
286
286
  // src/vector/index.ts
287
287
  var PgVector = class extends MastraVector {
288
288
  pool;
289
+ indexCache = /* @__PURE__ */ new Map();
289
290
  constructor(connectionString) {
290
291
  super();
291
292
  const basePool = new pg.Pool({
@@ -310,12 +311,27 @@ var PgVector = class extends MastraVector {
310
311
  const translatedFilter = pgFilter.translate(filter ?? {});
311
312
  return translatedFilter;
312
313
  }
313
- async query(indexName, queryVector, topK = 10, filter, includeVector = false, minScore = 0) {
314
+ async getIndexInfo(indexName) {
315
+ if (!this.indexCache.has(indexName)) {
316
+ this.indexCache.set(indexName, await this.describeIndex(indexName));
317
+ }
318
+ return this.indexCache.get(indexName);
319
+ }
320
+ async query(indexName, queryVector, topK = 10, filter, includeVector = false, minScore = 0, options) {
314
321
  const client = await this.pool.connect();
315
322
  try {
316
323
  const vectorStr = `[${queryVector.join(",")}]`;
317
324
  const translatedFilter = this.transformFilter(filter);
318
325
  const { sql: filterQuery, values: filterValues } = buildFilterQuery(translatedFilter, minScore);
326
+ const indexInfo = await this.getIndexInfo(indexName);
327
+ if (indexInfo.type === "hnsw") {
328
+ const calculatedEf = options?.ef ?? Math.max(topK, (indexInfo?.config?.m ?? 16) * topK);
329
+ const searchEf = Math.min(1e3, Math.max(1, calculatedEf));
330
+ await client.query(`SET LOCAL hnsw.ef_search = ${searchEf}`);
331
+ }
332
+ if (indexInfo.type === "ivfflat" && options?.probes) {
333
+ await client.query(`SET LOCAL ivfflat.probes = ${options.probes}`);
334
+ }
319
335
  const query = `
320
336
  WITH vector_scores AS (
321
337
  SELECT
@@ -349,13 +365,13 @@ var PgVector = class extends MastraVector {
349
365
  const vectorIds = ids || vectors.map(() => crypto.randomUUID());
350
366
  for (let i = 0; i < vectors.length; i++) {
351
367
  const query = `
352
- INSERT INTO ${indexName} (vector_id, embedding, metadata)
353
- VALUES ($1, $2::vector, $3::jsonb)
354
- ON CONFLICT (vector_id)
355
- DO UPDATE SET
356
- embedding = $2::vector,
357
- metadata = $3::jsonb
358
- RETURNING embedding::text
368
+ INSERT INTO ${indexName} (vector_id, embedding, metadata)
369
+ VALUES ($1, $2::vector, $3::jsonb)
370
+ ON CONFLICT (vector_id)
371
+ DO UPDATE SET
372
+ embedding = $2::vector,
373
+ metadata = $3::jsonb
374
+ RETURNING embedding::text
359
375
  `;
360
376
  await client.query(query, [vectorIds[i], `[${vectors[i]?.join(",")}]`, JSON.stringify(metadata?.[i] || {})]);
361
377
  }
@@ -368,7 +384,7 @@ var PgVector = class extends MastraVector {
368
384
  client.release();
369
385
  }
370
386
  }
371
- async createIndex(indexName, dimension, metric = "cosine") {
387
+ async createIndex(indexName, dimension, metric = "cosine", indexConfig = {}, defineIndex = true) {
372
388
  const client = await this.pool.connect();
373
389
  try {
374
390
  if (!indexName.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
@@ -394,13 +410,9 @@ var PgVector = class extends MastraVector {
394
410
  metadata JSONB DEFAULT '{}'::jsonb
395
411
  );
396
412
  `);
397
- const indexMethod = metric === "cosine" ? "vector_cosine_ops" : metric === "euclidean" ? "vector_l2_ops" : "vector_ip_ops";
398
- await client.query(`
399
- CREATE INDEX IF NOT EXISTS ${indexName}_vector_idx
400
- ON public.${indexName}
401
- USING ivfflat (embedding ${indexMethod})
402
- WITH (lists = 100);
403
- `);
413
+ if (defineIndex) {
414
+ await this.defineIndex(indexName, metric, indexConfig);
415
+ }
404
416
  } catch (error) {
405
417
  console.error("Failed to create vector table:", error);
406
418
  throw error;
@@ -408,6 +420,46 @@ var PgVector = class extends MastraVector {
408
420
  client.release();
409
421
  }
410
422
  }
423
+ async defineIndex(indexName, metric = "cosine", indexConfig) {
424
+ const client = await this.pool.connect();
425
+ try {
426
+ await client.query(`DROP INDEX IF EXISTS ${indexName}_vector_idx`);
427
+ if (indexConfig.type === "flat") return;
428
+ const metricOp = metric === "cosine" ? "vector_cosine_ops" : metric === "euclidean" ? "vector_l2_ops" : "vector_ip_ops";
429
+ let indexSQL;
430
+ if (indexConfig.type === "hnsw") {
431
+ const m = indexConfig.hnsw?.m ?? 8;
432
+ const efConstruction = indexConfig.hnsw?.efConstruction ?? 32;
433
+ indexSQL = `
434
+ CREATE INDEX ${indexName}_vector_idx
435
+ ON ${indexName}
436
+ USING hnsw (embedding ${metricOp})
437
+ WITH (
438
+ m = ${m},
439
+ ef_construction = ${efConstruction}
440
+ )
441
+ `;
442
+ } else {
443
+ let lists;
444
+ if (indexConfig.ivf?.lists) {
445
+ lists = indexConfig.ivf.lists;
446
+ } else {
447
+ const size = (await client.query(`SELECT COUNT(*) FROM ${indexName}`)).rows[0].count;
448
+ lists = Math.max(100, Math.min(4e3, Math.floor(Math.sqrt(size) * 2)));
449
+ }
450
+ indexSQL = `
451
+ CREATE INDEX ${indexName}_vector_idx
452
+ ON ${indexName}
453
+ USING ivfflat (embedding ${metricOp})
454
+ WITH (lists = ${lists});
455
+ `;
456
+ }
457
+ await client.query(indexSQL);
458
+ this.indexCache.delete(indexName);
459
+ } finally {
460
+ client.release();
461
+ }
462
+ }
411
463
  async listIndexes() {
412
464
  const client = await this.pool.connect();
413
465
  try {
@@ -436,9 +488,10 @@ var PgVector = class extends MastraVector {
436
488
  SELECT COUNT(*) as count
437
489
  FROM ${indexName};
438
490
  `;
439
- const metricQuery = `
491
+ const indexQuery = `
440
492
  SELECT
441
493
  am.amname as index_method,
494
+ pg_get_indexdef(i.indexrelid) as index_def,
442
495
  opclass.opcname as operator_class
443
496
  FROM pg_index i
444
497
  JOIN pg_class c ON i.indexrelid = c.oid
@@ -446,26 +499,33 @@ var PgVector = class extends MastraVector {
446
499
  JOIN pg_opclass opclass ON i.indclass[0] = opclass.oid
447
500
  WHERE c.relname = '${indexName}_vector_idx';
448
501
  `;
449
- const [dimResult, countResult, metricResult] = await Promise.all([
502
+ const [dimResult, countResult, indexResult] = await Promise.all([
450
503
  client.query(dimensionQuery, [indexName]),
451
504
  client.query(countQuery),
452
- client.query(metricQuery)
505
+ client.query(indexQuery)
453
506
  ]);
454
- let metric = "cosine";
455
- if (metricResult.rows.length > 0) {
456
- const operatorClass = metricResult.rows[0].operator_class;
457
- if (operatorClass.includes("l2")) {
458
- metric = "euclidean";
459
- } else if (operatorClass.includes("ip")) {
460
- metric = "dotproduct";
461
- } else if (operatorClass.includes("cosine")) {
462
- metric = "cosine";
463
- }
507
+ const { index_method, index_def, operator_class } = indexResult.rows[0] || {
508
+ index_method: "flat",
509
+ index_def: "",
510
+ operator_class: "cosine"
511
+ };
512
+ const metric = operator_class.includes("l2") ? "euclidean" : operator_class.includes("ip") ? "dotproduct" : "cosine";
513
+ const config = {};
514
+ if (index_method === "hnsw") {
515
+ const m = index_def.match(/m\s*=\s*'?(\d+)'?/)?.[1];
516
+ const efConstruction = index_def.match(/ef_construction\s*=\s*'?(\d+)'?/)?.[1];
517
+ if (m) config.m = parseInt(m);
518
+ if (efConstruction) config.efConstruction = parseInt(efConstruction);
519
+ } else if (index_method === "ivfflat") {
520
+ const lists = index_def.match(/lists\s*=\s*'?(\d+)'?/)?.[1];
521
+ if (lists) config.lists = parseInt(lists);
464
522
  }
465
523
  return {
466
524
  dimension: dimResult.rows[0].dimension,
467
525
  count: parseInt(countResult.rows[0].count),
468
- metric
526
+ metric,
527
+ type: index_method,
528
+ config
469
529
  };
470
530
  } catch (e) {
471
531
  await client.query("ROLLBACK");
@@ -0,0 +1,21 @@
1
+ services:
2
+ db:
3
+ image: pgvector/pgvector:pg16
4
+ container_name: 'pg-perf-test-db'
5
+ ports:
6
+ - '5435:5432'
7
+ environment:
8
+ POSTGRES_USER: ${POSTGRES_USER:-postgres}
9
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
10
+ POSTGRES_DB: ${POSTGRES_DB:-mastra}
11
+ shm_size: 1gb
12
+ command:
13
+ - "postgres"
14
+ - "-c"
15
+ - "shared_buffers=512MB"
16
+ - "-c"
17
+ - "maintenance_work_mem=1024MB"
18
+ - "-c"
19
+ - "work_mem=512MB"
20
+ tmpfs:
21
+ - /var/lib/postgresql/data
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/pg",
3
- "version": "0.1.4",
3
+ "version": "0.1.5-alpha.1",
4
4
  "description": "Postgres provider for Mastra - includes both vector and db storage capabilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,7 +17,7 @@
17
17
  "dependencies": {
18
18
  "pg": "^8.13.1",
19
19
  "pg-promise": "^11.5.4",
20
- "@mastra/core": "^0.4.1"
20
+ "@mastra/core": "^0.4.2-alpha.1"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@microsoft/api-extractor": "^7.49.2",
@@ -32,7 +32,10 @@
32
32
  "build:watch": "pnpm build --watch",
33
33
  "pretest": "docker compose up -d && (for i in $(seq 1 30); do docker compose exec -T db pg_isready -U postgres && break || (sleep 1; [ $i -eq 30 ] && exit 1); done)",
34
34
  "test": "vitest run",
35
+ "pretest:perf": "docker compose -f docker-compose.perf.yaml up -d && (for i in $(seq 1 30); do docker compose -f docker-compose.perf.yaml exec -T db pg_isready -U postgres && break || (sleep 1; [ $i -eq 30 ] && exit 1); done)",
36
+ "test:perf": "NODE_OPTIONS='--max-old-space-size=16384' vitest run -c vitest.perf.config.ts",
35
37
  "posttest": "docker compose down -v",
38
+ "posttest:perf": "docker compose -f docker-compose.perf.yaml down -v",
36
39
  "pretest:watch": "docker compose up -d",
37
40
  "test:watch": "vitest watch",
38
41
  "posttest:watch": "docker compose down -v"