@mastra/pg 0.14.5 → 0.14.6-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,926 +0,0 @@
1
- import { ErrorCategory, ErrorDomain, MastraError } from '@mastra/core/error';
2
- import { parseSqlIdentifier } from '@mastra/core/utils';
3
- import { MastraVector } from '@mastra/core/vector';
4
- import type {
5
- IndexStats,
6
- QueryResult,
7
- QueryVectorParams,
8
- CreateIndexParams,
9
- UpsertVectorParams,
10
- DescribeIndexParams,
11
- DeleteIndexParams,
12
- DeleteVectorParams,
13
- UpdateVectorParams,
14
- } from '@mastra/core/vector';
15
- import { Mutex } from 'async-mutex';
16
- import pg from 'pg';
17
- import xxhash from 'xxhash-wasm';
18
-
19
- import { PGFilterTranslator } from './filter';
20
- import type { PGVectorFilter } from './filter';
21
- import { buildFilterQuery } from './sql-builder';
22
- import type { IndexConfig, IndexType } from './types';
23
-
24
- export interface PGIndexStats extends IndexStats {
25
- type: IndexType;
26
- config: {
27
- m?: number;
28
- efConstruction?: number;
29
- lists?: number;
30
- probes?: number;
31
- };
32
- }
33
-
34
- interface PgQueryVectorParams extends QueryVectorParams<PGVectorFilter> {
35
- minScore?: number;
36
- /**
37
- * HNSW search parameter. Controls the size of the dynamic candidate
38
- * list during search. Higher values improve accuracy at the cost of speed.
39
- */
40
- ef?: number;
41
- /**
42
- * IVFFlat probe parameter. Number of cells to visit during search.
43
- * Higher values improve accuracy at the cost of speed.
44
- */
45
- probes?: number;
46
- }
47
-
48
- interface PgCreateIndexParams extends CreateIndexParams {
49
- indexConfig?: IndexConfig;
50
- buildIndex?: boolean;
51
- }
52
-
53
- interface PgDefineIndexParams {
54
- indexName: string;
55
- metric: 'cosine' | 'euclidean' | 'dotproduct';
56
- indexConfig: IndexConfig;
57
- }
58
-
59
- export class PgVector extends MastraVector<PGVectorFilter> {
60
- public pool: pg.Pool;
61
- private describeIndexCache: Map<string, PGIndexStats> = new Map();
62
- private createdIndexes = new Map<string, number>();
63
- private mutexesByName = new Map<string, Mutex>();
64
- private schema?: string;
65
- private setupSchemaPromise: Promise<void> | null = null;
66
- private installVectorExtensionPromise: Promise<void> | null = null;
67
- private vectorExtensionInstalled: boolean | undefined = undefined;
68
- private schemaSetupComplete: boolean | undefined = undefined;
69
-
70
- constructor({
71
- connectionString,
72
- schemaName,
73
- pgPoolOptions,
74
- }: {
75
- connectionString: string;
76
- schemaName?: string;
77
- pgPoolOptions?: Omit<pg.PoolConfig, 'connectionString'>;
78
- }) {
79
- try {
80
- if (!connectionString || connectionString.trim() === '') {
81
- throw new Error(
82
- 'PgVector: connectionString must be provided and cannot be empty. Passing an empty string may cause fallback to local Postgres defaults.',
83
- );
84
- }
85
- super();
86
-
87
- this.schema = schemaName;
88
-
89
- const basePool = new pg.Pool({
90
- connectionString,
91
- max: 20, // Maximum number of clients in the pool
92
- idleTimeoutMillis: 30000, // Close idle connections after 30 seconds
93
- connectionTimeoutMillis: 2000, // Fail fast if can't connect
94
- ...pgPoolOptions,
95
- });
96
-
97
- const telemetry = this.__getTelemetry();
98
-
99
- this.pool =
100
- telemetry?.traceClass(basePool, {
101
- spanNamePrefix: 'pg-vector',
102
- attributes: {
103
- 'vector.type': 'postgres',
104
- },
105
- }) ?? basePool;
106
-
107
- void (async () => {
108
- // warm the created indexes cache so we don't need to check if indexes exist every time
109
- const existingIndexes = await this.listIndexes();
110
- void existingIndexes.map(async indexName => {
111
- const info = await this.getIndexInfo({ indexName });
112
- const key = await this.getIndexCacheKey({
113
- indexName,
114
- metric: info.metric,
115
- dimension: info.dimension,
116
- type: info.type,
117
- });
118
- this.createdIndexes.set(indexName, key);
119
- });
120
- })();
121
- } catch (error) {
122
- throw new MastraError(
123
- {
124
- id: 'MASTRA_STORAGE_PG_VECTOR_INITIALIZATION_FAILED',
125
- domain: ErrorDomain.MASTRA_VECTOR,
126
- category: ErrorCategory.THIRD_PARTY,
127
- details: {
128
- schemaName: schemaName ?? '',
129
- },
130
- },
131
- error,
132
- );
133
- }
134
- }
135
-
136
- private getMutexByName(indexName: string) {
137
- if (!this.mutexesByName.has(indexName)) this.mutexesByName.set(indexName, new Mutex());
138
- return this.mutexesByName.get(indexName)!;
139
- }
140
-
141
- private getTableName(indexName: string) {
142
- const parsedIndexName = parseSqlIdentifier(indexName, 'index name');
143
- const quotedIndexName = `"${parsedIndexName}"`;
144
- const quotedSchemaName = this.getSchemaName();
145
- const quotedVectorName = `"${parsedIndexName}_vector_idx"`;
146
- return {
147
- tableName: quotedSchemaName ? `${quotedSchemaName}.${quotedIndexName}` : quotedIndexName,
148
- vectorIndexName: quotedVectorName,
149
- };
150
- }
151
-
152
- private getSchemaName() {
153
- return this.schema ? `"${parseSqlIdentifier(this.schema, 'schema name')}"` : undefined;
154
- }
155
-
156
- transformFilter(filter?: PGVectorFilter) {
157
- const translator = new PGFilterTranslator();
158
- return translator.translate(filter);
159
- }
160
-
161
- async getIndexInfo({ indexName }: DescribeIndexParams): Promise<PGIndexStats> {
162
- if (!this.describeIndexCache.has(indexName)) {
163
- this.describeIndexCache.set(indexName, await this.describeIndex({ indexName }));
164
- }
165
- return this.describeIndexCache.get(indexName)!;
166
- }
167
-
168
- async query({
169
- indexName,
170
- queryVector,
171
- topK = 10,
172
- filter,
173
- includeVector = false,
174
- minScore = -1,
175
- ef,
176
- probes,
177
- }: PgQueryVectorParams): Promise<QueryResult[]> {
178
- try {
179
- if (!Number.isInteger(topK) || topK <= 0) {
180
- throw new Error('topK must be a positive integer');
181
- }
182
- if (!Array.isArray(queryVector) || !queryVector.every(x => typeof x === 'number' && Number.isFinite(x))) {
183
- throw new Error('queryVector must be an array of finite numbers');
184
- }
185
- } catch (error) {
186
- const mastraError = new MastraError(
187
- {
188
- id: 'MASTRA_STORAGE_PG_VECTOR_QUERY_INVALID_INPUT',
189
- domain: ErrorDomain.MASTRA_VECTOR,
190
- category: ErrorCategory.USER,
191
- details: {
192
- indexName,
193
- },
194
- },
195
- error,
196
- );
197
- this.logger?.trackException(mastraError);
198
- throw mastraError;
199
- }
200
-
201
- const client = await this.pool.connect();
202
- try {
203
- await client.query('BEGIN');
204
- const vectorStr = `[${queryVector.join(',')}]`;
205
- const translatedFilter = this.transformFilter(filter);
206
- const { sql: filterQuery, values: filterValues } = buildFilterQuery(translatedFilter, minScore, topK);
207
-
208
- // Get index type and configuration
209
- const indexInfo = await this.getIndexInfo({ indexName });
210
-
211
- // Set HNSW search parameter if applicable
212
- if (indexInfo.type === 'hnsw') {
213
- // Calculate ef and clamp between 1 and 1000
214
- const calculatedEf = ef ?? Math.max(topK, (indexInfo?.config?.m ?? 16) * topK);
215
- const searchEf = Math.min(1000, Math.max(1, calculatedEf));
216
- await client.query(`SET LOCAL hnsw.ef_search = ${searchEf}`);
217
- }
218
-
219
- if (indexInfo.type === 'ivfflat' && probes) {
220
- await client.query(`SET LOCAL ivfflat.probes = ${probes}`);
221
- }
222
-
223
- const { tableName } = this.getTableName(indexName);
224
-
225
- const query = `
226
- WITH vector_scores AS (
227
- SELECT
228
- vector_id as id,
229
- 1 - (embedding <=> '${vectorStr}'::vector) as score,
230
- metadata
231
- ${includeVector ? ', embedding' : ''}
232
- FROM ${tableName}
233
- ${filterQuery}
234
- )
235
- SELECT *
236
- FROM vector_scores
237
- WHERE score > $1
238
- ORDER BY score DESC
239
- LIMIT $2`;
240
- const result = await client.query(query, filterValues);
241
- await client.query('COMMIT');
242
-
243
- return result.rows.map(({ id, score, metadata, embedding }) => ({
244
- id,
245
- score,
246
- metadata,
247
- ...(includeVector && embedding && { vector: JSON.parse(embedding) }),
248
- }));
249
- } catch (error) {
250
- await client.query('ROLLBACK');
251
- const mastraError = new MastraError(
252
- {
253
- id: 'MASTRA_STORAGE_PG_VECTOR_QUERY_FAILED',
254
- domain: ErrorDomain.MASTRA_VECTOR,
255
- category: ErrorCategory.THIRD_PARTY,
256
- details: {
257
- indexName,
258
- },
259
- },
260
- error,
261
- );
262
- this.logger?.trackException(mastraError);
263
- throw mastraError;
264
- } finally {
265
- client.release();
266
- }
267
- }
268
-
269
- async upsert({ indexName, vectors, metadata, ids }: UpsertVectorParams): Promise<string[]> {
270
- const { tableName } = this.getTableName(indexName);
271
-
272
- // Start a transaction
273
- const client = await this.pool.connect();
274
- try {
275
- await client.query('BEGIN');
276
- const vectorIds = ids || vectors.map(() => crypto.randomUUID());
277
-
278
- for (let i = 0; i < vectors.length; i++) {
279
- const query = `
280
- INSERT INTO ${tableName} (vector_id, embedding, metadata)
281
- VALUES ($1, $2::vector, $3::jsonb)
282
- ON CONFLICT (vector_id)
283
- DO UPDATE SET
284
- embedding = $2::vector,
285
- metadata = $3::jsonb
286
- RETURNING embedding::text
287
- `;
288
-
289
- await client.query(query, [vectorIds[i], `[${vectors[i]?.join(',')}]`, JSON.stringify(metadata?.[i] || {})]);
290
- }
291
-
292
- await client.query('COMMIT');
293
- return vectorIds;
294
- } catch (error) {
295
- await client.query('ROLLBACK');
296
- if (error instanceof Error && error.message?.includes('expected') && error.message?.includes('dimensions')) {
297
- const match = error.message.match(/expected (\d+) dimensions, not (\d+)/);
298
- if (match) {
299
- const [, expected, actual] = match;
300
- const mastraError = new MastraError(
301
- {
302
- id: 'MASTRA_STORAGE_PG_VECTOR_UPSERT_INVALID_INPUT',
303
- domain: ErrorDomain.MASTRA_VECTOR,
304
- category: ErrorCategory.USER,
305
- text:
306
- `Vector dimension mismatch: Index "${indexName}" expects ${expected} dimensions but got ${actual} dimensions. ` +
307
- `Either use a matching embedding model or delete and recreate the index with the new dimension.`,
308
- details: {
309
- indexName,
310
- expected: expected ?? '',
311
- actual: actual ?? '',
312
- },
313
- },
314
- error,
315
- );
316
- this.logger?.trackException(mastraError);
317
- throw mastraError;
318
- }
319
- }
320
-
321
- const mastraError = new MastraError(
322
- {
323
- id: 'MASTRA_STORAGE_PG_VECTOR_UPSERT_FAILED',
324
- domain: ErrorDomain.MASTRA_VECTOR,
325
- category: ErrorCategory.THIRD_PARTY,
326
- details: {
327
- indexName,
328
- },
329
- },
330
- error,
331
- );
332
- this.logger?.trackException(mastraError);
333
- throw mastraError;
334
- } finally {
335
- client.release();
336
- }
337
- }
338
-
339
- private hasher = xxhash();
340
- private async getIndexCacheKey({
341
- indexName,
342
- dimension,
343
- metric,
344
- type,
345
- }: CreateIndexParams & { type: IndexType | undefined }) {
346
- const input = indexName + dimension + metric + (type || 'ivfflat'); // ivfflat is default
347
- return (await this.hasher).h32(input);
348
- }
349
- private cachedIndexExists(indexName: string, newKey: number) {
350
- const existingIndexCacheKey = this.createdIndexes.get(indexName);
351
- return existingIndexCacheKey && existingIndexCacheKey === newKey;
352
- }
353
- private async setupSchema(client: pg.PoolClient) {
354
- if (!this.schema || this.schemaSetupComplete) {
355
- return;
356
- }
357
-
358
- if (!this.setupSchemaPromise) {
359
- this.setupSchemaPromise = (async () => {
360
- try {
361
- // First check if schema exists and we have usage permission
362
- const schemaCheck = await client.query(
363
- `
364
- SELECT EXISTS (
365
- SELECT 1 FROM information_schema.schemata
366
- WHERE schema_name = $1
367
- )
368
- `,
369
- [this.schema],
370
- );
371
-
372
- const schemaExists = schemaCheck.rows[0].exists;
373
-
374
- if (!schemaExists) {
375
- try {
376
- await client.query(`CREATE SCHEMA IF NOT EXISTS ${this.getSchemaName()}`);
377
- this.logger.info(`Schema "${this.schema}" created successfully`);
378
- } catch (error) {
379
- this.logger.error(`Failed to create schema "${this.schema}"`, { error });
380
- throw new Error(
381
- `Unable to create schema "${this.schema}". This requires CREATE privilege on the database. ` +
382
- `Either create the schema manually or grant CREATE privilege to the user.`,
383
- );
384
- }
385
- }
386
-
387
- // If we got here, schema exists and we can use it
388
- this.schemaSetupComplete = true;
389
- this.logger.debug(`Schema "${this.schema}" is ready for use`);
390
- } catch (error) {
391
- // Reset flags so we can retry
392
- this.schemaSetupComplete = undefined;
393
- this.setupSchemaPromise = null;
394
- throw error;
395
- } finally {
396
- this.setupSchemaPromise = null;
397
- }
398
- })();
399
- }
400
-
401
- await this.setupSchemaPromise;
402
- }
403
-
404
- async createIndex({
405
- indexName,
406
- dimension,
407
- metric = 'cosine',
408
- indexConfig = {},
409
- buildIndex = true,
410
- }: PgCreateIndexParams): Promise<void> {
411
- const { tableName } = this.getTableName(indexName);
412
-
413
- // Validate inputs
414
- try {
415
- if (!indexName.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
416
- throw new Error('Invalid index name format');
417
- }
418
- if (!Number.isInteger(dimension) || dimension <= 0) {
419
- throw new Error('Dimension must be a positive integer');
420
- }
421
- } catch (error) {
422
- const mastraError = new MastraError(
423
- {
424
- id: 'MASTRA_STORAGE_PG_VECTOR_CREATE_INDEX_INVALID_INPUT',
425
- domain: ErrorDomain.MASTRA_VECTOR,
426
- category: ErrorCategory.USER,
427
- details: {
428
- indexName,
429
- },
430
- },
431
- error,
432
- );
433
- this.logger?.trackException(mastraError);
434
- throw mastraError;
435
- }
436
-
437
- const indexCacheKey = await this.getIndexCacheKey({ indexName, dimension, type: indexConfig.type, metric });
438
- if (this.cachedIndexExists(indexName, indexCacheKey)) {
439
- // we already saw this index get created since the process started, no need to recreate it
440
- return;
441
- }
442
-
443
- const mutex = this.getMutexByName(`create-${indexName}`);
444
- // Use async-mutex instead of advisory lock for perf (over 2x as fast)
445
- await mutex
446
- .runExclusive(async () => {
447
- if (this.cachedIndexExists(indexName, indexCacheKey)) {
448
- // this may have been created while we were waiting to acquire a lock
449
- return;
450
- }
451
-
452
- const client = await this.pool.connect();
453
-
454
- try {
455
- // Setup schema if needed
456
- await this.setupSchema(client);
457
-
458
- // Install vector extension first (needs to be in public schema)
459
- await this.installVectorExtension(client);
460
- await client.query(`
461
- CREATE TABLE IF NOT EXISTS ${tableName} (
462
- id SERIAL PRIMARY KEY,
463
- vector_id TEXT UNIQUE NOT NULL,
464
- embedding vector(${dimension}),
465
- metadata JSONB DEFAULT '{}'::jsonb
466
- );
467
- `);
468
- this.createdIndexes.set(indexName, indexCacheKey);
469
-
470
- if (buildIndex) {
471
- await this.setupIndex({ indexName, metric, indexConfig }, client);
472
- }
473
- } catch (error: any) {
474
- this.createdIndexes.delete(indexName);
475
- throw error;
476
- } finally {
477
- client.release();
478
- }
479
- })
480
- .catch(error => {
481
- const mastraError = new MastraError(
482
- {
483
- id: 'MASTRA_STORAGE_PG_VECTOR_CREATE_INDEX_FAILED',
484
- domain: ErrorDomain.MASTRA_VECTOR,
485
- category: ErrorCategory.THIRD_PARTY,
486
- details: {
487
- indexName,
488
- },
489
- },
490
- error,
491
- );
492
- this.logger?.trackException(mastraError);
493
- throw mastraError;
494
- });
495
- }
496
-
497
- async buildIndex({ indexName, metric = 'cosine', indexConfig }: PgDefineIndexParams): Promise<void> {
498
- const client = await this.pool.connect();
499
- try {
500
- await this.setupIndex({ indexName, metric, indexConfig }, client);
501
- } catch (error: any) {
502
- const mastraError = new MastraError(
503
- {
504
- id: 'MASTRA_STORAGE_PG_VECTOR_BUILD_INDEX_FAILED',
505
- domain: ErrorDomain.MASTRA_VECTOR,
506
- category: ErrorCategory.THIRD_PARTY,
507
- details: {
508
- indexName,
509
- },
510
- },
511
- error,
512
- );
513
- this.logger?.trackException(mastraError);
514
- throw mastraError;
515
- } finally {
516
- client.release();
517
- }
518
- }
519
-
520
- private async setupIndex({ indexName, metric, indexConfig }: PgDefineIndexParams, client: pg.PoolClient) {
521
- const mutex = this.getMutexByName(`build-${indexName}`);
522
- // Use async-mutex instead of advisory lock for perf (over 2x as fast)
523
- await mutex.runExclusive(async () => {
524
- const { tableName, vectorIndexName } = this.getTableName(indexName);
525
-
526
- if (this.createdIndexes.has(indexName)) {
527
- await client.query(`DROP INDEX IF EXISTS ${vectorIndexName}`);
528
- }
529
-
530
- if (indexConfig.type === 'flat') {
531
- this.describeIndexCache.delete(indexName);
532
- return;
533
- }
534
-
535
- const metricOp =
536
- metric === 'cosine' ? 'vector_cosine_ops' : metric === 'euclidean' ? 'vector_l2_ops' : 'vector_ip_ops';
537
-
538
- let indexSQL: string;
539
- if (indexConfig.type === 'hnsw') {
540
- const m = indexConfig.hnsw?.m ?? 8;
541
- const efConstruction = indexConfig.hnsw?.efConstruction ?? 32;
542
-
543
- indexSQL = `
544
- CREATE INDEX IF NOT EXISTS ${vectorIndexName}
545
- ON ${tableName}
546
- USING hnsw (embedding ${metricOp})
547
- WITH (
548
- m = ${m},
549
- ef_construction = ${efConstruction}
550
- )
551
- `;
552
- } else {
553
- let lists: number;
554
- if (indexConfig.ivf?.lists) {
555
- lists = indexConfig.ivf.lists;
556
- } else {
557
- const size = (await client.query(`SELECT COUNT(*) FROM ${tableName}`)).rows[0].count;
558
- lists = Math.max(100, Math.min(4000, Math.floor(Math.sqrt(size) * 2)));
559
- }
560
- indexSQL = `
561
- CREATE INDEX IF NOT EXISTS ${vectorIndexName}
562
- ON ${tableName}
563
- USING ivfflat (embedding ${metricOp})
564
- WITH (lists = ${lists});
565
- `;
566
- }
567
-
568
- await client.query(indexSQL);
569
- });
570
- }
571
-
572
- private async installVectorExtension(client: pg.PoolClient) {
573
- // If we've already successfully installed, no need to do anything
574
- if (this.vectorExtensionInstalled) {
575
- return;
576
- }
577
-
578
- // If there's no existing installation attempt or the previous one failed
579
- if (!this.installVectorExtensionPromise) {
580
- this.installVectorExtensionPromise = (async () => {
581
- try {
582
- // First check if extension is already installed
583
- const extensionCheck = await client.query(`
584
- SELECT EXISTS (
585
- SELECT 1 FROM pg_extension WHERE extname = 'vector'
586
- );
587
- `);
588
-
589
- this.vectorExtensionInstalled = extensionCheck.rows[0].exists;
590
-
591
- if (!this.vectorExtensionInstalled) {
592
- try {
593
- await client.query('CREATE EXTENSION IF NOT EXISTS vector');
594
- this.vectorExtensionInstalled = true;
595
- this.logger.info('Vector extension installed successfully');
596
- } catch {
597
- this.logger.warn(
598
- 'Could not install vector extension. This requires superuser privileges. ' +
599
- 'If the extension is already installed globally, you can ignore this warning.',
600
- );
601
- // Don't set vectorExtensionInstalled to false here since we're not sure if it failed
602
- // due to permissions or if it's already installed globally
603
- }
604
- } else {
605
- this.logger.debug('Vector extension already installed, skipping installation');
606
- }
607
- } catch (error) {
608
- this.logger.error('Error checking vector extension status', { error });
609
- // Reset both the promise and the flag so we can retry
610
- this.vectorExtensionInstalled = undefined;
611
- this.installVectorExtensionPromise = null;
612
- throw error; // Re-throw so caller knows it failed
613
- } finally {
614
- // Clear the promise after completion (success or failure)
615
- this.installVectorExtensionPromise = null;
616
- }
617
- })();
618
- }
619
-
620
- // Wait for the installation process to complete
621
- await this.installVectorExtensionPromise;
622
- }
623
-
624
- async listIndexes(): Promise<string[]> {
625
- const client = await this.pool.connect();
626
- try {
627
- // Then let's see which ones have vector columns
628
- const vectorTablesQuery = `
629
- SELECT DISTINCT table_name
630
- FROM information_schema.columns
631
- WHERE table_schema = $1
632
- AND udt_name = 'vector';
633
- `;
634
- const vectorTables = await client.query(vectorTablesQuery, [this.schema || 'public']);
635
- return vectorTables.rows.map(row => row.table_name);
636
- } catch (e) {
637
- const mastraError = new MastraError(
638
- {
639
- id: 'MASTRA_STORAGE_PG_VECTOR_LIST_INDEXES_FAILED',
640
- domain: ErrorDomain.MASTRA_VECTOR,
641
- category: ErrorCategory.THIRD_PARTY,
642
- },
643
- e,
644
- );
645
- this.logger?.trackException(mastraError);
646
- throw mastraError;
647
- } finally {
648
- client.release();
649
- }
650
- }
651
-
652
- /**
653
- * Retrieves statistics about a vector index.
654
- *
655
- * @param {string} indexName - The name of the index to describe
656
- * @returns A promise that resolves to the index statistics including dimension, count and metric
657
- */
658
- async describeIndex({ indexName }: DescribeIndexParams): Promise<PGIndexStats> {
659
- const client = await this.pool.connect();
660
- try {
661
- const { tableName } = this.getTableName(indexName);
662
-
663
- // Check if table exists with a vector column
664
- const tableExistsQuery = `
665
- SELECT 1
666
- FROM information_schema.columns
667
- WHERE table_schema = $1
668
- AND table_name = $2
669
- AND udt_name = 'vector'
670
- LIMIT 1;
671
- `;
672
- const tableExists = await client.query(tableExistsQuery, [this.schema || 'public', indexName]);
673
-
674
- if (tableExists.rows.length === 0) {
675
- throw new Error(`Vector table ${tableName} does not exist`);
676
- }
677
-
678
- // Get vector dimension
679
- const dimensionQuery = `
680
- SELECT atttypmod as dimension
681
- FROM pg_attribute
682
- WHERE attrelid = $1::regclass
683
- AND attname = 'embedding';
684
- `;
685
-
686
- // Get row count
687
- const countQuery = `
688
- SELECT COUNT(*) as count
689
- FROM ${tableName};
690
- `;
691
-
692
- // Get index metric type
693
- const indexQuery = `
694
- SELECT
695
- am.amname as index_method,
696
- pg_get_indexdef(i.indexrelid) as index_def,
697
- opclass.opcname as operator_class
698
- FROM pg_index i
699
- JOIN pg_class c ON i.indexrelid = c.oid
700
- JOIN pg_am am ON c.relam = am.oid
701
- JOIN pg_opclass opclass ON i.indclass[0] = opclass.oid
702
- JOIN pg_namespace n ON c.relnamespace = n.oid
703
- WHERE c.relname = $1
704
- AND n.nspname = $2;
705
- `;
706
-
707
- const [dimResult, countResult, indexResult] = await Promise.all([
708
- client.query(dimensionQuery, [tableName]),
709
- client.query(countQuery),
710
- client.query(indexQuery, [`${indexName}_vector_idx`, this.schema || 'public']),
711
- ]);
712
-
713
- const { index_method, index_def, operator_class } = indexResult.rows[0] || {
714
- index_method: 'flat',
715
- index_def: '',
716
- operator_class: 'cosine',
717
- };
718
-
719
- // Convert pg_vector index method to our metric type
720
- const metric = operator_class.includes('l2')
721
- ? 'euclidean'
722
- : operator_class.includes('ip')
723
- ? 'dotproduct'
724
- : 'cosine';
725
-
726
- // Parse index configuration
727
- const config: { m?: number; efConstruction?: number; lists?: number } = {};
728
-
729
- if (index_method === 'hnsw') {
730
- const m = index_def.match(/m\s*=\s*'?(\d+)'?/)?.[1];
731
- const efConstruction = index_def.match(/ef_construction\s*=\s*'?(\d+)'?/)?.[1];
732
- if (m) config.m = parseInt(m);
733
- if (efConstruction) config.efConstruction = parseInt(efConstruction);
734
- } else if (index_method === 'ivfflat') {
735
- const lists = index_def.match(/lists\s*=\s*'?(\d+)'?/)?.[1];
736
- if (lists) config.lists = parseInt(lists);
737
- }
738
-
739
- return {
740
- dimension: dimResult.rows[0].dimension,
741
- count: parseInt(countResult.rows[0].count),
742
- metric,
743
- type: index_method as 'flat' | 'hnsw' | 'ivfflat',
744
- config,
745
- };
746
- } catch (e: any) {
747
- await client.query('ROLLBACK');
748
- const mastraError = new MastraError(
749
- {
750
- id: 'MASTRA_STORAGE_PG_VECTOR_DESCRIBE_INDEX_FAILED',
751
- domain: ErrorDomain.MASTRA_VECTOR,
752
- category: ErrorCategory.THIRD_PARTY,
753
- details: {
754
- indexName,
755
- },
756
- },
757
- e,
758
- );
759
- this.logger?.trackException(mastraError);
760
- throw mastraError;
761
- } finally {
762
- client.release();
763
- }
764
- }
765
-
766
- async deleteIndex({ indexName }: DeleteIndexParams): Promise<void> {
767
- const client = await this.pool.connect();
768
- try {
769
- const { tableName } = this.getTableName(indexName);
770
- // Drop the table
771
- await client.query(`DROP TABLE IF EXISTS ${tableName} CASCADE`);
772
- this.createdIndexes.delete(indexName);
773
- } catch (error: any) {
774
- await client.query('ROLLBACK');
775
- const mastraError = new MastraError(
776
- {
777
- id: 'MASTRA_STORAGE_PG_VECTOR_DELETE_INDEX_FAILED',
778
- domain: ErrorDomain.MASTRA_VECTOR,
779
- category: ErrorCategory.THIRD_PARTY,
780
- details: {
781
- indexName,
782
- },
783
- },
784
- error,
785
- );
786
- this.logger?.trackException(mastraError);
787
- throw mastraError;
788
- } finally {
789
- client.release();
790
- }
791
- }
792
-
793
- async truncateIndex({ indexName }: DeleteIndexParams): Promise<void> {
794
- const client = await this.pool.connect();
795
- try {
796
- const { tableName } = this.getTableName(indexName);
797
- await client.query(`TRUNCATE ${tableName}`);
798
- } catch (e: any) {
799
- await client.query('ROLLBACK');
800
- const mastraError = new MastraError(
801
- {
802
- id: 'MASTRA_STORAGE_PG_VECTOR_TRUNCATE_INDEX_FAILED',
803
- domain: ErrorDomain.MASTRA_VECTOR,
804
- category: ErrorCategory.THIRD_PARTY,
805
- details: {
806
- indexName,
807
- },
808
- },
809
- e,
810
- );
811
- this.logger?.trackException(mastraError);
812
- throw mastraError;
813
- } finally {
814
- client.release();
815
- }
816
- }
817
-
818
- async disconnect() {
819
- await this.pool.end();
820
- }
821
-
822
- /**
823
- * Updates a vector by its ID with the provided vector and/or metadata.
824
- * @param indexName - The name of the index containing the vector.
825
- * @param id - The ID of the vector to update.
826
- * @param update - An object containing the vector and/or metadata to update.
827
- * @param update.vector - An optional array of numbers representing the new vector.
828
- * @param update.metadata - An optional record containing the new metadata.
829
- * @returns A promise that resolves when the update is complete.
830
- * @throws Will throw an error if no updates are provided or if the update operation fails.
831
- */
832
- async updateVector({ indexName, id, update }: UpdateVectorParams): Promise<void> {
833
- let client;
834
- try {
835
- if (!update.vector && !update.metadata) {
836
- throw new Error('No updates provided');
837
- }
838
-
839
- client = await this.pool.connect();
840
- let updateParts = [];
841
- let values = [id];
842
- let valueIndex = 2;
843
-
844
- if (update.vector) {
845
- updateParts.push(`embedding = $${valueIndex}::vector`);
846
- values.push(`[${update.vector.join(',')}]`);
847
- valueIndex++;
848
- }
849
-
850
- if (update.metadata) {
851
- updateParts.push(`metadata = $${valueIndex}::jsonb`);
852
- values.push(JSON.stringify(update.metadata));
853
- }
854
-
855
- if (updateParts.length === 0) {
856
- return;
857
- }
858
-
859
- const { tableName } = this.getTableName(indexName);
860
-
861
- // query looks like this:
862
- // UPDATE table SET embedding = $2::vector, metadata = $3::jsonb WHERE id = $1
863
- const query = `
864
- UPDATE ${tableName}
865
- SET ${updateParts.join(', ')}
866
- WHERE vector_id = $1
867
- `;
868
-
869
- await client.query(query, values);
870
- } catch (error: any) {
871
- const mastraError = new MastraError(
872
- {
873
- id: 'MASTRA_STORAGE_PG_VECTOR_UPDATE_VECTOR_FAILED',
874
- domain: ErrorDomain.MASTRA_VECTOR,
875
- category: ErrorCategory.THIRD_PARTY,
876
- details: {
877
- indexName,
878
- id,
879
- },
880
- },
881
- error,
882
- );
883
- this.logger?.trackException(mastraError);
884
- throw mastraError;
885
- } finally {
886
- client?.release();
887
- }
888
- }
889
-
890
- /**
891
- * Deletes a vector by its ID.
892
- * @param indexName - The name of the index containing the vector.
893
- * @param id - The ID of the vector to delete.
894
- * @returns A promise that resolves when the deletion is complete.
895
- * @throws Will throw an error if the deletion operation fails.
896
- */
897
- async deleteVector({ indexName, id }: DeleteVectorParams): Promise<void> {
898
- let client;
899
- try {
900
- client = await this.pool.connect();
901
- const { tableName } = this.getTableName(indexName);
902
- const query = `
903
- DELETE FROM ${tableName}
904
- WHERE vector_id = $1
905
- `;
906
- await client.query(query, [id]);
907
- } catch (error: any) {
908
- const mastraError = new MastraError(
909
- {
910
- id: 'MASTRA_STORAGE_PG_VECTOR_DELETE_VECTOR_FAILED',
911
- domain: ErrorDomain.MASTRA_VECTOR,
912
- category: ErrorCategory.THIRD_PARTY,
913
- details: {
914
- indexName,
915
- id,
916
- },
917
- },
918
- error,
919
- );
920
- this.logger?.trackException(mastraError);
921
- throw mastraError;
922
- } finally {
923
- client?.release();
924
- }
925
- }
926
- }