diff --git a/docs/components/vectordbs/dbs/pgvector.mdx b/docs/components/vectordbs/dbs/pgvector.mdx index f322a0a95..ddd36c416 100644 --- a/docs/components/vectordbs/dbs/pgvector.mdx +++ b/docs/components/vectordbs/dbs/pgvector.mdx @@ -5,6 +5,12 @@ description: "Use pgvector as a vector store in Mem0 for PostgreSQL-based vector [pgvector](https://github.com/pgvector/pgvector) is an open-source vector similarity search extension for Postgres. After connecting to Postgres, run `CREATE EXTENSION IF NOT EXISTS vector;` to create the vector extension. +The TypeScript SDK loads the `pg` driver only when you use this store, so install it alongside `mem0ai`: + +```bash +npm install pg +``` + ### Usage diff --git a/mem0-ts/package.json b/mem0-ts/package.json index 3bb996079..e6f29d4cb 100644 --- a/mem0-ts/package.json +++ b/mem0-ts/package.json @@ -100,7 +100,8 @@ "tsup": "^8.3.0", "typescript": "5.5.4", "iovalkey": "^0.3.3", - "@mochow/mochow-sdk-node": "^2.1.5" + "@mochow/mochow-sdk-node": "^2.1.5", + "@types/jest": "^29.5.14" }, "dependencies": { "axios": "^1.18.0", @@ -127,8 +128,7 @@ "@qdrant/js-client-rest": "^1.18.0", "@supabase/supabase-js": "^2.49.1", "@turbopuffer/turbopuffer": "^2.0.0", - "@types/jest": "29.5.14", - "@types/pg": "8.11.0", + "@types/pg": "^8.11.0", "@upstash/vector": "^1.2.3", "better-sqlite3": "^12.6.2", "cassandra-driver": "4.8.0", @@ -141,7 +141,7 @@ "weaviate-client": "^3.0.0", "ollama": "^0.5.14", "oracledb": "^6.5.0 || ^7.0.0", - "pg": "8.11.3", + "pg": "^8.11.3", "redis": "^4.6.13", "@elastic/elasticsearch": "^9.0.0", "iovalkey": "^0.3.3", @@ -257,6 +257,15 @@ }, "oracledb": { "optional": true + }, + "pg": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "natural": { + "optional": true } }, "engines": { diff --git a/mem0-ts/pnpm-lock.yaml b/mem0-ts/pnpm-lock.yaml index 5deb30e7b..5e04cbfdd 100644 --- a/mem0-ts/pnpm-lock.yaml +++ b/mem0-ts/pnpm-lock.yaml @@ -109,11 +109,8 @@ importers: '@turbopuffer/turbopuffer': specifier: ^2.0.0 version: 2.5.0 - '@types/jest': - specifier: 29.5.14 - version: 29.5.14 '@types/pg': - specifier: 8.11.0 + specifier: ^8.11.0 version: 8.11.0 '@upstash/vector': specifier: ^1.2.3 @@ -167,8 +164,8 @@ importers: specifier: ^6.5.0 || ^7.0.0 version: 7.0.1 pg: - specifier: 8.11.3 - version: 8.11.3 + specifier: ^8.11.3 + version: 8.21.0 redis: specifier: ^4.6.13 version: 4.7.1 @@ -191,6 +188,9 @@ importers: '@types/better-sqlite3': specifier: ^7.6.13 version: 7.6.13 + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 '@types/node': specifier: ^22.7.6 version: 22.19.21 @@ -2228,10 +2228,6 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer-writer@2.0.0: - resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} - engines: {node: '>=4'} - buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -3846,9 +3842,6 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - packet-reader@1.0.0: - resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} - pad-left@2.1.0: resolution: {integrity: sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA==} engines: {node: '>=0.10.0'} @@ -3920,15 +3913,6 @@ packages: resolution: {integrity: sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==} engines: {node: '>=10'} - pg@8.11.3: - resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==} - engines: {node: '>= 8.0.0'} - peerDependencies: - pg-native: '>=3.0.1' - peerDependenciesMeta: - pg-native: - optional: true - pg@8.21.0: resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==} engines: {node: '>= 16.0.0'} @@ -7593,8 +7577,6 @@ snapshots: buffer-from@1.1.2: {} - buffer-writer@2.0.0: {} - buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -9409,8 +9391,6 @@ snapshots: package-json-from-dist@1.0.1: {} - packet-reader@1.0.0: {} - pad-left@2.1.0: dependencies: repeat-string: 1.6.1 @@ -9458,10 +9438,6 @@ snapshots: pg-numeric@1.0.2: {} - pg-pool@3.14.0(pg@8.11.3): - dependencies: - pg: 8.11.3 - pg-pool@3.14.0(pg@8.21.0): dependencies: pg: 8.21.0 @@ -9486,18 +9462,6 @@ snapshots: postgres-interval: 3.0.0 postgres-range: 1.1.4 - pg@8.11.3: - dependencies: - buffer-writer: 2.0.0 - packet-reader: 1.0.0 - pg-connection-string: 2.13.0 - pg-pool: 3.14.0(pg@8.11.3) - pg-protocol: 1.14.0 - pg-types: 2.2.0 - pgpass: 1.0.5 - optionalDependencies: - pg-cloudflare: 1.4.0 - pg@8.21.0: dependencies: pg-connection-string: 2.13.0 diff --git a/mem0-ts/src/oss/src/vector_stores/pgvector.ts b/mem0-ts/src/oss/src/vector_stores/pgvector.ts index 450ffc9a5..b7f2c2f69 100644 --- a/mem0-ts/src/oss/src/vector_stores/pgvector.ts +++ b/mem0-ts/src/oss/src/vector_stores/pgvector.ts @@ -1,8 +1,11 @@ import type { Client as ClientType, ClientConfig } from "pg"; -import pkg from "pg"; -const { Client, escapeIdentifier } = pkg; import { VectorStore } from "./base"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; +import { loadPeer } from "../utils/load_peer"; + +function escapeIdentifier(name: string): string { + return `"${name.replace(/"/g, '""')}"`; +} const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,127}$/; @@ -212,7 +215,7 @@ function buildClientConfig( } export class PGVector implements VectorStore { - private client: ClientType; + private client!: ClientType; private collectionName: string; private useDiskann: boolean; private useHnsw: boolean; @@ -234,13 +237,6 @@ export class PGVector implements VectorStore { ? "" : validateIdentifier(config.dbname || "vector_store", "dbname"); this.config = config; - - this.client = new Client( - buildClientConfig( - config, - this.useDirectConnection ? undefined : "postgres", - ), - ); this.initialize().catch(console.error); } @@ -257,6 +253,18 @@ export class PGVector implements VectorStore { private async _doInitialize(): Promise { try { + const pg = await loadPeer( + "pg", + "PGVector vector store", + () => import("pg"), + ); + const { Client } = pg.default ?? pg; + this.client = new Client( + buildClientConfig( + this.config, + this.useDirectConnection ? undefined : "postgres", + ), + ); await this.client.connect(); if (!this.useDirectConnection) { @@ -345,6 +353,7 @@ export class PGVector implements VectorStore { ids: string[], payloads: Record[], ): Promise { + await this.initialize(); const values = vectors.map((vector, i) => ({ id: ids[i], vector: `[${vector.join(",")}]`, @@ -368,6 +377,7 @@ export class PGVector implements VectorStore { topK: number = 5, filters?: SearchFilters, ): Promise { + await this.initialize(); try { const { conditions, @@ -406,6 +416,7 @@ export class PGVector implements VectorStore { topK: number = 5, filters?: SearchFilters, ): Promise { + await this.initialize(); const queryVector = `[${query.join(",")}]`; const { conditions, @@ -435,6 +446,7 @@ export class PGVector implements VectorStore { } async get(vectorId: string): Promise { + await this.initialize(); const result = await this.client.query( `SELECT id, payload FROM ${this.col()} WHERE id = $1`, [vectorId], @@ -453,6 +465,7 @@ export class PGVector implements VectorStore { vector: number[], payload: Record, ): Promise { + await this.initialize(); const vectorStr = `[${vector.join(",")}]`; await this.client.query( ` @@ -465,12 +478,14 @@ export class PGVector implements VectorStore { } async delete(vectorId: string): Promise { + await this.initialize(); await this.client.query(`DELETE FROM ${this.col()} WHERE id = $1`, [ vectorId, ]); } async deleteCol(): Promise { + await this.initialize(); await this.client.query(`DROP TABLE IF EXISTS ${this.col()}`); } @@ -487,6 +502,7 @@ export class PGVector implements VectorStore { filters?: SearchFilters, topK: number = 100, ): Promise<[VectorStoreResult[], number]> { + await this.initialize(); const { conditions, values: filterValues, @@ -525,10 +541,11 @@ export class PGVector implements VectorStore { } async close(): Promise { - await this.client.end(); + await this.client?.end(); } async getUserId(): Promise { + await this.initialize(); const result = await this.client.query( "SELECT user_id FROM memory_migrations LIMIT 1", ); @@ -549,6 +566,7 @@ export class PGVector implements VectorStore { } async setUserId(userId: string): Promise { + await this.initialize(); await this.client.query("DELETE FROM memory_migrations"); await this.client.query( "INSERT INTO memory_migrations (user_id) VALUES ($1)", diff --git a/mem0-ts/src/oss/tests/missing-optional-peers.test.ts b/mem0-ts/src/oss/tests/missing-optional-peers.test.ts new file mode 100644 index 000000000..910849c55 --- /dev/null +++ b/mem0-ts/src/oss/tests/missing-optional-peers.test.ts @@ -0,0 +1,39 @@ +jest.mock("pg", () => { + throw new Error("Cannot find module 'pg'"); +}); + +jest.mock("natural", () => { + throw new Error("Cannot find module 'natural'"); +}); + +describe("mem0ai/oss without pg or natural installed", () => { + beforeEach(() => { + jest.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test("mem0ai/oss loads", async () => { + await expect(import("../src")).resolves.toHaveProperty("Memory"); + }); + + test("PGVector explains how to install pg", async () => { + const { PGVector } = await import("../src/vector_stores/pgvector"); + const store = new PGVector({ + connectionString: "postgresql://localhost:5432/db", + embeddingModelDims: 3, + } as any); + + await expect(store.initialize()).rejects.toThrow( + "The 'pg' package is required to use the PGVector vector store. Install it with: npm install pg", + ); + }); + + test("BM25 lemmatization falls back to the built-in stemmer", async () => { + const { lemmatizeForBm25 } = await import("../src/utils/lemmatization"); + + expect(lemmatizeForBm25("The dogs were running")).toBe("dog runn running"); + }); +});