feat(ts-sdk): add Chroma vector store provider (#6145)

Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
sahithreddy05
2026-07-08 19:14:05 +05:30
committed by GitHub
co-authored by kartik-mem0
parent f89edb45dc
commit 22f70d50e1
7 changed files with 949 additions and 4 deletions
+54 -4
View File
@@ -6,9 +6,8 @@ description: "Use Chroma as a vector database in Mem0 for local or cloud-hosted
### Usage
#### Local Installation
```python
<CodeGroup>
```python Python
import os
from mem0 import Memory
@@ -37,10 +36,46 @@ messages = [
m.add(messages, user_id="alice", metadata={"category": "movies"})
```
```typescript TypeScript
import { Memory } from 'mem0ai/oss';
// The Node.js client connects to a running Chroma server.
// Start one locally with: chroma run --host localhost --port 8000
const config = {
vectorStore: {
provider: 'chroma',
config: {
collectionName: 'memories',
host: 'localhost',
port: 8000,
// Optional: ChromaDB Cloud configuration
// apiKey: 'your-chroma-cloud-api-key',
// tenant: 'your-chroma-cloud-tenant-id',
},
},
};
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" } });
```
</CodeGroup>
<Note>
The Node.js SDK uses the `chromadb` v3 client, which talks to a Chroma server over HTTP (local server or ChromaDB Cloud). Install it with `npm install chromadb`. Mem0 supplies the embeddings, so the collection is created without an embedding function.
</Note>
### Config
Here are the parameters available for configuring Chroma:
<Tabs>
<Tab title="Python">
| Parameter | Description | Default Value |
| --- | --- | --- |
| `collection_name` | The name of the collection | `mem0` |
@@ -49,4 +84,19 @@ Here are the parameters available for configuring Chroma:
| `host` | The host where the Chroma server is running | `None` |
| `port` | The port where the Chroma server is running | `None` |
| `api_key` | ChromaDB Cloud API key (for cloud usage) | `None` |
| `tenant` | ChromaDB Cloud tenant ID (for cloud usage) | `None` |
| `tenant` | ChromaDB Cloud tenant ID (for cloud usage) | `None` |
</Tab>
<Tab title="TypeScript">
| Parameter | Description | Default Value |
| --- | --- | --- |
| `collectionName` | The name of the collection | `mem0` |
| `client` | Pre-configured `ChromaClient` or `CloudClient` instance | `None` |
| `host` | The host where the Chroma server is running | `None` |
| `port` | The port where the Chroma server is running | `None` |
| `ssl` | Whether to use SSL when connecting to the Chroma server | `false` |
| `path` | Full URL of a Chroma server, e.g. `http://localhost:8000` (alternative to `host` and `port`) | `None` |
| `apiKey` | ChromaDB Cloud API key (for cloud usage) | `None` |
| `tenant` | ChromaDB Cloud tenant ID (for cloud usage) | `None` |
| `database` | ChromaDB Cloud database name (for cloud usage) | `mem0` |
</Tab>
</Tabs>
+1
View File
@@ -126,6 +126,7 @@
"@upstash/vector": "^1.2.3",
"better-sqlite3": "^12.6.2",
"cassandra-driver": "4.8.0",
"chromadb": "^3.5.0",
"cloudflare": "^4.2.0",
"fastembed": "^2.1.0",
"groq-sdk": "0.3.0",
+63
View File
@@ -94,6 +94,9 @@ importers:
cassandra-driver:
specifier: 4.8.0
version: 4.8.0
chromadb:
specifier: ^3.5.0
version: 3.5.0
cloudflare:
specifier: ^4.2.0
version: 4.5.0
@@ -1682,6 +1685,41 @@ packages:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
chromadb-js-bindings-darwin-arm64@1.3.4:
resolution: {integrity: sha512-pDdWCcR0hCz3pSDiIijBito7YG6FumewfzYE2mIjSwjrO1CKSfQKLwDdTVK4ZNpVGQa16ePoMX+pg7ShSUARJg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
chromadb-js-bindings-darwin-x64@1.3.4:
resolution: {integrity: sha512-5vKVFXSFo+S9qC9by/7oofjcXqQK4IGHiUnPrvTfc7B52gNlPSKeyDO1wha556COc3KtXSZjICEH0nYBJ8UXng==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
chromadb-js-bindings-linux-arm64-gnu@1.3.4:
resolution: {integrity: sha512-ELiqDZzU5mZd1BvZugGrhoMkVeNyQaa+PGizd06WIpIJVoHY+S+9ZBjdeM6ZkRnL3vTxD79XBrosxNWhzqy/kg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
chromadb-js-bindings-linux-x64-gnu@1.3.4:
resolution: {integrity: sha512-HmTUe7PEIaBngCQ70Yyle81z2wYyg9OMgJYyRUNBYCBp4ED6zTmdaOro41Vs/c/h2UyQeBXFIKNmHDeD2Nr2fw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
chromadb-js-bindings-win32-x64-msvc@1.3.4:
resolution: {integrity: sha512-punrofQRzKspNMxHmv24qoPeRycV2T3KfedekGV4lOuPPG14i6qggS4x/OWLSAK7ozPfBbrzCejvCa5wfvujhQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
chromadb@3.5.0:
resolution: {integrity: sha512-A68f2RQjY3R95Pzy0j3ykOu6fi+WWCN1tjbvzrZXZoYQ1d7ZjgWr1FyzitZCeaz/0qmc8y2LEK7QmlxloOAs9Q==}
engines: {node: '>=20'}
hasBin: true
ci-info@3.9.0:
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
engines: {node: '>=8'}
@@ -5602,6 +5640,31 @@ snapshots:
chownr@3.0.0: {}
chromadb-js-bindings-darwin-arm64@1.3.4:
optional: true
chromadb-js-bindings-darwin-x64@1.3.4:
optional: true
chromadb-js-bindings-linux-arm64-gnu@1.3.4:
optional: true
chromadb-js-bindings-linux-x64-gnu@1.3.4:
optional: true
chromadb-js-bindings-win32-x64-msvc@1.3.4:
optional: true
chromadb@3.5.0:
dependencies:
semver: 7.8.4
optionalDependencies:
chromadb-js-bindings-darwin-arm64: 1.3.4
chromadb-js-bindings-darwin-x64: 1.3.4
chromadb-js-bindings-linux-arm64-gnu: 1.3.4
chromadb-js-bindings-linux-x64-gnu: 1.3.4
chromadb-js-bindings-win32-x64-msvc: 1.3.4
ci-info@3.9.0: {}
cjs-module-lexer@1.4.3: {}
+457
View File
@@ -0,0 +1,457 @@
// jest.mock is hoisted above variable declarations, so shared mock functions
// are attached to the module-level `__mocks__` object populated inside the
// factory. The hoisted mock reaches them through this stable reference.
const __mocks__: {
add: jest.Mock;
query: jest.Mock;
get: jest.Mock;
update: jest.Mock;
upsert: jest.Mock;
deleteFn: jest.Mock;
getOrCreateCollection: jest.Mock;
deleteCollection: jest.Mock;
ChromaClient: jest.Mock;
CloudClient: jest.Mock;
} = {} as any;
jest.mock("chromadb", () => {
const add = jest.fn().mockResolvedValue(undefined);
const query = jest
.fn()
.mockResolvedValue({ ids: [[]], distances: [[]], metadatas: [[]] });
const get = jest.fn().mockResolvedValue({ ids: [], metadatas: [] });
const update = jest.fn().mockResolvedValue(undefined);
const upsert = jest.fn().mockResolvedValue(undefined);
const deleteFn = jest.fn().mockResolvedValue(undefined);
const collectionHandle = {
add,
query,
get,
update,
upsert,
delete: deleteFn,
};
const getOrCreateCollection = jest.fn().mockResolvedValue(collectionHandle);
const deleteCollection = jest.fn().mockResolvedValue(undefined);
const clientImpl = () => ({ getOrCreateCollection, deleteCollection });
const ChromaClient = jest.fn().mockImplementation(clientImpl);
const CloudClient = jest.fn().mockImplementation(clientImpl);
Object.assign(__mocks__, {
add,
query,
get,
update,
upsert,
deleteFn,
getOrCreateCollection,
deleteCollection,
ChromaClient,
CloudClient,
});
return { ChromaClient, CloudClient };
});
import { ChromaDB } from "../vector_stores/chroma";
import { VectorStoreFactory } from "../utils/factory";
// --- Helpers ---
function makeDb(overrides: Record<string, any> = {}): ChromaDB {
return new ChromaDB({
collectionName: "test-collection",
host: "localhost",
port: 8000,
...overrides,
} as any);
}
async function initDb(overrides: Record<string, any> = {}): Promise<ChromaDB> {
const db = makeDb(overrides);
await db.initialize();
return db;
}
// --- Reset mocks between tests ---
beforeEach(() => {
jest.clearAllMocks();
__mocks__.add.mockResolvedValue(undefined);
__mocks__.query.mockResolvedValue({
ids: [[]],
distances: [[]],
metadatas: [[]],
});
__mocks__.get.mockResolvedValue({ ids: [], metadatas: [] });
__mocks__.update.mockResolvedValue(undefined);
__mocks__.upsert.mockResolvedValue(undefined);
__mocks__.deleteFn.mockResolvedValue(undefined);
const collectionHandle = {
add: __mocks__.add,
query: __mocks__.query,
get: __mocks__.get,
update: __mocks__.update,
upsert: __mocks__.upsert,
delete: __mocks__.deleteFn,
};
__mocks__.getOrCreateCollection.mockResolvedValue(collectionHandle);
__mocks__.deleteCollection.mockResolvedValue(undefined);
__mocks__.ChromaClient.mockImplementation(() => ({
getOrCreateCollection: __mocks__.getOrCreateCollection,
deleteCollection: __mocks__.deleteCollection,
}));
__mocks__.CloudClient.mockImplementation(() => ({
getOrCreateCollection: __mocks__.getOrCreateCollection,
deleteCollection: __mocks__.deleteCollection,
}));
});
// --- Test suites ---
describe("VectorStoreFactory", () => {
it("returns a ChromaDB instance for provider 'chroma'", async () => {
const db = VectorStoreFactory.create("chroma", {
collectionName: "x",
host: "localhost",
port: 8000,
} as any);
expect(db).toBeInstanceOf(ChromaDB);
await (db as any).initialize();
});
});
describe("Constructor", () => {
it("builds a local ChromaClient with host and port", () => {
makeDb({ host: "localhost", port: 8000 });
expect(__mocks__.ChromaClient).toHaveBeenCalledWith({
host: "localhost",
port: 8000,
});
expect(__mocks__.CloudClient).not.toHaveBeenCalled();
});
it("passes ssl and path through to ChromaClient when provided", () => {
makeDb({ host: "example.com", port: 443, ssl: true, path: "/db" });
expect(__mocks__.ChromaClient).toHaveBeenCalledWith({
host: "example.com",
port: 443,
ssl: true,
path: "/db",
});
});
it("builds a CloudClient when apiKey and tenant are set", () => {
new ChromaDB({
collectionName: "test-collection",
apiKey: "key-123",
tenant: "tenant-abc",
} as any);
expect(__mocks__.CloudClient).toHaveBeenCalledWith({
apiKey: "key-123",
tenant: "tenant-abc",
database: "mem0",
});
expect(__mocks__.ChromaClient).not.toHaveBeenCalled();
});
it("honors an explicit cloud database name", () => {
new ChromaDB({
collectionName: "test-collection",
apiKey: "key-123",
tenant: "tenant-abc",
database: "custom-db",
} as any);
expect(__mocks__.CloudClient).toHaveBeenCalledWith(
expect.objectContaining({ database: "custom-db" }),
);
});
it("accepts a pre-built client via config.client", async () => {
const fakeClient = {
getOrCreateCollection: __mocks__.getOrCreateCollection,
deleteCollection: __mocks__.deleteCollection,
};
const db = new ChromaDB({
collectionName: "test-collection",
client: fakeClient,
} as any);
await db.initialize();
expect(__mocks__.ChromaClient).not.toHaveBeenCalled();
expect(__mocks__.CloudClient).not.toHaveBeenCalled();
expect(__mocks__.getOrCreateCollection).toHaveBeenCalled();
});
});
describe("initialize", () => {
it("creates the collection with embeddingFunction null (mem0 supplies embeddings)", async () => {
await initDb();
expect(__mocks__.getOrCreateCollection).toHaveBeenCalledWith({
name: "test-collection",
embeddingFunction: null,
});
});
it("also creates the migrations collection", async () => {
await initDb();
expect(__mocks__.getOrCreateCollection).toHaveBeenCalledWith({
name: "memory_migrations",
embeddingFunction: null,
});
});
});
describe("insert", () => {
it("adds records with ids, embeddings, and metadatas", async () => {
const db = await initDb();
await db.insert([[1, 2, 3]], ["id-1"], [{ text: "hello" }]);
expect(__mocks__.add).toHaveBeenCalledWith({
ids: ["id-1"],
embeddings: [[1, 2, 3]],
metadatas: [{ text: "hello" }],
});
});
});
describe("search", () => {
it("queries with embeddings, nResults, and where", async () => {
const db = await initDb();
await db.search([1, 2, 3], 10, { user_id: "alice" });
expect(__mocks__.query).toHaveBeenCalledWith({
queryEmbeddings: [[1, 2, 3]],
nResults: 10,
where: { user_id: { $eq: "alice" } },
});
});
it("maps a nested query response to VectorStoreResult with 1/(1+distance) scores", async () => {
__mocks__.query.mockResolvedValue({
ids: [["a", "b"]],
distances: [[0, 1]],
metadatas: [[{ k: "v1" }, { k: "v2" }]],
});
const db = await initDb();
const results = await db.search([1, 2, 3]);
expect(results).toEqual([
{ id: "a", payload: { k: "v1" }, score: 1 },
{ id: "b", payload: { k: "v2" }, score: 0.5 },
]);
});
it("returns [] when the query yields no matches", async () => {
__mocks__.query.mockResolvedValue({
ids: [[]],
distances: [[]],
metadatas: [[]],
});
const db = await initDb();
const results = await db.search([1, 2, 3]);
expect(results).toEqual([]);
});
it("passes where undefined when there are no filters", async () => {
const db = await initDb();
await db.search([1, 2, 3], 5);
expect(__mocks__.query).toHaveBeenCalledWith(
expect.objectContaining({ where: undefined }),
);
});
});
describe("get", () => {
it("fetches by id and returns the first parsed result", async () => {
__mocks__.get.mockResolvedValue({
ids: ["vec-1"],
metadatas: [{ text: "foo" }],
});
const db = await initDb();
const result = await db.get("vec-1");
expect(__mocks__.get).toHaveBeenCalledWith({ ids: ["vec-1"] });
expect(result).toEqual({ id: "vec-1", payload: { text: "foo" } });
});
it("returns null when the id is not found", async () => {
__mocks__.get.mockResolvedValue({ ids: [], metadatas: [] });
const db = await initDb();
const result = await db.get("missing");
expect(result).toBeNull();
});
});
describe("update", () => {
it("updates a single record with embedding and metadata", async () => {
const db = await initDb();
await db.update("vec-1", [1, 2, 3], { text: "updated" });
expect(__mocks__.update).toHaveBeenCalledWith({
ids: ["vec-1"],
embeddings: [[1, 2, 3]],
metadatas: [{ text: "updated" }],
});
});
});
describe("delete", () => {
it("deletes by id", async () => {
const db = await initDb();
await db.delete("vec-1");
expect(__mocks__.deleteFn).toHaveBeenCalledWith({ ids: ["vec-1"] });
});
});
describe("deleteCol", () => {
it("deletes the collection and resets its cached handle", async () => {
const db = await initDb();
await db.deleteCol();
expect(__mocks__.deleteCollection).toHaveBeenCalledWith({
name: "test-collection",
});
// Cached collection promise is cleared, so the next call re-creates it.
__mocks__.getOrCreateCollection.mockClear();
await db.search([1, 2, 3]);
expect(__mocks__.getOrCreateCollection).toHaveBeenCalledWith({
name: "test-collection",
embeddingFunction: null,
});
});
});
describe("list", () => {
it("gets with where and limit and returns [results, count]", async () => {
__mocks__.get.mockResolvedValue({
ids: ["a", "b"],
metadatas: [{ k: 1 }, { k: 2 }],
});
const db = await initDb();
const [results, count] = await db.list({ user_id: "alice" }, 50);
expect(__mocks__.get).toHaveBeenCalledWith({
where: { user_id: { $eq: "alice" } },
limit: 50,
});
expect(results).toHaveLength(2);
expect(count).toBe(2);
});
});
describe("getUserId", () => {
it("returns an existing user_id from the migrations collection", async () => {
__mocks__.get.mockResolvedValue({
ids: ["mig-1"],
metadatas: [{ user_id: "u-123" }],
});
const db = await initDb();
const uid = await db.getUserId();
expect(uid).toBe("u-123");
expect(__mocks__.add).not.toHaveBeenCalled();
});
it("generates and stores a new user_id when none exists", async () => {
__mocks__.get.mockResolvedValue({ ids: [], metadatas: [] });
const db = await initDb();
const uid = await db.getUserId();
expect(typeof uid).toBe("string");
expect(uid.length).toBeGreaterThan(0);
expect(__mocks__.add).toHaveBeenCalledWith(
expect.objectContaining({
embeddings: [[0]],
metadatas: [{ user_id: uid }],
}),
);
});
});
describe("setUserId", () => {
it("upserts the user_id onto the existing migration marker", async () => {
__mocks__.get.mockResolvedValue({
ids: ["mig-1"],
metadatas: [{ user_id: "old" }],
});
const db = await initDb();
await db.setUserId("u-456");
expect(__mocks__.upsert).toHaveBeenCalledWith({
ids: ["mig-1"],
embeddings: [[0]],
metadatas: [{ user_id: "u-456" }],
});
});
});
describe("keywordSearch", () => {
it("returns null (Chroma has no keyword search)", async () => {
const db = await initDb();
const result = await db.keywordSearch();
expect(result).toBeNull();
});
});
describe("generateWhereClause", () => {
it("returns undefined for undefined and empty filters", () => {
expect(ChromaDB.generateWhereClause(undefined)).toBeUndefined();
expect(ChromaDB.generateWhereClause({})).toBeUndefined();
});
it("converts a single equality filter", () => {
expect(ChromaDB.generateWhereClause({ user_id: "alice" })).toEqual({
user_id: { $eq: "alice" },
});
});
it("combines multiple keys with $and", () => {
expect(
ChromaDB.generateWhereClause({ user_id: "alice", agent_id: "bot" }),
).toEqual({
$and: [{ user_id: { $eq: "alice" } }, { agent_id: { $eq: "bot" } }],
});
});
it("converts an array value to $in", () => {
expect(ChromaDB.generateWhereClause({ tags: ["x", "y"] })).toEqual({
tags: { $in: ["x", "y"] },
});
});
it("skips a wildcard filter", () => {
expect(ChromaDB.generateWhereClause({ user_id: "*" })).toBeUndefined();
});
it("maps comparison operators", () => {
expect(ChromaDB.generateWhereClause({ age: { gte: 18 } })).toEqual({
age: { $gte: 18 },
});
});
it("converts an $or block", () => {
expect(
ChromaDB.generateWhereClause({ $or: [{ a: "x" }, { b: "y" }] }),
).toEqual({ $or: [{ a: { $eq: "x" } }, { b: { $eq: "y" } }] });
});
it("collapses a single-branch $or", () => {
expect(ChromaDB.generateWhereClause({ $or: [{ a: "x" }] })).toEqual({
a: { $eq: "x" },
});
});
it("negates a single-field $not condition", () => {
expect(ChromaDB.generateWhereClause({ $not: [{ a: "x" }] })).toEqual({
a: { $ne: "x" },
});
});
it("applies De Morgan to a multi-field $not condition", () => {
// NOT(a=x AND b=y) is (a!=x) OR (b!=y)
expect(
ChromaDB.generateWhereClause({ $not: [{ a: "x", b: "y" }] }),
).toEqual({ $or: [{ a: { $ne: "x" } }, { b: { $ne: "y" } }] });
});
it("negates a $not comparison operator", () => {
expect(
ChromaDB.generateWhereClause({ $not: [{ age: { gt: 18 } }] }),
).toEqual({ age: { $lte: 18 } });
});
});
+3
View File
@@ -18,6 +18,7 @@ import { Embedder } from "../embeddings/base";
import { LLM } from "../llms/base";
import { VectorStore } from "../vector_stores/base";
import { Qdrant } from "../vector_stores/qdrant";
import { ChromaDB } from "../vector_stores/chroma";
import { VectorizeDB } from "../vector_stores/vectorize";
import { RedisDB } from "../vector_stores/redis";
import { ValkeyDB } from "../vector_stores/valkey";
@@ -133,6 +134,8 @@ export class VectorStoreFactory {
return new MemoryVectorStore(config);
case "qdrant":
return new Qdrant(config as any);
case "chroma":
return new ChromaDB(config as any);
case "redis":
return new RedisDB(config as any);
case "valkey":
+370
View File
@@ -0,0 +1,370 @@
import { ChromaClient, CloudClient } from "chromadb";
import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
interface ChromaConfig extends VectorStoreConfig {
/** Pre-configured ChromaDB client instance. */
client?: ChromaClient | CloudClient;
collectionName: string;
/** Host address for a ChromaDB server (defaults to the client default). */
host?: string;
/** Port for a ChromaDB server. */
port?: number;
/** Whether to use SSL when connecting to a ChromaDB server. */
ssl?: boolean;
/** Path for a local ChromaDB server. */
path?: string;
/** ChromaDB Cloud API key. */
apiKey?: string;
/** ChromaDB Cloud tenant ID. */
tenant?: string;
/** ChromaDB Cloud database name. */
database?: string;
}
const MIGRATIONS_COLLECTION = "memory_migrations";
/**
* ChromaDB vector store provider.
*
* Mirrors the Python SDK's `mem0.vector_stores.chroma.ChromaDB` behavior using
* the `chromadb` v3 JavaScript client. Embeddings are always supplied by mem0,
* so no embedding function is required on the collection.
*/
export class ChromaDB implements VectorStore {
private client: any;
private readonly collectionName: string;
private collectionPromise?: Promise<any>;
private migrationsPromise?: Promise<any>;
constructor(config: ChromaConfig) {
if (config.client) {
this.client = config.client;
} else if (config.apiKey && config.tenant) {
this.client = new CloudClient({
apiKey: config.apiKey,
tenant: config.tenant,
database: config.database || "mem0",
} as any);
} else {
const params: Record<string, any> = {};
if (config.host) params.host = config.host;
if (config.port) params.port = config.port;
if (config.ssl !== undefined) params.ssl = config.ssl;
if (config.path) params.path = config.path;
this.client = new ChromaClient(params as any);
}
this.collectionName = config.collectionName;
this.initialize().catch(console.error);
}
private async getCollection(): Promise<any> {
if (!this.collectionPromise) {
this.collectionPromise = this.client.getOrCreateCollection({
name: this.collectionName,
embeddingFunction: null,
});
}
return this.collectionPromise;
}
private async getMigrationsCollection(): Promise<any> {
if (!this.migrationsPromise) {
this.migrationsPromise = this.client.getOrCreateCollection({
name: MIGRATIONS_COLLECTION,
embeddingFunction: null,
});
}
return this.migrationsPromise;
}
private flatten(value: any): any[] {
if (Array.isArray(value) && value.length > 0 && Array.isArray(value[0])) {
return value[0];
}
return Array.isArray(value) ? value : [];
}
/** Parse a ChromaDB `get`/`query` response into VectorStoreResult objects. */
private parseOutput(data: any): VectorStoreResult[] {
const ids = this.flatten(data?.ids);
const distances = this.flatten(data?.distances);
const metadatas = this.flatten(data?.metadatas);
const length = Math.max(ids.length, metadatas.length);
const results: VectorStoreResult[] = [];
for (let i = 0; i < length; i++) {
const rawDistance = distances[i];
const score =
rawDistance !== undefined && rawDistance !== null
? 1.0 / (1.0 + rawDistance)
: undefined;
results.push({
id: String(ids[i]),
payload: (metadatas[i] as Record<string, any>) || {},
score,
});
}
return results;
}
async insert(
vectors: number[][],
ids: string[],
payloads: Record<string, any>[],
): Promise<void> {
const collection = await this.getCollection();
await collection.add({
ids,
embeddings: vectors,
metadatas: payloads as any,
});
}
async keywordSearch(): Promise<null> {
return null;
}
async search(
query: number[],
topK: number = 5,
filters?: SearchFilters,
): Promise<VectorStoreResult[]> {
const collection = await this.getCollection();
const where = ChromaDB.generateWhereClause(filters);
const results = await collection.query({
queryEmbeddings: [query],
nResults: topK,
where: where as any,
});
return this.parseOutput(results);
}
async get(vectorId: string): Promise<VectorStoreResult | null> {
const collection = await this.getCollection();
const results = await collection.get({ ids: [vectorId] });
const parsed = this.parseOutput(results);
return parsed.length > 0 ? parsed[0] : null;
}
async update(
vectorId: string,
vector: number[],
payload: Record<string, any>,
): Promise<void> {
const collection = await this.getCollection();
await collection.update({
ids: [vectorId],
embeddings: vector ? [vector] : undefined,
metadatas: payload ? [payload] : undefined,
} as any);
}
async delete(vectorId: string): Promise<void> {
const collection = await this.getCollection();
await collection.delete({ ids: [vectorId] });
}
async deleteCol(): Promise<void> {
await this.client.deleteCollection({ name: this.collectionName });
this.collectionPromise = undefined;
}
async list(
filters?: SearchFilters,
topK: number = 100,
): Promise<[VectorStoreResult[], number]> {
const collection = await this.getCollection();
const where = ChromaDB.generateWhereClause(filters);
const results = await collection.get({ where: where as any, limit: topK });
const parsed = this.parseOutput(results);
return [parsed, parsed.length];
}
private generateUUID(): string {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
/[xy]/g,
function (c) {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
},
);
}
async getUserId(): Promise<string> {
const collection = await this.getMigrationsCollection();
const result = await collection.get({ limit: 1 });
const ids = Array.isArray(result?.ids) ? result.ids : [];
const metadatas = Array.isArray(result?.metadatas) ? result.metadatas : [];
if (ids.length > 0 && metadatas[0]?.user_id) {
return String(metadatas[0].user_id);
}
const randomUserId =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
await collection.add({
ids: [this.generateUUID()],
embeddings: [[0]],
metadatas: [{ user_id: randomUserId }] as any,
});
return randomUserId;
}
async setUserId(userId: string): Promise<void> {
const collection = await this.getMigrationsCollection();
const result = await collection.get({ limit: 1 });
const ids = Array.isArray(result?.ids) ? result.ids : [];
const pointId = ids.length > 0 ? String(ids[0]) : this.generateUUID();
await collection.upsert({
ids: [pointId],
embeddings: [[0]],
metadatas: [{ user_id: userId }] as any,
});
}
async initialize(): Promise<void> {
await this.getCollection();
await this.getMigrationsCollection();
}
/** Convert a single field filter into a ChromaDB where condition. */
private static convertCondition(
key: string,
value: any,
): Record<string, any> | null {
// Wildcard - ChromaDB has no direct wildcard, so skip this filter.
if (value === "*") {
return null;
}
if (Array.isArray(value)) {
return { [key]: { $in: value } };
}
if (value !== null && typeof value === "object") {
const opMap: Record<string, string> = {
eq: "$eq",
ne: "$ne",
gt: "$gt",
gte: "$gte",
lt: "$lt",
lte: "$lte",
in: "$in",
nin: "$nin",
};
const condition: Record<string, any> = {};
for (const [op, val] of Object.entries(value)) {
if (op in opMap) {
condition[key] = { [opMap[op]]: val };
} else {
// contains/icontains and unknown operators fall back to equality.
condition[key] = { $eq: val };
}
}
return condition;
}
return { [key]: { $eq: value } };
}
/**
* Generate a properly formatted `where` clause for ChromaDB from mem0's
* universal filter format. Supports comparison operators plus $or/$not.
*/
static generateWhereClause(
filters?: SearchFilters,
): Record<string, any> | undefined {
if (!filters) {
return undefined;
}
const negateOp: Record<string, string> = {
eq: "ne",
ne: "eq",
gt: "lte",
gte: "lt",
lt: "gte",
lte: "gt",
in: "nin",
nin: "in",
};
const processed: any[] = [];
for (const [key, value] of Object.entries(filters)) {
if (key === "$or" || key === "OR") {
const orConditions: any[] = [];
for (const condition of value as any[]) {
const built: Record<string, any> = {};
for (const [subKey, subValue] of Object.entries(condition)) {
const converted = ChromaDB.convertCondition(subKey, subValue);
if (converted) Object.assign(built, converted);
}
if (Object.keys(built).length > 0) orConditions.push(built);
}
if (orConditions.length > 1) {
processed.push({ $or: orConditions });
} else if (orConditions.length === 1) {
processed.push(orConditions[0]);
}
} else if (key === "$not" || key === "NOT") {
// De Morgan: NOT(a AND b) is (NOT a) OR (NOT b), so the negated fields
// within one condition are combined with $or, and separate conditions
// are combined with $and. This mirrors the Python SDK's ChromaDB port.
const negatedPerGroup: any[] = [];
for (const condition of value as any[]) {
const negatedFields: any[] = [];
for (const [subKey, subValue] of Object.entries(condition)) {
if (subValue !== null && typeof subValue === "object") {
for (const [op, val] of Object.entries(subValue as any)) {
const neg = negateOp[op];
if (neg) {
const converted = ChromaDB.convertCondition(subKey, {
[neg]: val,
});
if (converted) negatedFields.push(converted);
}
}
} else {
const converted = ChromaDB.convertCondition(subKey, {
ne: subValue,
});
if (converted) negatedFields.push(converted);
}
}
if (negatedFields.length > 1) {
negatedPerGroup.push({ $or: negatedFields });
} else if (negatedFields.length === 1) {
negatedPerGroup.push(negatedFields[0]);
}
}
if (negatedPerGroup.length > 1) {
processed.push({ $and: negatedPerGroup });
} else if (negatedPerGroup.length === 1) {
processed.push(negatedPerGroup[0]);
}
} else {
const converted = ChromaDB.convertCondition(key, value);
if (converted) processed.push(converted);
}
}
if (processed.length === 0) {
return undefined;
}
if (processed.length === 1) {
return processed[0];
}
return { $and: processed };
}
}
+1
View File
@@ -34,6 +34,7 @@ const external = [
"mongodb",
"@opensearch-project/opensearch",
"@elastic/elasticsearch",
"chromadb",
];
const define = {