fix(mem0-ts): support pgvector connection strings and ssl (#5789)

This commit is contained in:
Rod Boev
2026-06-25 11:57:37 +05:30
committed by GitHub
parent ac296f7534
commit 6bb1d328ad
5 changed files with 279 additions and 121 deletions
+35 -37
View File
@@ -21,49 +21,47 @@ from mem0 import Memory
load_dotenv() load_dotenv()
config = { config = {
"vector_store": { "vector_store": {
"provider": "pgvector", "provider": "pgvector",
"config": { "config": {
"connection_string": os.environ["DATABASE_URL"], "connection_string": os.environ["DATABASE_URL"],
"collection_name": "memories", "collection_name": "memories",
"embedding_model_dims": 1536, "embedding_model_dims": 1536,
"hnsw": True, "hnsw": True,
}, },
}, },
} }
m = Memory.from_config(config) m = Memory.from_config(config)
messages = [ messages = [
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"}, {"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."}, {"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."}, {"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}, {"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."},
] ]
m.add(messages, user_id="alice", metadata={"category": "movies"}) m.add(messages, user_id="alice", metadata={"category": "movies"})
results = m.search( results = m.search(
"What movies should I recommend?", "What movies should I recommend?",
filters={"user_id": "alice"}, filters={"user_id": "alice"},
) )
print(results) print(results)
```
````
```typescript TypeScript ```typescript TypeScript
import "dotenv/config"; import "dotenv/config";
import { Memory } from "mem0ai/oss"; import { Memory } from "mem0ai/oss";
const databaseUrl = new URL(process.env.DATABASE_URL!);
const m = new Memory({ const m = new Memory({
vectorStore: { vectorStore: {
provider: "pgvector", provider: "pgvector",
config: { config: {
user: decodeURIComponent(databaseUrl.username), connectionString: process.env.DATABASE_URL!,
password: decodeURIComponent(databaseUrl.password), ssl: {
host: databaseUrl.hostname, rejectUnauthorized: false,
port: Number(databaseUrl.port || 5432), },
dbname: databaseUrl.pathname.slice(1) || "neondb",
collectionName: "memories", collectionName: "memories",
dimension: 1536, dimension: 1536,
embeddingModelDims: 1536, embeddingModelDims: 1536,
@@ -89,7 +87,8 @@ const results = await m.search("What movies should I recommend?", {
}); });
console.log(results); console.log(results);
``` ````
</CodeGroup> </CodeGroup>
## SQL Migration ## SQL Migration
@@ -116,20 +115,19 @@ DATABASE_URL=postgresql://user:password@ep-example.us-east-2.aws.neon.tech/neond
| `sslmode` | PostgreSQL SSL mode. Use `require` for Neon. | Driver default | | `sslmode` | PostgreSQL SSL mode. Use `require` for Neon. | Driver default |
</Tab> </Tab>
<Tab title="TypeScript"> <Tab title="TypeScript">
The current Mem0 TypeScript `pgvector` adapter takes individual Postgres fields, Use the Neon `DATABASE_URL` directly with `connectionString`. Set `ssl` if your runtime needs an explicit TLS config object.
so parse `DATABASE_URL` before creating `Memory`.
| Parameter | Description | Default |
| -------------------- | ---------------------------------------------- | -------------- |
| `connectionString` | Neon Postgres connection string. | Required |
| `ssl` | Optional TLS settings passed directly to `pg`. | Driver default |
| `collectionName` | Name for the vector collection. | `memories` |
| `dimension` | Vector dimension for Mem0 config. | Auto-detected |
| `embeddingModelDims` | Embedding model dimensions for table creation. | Required |
| `hnsw` | Enables HNSW indexing. | `false` |
**TLS note:** `ssl: true` is sufficient for most Neon connections since Neon uses valid certificates. Use `ssl: { rejectUnauthorized: false }` only when connecting through Neon's connection pooler on certain edge runtimes (e.g. Cloudflare Workers) that require it, or when your environment does not trust the Neon CA chain.
| Parameter | Description | Default |
| --- | --- | --- |
| `user` | Database user. | Required |
| `password` | Database password. | Required |
| `host` | Database host. | Required |
| `port` | Database port. | `5432` |
| `dbname` | Database name. | `vector_store` |
| `collectionName` | Name for the vector collection. | `memories` |
| `dimension` | Vector dimension for Mem0 config. | Auto-detected |
| `embeddingModelDims` | Embedding model dimensions for table creation. | Required |
| `hnsw` | Enables HNSW indexing. | `false` |
</Tab> </Tab>
</Tabs> </Tabs>
+50 -45
View File
@@ -2,6 +2,7 @@
title: "pgvector" title: "pgvector"
description: "Use pgvector as a vector store in Mem0 for PostgreSQL-based vector similarity search with open-source simplicity." description: "Use pgvector as a vector store in Mem0 for PostgreSQL-based vector similarity search with open-source simplicity."
--- ---
[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. [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.
### Usage ### Usage
@@ -14,41 +15,38 @@ from mem0 import Memory
os.environ["OPENAI_API_KEY"] = "sk-xx" os.environ["OPENAI_API_KEY"] = "sk-xx"
config = { config = {
"vector_store": { "vector_store": {
"provider": "pgvector", "provider": "pgvector",
"config": { "config": {
"user": "test", "user": "test",
"password": "123", "password": "123",
"host": "127.0.0.1", "host": "127.0.0.1",
"port": "5432", "port": "5432",
} },
} }
} }
m = Memory.from_config(config) m = Memory.from_config(config)
messages = [ messages = [
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"}, {"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."}, {"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."}, {"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."} {"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."},
] ]
m.add(messages, user_id="alice", metadata={"category": "movies"}) m.add(messages, user_id="alice", metadata={"category": "movies"})
```
````
```typescript TypeScript ```typescript TypeScript
import { Memory } from 'mem0ai/oss'; import { Memory } from "mem0ai/oss";
const config = { const config = {
vectorStore: { vectorStore: {
provider: 'pgvector', provider: "pgvector",
config: { config: {
collectionName: 'memories', collectionName: "memories",
embeddingModelDims: 1536, embeddingModelDims: 1536,
user: 'test', connectionString: "postgresql://test:123@localhost:5432/vector_store",
password: '123',
host: '127.0.0.1',
port: 5432,
dbname: 'vector_store', // Optional; TypeScript OSS defaults to `vector_store` when omitted
diskann: false, // Optional, requires pgvectorscale extension diskann: false, // Optional, requires pgvectorscale extension
hnsw: false, // Optional, for HNSW indexing hnsw: false, // Optional, for HNSW indexing
}, },
@@ -57,37 +55,44 @@ const config = {
const memory = new Memory(config); const memory = new Memory(config);
const messages = [ const messages = [
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"}, { role: "user", content: "I'm planning to watch a movie tonight. Any recommendations?" },
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."}, { role: "assistant", content: "How about thriller movies? They can be quite engaging." },
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."}, { role: "user", content: "I'm not a big fan of thriller movies but I love sci-fi movies." },
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."} { role: "assistant", content: "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future." },
] ];
await memory.add(messages, { userId: "alice", metadata: { category: "movies" } }); await memory.add(messages, { userId: "alice", metadata: { category: "movies" } });
``` ````
</CodeGroup> </CodeGroup>
### Config ### Config
Here are the parameters available for configuring pgvector: Here are the parameters available for configuring pgvector:
| Parameter | Description | Default Value | | Parameter | SDK | Description | Default Value |
| --- | --- | --- | | -------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| `dbname` | The name of the database | `postgres` | | `connectionString` | TypeScript OSS | PostgreSQL connection string for direct connections. When set, Mem0 connects to the target database directly and skips the bootstrap `postgres` database flow. | `None` |
| `collection_name` | The name of the collection | `mem0` | | `ssl` | TypeScript OSS | SSL option passed directly to `pg`, either `true` or an SSL config object, for both `connectionString` and split-field connections. | `None` |
| `embedding_model_dims` | Dimensions of the embedding model | `1536` | | `dbname` | TypeScript OSS | Split-field database name. This is only used when `connectionString` is absent. | `vector_store` |
| `user` | User name to connect to the database | `None` | | `collectionName` | TypeScript OSS | Collection name. | `memories` |
| `password` | Password to connect to the database | `None` | | `embeddingModelDims` | TypeScript OSS | Dimensions of the embedding model. | Required |
| `host` | The host where the Postgres server is running | `None` | | `user` | TypeScript OSS + Python | Database user for split-field connections. | `None` |
| `port` | The port where the Postgres server is running | `None` | | `password` | TypeScript OSS + Python | Database password for split-field connections. | `None` |
| `diskann` | Whether to use diskann for vector similarity search (requires pgvectorscale) | `True` | | `host` | TypeScript OSS + Python | Database host for split-field connections. | `None` |
| `hnsw` | Whether to use hnsw for vector similarity search | `False` | | `port` | TypeScript OSS + Python | Database port for split-field connections. | `None` |
| `sslmode` | SSL mode for PostgreSQL connection (e.g., 'require', 'prefer', 'disable') | `None` | | `diskann` | TypeScript OSS + Python | Whether to use DiskANN for vector similarity search, requires pgvectorscale. | `False` |
| `connection_string` | PostgreSQL connection string (overrides individual connection parameters) | `None` | | `hnsw` | TypeScript OSS + Python | Whether to use HNSW for vector similarity search. | TypeScript OSS: `False`, Python: `True` |
| `connection_pool` | psycopg2 connection pool object (overrides connection string and individual parameters) | `None` | | `connection_string` | Python only | PostgreSQL connection string, overrides individual connection parameters. | `None` |
| `sslmode` | Python only | SSL mode for PostgreSQL connections, such as `require`, `prefer`, or `disable`. | `None` |
| `connection_pool` | Python only | psycopg connection pool object, overrides connection string and individual connection parameters. | `None` |
**Note (TypeScript OSS):** If you omit `dbname`, the TypeScript client uses the database name `vector_store`. Python defaults to `postgres` for `dbname`, as in the table above. **TypeScript OSS:** Use `connectionString` plus optional `ssl` for managed Postgres setups. If you omit `connectionString`, Mem0 falls back to split fields and uses `dbname`, `user`, `password`, `host`, `port`, and optional `ssl`.
**Python:** The Python SDK uses snake_case keys such as `connection_string`, `sslmode`, `collection_name`, and `embedding_model_dims`.
**Python connection priority**:
**Note**: The connection parameters have the following priority:
1. `connection_pool` (highest priority) 1. `connection_pool` (highest priority)
2. `connection_string` 2. `connection_string`
3. Individual connection parameters (`user`, `password`, `host`, `port`, `sslmode`) 3. Individual connection parameters (`user`, `password`, `host`, `port`, `sslmode`)
+72 -33
View File
@@ -1,4 +1,4 @@
import type { Client as ClientType } from "pg"; import type { Client as ClientType, ClientConfig } from "pg";
import pkg from "pg"; import pkg from "pg";
const { Client, escapeIdentifier } = pkg; const { Client, escapeIdentifier } = pkg;
import { VectorStore } from "./base"; import { VectorStore } from "./base";
@@ -157,41 +157,90 @@ export function buildFilterConditions(
interface PGVectorConfig extends VectorStoreConfig { interface PGVectorConfig extends VectorStoreConfig {
dbname?: string; dbname?: string;
user: string; user?: string;
password: string; password?: string;
host: string; host?: string;
port: number; port?: number;
connectionString?: string;
ssl?: ClientConfig["ssl"];
embeddingModelDims: number; embeddingModelDims: number;
diskann?: boolean; diskann?: boolean;
hnsw?: boolean; hnsw?: boolean;
} }
function getConnectionString(config: PGVectorConfig): string | undefined {
return config.connectionString?.trim() || undefined;
}
function validateConnectionConfig(config: PGVectorConfig): void {
if (getConnectionString(config)) {
return;
}
const missingFields = ["user", "password", "host", "port"].filter((field) => {
const v = config[field as keyof PGVectorConfig];
return v === undefined || v === null || v === "";
});
if (missingFields.length > 0) {
throw new Error(
`PGVector requires either connectionString or ${missingFields.join(", ")}`,
);
}
}
function buildClientConfig(
config: PGVectorConfig,
database?: string,
): ClientConfig {
const connectionString = getConnectionString(config);
if (connectionString) {
return {
connectionString,
...(config.ssl !== undefined ? { ssl: config.ssl } : {}),
};
}
return {
database,
user: config.user,
password: config.password,
host: config.host,
port: config.port,
...(config.ssl !== undefined ? { ssl: config.ssl } : {}),
};
}
export class PGVector implements VectorStore { export class PGVector implements VectorStore {
private client: ClientType; private client: ClientType;
private collectionName: string; private collectionName: string;
private useDiskann: boolean; private useDiskann: boolean;
private useHnsw: boolean; private useHnsw: boolean;
private readonly dbName: string; private readonly dbName: string;
private readonly useDirectConnection: boolean;
private config: PGVectorConfig; private config: PGVectorConfig;
private _initPromise?: Promise<void>; private _initPromise?: Promise<void>;
constructor(config: PGVectorConfig) { constructor(config: PGVectorConfig) {
validateConnectionConfig(config);
this.collectionName = validateIdentifier( this.collectionName = validateIdentifier(
config.collectionName || "memories", config.collectionName || "memories",
"collectionName", "collectionName",
); );
this.useDiskann = config.diskann || false; this.useDiskann = config.diskann || false;
this.useHnsw = config.hnsw || false; this.useHnsw = config.hnsw || false;
this.dbName = validateIdentifier(config.dbname || "vector_store", "dbname"); this.useDirectConnection = !!getConnectionString(config);
this.dbName = this.useDirectConnection
? ""
: validateIdentifier(config.dbname || "vector_store", "dbname");
this.config = config; this.config = config;
this.client = new Client({ this.client = new Client(
database: "postgres", // Initially connect to default postgres database buildClientConfig(
user: config.user, config,
password: config.password, this.useDirectConnection ? undefined : "postgres",
host: config.host, ),
port: config.port, );
});
this.initialize().catch(console.error); this.initialize().catch(console.error);
} }
@@ -210,29 +259,20 @@ export class PGVector implements VectorStore {
try { try {
await this.client.connect(); await this.client.connect();
// Check if database exists if (!this.useDirectConnection) {
const dbExists = await this.checkDatabaseExists(this.dbName); const dbExists = await this.checkDatabaseExists(this.dbName);
if (!dbExists) { if (!dbExists) {
await this.createDatabase(this.dbName); await this.createDatabase(this.dbName);
}
await this.client.end();
this.client = new Client(buildClientConfig(this.config, this.dbName));
await this.client.connect();
} }
// Disconnect from postgres database
await this.client.end();
// Connect to the target database
this.client = new Client({
database: this.dbName,
user: this.config.user,
password: this.config.password,
host: this.config.host,
port: this.config.port,
});
await this.client.connect();
// Create vector extension
await this.client.query("CREATE EXTENSION IF NOT EXISTS vector"); await this.client.query("CREATE EXTENSION IF NOT EXISTS vector");
// Create memory_migrations table
await this.client.query(` await this.client.query(`
CREATE TABLE IF NOT EXISTS memory_migrations ( CREATE TABLE IF NOT EXISTS memory_migrations (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
@@ -240,7 +280,6 @@ export class PGVector implements VectorStore {
) )
`); `);
// Check if the collection exists
const collections = await this.listCols(); const collections = await this.listCols();
if (!collections.includes(this.collectionName)) { if (!collections.includes(this.collectionName)) {
await this.createCol(this.config.embeddingModelDims); await this.createCol(this.config.embeddingModelDims);
@@ -1,5 +1,3 @@
/// <reference types="jest" />
jest.mock("pg", () => { jest.mock("pg", () => {
const Client = jest.fn().mockImplementation(() => ({ const Client = jest.fn().mockImplementation(() => ({
connect: jest.fn().mockResolvedValue(undefined), connect: jest.fn().mockResolvedValue(undefined),
+122 -4
View File
@@ -1,5 +1,3 @@
/// <reference types="jest" />
const searchRows = [ const searchRows = [
{ {
id: "a", id: "a",
@@ -23,9 +21,13 @@ const searchRows = [
}, },
]; ];
const mockState = {
databaseExists: true,
};
function mockPgQuery(sql: string) { function mockPgQuery(sql: string) {
if (sql.includes("SELECT 1 FROM pg_database")) { if (sql.includes("SELECT 1 FROM pg_database")) {
return { rows: [{ "?column?": 1 }] }; return { rows: mockState.databaseExists ? [{ "?column?": 1 }] : [] };
} }
if (sql.includes("FROM information_schema.tables")) { if (sql.includes("FROM information_schema.tables")) {
@@ -69,13 +71,129 @@ jest.mock("pg", () => {
import { PGVector } from "../src/vector_stores/pgvector"; import { PGVector } from "../src/vector_stores/pgvector";
describe("PGVector - search()", () => { function getClientQueries(client: { query: jest.Mock }) {
return client.query.mock.calls.map(([sql]) => sql as string);
}
describe("PGVector", () => {
beforeEach(() => { beforeEach(() => {
const pg = require("pg"); const pg = require("pg");
mockState.databaseExists = true;
pg.__mock.Client.mockClear(); pg.__mock.Client.mockClear();
pg.__mock.clients.length = 0; pg.__mock.clients.length = 0;
}); });
test("uses one direct client for connectionString mode and skips bootstrap database creation", async () => {
mockState.databaseExists = false;
const ssl = { rejectUnauthorized: false };
const store = new PGVector({
collectionName: "memories",
connectionString:
"postgresql://postgres:postgres@db.example.com:5432/neondb",
ssl,
embeddingModelDims: 3,
dimension: 3,
} as any);
await store.initialize();
const pg = require("pg");
expect(pg.__mock.Client).toHaveBeenCalledTimes(1);
expect(pg.__mock.Client).toHaveBeenCalledWith({
connectionString:
"postgresql://postgres:postgres@db.example.com:5432/neondb",
ssl,
});
const directClient = pg.__mock.clients[0];
const queries = getClientQueries(directClient);
expect(queries).not.toEqual(
expect.arrayContaining([
expect.stringContaining("SELECT 1 FROM pg_database"),
]),
);
expect(queries).not.toEqual(
expect.arrayContaining([expect.stringContaining("CREATE DATABASE")]),
);
expect(queries).toEqual(
expect.arrayContaining([
"CREATE EXTENSION IF NOT EXISTS vector",
expect.stringContaining("FROM information_schema.tables"),
]),
);
});
test("keeps the split-field bootstrap flow when connectionString is absent", async () => {
mockState.databaseExists = false;
const ssl = { rejectUnauthorized: false };
const store = new PGVector({
collectionName: "memories",
user: "postgres",
password: "postgres",
host: "localhost",
port: 5432,
dbname: "vector_store",
ssl,
embeddingModelDims: 3,
dimension: 3,
} as any);
await store.initialize();
const pg = require("pg");
expect(pg.__mock.Client).toHaveBeenCalledTimes(2);
expect(pg.__mock.Client).toHaveBeenNthCalledWith(1, {
database: "postgres",
user: "postgres",
password: "postgres",
host: "localhost",
port: 5432,
ssl,
});
expect(pg.__mock.Client).toHaveBeenNthCalledWith(2, {
database: "vector_store",
user: "postgres",
password: "postgres",
host: "localhost",
port: 5432,
ssl,
});
const bootstrapClient = pg.__mock.clients[0];
const activeClient = pg.__mock.clients[1];
const bootstrapQueries = getClientQueries(bootstrapClient);
expect(bootstrapQueries).toEqual(
expect.arrayContaining([
"SELECT 1 FROM pg_database WHERE datname = $1",
'CREATE DATABASE "vector_store"',
]),
);
expect(bootstrapClient.end).toHaveBeenCalledTimes(1);
expect(getClientQueries(activeClient)).toEqual(
expect.arrayContaining([
"CREATE EXTENSION IF NOT EXISTS vector",
expect.stringContaining("FROM information_schema.tables"),
]),
);
});
test("throws when connectionString is absent and split-field params are missing", () => {
expect(
() =>
new PGVector({
collectionName: "memories",
embeddingModelDims: 3,
dimension: 3,
} as any),
).toThrow(
"PGVector requires either connectionString or user, password, host, port",
);
});
test("returns similarity score (1 - distance) clamped to [0, 1]", async () => { test("returns similarity score (1 - distance) clamped to [0, 1]", async () => {
const store = new PGVector({ const store = new PGVector({
collectionName: "memories", collectionName: "memories",