diff --git a/docs/components/vectordbs/dbs/mongodb.mdx b/docs/components/vectordbs/dbs/mongodb.mdx
index 57f3fd432..039846d44 100644
--- a/docs/components/vectordbs/dbs/mongodb.mdx
+++ b/docs/components/vectordbs/dbs/mongodb.mdx
@@ -2,13 +2,15 @@
title: "MongoDB"
description: "Use MongoDB as a vector database in Mem0 with built-in vector search for high-dimensional similarity queries."
---
+
# MongoDB
[MongoDB](https://www.mongodb.com/) is a versatile document database that supports vector search capabilities, allowing for efficient high-dimensional similarity searches over large datasets with robust scalability and performance.
## Usage
-```python
+
+```python Python
import os
from mem0 import Memory
@@ -20,30 +22,90 @@ config = {
"config": {
"db_name": "mem0-db",
"collection_name": "mem0-collection",
- "mongo_uri":"mongodb://username:password@localhost:27017"
+ "mongo_uri": "mongodb://username:password@localhost:27017"
}
}
}
m = Memory.from_config(config)
messages = [
- {"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": "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": "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": "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.",
+ },
]
+
m.add(messages, user_id="alice", metadata={"category": "movies"})
```
+```typescript TypeScript
+import { Memory } from "mem0ai/oss";
+
+const config = {
+ vectorStore: {
+ provider: "mongodb",
+ config: {
+ dbName: "mem0-db",
+ collectionName: "mem0-collection",
+ url: "mongodb://username:password@localhost:27017",
+ },
+ },
+};
+
+const memory = new Memory(config);
+
+const messages = [
+ {
+ 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: "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.",
+ },
+];
+
+await memory.add(messages, {
+ userId: "alice",
+ metadata: {
+ category: "movies",
+ },
+});
+```
+
+
## Config
Here are the parameters available for configuring MongoDB:
-| Parameter | Description | Default Value |
-| --- | --- | --- |
-| db_name | Name of the MongoDB database | `"mem0_db"` |
-| collection_name | Name of the MongoDB collection | `"mem0"` |
-| embedding_model_dims | Dimensions of the embedding vectors | `1536` |
-| mongo_uri | The MongoDB URI connection string | `mongodb://localhost:27017` |
+| Python | TypeScript | Description | Default Value |
+| --- | --- | --- | --- |
+| db_name | dbName | Name of the MongoDB database | "mem0_db" |
+| collection_name | collectionName | Name of the MongoDB collection | "mem0" |
+| embedding_model_dims | embeddingModelDims | Dimensions of the embedding vectors | 1536 |
+| mongo_uri | url | The MongoDB URI connection string | mongodb://localhost:27017 |
-> **Note**: If `mongo_uri` is not provided, it will default to `mongodb://localhost:27017`.
+> **Note**: If `mongo_uri` (Python) or `url` (TypeScript) is not provided, it defaults to `mongodb://localhost:27017`. A local instance must be running MongoDB v8.2+ for vector search to work.
+
+> **Note**: The vector search index builds asynchronously after the first write. A search issued right after the first `add()` may return no results (and log an "index not initialized" message) until the index finishes building. This takes a few seconds on a local deployment and up to about a minute on Atlas. This is expected; the search returns results once the index is ready.
diff --git a/mem0-ts/package.json b/mem0-ts/package.json
index 2398063fc..231eacfac 100644
--- a/mem0-ts/package.json
+++ b/mem0-ts/package.json
@@ -129,6 +129,7 @@
"cloudflare": "^4.2.0",
"fastembed": "^2.1.0",
"groq-sdk": "0.3.0",
+ "mongodb": "^7.0.0",
"ollama": "^0.5.14",
"pg": "8.11.3",
"redis": "^4.6.13",
diff --git a/mem0-ts/pnpm-lock.yaml b/mem0-ts/pnpm-lock.yaml
index 145bb3988..01b2b4440 100644
--- a/mem0-ts/pnpm-lock.yaml
+++ b/mem0-ts/pnpm-lock.yaml
@@ -104,6 +104,9 @@ importers:
groq-sdk:
specifier: 0.3.0
version: 0.3.0
+ mongodb:
+ specifier: ^7.0.0
+ version: 7.2.0
mysql2:
specifier: ^3.0.0
version: 3.22.5(@types/node@22.19.21)
diff --git a/mem0-ts/src/oss/src/index.ts b/mem0-ts/src/oss/src/index.ts
index d902cbdcc..49742ac53 100644
--- a/mem0-ts/src/oss/src/index.ts
+++ b/mem0-ts/src/oss/src/index.ts
@@ -40,5 +40,6 @@ export * from "./vector_stores/s3_vectors";
export * from "./vector_stores/vertex_ai_vector_search";
export * from "./vector_stores/pinecone";
export * from "./vector_stores/turbopuffer";
+export * from "./vector_stores/mongodb";
export * from "./vector_stores/opensearch";
export * from "./utils/factory";
diff --git a/mem0-ts/src/oss/src/tests/mongodb.test.ts b/mem0-ts/src/oss/src/tests/mongodb.test.ts
new file mode 100644
index 000000000..d339de582
--- /dev/null
+++ b/mem0-ts/src/oss/src/tests/mongodb.test.ts
@@ -0,0 +1,292 @@
+const mockInsertOne = jest.fn();
+const mockDeleteOne = jest.fn();
+const mockInsertMany = jest.fn();
+const mockFindOne = jest.fn();
+const mockUpdateOne = jest.fn();
+const mockListSearchIndexes = jest.fn();
+const mockCreateSearchIndex = jest.fn();
+const mockDrop = jest.fn();
+const mockToArray = jest.fn();
+const mockLimit = jest.fn().mockReturnThis();
+const mockFind = jest.fn().mockReturnValue({
+ limit: mockLimit,
+ toArray: mockToArray,
+});
+const mockAggregate = jest.fn().mockReturnValue({
+ toArray: mockToArray,
+});
+const mockListCollections = jest.fn();
+const mockClose = jest.fn();
+
+const mockCollection = {
+ insertOne: mockInsertOne,
+ deleteOne: mockDeleteOne,
+ insertMany: mockInsertMany,
+ findOne: mockFindOne,
+ updateOne: mockUpdateOne,
+ listSearchIndexes: mockListSearchIndexes,
+ createSearchIndex: mockCreateSearchIndex,
+ drop: mockDrop,
+ find: mockFind,
+ aggregate: mockAggregate,
+};
+
+const mockDb = {
+ collection: jest.fn().mockReturnValue(mockCollection),
+ listCollections: mockListCollections,
+};
+
+const mockMongoClient = jest.fn().mockImplementation(() => {
+ return {
+ db: jest.fn().mockReturnValue(mockDb),
+ close: mockClose,
+ };
+});
+
+jest.mock("mongodb", () => {
+ return {
+ MongoClient: mockMongoClient,
+ };
+});
+
+import { MongoDB } from "../vector_stores/mongodb";
+
+describe("MongoDB Vector Store", () => {
+ let store: MongoDB;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockListCollections.mockReturnValue({
+ toArray: jest.fn().mockResolvedValue([]),
+ });
+ mockListSearchIndexes.mockReturnValue({
+ toArray: jest.fn().mockResolvedValue([]),
+ });
+ store = new MongoDB({
+ url: "mongodb://localhost:27017",
+ dbName: "test_db",
+ collectionName: "test_col",
+ embeddingModelDims: 4,
+ });
+ });
+
+ afterEach(async () => {
+ await store.close();
+ });
+
+ it("should initialize client and check/create collection and indexes", async () => {
+ await store.initialize();
+
+ expect(mockListCollections).toHaveBeenCalledWith({ name: "test_col" });
+ expect(mockCollection.insertOne).toHaveBeenCalledWith({
+ _id: 0,
+ placeholder: true,
+ });
+ expect(mockCollection.deleteOne).toHaveBeenCalledWith({ _id: 0 });
+ expect(mockCreateSearchIndex).toHaveBeenCalledTimes(2);
+ });
+
+ it("should insert documents correctly", async () => {
+ mockInsertMany.mockResolvedValue({ insertedCount: 2 });
+
+ await store.insert(
+ [
+ [0.1, 0.2, 0.3, 0.4],
+ [0.5, 0.6, 0.7, 0.8],
+ ],
+ ["id1", "id2"],
+ [{ user: "alice" }, { user: "bob" }],
+ );
+
+ expect(mockInsertMany).toHaveBeenCalledWith([
+ {
+ _id: "id1",
+ embedding: [0.1, 0.2, 0.3, 0.4],
+ payload: { user: "alice" },
+ },
+ { _id: "id2", embedding: [0.5, 0.6, 0.7, 0.8], payload: { user: "bob" } },
+ ]);
+ });
+
+ it("should perform vector search correctly without filters", async () => {
+ mockListSearchIndexes.mockReturnValue({
+ toArray: jest.fn().mockResolvedValue([{ name: "test_col_vector_index" }]),
+ });
+ mockToArray.mockResolvedValue([
+ { _id: "id1", score: 0.95, payload: { text: "hello" } },
+ { _id: "id2", score: 0.85, payload: { text: "world" } },
+ ]);
+
+ const results = await store.search([0.1, 0.2, 0.3, 0.4], 2);
+
+ expect(results).toEqual([
+ { id: "id1", score: 0.95, payload: { text: "hello" } },
+ { id: "id2", score: 0.85, payload: { text: "world" } },
+ ]);
+
+ expect(mockAggregate).toHaveBeenCalledWith([
+ {
+ $vectorSearch: {
+ index: "test_col_vector_index",
+ limit: 2,
+ numCandidates: 40,
+ queryVector: [0.1, 0.2, 0.3, 0.4],
+ path: "embedding",
+ },
+ },
+ { $set: { score: { $meta: "vectorSearchScore" } } },
+ { $project: { embedding: 0 } },
+ ]);
+ });
+
+ it("should perform vector search correctly with filters", async () => {
+ mockListSearchIndexes.mockReturnValue({
+ toArray: jest.fn().mockResolvedValue([{ name: "test_col_vector_index" }]),
+ });
+ mockToArray.mockResolvedValue([]);
+
+ await store.search([0.1, 0.2, 0.3, 0.4], 2, {
+ user: "alice",
+ role: "admin",
+ });
+
+ expect(mockAggregate).toHaveBeenCalledWith([
+ {
+ $vectorSearch: {
+ index: "test_col_vector_index",
+ limit: 2,
+ numCandidates: 40,
+ queryVector: [0.1, 0.2, 0.3, 0.4],
+ path: "embedding",
+ },
+ },
+ {
+ $match: {
+ $and: [{ "payload.user": "alice" }, { "payload.role": "admin" }],
+ },
+ },
+ { $set: { score: { $meta: "vectorSearchScore" } } },
+ { $project: { embedding: 0 } },
+ ]);
+ });
+
+ it("should reject invalid object/dict filter values", async () => {
+ await expect(
+ store.search([0.1, 0.2, 0.3, 0.4], 5, { user: { name: "alice" } }),
+ ).rejects.toThrow("Filter value for 'user' must be a scalar");
+
+ await expect(
+ store.search([0.1, 0.2, 0.3, 0.4], 5, { user: [{ name: "alice" }] }),
+ ).rejects.toThrow("Filter list for 'user' contains an object");
+ });
+
+ it("should perform keyword search correctly", async () => {
+ mockToArray.mockResolvedValue([
+ { _id: "id1", score: 1.5, payload: { data: "test search" } },
+ ]);
+
+ const results = await store.keywordSearch("test", 1);
+
+ expect(results).toEqual([
+ { id: "id1", score: 1.5, payload: { data: "test search" } },
+ ]);
+
+ expect(mockAggregate).toHaveBeenCalledWith([
+ {
+ $search: {
+ index: "test_col_text_search_index",
+ text: {
+ query: "test",
+ path: ["payload.data", "payload.text_lemmatized"],
+ },
+ },
+ },
+ { $set: { score: { $meta: "searchScore" } } },
+ { $project: { embedding: 0 } },
+ { $limit: 1 },
+ ]);
+ });
+
+ it("should perform get correctly", async () => {
+ mockFindOne.mockResolvedValue({
+ _id: "id1",
+ payload: { data: "get-test" },
+ });
+
+ const result = await store.get("id1");
+ expect(result).toEqual({ id: "id1", payload: { data: "get-test" } });
+ expect(mockFindOne).toHaveBeenCalledWith({ _id: "id1" });
+ });
+
+ it("should return null when get document does not exist", async () => {
+ mockFindOne.mockResolvedValue(null);
+
+ const result = await store.get("id-non-existent");
+ expect(result).toBeNull();
+ });
+
+ it("should update document correctly", async () => {
+ mockUpdateOne.mockResolvedValue({ matchedCount: 1 });
+
+ await store.update("id1", [0.1, 0.2, 0.3, 0.4], { name: "new-alice" });
+
+ expect(mockUpdateOne).toHaveBeenCalledWith(
+ { _id: "id1" },
+ {
+ $set: {
+ embedding: [0.1, 0.2, 0.3, 0.4],
+ "payload.name": "new-alice",
+ },
+ },
+ );
+ });
+
+ it("should delete document correctly", async () => {
+ mockDeleteOne.mockResolvedValue({ deletedCount: 1 });
+
+ await store.delete("id1");
+
+ expect(mockDeleteOne).toHaveBeenCalledWith({ _id: "id1" });
+ });
+
+ it("should delete collection correctly", async () => {
+ mockDrop.mockResolvedValue(true);
+
+ await store.deleteCol();
+
+ expect(mockDrop).toHaveBeenCalled();
+ });
+
+ it("should list documents correctly with filters", async () => {
+ mockToArray.mockResolvedValue([{ _id: "id1", payload: { user: "alice" } }]);
+
+ const [results, count] = await store.list({ user: "alice" }, 10);
+
+ expect(results).toEqual([{ id: "id1", payload: { user: "alice" } }]);
+ expect(count).toBe(1);
+ expect(mockFind).toHaveBeenCalledWith({
+ $and: [{ "payload.user": "alice" }],
+ });
+ expect(mockLimit).toHaveBeenCalledWith(10);
+ });
+
+ it("should manage user ID correctly", async () => {
+ mockFindOne.mockResolvedValue(null);
+ mockUpdateOne.mockResolvedValue({});
+
+ const userId1 = await store.getUserId();
+ expect(userId1).toBeDefined();
+ expect(typeof userId1).toBe("string");
+
+ mockFindOne.mockResolvedValue({ user_id: "custom-user-123" });
+ const userId2 = await store.getUserId();
+ expect(userId2).toBe("custom-user-123");
+
+ await store.setUserId("new-custom-user");
+ expect(mockUpdateOne).toHaveBeenCalledWith(
+ {},
+ { $set: { user_id: "new-custom-user" } },
+ { upsert: true },
+ );
+ });
+});
diff --git a/mem0-ts/src/oss/src/utils/factory.ts b/mem0-ts/src/oss/src/utils/factory.ts
index f41a05f5b..66998aa28 100644
--- a/mem0-ts/src/oss/src/utils/factory.ts
+++ b/mem0-ts/src/oss/src/utils/factory.ts
@@ -52,6 +52,7 @@ import { CassandraDB } from "../vector_stores/cassandra";
import { PineconeDB } from "../vector_stores/pinecone";
import { S3Vectors } from "../vector_stores/s3_vectors";
import { TurbopufferDB } from "../vector_stores/turbopuffer";
+import { MongoDB } from "../vector_stores/mongodb";
export class EmbedderFactory {
static create(provider: string, config: EmbeddingConfig): Embedder {
@@ -159,6 +160,8 @@ export class VectorStoreFactory {
return new S3Vectors(config as any);
case "turbopuffer":
return new TurbopufferDB(config as any);
+ case "mongodb":
+ return new MongoDB(config as any);
default:
throw new Error(`Unsupported vector store provider: ${provider}`);
}
diff --git a/mem0-ts/src/oss/src/vector_stores/mongodb.ts b/mem0-ts/src/oss/src/vector_stores/mongodb.ts
new file mode 100644
index 000000000..7eb3d0b9c
--- /dev/null
+++ b/mem0-ts/src/oss/src/vector_stores/mongodb.ts
@@ -0,0 +1,462 @@
+import { MongoClient, Collection, Db } from "mongodb";
+import { VectorStore } from "./base";
+import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
+
+export interface MongoDBConfig extends VectorStoreConfig {
+ url?: string;
+ dbName?: string;
+ collectionName?: string;
+ embeddingModelDims?: number;
+ dimension?: number;
+ client?: MongoClient;
+}
+
+export class MongoDB implements VectorStore {
+ private client: MongoClient;
+ private db: Db;
+ private collection!: Collection;
+ private readonly collectionName: string;
+ private readonly dbName: string;
+ private readonly embeddingModelDims: number;
+ private readonly indexName: string;
+ private _initPromise?: Promise;
+
+ constructor(config: MongoDBConfig) {
+ this.collectionName = config.collectionName || "mem0";
+ this.dbName = config.dbName || "mem0_db";
+ this.embeddingModelDims =
+ config.embeddingModelDims || config.dimension || 1536;
+ this.indexName = `${this.collectionName}_vector_index`;
+
+ if (config.client) {
+ this.client = config.client;
+ } else {
+ const url = config.url || "mongodb://localhost:27017";
+ this.client = new MongoClient(url, { appName: "Mem0" });
+ }
+
+ this.db = this.client.db(this.dbName);
+ }
+
+ async initialize(): Promise {
+ if (!this._initPromise) {
+ this._initPromise = this._doInitialize();
+ }
+ return this._initPromise;
+ }
+
+ private async _doInitialize(): Promise {
+ try {
+ const collections = await this.db
+ .listCollections({ name: this.collectionName })
+ .toArray();
+ if (collections.length === 0) {
+ this.collection = this.db.collection(this.collectionName);
+ await this.collection.insertOne({ _id: 0 as any, placeholder: true });
+ await this.collection.deleteOne({ _id: 0 as any });
+ } else {
+ this.collection = this.db.collection(this.collectionName);
+ }
+
+ // Create Vector Search Index
+ try {
+ let foundIndex = false;
+ try {
+ const indexes = await this.collection.listSearchIndexes().toArray();
+ foundIndex = indexes.some((idx) => idx.name === this.indexName);
+ } catch (e) {
+ // listSearchIndexes might not be supported/available on non-Atlas or legacy clusters
+ }
+
+ if (!foundIndex) {
+ await this.collection.createSearchIndex({
+ name: this.indexName,
+ type: "vectorSearch",
+ definition: {
+ fields: [
+ {
+ type: "vector",
+ path: "embedding",
+ numDimensions: this.embeddingModelDims,
+ similarity: "cosine",
+ },
+ ],
+ },
+ });
+ }
+ } catch (e: any) {
+ console.warn(
+ `Could not verify or create vector search index: ${e.message}`,
+ );
+ }
+
+ // Create Text Search Index for keywordSearch
+ const textIndexName = `${this.collectionName}_text_search_index`;
+ try {
+ let foundTextIndex = false;
+ try {
+ const indexes = await this.collection.listSearchIndexes().toArray();
+ foundTextIndex = indexes.some((idx) => idx.name === textIndexName);
+ } catch (e) {
+ // ignore
+ }
+
+ if (!foundTextIndex) {
+ await this.collection.createSearchIndex({
+ name: textIndexName,
+ definition: {
+ mappings: {
+ dynamic: false,
+ fields: {
+ payload: {
+ type: "document",
+ fields: {
+ data: { type: "string" },
+ text_lemmatized: { type: "string" },
+ },
+ },
+ },
+ },
+ },
+ });
+ }
+ } catch (e: any) {
+ console.warn(
+ `Could not create text search index '${textIndexName}': ${e.message}. ` +
+ `Atlas Search may not be available. keywordSearch() will not work.`,
+ );
+ }
+ } catch (error) {
+ console.error("Error initializing MongoDB:", error);
+ throw error;
+ }
+ }
+
+ private validateFilterValue(key: string, value: any): void {
+ if (typeof value === "object" && value !== null) {
+ if (Array.isArray(value)) {
+ for (const item of value) {
+ if (
+ typeof item === "object" &&
+ item !== null &&
+ !Array.isArray(item)
+ ) {
+ throw new Error(
+ `Filter list for '${key}' contains an object, which may contain MongoDB query operators.`,
+ );
+ }
+ }
+ } else {
+ throw new Error(
+ `Filter value for '${key}' must be a scalar (string, number, boolean), not an object. Objects may contain MongoDB query operators.`,
+ );
+ }
+ }
+ }
+
+ async insert(
+ vectors: number[][],
+ ids: string[],
+ payloads: Record[],
+ ): Promise {
+ await this.initialize();
+
+ const documents = vectors.map((vector, idx) => ({
+ _id: ids[idx] as any,
+ embedding: vector,
+ payload: payloads[idx] || {},
+ }));
+
+ try {
+ await this.collection.insertMany(documents);
+ } catch (error) {
+ console.error("Error inserting data:", error);
+ throw error;
+ }
+ }
+
+ async search(
+ query: number[],
+ topK: number = 5,
+ filters?: SearchFilters,
+ ): Promise {
+ await this.initialize();
+
+ if (filters) {
+ for (const [key, value] of Object.entries(filters)) {
+ this.validateFilterValue(key, value);
+ }
+ }
+
+ try {
+ let foundIndex = false;
+ try {
+ const indexes = await this.collection.listSearchIndexes().toArray();
+ foundIndex = indexes.some((idx) => idx.name === this.indexName);
+ } catch (e) {
+ // listSearchIndexes might not be supported/available on non-Atlas or legacy clusters
+ foundIndex = true;
+ }
+
+ if (!foundIndex) {
+ console.error(`Index '${this.indexName}' does not exist.`);
+ return [];
+ }
+
+ const pipeline: any[] = [
+ {
+ $vectorSearch: {
+ index: this.indexName,
+ limit: topK,
+ numCandidates: Math.min(topK * 20, 10000),
+ queryVector: query,
+ path: "embedding",
+ },
+ },
+ { $set: { score: { $meta: "vectorSearchScore" } } },
+ { $project: { embedding: 0 } },
+ ];
+
+ if (filters && Object.keys(filters).length > 0) {
+ const filterConditions: any[] = [];
+ for (const [key, value] of Object.entries(filters)) {
+ filterConditions.push({ [`payload.${key}`]: value });
+ }
+ if (filterConditions.length > 0) {
+ pipeline.splice(1, 0, { $match: { $and: filterConditions } });
+ }
+ }
+
+ const results = await this.collection.aggregate(pipeline).toArray();
+
+ return results.map((doc) => ({
+ id: String(doc._id),
+ score: doc.score,
+ payload: doc.payload || {},
+ }));
+ } catch (error) {
+ // The vector index builds asynchronously after creation; a search issued
+ // before it is queryable throws "Index not initialized". Log the message
+ // (matching the Python provider) rather than the full error object, and
+ // return no results until the index finishes building.
+ console.error(
+ "Error during vector search:",
+ error instanceof Error ? error.message : error,
+ );
+ return [];
+ }
+ }
+
+ async keywordSearch(
+ query: string,
+ topK: number = 5,
+ filters?: SearchFilters,
+ ): Promise {
+ await this.initialize();
+
+ if (filters) {
+ for (const [key, value] of Object.entries(filters)) {
+ this.validateFilterValue(key, value);
+ }
+ }
+
+ try {
+ const textIndexName = `${this.collectionName}_text_search_index`;
+ const pipeline: any[] = [
+ {
+ $search: {
+ index: textIndexName,
+ text: {
+ query: query,
+ path: ["payload.data", "payload.text_lemmatized"],
+ },
+ },
+ },
+ { $set: { score: { $meta: "searchScore" } } },
+ { $project: { embedding: 0 } },
+ ];
+
+ if (filters && Object.keys(filters).length > 0) {
+ const filterConditions: any[] = [];
+ for (const [key, value] of Object.entries(filters)) {
+ filterConditions.push({ [`payload.${key}`]: value });
+ }
+ if (filterConditions.length > 0) {
+ pipeline.splice(1, 0, { $match: { $and: filterConditions } });
+ }
+ }
+
+ pipeline.push({ $limit: topK });
+
+ const results = await this.collection.aggregate(pipeline).toArray();
+
+ return results.map((doc) => ({
+ id: String(doc._id),
+ score: doc.score,
+ payload: doc.payload || {},
+ }));
+ } catch (error) {
+ console.error(
+ "Error during keyword search:",
+ error instanceof Error ? error.message : error,
+ );
+ return null;
+ }
+ }
+
+ async get(vectorId: string): Promise {
+ await this.initialize();
+ try {
+ const doc = await this.collection.findOne({ _id: vectorId as any });
+ if (doc) {
+ return {
+ id: String(doc._id),
+ payload: doc.payload || {},
+ };
+ }
+ return null;
+ } catch (error) {
+ console.error("Error retrieving document:", error);
+ return null;
+ }
+ }
+
+ async update(
+ vectorId: string,
+ vector: number[],
+ payload: Record,
+ ): Promise {
+ await this.initialize();
+ const updateFields: any = {};
+ if (vector) {
+ updateFields.embedding = vector;
+ }
+ if (payload) {
+ for (const [key, value] of Object.entries(payload)) {
+ updateFields[`payload.${key}`] = value;
+ }
+ }
+
+ if (Object.keys(updateFields).length > 0) {
+ try {
+ const result = await this.collection.updateOne(
+ { _id: vectorId as any },
+ { $set: updateFields },
+ );
+ if (result.matchedCount === 0) {
+ console.warn(`No document found with ID '${vectorId}' to update.`);
+ }
+ } catch (error) {
+ console.error("Error updating document:", error);
+ throw error;
+ }
+ }
+ }
+
+ async delete(vectorId: string): Promise {
+ await this.initialize();
+ try {
+ const result = await this.collection.deleteOne({ _id: vectorId as any });
+ if (result.deletedCount === 0) {
+ console.warn(`No document found with ID '${vectorId}' to delete.`);
+ }
+ } catch (error) {
+ console.error("Error deleting document:", error);
+ throw error;
+ }
+ }
+
+ async deleteCol(): Promise {
+ await this.initialize();
+ try {
+ await this.collection.drop();
+ } catch (error) {
+ console.error("Error deleting collection:", error);
+ throw error;
+ }
+ }
+
+ async list(
+ filters?: SearchFilters,
+ topK: number = 100,
+ ): Promise<[VectorStoreResult[], number]> {
+ await this.initialize();
+
+ if (filters) {
+ for (const [key, value] of Object.entries(filters)) {
+ this.validateFilterValue(key, value);
+ }
+ }
+
+ try {
+ let query: any = {};
+ if (filters && Object.keys(filters).length > 0) {
+ const filterConditions: any[] = [];
+ for (const [key, value] of Object.entries(filters)) {
+ filterConditions.push({ [`payload.${key}`]: value });
+ }
+ if (filterConditions.length > 0) {
+ query = { $and: filterConditions };
+ }
+ }
+
+ const results = await this.collection.find(query).limit(topK).toArray();
+
+ const output = results.map((doc) => ({
+ id: String(doc._id),
+ payload: doc.payload || {},
+ }));
+
+ return [output, results.length];
+ } catch (error) {
+ console.error("Error listing documents:", error);
+ return [[], 0];
+ }
+ }
+
+ async getUserId(): Promise {
+ await this.initialize();
+ try {
+ const migrationsCol = this.db.collection("memory_migrations");
+ const doc = await migrationsCol.findOne({});
+ if (doc && doc.user_id) {
+ return doc.user_id;
+ }
+
+ const randomUserId =
+ Math.random().toString(36).substring(2, 15) +
+ Math.random().toString(36).substring(2, 15);
+
+ await migrationsCol.updateOne(
+ {},
+ { $set: { user_id: randomUserId } },
+ { upsert: true },
+ );
+ return randomUserId;
+ } catch (error) {
+ console.error("Error getting user ID:", error);
+ throw error;
+ }
+ }
+
+ async setUserId(userId: string): Promise {
+ await this.initialize();
+ try {
+ const migrationsCol = this.db.collection("memory_migrations");
+ await migrationsCol.updateOne(
+ {},
+ { $set: { user_id: userId } },
+ { upsert: true },
+ );
+ } catch (error) {
+ console.error("Error setting user ID:", error);
+ throw error;
+ }
+ }
+
+ async close(): Promise {
+ if (this.client) {
+ await this.client.close();
+ }
+ }
+}
diff --git a/mem0-ts/tsup.config.ts b/mem0-ts/tsup.config.ts
index 96ed43523..b0f91aac6 100644
--- a/mem0-ts/tsup.config.ts
+++ b/mem0-ts/tsup.config.ts
@@ -31,6 +31,7 @@ const external = [
"natural",
"mysql2",
"@turbopuffer/turbopuffer",
+ "mongodb",
"@opensearch-project/opensearch",
"@elastic/elasticsearch",
];