refactor(ts-sdk): lazy-load optional provider SDKs so importing mem0ai/oss never requires them (#6280)

This commit is contained in:
Kartik
2026-07-13 15:23:12 +05:30
committed by GitHub
parent 17836748d7
commit d89793b666
18 changed files with 726 additions and 312 deletions
+48
View File
@@ -168,6 +168,54 @@
}, },
"@aws-sdk/client-neptune-graph": { "@aws-sdk/client-neptune-graph": {
"optional": true "optional": true
},
"chromadb": {
"optional": true
},
"mongodb": {
"optional": true
},
"weaviate-client": {
"optional": true
},
"cassandra-driver": {
"optional": true
},
"@pinecone-database/pinecone": {
"optional": true
},
"@aws-sdk/client-s3vectors": {
"optional": true
},
"@turbopuffer/turbopuffer": {
"optional": true
},
"@upstash/vector": {
"optional": true
},
"@elastic/elasticsearch": {
"optional": true
},
"@opensearch-project/opensearch": {
"optional": true
},
"cohere-ai": {
"optional": true
},
"zeroentropy": {
"optional": true
},
"fastembed": {
"optional": true
},
"@google-cloud/aiplatform": {
"optional": true
},
"@huggingface/transformers": {
"optional": true
},
"iovalkey": {
"optional": true
} }
}, },
"engines": { "engines": {
+37 -14
View File
@@ -1,19 +1,27 @@
import { EmbeddingModel, FlagEmbedding } from "fastembed"; import type { FlagEmbedding } from "fastembed";
import { Embedder } from "./base"; import { Embedder } from "./base";
import { EmbeddingConfig } from "../types"; import { EmbeddingConfig } from "../types";
const DEFAULT_MODEL = EmbeddingModel.BGESmallENV15; // FastEmbed only ships a fixed set of ONNX models (fastembed's `EmbeddingModel`
type FastEmbedModel = Exclude<EmbeddingModel, EmbeddingModel.CUSTOM>; // enum, minus CUSTOM). Mirrored here as literals so an invalid model name can
// be rejected synchronously in the constructor — with a clear message instead
// FastEmbed only ships a fixed set of ONNX models. Keep the list handy so we can // of a `FlagEmbedding.init()` download error — without eagerly importing the
// reject unknown model names up front with a clear message instead of letting // optional 'fastembed' package just to read its enum. Keep in sync if
// FlagEmbedding.init fail later with an opaque download error. // fastembed adds a model.
const SUPPORTED_MODELS = Object.values(EmbeddingModel).filter( const SUPPORTED_MODELS = [
(model) => model !== EmbeddingModel.CUSTOM, "fast-all-MiniLM-L6-v2",
) as FastEmbedModel[]; "fast-bge-base-en",
"fast-bge-base-en-v1.5",
"fast-bge-small-en",
"fast-bge-small-en-v1.5",
"fast-bge-small-zh-v1.5",
"fast-multilingual-e5-large",
] as const;
type FastEmbedModel = (typeof SUPPORTED_MODELS)[number];
const DEFAULT_MODEL: FastEmbedModel = "fast-bge-small-en-v1.5";
export class FastEmbedEmbedder implements Embedder { export class FastEmbedEmbedder implements Embedder {
private modelName: FastEmbedModel; private readonly modelName: FastEmbedModel;
private embeddingModel?: Promise<FlagEmbedding>; private embeddingModel?: Promise<FlagEmbedding>;
constructor(config: EmbeddingConfig) { constructor(config: EmbeddingConfig) {
@@ -32,9 +40,7 @@ export class FastEmbedEmbedder implements Embedder {
private getEmbeddingModel(): Promise<FlagEmbedding> { private getEmbeddingModel(): Promise<FlagEmbedding> {
if (!this.embeddingModel) { if (!this.embeddingModel) {
this.embeddingModel = FlagEmbedding.init({ this.embeddingModel = this.initEmbeddingModel().catch((error) => {
model: this.modelName,
}).catch((error) => {
this.embeddingModel = undefined; this.embeddingModel = undefined;
throw error; throw error;
}); });
@@ -43,6 +49,23 @@ export class FastEmbedEmbedder implements Embedder {
return this.embeddingModel; return this.embeddingModel;
} }
/**
* Lazily import the optional `fastembed` peer and initialize the model, so
* consumers that never touch FastEmbed don't need it installed.
*/
private async initEmbeddingModel(): Promise<FlagEmbedding> {
let sdk: any;
try {
sdk = await import("fastembed");
} catch {
throw new Error(
"The 'fastembed' package is required to use the FastEmbed embedder. Install it with: npm install fastembed",
);
}
return sdk.FlagEmbedding.init({ model: this.modelName });
}
private normalizeInput(text: string): string { private normalizeInput(text: string): string {
return text.replace(/\n/g, " "); return text.replace(/\n/g, " ");
} }
+33 -5
View File
@@ -1,11 +1,12 @@
import { CohereClient } from "cohere-ai";
import { RerankerConfig } from "../types"; import { RerankerConfig } from "../types";
import { Reranker, RerankResult } from "./base"; import { Reranker, RerankResult } from "./base";
const DEFAULT_MODEL = "rerank-v3.5"; const DEFAULT_MODEL = "rerank-v3.5";
export class CohereReranker implements Reranker { export class CohereReranker implements Reranker {
private client: CohereClient; private clientInstance?: any;
private clientPromise?: Promise<any>;
private readonly apiKey: string;
private model: string; private model: string;
private topK?: number; private topK?: number;
private returnDocuments: boolean; private returnDocuments: boolean;
@@ -18,13 +19,39 @@ export class CohereReranker implements Reranker {
"Cohere API key is required. Set COHERE_API_KEY environment variable or pass apiKey in config.", "Cohere API key is required. Set COHERE_API_KEY environment variable or pass apiKey in config.",
); );
} }
this.client = new CohereClient({ token: apiKey }); this.apiKey = apiKey;
this.model = config.model || DEFAULT_MODEL; this.model = config.model || DEFAULT_MODEL;
this.topK = config.topK; this.topK = config.topK;
this.returnDocuments = config.returnDocuments ?? false; this.returnDocuments = config.returnDocuments ?? false;
this.maxChunksPerDoc = config.maxChunksPerDoc; this.maxChunksPerDoc = config.maxChunksPerDoc;
} }
/**
* Lazily construct (or reuse) the Cohere client, importing the optional
* `cohere-ai` peer only when the reranker is first used so consumers that
* never touch Cohere don't need it installed.
*/
private async getClient(): Promise<any> {
if (this.clientInstance) return this.clientInstance;
if (!this.clientPromise) {
this.clientPromise = this.createClient();
}
this.clientInstance = await this.clientPromise;
return this.clientInstance;
}
private async createClient(): Promise<any> {
let sdk: any;
try {
sdk = await import("cohere-ai");
} catch {
throw new Error(
"The 'cohere-ai' package is required to use the Cohere reranker. Install it with: npm install cohere-ai",
);
}
return new sdk.CohereClient({ token: this.apiKey });
}
async rerank( async rerank(
query: string, query: string,
documents: string[], documents: string[],
@@ -33,7 +60,8 @@ export class CohereReranker implements Reranker {
if (documents.length === 0) return []; if (documents.length === 0) return [];
try { try {
const response = await this.client.rerank({ const client = await this.getClient();
const response = await client.rerank({
model: this.model, model: this.model,
query, query,
documents, documents,
@@ -42,7 +70,7 @@ export class CohereReranker implements Reranker {
maxChunksPerDoc: this.maxChunksPerDoc, maxChunksPerDoc: this.maxChunksPerDoc,
}); });
return response.results.map((result) => ({ return response.results.map((result: any) => ({
index: result.index, index: result.index,
rerankScore: result.relevanceScore, rerankScore: result.relevanceScore,
})); }));
+33 -5
View File
@@ -1,11 +1,12 @@
import { ZeroEntropy } from "zeroentropy";
import { RerankerConfig } from "../types"; import { RerankerConfig } from "../types";
import { Reranker, RerankResult } from "./base"; import { Reranker, RerankResult } from "./base";
const DEFAULT_MODEL = "zerank-1"; const DEFAULT_MODEL = "zerank-1";
export class ZeroEntropyReranker implements Reranker { export class ZeroEntropyReranker implements Reranker {
private client: ZeroEntropy; private clientInstance?: any;
private clientPromise?: Promise<any>;
private readonly apiKey: string;
private model: string; private model: string;
private topK?: number; private topK?: number;
@@ -16,11 +17,37 @@ export class ZeroEntropyReranker implements Reranker {
"Zero Entropy API key is required. Set ZERO_ENTROPY_API_KEY environment variable or pass apiKey in config.", "Zero Entropy API key is required. Set ZERO_ENTROPY_API_KEY environment variable or pass apiKey in config.",
); );
} }
this.client = new ZeroEntropy({ apiKey }); this.apiKey = apiKey;
this.model = config.model || DEFAULT_MODEL; this.model = config.model || DEFAULT_MODEL;
this.topK = config.topK; this.topK = config.topK;
} }
/**
* Lazily construct (or reuse) the ZeroEntropy client, importing the
* optional `zeroentropy` peer only when the reranker is first used so
* consumers that never touch ZeroEntropy don't need it installed.
*/
private async getClient(): Promise<any> {
if (this.clientInstance) return this.clientInstance;
if (!this.clientPromise) {
this.clientPromise = this.createClient();
}
this.clientInstance = await this.clientPromise;
return this.clientInstance;
}
private async createClient(): Promise<any> {
let sdk: any;
try {
sdk = await import("zeroentropy");
} catch {
throw new Error(
"The 'zeroentropy' package is required to use the ZeroEntropy reranker. Install it with: npm install zeroentropy",
);
}
return new sdk.ZeroEntropy({ apiKey: this.apiKey });
}
async rerank( async rerank(
query: string, query: string,
documents: string[], documents: string[],
@@ -29,13 +56,14 @@ export class ZeroEntropyReranker implements Reranker {
if (documents.length === 0) return []; if (documents.length === 0) return [];
try { try {
const response = await this.client.models.rerank({ const client = await this.getClient();
const response = await client.models.rerank({
model: this.model, model: this.model,
query, query,
documents, documents,
}); });
const scored = response.results.map((result) => ({ const scored: RerankResult[] = response.results.map((result: any) => ({
index: result.index, index: result.index,
rerankScore: result.relevance_score, rerankScore: result.relevance_score,
})); }));
+47 -61
View File
@@ -1,60 +1,42 @@
// jest.mock is hoisted above variable declarations, so shared mock functions // The provider imports `chromadb` lazily (await import) only on first use, so
// are attached to the module-level `__mocks__` object populated inside the // the jest.mock factory no longer runs at module-eval time. Create the shared
// factory. The hoisted mock reaches them through this stable reference. // mock handles at module top-level (not inside the factory) so `beforeEach` can
// reach them before the lazy import has fired; the factory, which runs on first
// use, just returns references to them.
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 __mocks__: { const collectionHandle = { add, query, get, update, upsert, delete: deleteFn };
add: jest.Mock; const getOrCreateCollection = jest.fn().mockResolvedValue(collectionHandle);
query: jest.Mock; const deleteCollection = jest.fn().mockResolvedValue(undefined);
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 clientImpl = () => ({ getOrCreateCollection, deleteCollection });
const add = jest.fn().mockResolvedValue(undefined); const ChromaClient = jest.fn().mockImplementation(clientImpl);
const query = jest const CloudClient = jest.fn().mockImplementation(clientImpl);
.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 = { const __mocks__ = {
add, add,
query, query,
get, get,
update, update,
upsert, upsert,
delete: deleteFn, deleteFn,
}; getOrCreateCollection,
const getOrCreateCollection = jest.fn().mockResolvedValue(collectionHandle); deleteCollection,
const deleteCollection = jest.fn().mockResolvedValue(undefined); ChromaClient,
CloudClient,
};
const clientImpl = () => ({ getOrCreateCollection, deleteCollection }); jest.mock("chromadb", () => ({
const ChromaClient = jest.fn().mockImplementation(clientImpl); ChromaClient: __mocks__.ChromaClient,
const CloudClient = jest.fn().mockImplementation(clientImpl); CloudClient: __mocks__.CloudClient,
}));
Object.assign(__mocks__, {
add,
query,
get,
update,
upsert,
deleteFn,
getOrCreateCollection,
deleteCollection,
ChromaClient,
CloudClient,
});
return { ChromaClient, CloudClient };
});
import { ChromaDB } from "../vector_stores/chroma"; import { ChromaDB } from "../vector_stores/chroma";
import { VectorStoreFactory } from "../utils/factory"; import { VectorStoreFactory } from "../utils/factory";
@@ -127,8 +109,10 @@ describe("VectorStoreFactory", () => {
}); });
describe("Constructor", () => { describe("Constructor", () => {
it("builds a local ChromaClient with host and port", () => { // The client is built lazily on first use, so trigger initialize() before
makeDb({ host: "localhost", port: 8000 }); // asserting how it was constructed.
it("builds a local ChromaClient with host and port", async () => {
await initDb({ host: "localhost", port: 8000 });
expect(__mocks__.ChromaClient).toHaveBeenCalledWith({ expect(__mocks__.ChromaClient).toHaveBeenCalledWith({
host: "localhost", host: "localhost",
port: 8000, port: 8000,
@@ -136,8 +120,8 @@ describe("Constructor", () => {
expect(__mocks__.CloudClient).not.toHaveBeenCalled(); expect(__mocks__.CloudClient).not.toHaveBeenCalled();
}); });
it("passes ssl and path through to ChromaClient when provided", () => { it("passes ssl and path through to ChromaClient when provided", async () => {
makeDb({ host: "example.com", port: 443, ssl: true, path: "/db" }); await initDb({ host: "example.com", port: 443, ssl: true, path: "/db" });
expect(__mocks__.ChromaClient).toHaveBeenCalledWith({ expect(__mocks__.ChromaClient).toHaveBeenCalledWith({
host: "example.com", host: "example.com",
port: 443, port: 443,
@@ -146,12 +130,13 @@ describe("Constructor", () => {
}); });
}); });
it("builds a CloudClient when apiKey and tenant are set", () => { it("builds a CloudClient when apiKey and tenant are set", async () => {
new ChromaDB({ const db = new ChromaDB({
collectionName: "test-collection", collectionName: "test-collection",
apiKey: "key-123", apiKey: "key-123",
tenant: "tenant-abc", tenant: "tenant-abc",
} as any); } as any);
await db.initialize();
expect(__mocks__.CloudClient).toHaveBeenCalledWith({ expect(__mocks__.CloudClient).toHaveBeenCalledWith({
apiKey: "key-123", apiKey: "key-123",
tenant: "tenant-abc", tenant: "tenant-abc",
@@ -160,13 +145,14 @@ describe("Constructor", () => {
expect(__mocks__.ChromaClient).not.toHaveBeenCalled(); expect(__mocks__.ChromaClient).not.toHaveBeenCalled();
}); });
it("honors an explicit cloud database name", () => { it("honors an explicit cloud database name", async () => {
new ChromaDB({ const db = new ChromaDB({
collectionName: "test-collection", collectionName: "test-collection",
apiKey: "key-123", apiKey: "key-123",
tenant: "tenant-abc", tenant: "tenant-abc",
database: "custom-db", database: "custom-db",
} as any); } as any);
await db.initialize();
expect(__mocks__.CloudClient).toHaveBeenCalledWith( expect(__mocks__.CloudClient).toHaveBeenCalledWith(
expect.objectContaining({ database: "custom-db" }), expect.objectContaining({ database: "custom-db" }),
); );
+49 -60
View File
@@ -3,73 +3,62 @@
// the module-level `__mocks__` object that is populated inside the factory so // the module-level `__mocks__` object that is populated inside the factory so
// that the hoisted mock can reach them via a stable reference. // that the hoisted mock can reach them via a stable reference.
const __mocks__: { // The provider imports `@pinecone-database/pinecone` lazily (await import) only
upsert: jest.Mock; // on first use, so the jest.mock factory no longer runs at module-eval time.
query: jest.Mock; // Create the shared mock handles at module top-level (not inside the factory)
fetch: jest.Mock; // so `beforeEach` can reach them before the lazy import has fired; the factory,
deleteOne: jest.Mock; // which runs on first use, just returns a reference to the Pinecone class mock.
namespace: jest.Mock; const upsert = jest.fn().mockResolvedValue(undefined);
describeIndexStats: jest.Mock; const query = jest.fn().mockResolvedValue({ matches: [] });
index: jest.Mock; const fetch = jest.fn().mockResolvedValue({ records: {} });
listIndexes: jest.Mock; const deleteOne = jest.fn().mockResolvedValue(undefined);
createIndex: jest.Mock;
deleteIndex: jest.Mock;
Pinecone: jest.Mock;
} = {} as any;
jest.mock("@pinecone-database/pinecone", () => { const nsHandle = { upsert, query, fetch, deleteOne };
// These are created fresh inside the factory so hoisting is safe. const namespace = jest.fn().mockReturnValue(nsHandle);
const upsert = jest.fn().mockResolvedValue(undefined);
const query = jest.fn().mockResolvedValue({ matches: [] });
const fetch = jest.fn().mockResolvedValue({ records: {} });
const deleteOne = jest.fn().mockResolvedValue(undefined);
const nsHandle = { upsert, query, fetch, deleteOne }; const describeIndexStats = jest
const namespace = jest.fn().mockReturnValue(nsHandle); .fn()
.mockResolvedValue({ totalRecordCount: 0, namespaces: {} });
const describeIndexStats = jest const indexHandle = {
.fn() namespace,
.mockResolvedValue({ totalRecordCount: 0, namespaces: {} }); describeIndexStats,
// expose ops directly for the no-namespace path
upsert,
query,
fetch,
deleteOne,
};
const index = jest.fn().mockReturnValue(indexHandle);
const indexHandle = { const listIndexes = jest.fn().mockResolvedValue({ indexes: [] });
namespace, const createIndex = jest.fn().mockResolvedValue(undefined);
describeIndexStats, const deleteIndex = jest.fn().mockResolvedValue(undefined);
// expose ops directly for the no-namespace path
upsert,
query,
fetch,
deleteOne,
};
const index = jest.fn().mockReturnValue(indexHandle);
const listIndexes = jest.fn().mockResolvedValue({ indexes: [] }); const Pinecone = jest.fn().mockImplementation(() => ({
const createIndex = jest.fn().mockResolvedValue(undefined); listIndexes,
const deleteIndex = jest.fn().mockResolvedValue(undefined); createIndex,
deleteIndex,
index,
}));
const Pinecone = jest.fn().mockImplementation(() => ({ const __mocks__ = {
listIndexes, upsert,
createIndex, query,
deleteIndex, fetch,
index, deleteOne,
})); namespace,
describeIndexStats,
index,
listIndexes,
createIndex,
deleteIndex,
Pinecone,
};
// Populate the shared reference so tests can reach the mocks. jest.mock("@pinecone-database/pinecone", () => ({
Object.assign(__mocks__, { Pinecone: __mocks__.Pinecone,
upsert, }));
query,
fetch,
deleteOne,
namespace,
describeIndexStats,
index,
listIndexes,
createIndex,
deleteIndex,
Pinecone,
});
return { Pinecone };
});
import { PineconeDB } from "../vector_stores/pinecone"; import { PineconeDB } from "../vector_stores/pinecone";
import { VectorStoreFactory } from "../utils/factory"; import { VectorStoreFactory } from "../utils/factory";
+24 -8
View File
@@ -1,4 +1,3 @@
import cassandra from "cassandra-driver";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
@@ -18,7 +17,9 @@ interface CassandraConfig extends VectorStoreConfig {
protocolVersion?: number; protocolVersion?: number;
loadBalancingPolicy?: any; loadBalancingPolicy?: any;
client?: CassandraClientLike; client?: CassandraClientLike;
driver?: typeof cassandra; /** Pre-configured Cassandra driver module (typed as `any` to keep the
* optional driver's types out of the published type declarations). */
driver?: any;
} }
interface CassandraClientLike { interface CassandraClientLike {
@@ -38,7 +39,7 @@ interface CassandraVector {
export class CassandraDB implements VectorStore { export class CassandraDB implements VectorStore {
private static readonly PAGE_SIZE = 500; private static readonly PAGE_SIZE = 500;
private readonly driver: typeof cassandra; private readonly driver?: any;
private readonly contactPoints?: string[]; private readonly contactPoints?: string[];
private readonly port: number; private readonly port: number;
private readonly username?: string; private readonly username?: string;
@@ -54,7 +55,7 @@ export class CassandraDB implements VectorStore {
private _initPromise?: Promise<void>; private _initPromise?: Promise<void>;
constructor(config: CassandraConfig) { constructor(config: CassandraConfig) {
this.driver = config.driver || cassandra; this.driver = config.driver;
this.contactPoints = config.contactPoints; this.contactPoints = config.contactPoints;
this.port = config.port || 9042; this.port = config.port || 9042;
this.username = config.username; this.username = config.username;
@@ -85,7 +86,7 @@ export class CassandraDB implements VectorStore {
private async _doInitialize(): Promise<void> { private async _doInitialize(): Promise<void> {
if (!this.client) { if (!this.client) {
this.client = this.createClient(); this.client = await this.createClient();
} }
if (typeof this.client.connect === "function") { if (typeof this.client.connect === "function") {
await this.client.connect(); await this.client.connect();
@@ -326,7 +327,8 @@ export class CassandraDB implements VectorStore {
); );
} }
private createClient(): CassandraClientLike { private async createClient(): Promise<CassandraClientLike> {
const driver = this.driver ?? (await this.loadDriver());
const clientConfig: Record<string, any> = {}; const clientConfig: Record<string, any> = {};
if (this.secureConnectBundle) { if (this.secureConnectBundle) {
@@ -363,13 +365,27 @@ export class CassandraDB implements VectorStore {
}; };
} }
if (this.username && this.password) { if (this.username && this.password) {
clientConfig.authProvider = new this.driver.auth.PlainTextAuthProvider( clientConfig.authProvider = new driver.auth.PlainTextAuthProvider(
this.username, this.username,
this.password, this.password,
); );
} }
return new this.driver.Client(clientConfig); return new driver.Client(clientConfig);
}
// Loaded dynamically: cassandra-driver is an optional peer dependency, so a static
// value import would break `import { Memory } from "mem0ai/oss"` for everyone else.
private async loadDriver(): Promise<any> {
let sdk: any;
try {
sdk = await import("cassandra-driver");
} catch {
throw new Error(
"The 'cassandra-driver' package is required to use the Cassandra vector store. Install it with: npm install cassandra-driver",
);
}
return sdk.default ?? sdk;
} }
private validateIdentifier(name: string, label: string): string { private validateIdentifier(name: string, label: string): string {
+56 -22
View File
@@ -1,4 +1,4 @@
import { ChromaClient, CloudClient } from "chromadb"; import type { ChromaClient, CloudClient } from "chromadb";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
@@ -32,36 +32,68 @@ const MIGRATIONS_COLLECTION = "memory_migrations";
* so no embedding function is required on the collection. * so no embedding function is required on the collection.
*/ */
export class ChromaDB implements VectorStore { export class ChromaDB implements VectorStore {
private client: any; private clientInstance?: any;
private clientPromise?: Promise<any>;
private readonly config: ChromaConfig;
private readonly collectionName: string; private readonly collectionName: string;
private collectionPromise?: Promise<any>; private collectionPromise?: Promise<any>;
private migrationsPromise?: Promise<any>; private migrationsPromise?: Promise<any>;
constructor(config: ChromaConfig) { constructor(config: ChromaConfig) {
if (config.client) { this.config = config;
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.collectionName = config.collectionName;
this.initialize().catch(console.error); this.initialize().catch(console.error);
} }
/**
* Lazily construct (or reuse) the ChromaDB client, importing the optional
* `chromadb` peer only when the store is first used so consumers that never
* touch Chroma don't need it installed.
*/
private async getClient(): Promise<any> {
if (this.clientInstance) return this.clientInstance;
if (!this.clientPromise) {
this.clientPromise = this.createClient();
}
this.clientInstance = await this.clientPromise;
return this.clientInstance;
}
private async createClient(): Promise<any> {
const config = this.config;
if (config.client) {
return config.client;
}
let sdk: any;
try {
sdk = await import("chromadb");
} catch {
throw new Error(
"The 'chromadb' package is required to use the Chroma vector store. Install it with: npm install chromadb",
);
}
if (config.apiKey && config.tenant) {
return new sdk.CloudClient({
apiKey: config.apiKey,
tenant: config.tenant,
database: config.database || "mem0",
} as any);
}
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;
return new sdk.ChromaClient(params as any);
}
private async getCollection(): Promise<any> { private async getCollection(): Promise<any> {
if (!this.collectionPromise) { if (!this.collectionPromise) {
this.collectionPromise = this.client.getOrCreateCollection({ const client = await this.getClient();
this.collectionPromise = client.getOrCreateCollection({
name: this.collectionName, name: this.collectionName,
embeddingFunction: null, embeddingFunction: null,
}); });
@@ -71,7 +103,8 @@ export class ChromaDB implements VectorStore {
private async getMigrationsCollection(): Promise<any> { private async getMigrationsCollection(): Promise<any> {
if (!this.migrationsPromise) { if (!this.migrationsPromise) {
this.migrationsPromise = this.client.getOrCreateCollection({ const client = await this.getClient();
this.migrationsPromise = client.getOrCreateCollection({
name: MIGRATIONS_COLLECTION, name: MIGRATIONS_COLLECTION,
embeddingFunction: null, embeddingFunction: null,
}); });
@@ -170,7 +203,8 @@ export class ChromaDB implements VectorStore {
} }
async deleteCol(): Promise<void> { async deleteCol(): Promise<void> {
await this.client.deleteCollection({ name: this.collectionName }); const client = await this.getClient();
await client.deleteCollection({ name: this.collectionName });
this.collectionPromise = undefined; this.collectionPromise = undefined;
} }
@@ -1,9 +1,11 @@
import { Client } from "@elastic/elasticsearch"; import type { Client } from "@elastic/elasticsearch";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
interface ElasticsearchConfig extends VectorStoreConfig { interface ElasticsearchConfig extends VectorStoreConfig {
client?: Client; /** Pre-configured Elasticsearch client instance (typed as `any` to keep the
* optional driver's types out of the published type declarations). */
client?: any;
host?: string; host?: string;
port?: number; port?: number;
cloudId?: string; cloudId?: string;
@@ -41,17 +43,28 @@ function validateFilter(key: string, value: unknown): void {
} }
export class ElasticsearchDB implements VectorStore { export class ElasticsearchDB implements VectorStore {
private client: Client; private client!: Client;
private readonly config: ElasticsearchConfig;
private readonly collectionName: string; private readonly collectionName: string;
private readonly dimension: number; private readonly dimension: number;
private readonly autoCreateIndex: boolean; private readonly autoCreateIndex: boolean;
private _initPromise?: Promise<void>; private _initPromise?: Promise<void>;
constructor(config: ElasticsearchConfig) { constructor(config: ElasticsearchConfig) {
this.config = config;
this.collectionName = config.collectionName; this.collectionName = config.collectionName;
this.dimension = config.dimension || config.embeddingModelDims || 1536; this.dimension = config.dimension || config.embeddingModelDims || 1536;
this.autoCreateIndex = config.autoCreateIndex !== false; this.autoCreateIndex = config.autoCreateIndex !== false;
this.initialize().catch(console.error);
}
// The client is created lazily on first initialize() so the optional
// `@elastic/elasticsearch` peer is only loaded when the store is actually used.
private async ensureClient(): Promise<void> {
if (this.client) return;
const config = this.config;
if (config.client) { if (config.client) {
this.client = config.client; this.client = config.client;
} else { } else {
@@ -87,10 +100,17 @@ export class ElasticsearchDB implements VectorStore {
params.headers = config.headers; params.headers = config.headers;
} }
this.client = new Client(params); let sdk: any;
} try {
sdk = await import("@elastic/elasticsearch");
} catch {
throw new Error(
"The '@elastic/elasticsearch' package is required to use the Elasticsearch vector store. Install it with: npm install @elastic/elasticsearch",
);
}
this.initialize().catch(console.error); this.client = new sdk.Client(params);
}
} }
async initialize(): Promise<void> { async initialize(): Promise<void> {
@@ -101,6 +121,7 @@ export class ElasticsearchDB implements VectorStore {
} }
private async _doInitialize(): Promise<void> { private async _doInitialize(): Promise<void> {
await this.ensureClient();
try { try {
if (this.autoCreateIndex) { if (this.autoCreateIndex) {
await this.ensureIndex(this.collectionName, this.dimension); await this.ensureIndex(this.collectionName, this.dimension);
@@ -149,6 +170,7 @@ export class ElasticsearchDB implements VectorStore {
ids: string[], ids: string[],
payloads: Record<string, any>[], payloads: Record<string, any>[],
): Promise<void> { ): Promise<void> {
await this.initialize();
const operations: any[] = []; const operations: any[] = [];
for (let i = 0; i < vectors.length; i++) { for (let i = 0; i < vectors.length; i++) {
operations.push( operations.push(
@@ -165,6 +187,7 @@ export class ElasticsearchDB implements VectorStore {
topK: number = 5, topK: number = 5,
filters?: SearchFilters, filters?: SearchFilters,
): Promise<VectorStoreResult[]> { ): Promise<VectorStoreResult[]> {
await this.initialize();
const searchBody: Record<string, any> = { const searchBody: Record<string, any> = {
knn: { knn: {
field: "vector", field: "vector",
@@ -195,6 +218,7 @@ export class ElasticsearchDB implements VectorStore {
} }
async get(vectorId: string): Promise<VectorStoreResult | null> { async get(vectorId: string): Promise<VectorStoreResult | null> {
await this.initialize();
try { try {
const response = await this.client.get({ const response = await this.client.get({
index: this.collectionName, index: this.collectionName,
@@ -215,6 +239,7 @@ export class ElasticsearchDB implements VectorStore {
vector: number[], vector: number[],
payload: Record<string, any>, payload: Record<string, any>,
): Promise<void> { ): Promise<void> {
await this.initialize();
const doc: Record<string, any> = {}; const doc: Record<string, any> = {};
if (vector) doc.vector = vector; if (vector) doc.vector = vector;
if (payload) doc.metadata = payload; if (payload) doc.metadata = payload;
@@ -227,6 +252,7 @@ export class ElasticsearchDB implements VectorStore {
} }
async delete(vectorId: string): Promise<void> { async delete(vectorId: string): Promise<void> {
await this.initialize();
await this.client.delete({ await this.client.delete({
index: this.collectionName, index: this.collectionName,
id: vectorId, id: vectorId,
@@ -234,6 +260,7 @@ export class ElasticsearchDB implements VectorStore {
} }
async deleteCol(): Promise<void> { async deleteCol(): Promise<void> {
await this.initialize();
await this.client.indices.delete({ index: this.collectionName }); await this.client.indices.delete({ index: this.collectionName });
} }
@@ -241,6 +268,7 @@ export class ElasticsearchDB implements VectorStore {
filters?: SearchFilters, filters?: SearchFilters,
topK: number = 100, topK: number = 100,
): Promise<[VectorStoreResult[], number]> { ): Promise<[VectorStoreResult[], number]> {
await this.initialize();
const query: Record<string, any> = { query: { match_all: {} } }; const query: Record<string, any> = { query: { match_all: {} } };
if (filters && Object.keys(filters).length > 0) { if (filters && Object.keys(filters).length > 0) {
@@ -275,6 +303,7 @@ export class ElasticsearchDB implements VectorStore {
} }
async getUserId(): Promise<string> { async getUserId(): Promise<string> {
await this.initialize();
try { try {
const response = await this.client.search({ const response = await this.client.search({
index: "memory_migrations", index: "memory_migrations",
@@ -308,6 +337,7 @@ export class ElasticsearchDB implements VectorStore {
} }
async setUserId(userId: string): Promise<void> { async setUserId(userId: string): Promise<void> {
await this.initialize();
try { try {
const response = await this.client.search({ const response = await this.client.search({
index: "memory_migrations", index: "memory_migrations",
+25 -5
View File
@@ -1,4 +1,4 @@
import { MongoClient, Collection, Db } from "mongodb"; import type { MongoClient, Collection, Db } from "mongodb";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
@@ -8,13 +8,16 @@ export interface MongoDBConfig extends VectorStoreConfig {
collectionName?: string; collectionName?: string;
embeddingModelDims?: number; embeddingModelDims?: number;
dimension?: number; dimension?: number;
client?: MongoClient; /** Pre-configured MongoDB client instance (typed as `any` to keep the
* optional driver's types out of the published type declarations). */
client?: any;
} }
export class MongoDB implements VectorStore { export class MongoDB implements VectorStore {
private client: MongoClient; private client!: MongoClient;
private db: Db; private db!: Db;
private collection!: Collection; private collection!: Collection;
private readonly config: MongoDBConfig;
private readonly collectionName: string; private readonly collectionName: string;
private readonly dbName: string; private readonly dbName: string;
private readonly embeddingModelDims: number; private readonly embeddingModelDims: number;
@@ -22,17 +25,33 @@ export class MongoDB implements VectorStore {
private _initPromise?: Promise<void>; private _initPromise?: Promise<void>;
constructor(config: MongoDBConfig) { constructor(config: MongoDBConfig) {
this.config = config;
this.collectionName = config.collectionName || "mem0"; this.collectionName = config.collectionName || "mem0";
this.dbName = config.dbName || "mem0_db"; this.dbName = config.dbName || "mem0_db";
this.embeddingModelDims = this.embeddingModelDims =
config.embeddingModelDims || config.dimension || 1536; config.embeddingModelDims || config.dimension || 1536;
this.indexName = `${this.collectionName}_vector_index`; this.indexName = `${this.collectionName}_vector_index`;
// The client/db are created lazily on first initialize() so the optional
// `mongodb` peer is only loaded when the store is actually used.
}
private async ensureClient(): Promise<void> {
if (this.client) return;
const config = this.config;
if (config.client) { if (config.client) {
this.client = config.client; this.client = config.client;
} else { } else {
let sdk: any;
try {
sdk = await import("mongodb");
} catch {
throw new Error(
"The 'mongodb' package is required to use the MongoDB vector store. Install it with: npm install mongodb",
);
}
const url = config.url || "mongodb://localhost:27017"; const url = config.url || "mongodb://localhost:27017";
this.client = new MongoClient(url, { appName: "Mem0" }); this.client = new sdk.MongoClient(url, { appName: "Mem0" });
} }
this.db = this.client.db(this.dbName); this.db = this.client.db(this.dbName);
@@ -46,6 +65,7 @@ export class MongoDB implements VectorStore {
} }
private async _doInitialize(): Promise<void> { private async _doInitialize(): Promise<void> {
await this.ensureClient();
try { try {
const collections = await this.db const collections = await this.db
.listCollections({ name: this.collectionName }) .listCollections({ name: this.collectionName })
@@ -1,4 +1,4 @@
import { Client } from "@opensearch-project/opensearch"; import type { Client } from "@opensearch-project/opensearch";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
@@ -14,7 +14,9 @@ type OpenSearchAuth =
| Record<string, any>; | Record<string, any>;
interface OpenSearchConfig extends VectorStoreConfig { interface OpenSearchConfig extends VectorStoreConfig {
client?: Client; /** Pre-configured OpenSearch client instance (typed as `any` to keep the
* optional driver's types out of the published type declarations). */
client?: any;
host?: string; host?: string;
port?: number; port?: number;
httpAuth?: OpenSearchAuth | [string, string]; httpAuth?: OpenSearchAuth | [string, string];
@@ -61,17 +63,29 @@ function escapeWildcard(value: string): string {
} }
export class OpenSearchDB implements VectorStore { export class OpenSearchDB implements VectorStore {
private client: Client; private client!: Client;
private readonly config: OpenSearchConfig;
private readonly collectionName: string; private readonly collectionName: string;
private readonly embeddingModelDims: number; private readonly embeddingModelDims: number;
private readonly autoRefresh: boolean; private readonly autoRefresh: boolean;
private _initPromise?: Promise<void>; private _initPromise?: Promise<void>;
constructor(config: OpenSearchConfig) { constructor(config: OpenSearchConfig) {
this.config = config;
this.collectionName = config.collectionName; this.collectionName = config.collectionName;
this.embeddingModelDims = config.embeddingModelDims; this.embeddingModelDims = config.embeddingModelDims;
this.autoRefresh = config.autoRefresh ?? false; this.autoRefresh = config.autoRefresh ?? false;
this.initialize().catch(console.error);
}
// The client is created lazily on first initialize() so the optional
// `@opensearch-project/opensearch` peer is only loaded when the store is
// actually used.
private async ensureClient(): Promise<void> {
if (this.client) return;
const config = this.config;
if (config.client) { if (config.client) {
this.client = config.client; this.client = config.client;
} else { } else {
@@ -84,7 +98,16 @@ export class OpenSearchDB implements VectorStore {
? { username: config.user, password: config.password } ? { username: config.user, password: config.password }
: undefined); : undefined);
this.client = new Client({ let sdk: any;
try {
sdk = await import("@opensearch-project/opensearch");
} catch {
throw new Error(
"The '@opensearch-project/opensearch' package is required to use the OpenSearch vector store. Install it with: npm install @opensearch-project/opensearch",
);
}
this.client = new sdk.Client({
node: `${useSSL ? "https" : "http"}://${host}:${port}`, node: `${useSSL ? "https" : "http"}://${host}:${port}`,
auth: this.normalizeAuth(auth), auth: this.normalizeAuth(auth),
ssl: { ssl: {
@@ -94,8 +117,6 @@ export class OpenSearchDB implements VectorStore {
}, },
}); });
} }
this.initialize().catch(console.error);
} }
async initialize(): Promise<void> { async initialize(): Promise<void> {
@@ -107,6 +128,7 @@ export class OpenSearchDB implements VectorStore {
} }
private async _doInitialize(): Promise<void> { private async _doInitialize(): Promise<void> {
await this.ensureClient();
await this.createCol(this.collectionName, this.embeddingModelDims); await this.createCol(this.collectionName, this.embeddingModelDims);
await this.ensureMigrationIndex(); await this.ensureMigrationIndex();
} }
@@ -210,6 +232,7 @@ export class OpenSearchDB implements VectorStore {
ids: string[], ids: string[],
payloads: Record<string, any>[], payloads: Record<string, any>[],
): Promise<void> { ): Promise<void> {
await this.initialize();
vectors.forEach((vector, index) => this.validateVector(vector, index)); vectors.forEach((vector, index) => this.validateVector(vector, index));
const operations = vectors.flatMap((vector, index) => { const operations = vectors.flatMap((vector, index) => {
@@ -250,6 +273,7 @@ export class OpenSearchDB implements VectorStore {
topK: number = 5, topK: number = 5,
filters?: SearchFilters, filters?: SearchFilters,
): Promise<VectorStoreResult[] | null> { ): Promise<VectorStoreResult[] | null> {
await this.initialize();
const boolQuery: Record<string, any> = { const boolQuery: Record<string, any> = {
should: [ should: [
{ match: { "payload.data": query } }, { match: { "payload.data": query } },
@@ -281,6 +305,7 @@ export class OpenSearchDB implements VectorStore {
topK: number = 5, topK: number = 5,
filters?: SearchFilters, filters?: SearchFilters,
): Promise<VectorStoreResult[]> { ): Promise<VectorStoreResult[]> {
await this.initialize();
const knnQuery = { const knnQuery = {
knn: { knn: {
vector_field: { vector_field: {
@@ -316,6 +341,7 @@ export class OpenSearchDB implements VectorStore {
} }
async get(vectorId: string): Promise<VectorStoreResult | null> { async get(vectorId: string): Promise<VectorStoreResult | null> {
await this.initialize();
try { try {
const response = responseBody<{ _source?: OpenSearchHit["_source"] }>( const response = responseBody<{ _source?: OpenSearchHit["_source"] }>(
await this.client.get({ await this.client.get({
@@ -343,6 +369,7 @@ export class OpenSearchDB implements VectorStore {
vector: number[], vector: number[],
payload: Record<string, any>, payload: Record<string, any>,
): Promise<void> { ): Promise<void> {
await this.initialize();
if (vector) { if (vector) {
this.validateVector(vector, 0); this.validateVector(vector, 0);
} }
@@ -362,6 +389,7 @@ export class OpenSearchDB implements VectorStore {
} }
async delete(vectorId: string): Promise<void> { async delete(vectorId: string): Promise<void> {
await this.initialize();
try { try {
await this.client.delete({ await this.client.delete({
index: this.collectionName, index: this.collectionName,
@@ -377,6 +405,7 @@ export class OpenSearchDB implements VectorStore {
} }
async deleteCol(): Promise<void> { async deleteCol(): Promise<void> {
await this.initialize();
if (!(await this.indexExists(this.collectionName))) return; if (!(await this.indexExists(this.collectionName))) return;
await this.client.indices.delete({ index: this.collectionName }); await this.client.indices.delete({ index: this.collectionName });
} }
@@ -385,6 +414,7 @@ export class OpenSearchDB implements VectorStore {
filters?: SearchFilters, filters?: SearchFilters,
topK: number = 100, topK: number = 100,
): Promise<[VectorStoreResult[], number]> { ): Promise<[VectorStoreResult[], number]> {
await this.initialize();
const filter = this.buildFilterClauses(filters); const filter = this.buildFilterClauses(filters);
const query = filter.length ? { bool: { filter } } : { match_all: {} }; const query = filter.length ? { bool: { filter } } : { match_all: {} };
@@ -413,11 +443,13 @@ export class OpenSearchDB implements VectorStore {
} }
async reset(): Promise<void> { async reset(): Promise<void> {
await this.initialize();
await this.deleteCol(); await this.deleteCol();
await this.createCol(this.collectionName, this.embeddingModelDims); await this.createCol(this.collectionName, this.embeddingModelDims);
} }
async getUserId(): Promise<string> { async getUserId(): Promise<string> {
await this.initialize();
await this.ensureMigrationIndex(); await this.ensureMigrationIndex();
const response = responseBody<{ hits: { hits: OpenSearchHit[] } }>( const response = responseBody<{ hits: { hits: OpenSearchHit[] } }>(
@@ -442,6 +474,7 @@ export class OpenSearchDB implements VectorStore {
} }
async setUserId(userId: string): Promise<void> { async setUserId(userId: string): Promise<void> {
await this.initialize();
await this.ensureMigrationIndex(); await this.ensureMigrationIndex();
await this.client.index({ await this.client.index({
index: "memory_migrations", index: "memory_migrations",
+34 -8
View File
@@ -1,5 +1,4 @@
import { Pinecone } from "@pinecone-database/pinecone"; import type { Pinecone, Index } from "@pinecone-database/pinecone";
import type { Index } from "@pinecone-database/pinecone";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
@@ -9,7 +8,9 @@ const MIGRATIONS_RECORD_ID = "mem0-user-id";
interface PineconeDBConfig extends VectorStoreConfig { interface PineconeDBConfig extends VectorStoreConfig {
collectionName: string; collectionName: string;
embeddingModelDims: number; embeddingModelDims: number;
client?: Pinecone; /** Pre-configured Pinecone client instance (typed as `any` to keep the
* optional driver's types out of the published type declarations). */
client?: any;
apiKey?: string; apiKey?: string;
serverlessConfig?: { cloud: string; region: string }; serverlessConfig?: { cloud: string; region: string };
podConfig?: { podConfig?: {
@@ -26,7 +27,8 @@ interface PineconeDBConfig extends VectorStoreConfig {
} }
export class PineconeDB implements VectorStore { export class PineconeDB implements VectorStore {
private client: Pinecone; private client!: Pinecone;
private readonly config: PineconeDBConfig;
private readonly collectionName: string; private readonly collectionName: string;
private readonly dimension: number; private readonly dimension: number;
private readonly metric: "cosine" | "dotproduct" | "euclidean"; private readonly metric: "cosine" | "dotproduct" | "euclidean";
@@ -45,18 +47,16 @@ export class PineconeDB implements VectorStore {
private _initPromise?: Promise<void>; private _initPromise?: Promise<void>;
constructor(config: PineconeDBConfig) { constructor(config: PineconeDBConfig) {
if (config.client) { if (!config.client) {
this.client = config.client;
} else {
const apiKey = config.apiKey || process.env.PINECONE_API_KEY; const apiKey = config.apiKey || process.env.PINECONE_API_KEY;
if (!apiKey) { if (!apiKey) {
throw new Error( throw new Error(
"Pinecone API key required: pass apiKey or set PINECONE_API_KEY env var", "Pinecone API key required: pass apiKey or set PINECONE_API_KEY env var",
); );
} }
this.client = new Pinecone({ apiKey });
} }
this.config = config;
this.collectionName = config.collectionName; this.collectionName = config.collectionName;
this.dimension = config.embeddingModelDims || config.dimension || 1536; this.dimension = config.embeddingModelDims || config.dimension || 1536;
this.metric = config.metric || "cosine"; this.metric = config.metric || "cosine";
@@ -69,6 +69,31 @@ export class PineconeDB implements VectorStore {
this.initialize().catch(console.error); this.initialize().catch(console.error);
} }
/**
* Lazily construct (or reuse) the Pinecone client, importing the optional
* `@pinecone-database/pinecone` peer only when the store is first used so
* consumers that never touch Pinecone don't need it installed.
*/
private async ensureClient(): Promise<void> {
if (this.client) return;
const config = this.config;
if (config.client) {
this.client = config.client;
} else {
const apiKey = config.apiKey || process.env.PINECONE_API_KEY;
let sdk: any;
try {
sdk = await import("@pinecone-database/pinecone");
} catch {
throw new Error(
"The '@pinecone-database/pinecone' package is required to use the Pinecone vector store. Install it with: npm install @pinecone-database/pinecone",
);
}
this.client = new sdk.Pinecone({ apiKey });
}
}
async initialize(): Promise<void> { async initialize(): Promise<void> {
if (!this._initPromise) { if (!this._initPromise) {
this._initPromise = this._doInitialize(); this._initPromise = this._doInitialize();
@@ -77,6 +102,7 @@ export class PineconeDB implements VectorStore {
} }
private async _doInitialize(): Promise<void> { private async _doInitialize(): Promise<void> {
await this.ensureClient();
await this._ensureIndex(); await this._ensureIndex();
this._index = this.client.index({ name: this.collectionName }); this._index = this.client.index({ name: this.collectionName });
} }
+110 -62
View File
@@ -1,21 +1,8 @@
import { import type {
CreateIndexCommand, GetOutputVector,
CreateVectorBucketCommand, ListOutputVector,
DeleteIndexCommand, QueryOutputVector,
DeleteVectorsCommand, VectorData,
GetIndexCommand,
GetVectorBucketCommand,
GetVectorsCommand,
ListVectorsCommand,
PutVectorsCommand,
QueryVectorsCommand,
S3VectorsClient,
type DistanceMetric,
type GetOutputVector,
type ListOutputVector,
type QueryOutputVector,
type S3VectorsClientConfig,
type VectorData,
} from "@aws-sdk/client-s3vectors"; } from "@aws-sdk/client-s3vectors";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
@@ -32,11 +19,13 @@ interface S3VectorsConfig extends VectorStoreConfig {
collectionName: string; collectionName: string;
embeddingModelDims?: number; embeddingModelDims?: number;
dimension?: number; dimension?: number;
distanceMetric?: DistanceMetric | "cosine" | "euclidean"; distanceMetric?: "cosine" | "euclidean";
region?: string; region?: string;
regionName?: string; regionName?: string;
client?: S3VectorsClientLike; client?: S3VectorsClientLike;
clientConfig?: S3VectorsClientConfig; /** Pre-configured S3 Vectors client options (typed as `any` to keep the
* optional SDK's types out of the published type declarations). */
clientConfig?: any;
} }
interface S3VectorsClientLike { interface S3VectorsClientLike {
@@ -44,11 +33,14 @@ interface S3VectorsClientLike {
} }
export class S3Vectors implements VectorStore { export class S3Vectors implements VectorStore {
private readonly client: S3VectorsClientLike; private readonly config: S3VectorsConfig;
private readonly vectorBucketName: string; private readonly vectorBucketName: string;
private readonly collectionName: string; private readonly collectionName: string;
private readonly dimension: number; private readonly dimension: number;
private readonly distanceMetric: DistanceMetric | "cosine" | "euclidean"; private readonly distanceMetric: "cosine" | "euclidean";
private client?: S3VectorsClientLike;
private clientPromise?: Promise<S3VectorsClientLike>;
private sdkPromise?: Promise<any>;
private _initPromise?: Promise<void>; private _initPromise?: Promise<void>;
private cachedUserId?: string; private cachedUserId?: string;
@@ -65,22 +57,55 @@ export class S3Vectors implements VectorStore {
throw new Error("embeddingModelDims or dimension is required"); throw new Error("embeddingModelDims or dimension is required");
} }
this.config = config;
this.vectorBucketName = config.vectorBucketName; this.vectorBucketName = config.vectorBucketName;
this.collectionName = config.collectionName; this.collectionName = config.collectionName;
this.dimension = dimension; this.dimension = dimension;
this.distanceMetric = config.distanceMetric || "cosine"; this.distanceMetric = config.distanceMetric || "cosine";
this.client =
config.client ||
new S3VectorsClient({
...(config.clientConfig || {}),
...(config.region || config.regionName
? { region: config.region || config.regionName }
: {}),
});
void this.initialize().catch(console.error); void this.initialize().catch(console.error);
} }
/**
* Lazily import the optional `@aws-sdk/client-s3vectors` peer so consumers
* who never use the S3 Vectors store don't need it installed.
*/
private getSdk(): Promise<any> {
if (!this.sdkPromise) {
this.sdkPromise = import("@aws-sdk/client-s3vectors").catch(() => {
throw new Error(
"The '@aws-sdk/client-s3vectors' package is required to use the S3 Vectors store. Install it with: npm install @aws-sdk/client-s3vectors",
);
});
}
return this.sdkPromise;
}
/** Lazily construct (or reuse) the S3 Vectors client. */
private async getClient(): Promise<S3VectorsClientLike> {
if (this.client) return this.client;
if (!this.clientPromise) {
this.clientPromise = this.createClient();
}
this.client = await this.clientPromise;
return this.client;
}
private async createClient(): Promise<S3VectorsClientLike> {
const config = this.config;
if (config.client) {
return config.client;
}
const sdk = await this.getSdk();
return new sdk.S3VectorsClient({
...(config.clientConfig || {}),
...(config.region || config.regionName
? { region: config.region || config.regionName }
: {}),
});
}
async initialize(): Promise<void> { async initialize(): Promise<void> {
if (!this._initPromise) { if (!this._initPromise) {
this._initPromise = this._doInitialize(); this._initPromise = this._doInitialize();
@@ -105,8 +130,10 @@ export class S3Vectors implements VectorStore {
await this.initialize(); await this.initialize();
this.assertBatchDimensions(vectors, "Insert"); this.assertBatchDimensions(vectors, "Insert");
await this.client.send( const sdk = await this.getSdk();
new PutVectorsCommand({ const client = await this.getClient();
await client.send(
new sdk.PutVectorsCommand({
vectorBucketName: this.vectorBucketName, vectorBucketName: this.vectorBucketName,
indexName: this.collectionName, indexName: this.collectionName,
vectors: vectors.map((vector, index) => ({ vectors: vectors.map((vector, index) => ({
@@ -134,8 +161,10 @@ export class S3Vectors implements VectorStore {
if (filter && this.isAlwaysFalseFilter(filter)) { if (filter && this.isAlwaysFalseFilter(filter)) {
return []; return [];
} }
const response = await this.client.send( const sdk = await this.getSdk();
new QueryVectorsCommand({ const client = await this.getClient();
const response = await client.send(
new sdk.QueryVectorsCommand({
vectorBucketName: this.vectorBucketName, vectorBucketName: this.vectorBucketName,
indexName: this.collectionName, indexName: this.collectionName,
queryVector: this.toVectorData(query), queryVector: this.toVectorData(query),
@@ -158,9 +187,11 @@ export class S3Vectors implements VectorStore {
async get(vectorId: string): Promise<VectorStoreResult | null> { async get(vectorId: string): Promise<VectorStoreResult | null> {
await this.initialize(); await this.initialize();
const sdk = await this.getSdk();
const client = await this.getClient();
try { try {
const response = await this.client.send( const response = await client.send(
new GetVectorsCommand({ new sdk.GetVectorsCommand({
vectorBucketName: this.vectorBucketName, vectorBucketName: this.vectorBucketName,
indexName: this.collectionName, indexName: this.collectionName,
keys: [vectorId], keys: [vectorId],
@@ -203,8 +234,10 @@ export class S3Vectors implements VectorStore {
this.assertVectorDimension(nextVector, "Vector"); this.assertVectorDimension(nextVector, "Vector");
await this.client.send( const sdk = await this.getSdk();
new PutVectorsCommand({ const client = await this.getClient();
await client.send(
new sdk.PutVectorsCommand({
vectorBucketName: this.vectorBucketName, vectorBucketName: this.vectorBucketName,
indexName: this.collectionName, indexName: this.collectionName,
vectors: [ vectors: [
@@ -221,8 +254,10 @@ export class S3Vectors implements VectorStore {
async delete(vectorId: string): Promise<void> { async delete(vectorId: string): Promise<void> {
await this.initialize(); await this.initialize();
await this.client.send( const sdk = await this.getSdk();
new DeleteVectorsCommand({ const client = await this.getClient();
await client.send(
new sdk.DeleteVectorsCommand({
vectorBucketName: this.vectorBucketName, vectorBucketName: this.vectorBucketName,
indexName: this.collectionName, indexName: this.collectionName,
keys: [vectorId], keys: [vectorId],
@@ -233,9 +268,11 @@ export class S3Vectors implements VectorStore {
async deleteCol(): Promise<void> { async deleteCol(): Promise<void> {
await this.initialize(); await this.initialize();
const sdk = await this.getSdk();
const client = await this.getClient();
try { try {
await this.client.send( await client.send(
new DeleteIndexCommand({ new sdk.DeleteIndexCommand({
vectorBucketName: this.vectorBucketName, vectorBucketName: this.vectorBucketName,
indexName: this.collectionName, indexName: this.collectionName,
}), }),
@@ -257,14 +294,16 @@ export class S3Vectors implements VectorStore {
const filter = this.convertFilters(filters); const filter = this.convertFilters(filters);
const results: VectorStoreResult[] = []; const results: VectorStoreResult[] = [];
let nextToken: string | undefined; let nextToken: string | undefined;
const sdk = await this.getSdk();
const client = await this.getClient();
// Stop paginating once we have topK matches. Both callers of list() // Stop paginating once we have topK matches. Both callers of list()
// discard the count, so scanning the whole index just to total every // discard the count, so scanning the whole index just to total every
// match is wasted round-trips (O(index size) on getAll/deleteAll). // match is wasted round-trips (O(index size) on getAll/deleteAll).
// Return the page length like qdrant does. // Return the page length like qdrant does.
do { do {
const response = await this.client.send( const response = await client.send(
new ListVectorsCommand({ new sdk.ListVectorsCommand({
vectorBucketName: this.vectorBucketName, vectorBucketName: this.vectorBucketName,
indexName: this.collectionName, indexName: this.collectionName,
maxResults: DEFAULT_PAGE_SIZE, maxResults: DEFAULT_PAGE_SIZE,
@@ -298,8 +337,10 @@ export class S3Vectors implements VectorStore {
await this.ensureMigrationIndex(); await this.ensureMigrationIndex();
const response = await this.client.send( const sdk = await this.getSdk();
new GetVectorsCommand({ const client = await this.getClient();
const response = await client.send(
new sdk.GetVectorsCommand({
vectorBucketName: this.vectorBucketName, vectorBucketName: this.vectorBucketName,
indexName: MIGRATION_INDEX_NAME, indexName: MIGRATION_INDEX_NAME,
keys: [MIGRATION_VECTOR_KEY], keys: [MIGRATION_VECTOR_KEY],
@@ -325,8 +366,10 @@ export class S3Vectors implements VectorStore {
await this.initialize(); await this.initialize();
await this.ensureMigrationIndex(); await this.ensureMigrationIndex();
await this.client.send( const sdk = await this.getSdk();
new PutVectorsCommand({ const client = await this.getClient();
await client.send(
new sdk.PutVectorsCommand({
vectorBucketName: this.vectorBucketName, vectorBucketName: this.vectorBucketName,
indexName: MIGRATION_INDEX_NAME, indexName: MIGRATION_INDEX_NAME,
vectors: [ vectors: [
@@ -343,9 +386,11 @@ export class S3Vectors implements VectorStore {
} }
private async ensureBucketExists(): Promise<void> { private async ensureBucketExists(): Promise<void> {
const sdk = await this.getSdk();
const client = await this.getClient();
try { try {
await this.client.send( await client.send(
new GetVectorBucketCommand({ new sdk.GetVectorBucketCommand({
vectorBucketName: this.vectorBucketName, vectorBucketName: this.vectorBucketName,
}), }),
); );
@@ -355,8 +400,8 @@ export class S3Vectors implements VectorStore {
} }
try { try {
await this.client.send( await client.send(
new CreateVectorBucketCommand({ new sdk.CreateVectorBucketCommand({
vectorBucketName: this.vectorBucketName, vectorBucketName: this.vectorBucketName,
}), }),
); );
@@ -371,11 +416,13 @@ export class S3Vectors implements VectorStore {
private async ensureIndexExists( private async ensureIndexExists(
indexName: string, indexName: string,
dimension: number, dimension: number,
distanceMetric: DistanceMetric | "cosine" | "euclidean", distanceMetric: "cosine" | "euclidean",
): Promise<void> { ): Promise<void> {
const sdk = await this.getSdk();
const client = await this.getClient();
try { try {
await this.client.send( await client.send(
new GetIndexCommand({ new sdk.GetIndexCommand({
vectorBucketName: this.vectorBucketName, vectorBucketName: this.vectorBucketName,
indexName, indexName,
}), }),
@@ -386,8 +433,8 @@ export class S3Vectors implements VectorStore {
} }
try { try {
await this.client.send( await client.send(
new CreateIndexCommand({ new sdk.CreateIndexCommand({
vectorBucketName: this.vectorBucketName, vectorBucketName: this.vectorBucketName,
indexName, indexName,
dataType: "float32", dataType: "float32",
@@ -410,8 +457,10 @@ export class S3Vectors implements VectorStore {
private async fetchStoredVector( private async fetchStoredVector(
vectorId: string, vectorId: string,
): Promise<{ vector: number[]; payload: Record<string, any> } | null> { ): Promise<{ vector: number[]; payload: Record<string, any> } | null> {
const response = await this.client.send( const sdk = await this.getSdk();
new GetVectorsCommand({ const client = await this.getClient();
const response = await client.send(
new sdk.GetVectorsCommand({
vectorBucketName: this.vectorBucketName, vectorBucketName: this.vectorBucketName,
indexName: this.collectionName, indexName: this.collectionName,
keys: [vectorId], keys: [vectorId],
@@ -770,7 +819,7 @@ export class S3Vectors implements VectorStore {
private normalizeQueryVector( private normalizeQueryVector(
vector: QueryOutputVector, vector: QueryOutputVector,
distanceMetric: DistanceMetric | "cosine" | "euclidean", distanceMetric: "cosine" | "euclidean",
): VectorStoreResult { ): VectorStoreResult {
return { return {
id: String(vector.key), id: String(vector.key),
@@ -794,8 +843,7 @@ export class S3Vectors implements VectorStore {
private normalizeScore( private normalizeScore(
distance?: number, distance?: number,
distanceMetric: DistanceMetric | "cosine" | "euclidean" = this distanceMetric: "cosine" | "euclidean" = this.distanceMetric,
.distanceMetric,
): number | undefined { ): number | undefined {
if (distance === undefined || distance === null) { if (distance === undefined || distance === null) {
return undefined; return undefined;
@@ -1,4 +1,3 @@
import Turbopuffer from "@turbopuffer/turbopuffer";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
@@ -11,11 +10,10 @@ interface TurbopufferConfig extends VectorStoreConfig {
} }
export class TurbopufferDB implements VectorStore { export class TurbopufferDB implements VectorStore {
private client: Turbopuffer; private clientInstance?: any;
private ns: ReturnType<InstanceType<typeof Turbopuffer>["namespace"]>; private clientPromise?: Promise<any>;
private migrationsNs: ReturnType< private readonly apiKey: string;
InstanceType<typeof Turbopuffer>["namespace"] private readonly region: string;
>;
private readonly collectionName: string; private readonly collectionName: string;
private readonly distanceMetric: string; private readonly distanceMetric: string;
private readonly batchSize: number; private readonly batchSize: number;
@@ -28,17 +26,55 @@ export class TurbopufferDB implements VectorStore {
); );
} }
this.client = new Turbopuffer({ this.apiKey = apiKey;
apiKey, this.region = config.region ?? "gcp-us-central1";
region: config.region ?? "gcp-us-central1",
});
this.collectionName = config.collectionName; this.collectionName = config.collectionName;
this.distanceMetric = config.distanceMetric ?? "cosine_distance"; this.distanceMetric = config.distanceMetric ?? "cosine_distance";
this.batchSize = config.batchSize ?? 100; this.batchSize = config.batchSize ?? 100;
this.ns = this.client.namespace(this.collectionName); }
this.migrationsNs = this.client.namespace(
this.collectionName + "_migrations", /**
); * Lazily construct (or reuse) the Turbopuffer client, importing the optional
* `@turbopuffer/turbopuffer` peer only when the store is first used so
* consumers that never touch Turbopuffer don't need it installed.
*/
private async getClient(): Promise<any> {
if (this.clientInstance) return this.clientInstance;
if (!this.clientPromise) {
this.clientPromise = this.createClient();
}
this.clientInstance = await this.clientPromise;
return this.clientInstance;
}
private async createClient(): Promise<any> {
let sdk: any;
try {
sdk = await import("@turbopuffer/turbopuffer");
} catch {
throw new Error(
"The '@turbopuffer/turbopuffer' package is required to use the Turbopuffer vector store. Install it with: npm install @turbopuffer/turbopuffer",
);
}
// @turbopuffer/turbopuffer ships `Turbopuffer` as both the default export
// and a named export pointing at the same class. Use `.default` since
// that's what a plain `import Turbopuffer from "..."` resolves to (and
// what test doubles for this module mock).
return new sdk.default({
apiKey: this.apiKey,
region: this.region,
});
}
private async getNs(): Promise<any> {
const client = await this.getClient();
return client.namespace(this.collectionName);
}
private async getMigrationsNs(): Promise<any> {
const client = await this.getClient();
return client.namespace(this.collectionName + "_migrations");
} }
async initialize(): Promise<void> { async initialize(): Promise<void> {
@@ -50,6 +86,7 @@ export class TurbopufferDB implements VectorStore {
ids: string[], ids: string[],
payloads: Record<string, any>[], payloads: Record<string, any>[],
): Promise<void> { ): Promise<void> {
const ns = await this.getNs();
for (let i = 0; i < vectors.length; i += this.batchSize) { for (let i = 0; i < vectors.length; i += this.batchSize) {
const batchVectors = vectors.slice(i, i + this.batchSize); const batchVectors = vectors.slice(i, i + this.batchSize);
const batchIds = ids.slice(i, i + this.batchSize); const batchIds = ids.slice(i, i + this.batchSize);
@@ -61,7 +98,7 @@ export class TurbopufferDB implements VectorStore {
vector, vector,
})); }));
await this.ns.write({ await ns.write({
upsert_rows, upsert_rows,
distance_metric: this.distanceMetric as any, distance_metric: this.distanceMetric as any,
}); });
@@ -82,8 +119,9 @@ export class TurbopufferDB implements VectorStore {
const tpufFilters = this.convertFilters(filters); const tpufFilters = this.convertFilters(filters);
if (tpufFilters !== null) queryParams.filters = tpufFilters; if (tpufFilters !== null) queryParams.filters = tpufFilters;
const ns = await this.getNs();
try { try {
const result = await this.ns.query(queryParams); const result = await ns.query(queryParams);
return this.parseRows(result.rows ?? []); return this.parseRows(result.rows ?? []);
} catch (err) { } catch (err) {
console.error("Turbopuffer search error:", err); console.error("Turbopuffer search error:", err);
@@ -96,8 +134,9 @@ export class TurbopufferDB implements VectorStore {
} }
async get(vectorId: string): Promise<VectorStoreResult | null> { async get(vectorId: string): Promise<VectorStoreResult | null> {
const ns = await this.getNs();
try { try {
const result = await this.ns.query({ const result = await ns.query({
rank_by: ["id", "asc"] as any, rank_by: ["id", "asc"] as any,
top_k: 1, top_k: 1,
include_attributes: true, include_attributes: true,
@@ -116,24 +155,27 @@ export class TurbopufferDB implements VectorStore {
vector: number[], vector: number[],
payload: Record<string, any>, payload: Record<string, any>,
): Promise<void> { ): Promise<void> {
const ns = await this.getNs();
if (vector && vector.length > 0) { if (vector && vector.length > 0) {
await this.ns.write({ await ns.write({
upsert_rows: [{ ...payload, id: vectorId, vector }], upsert_rows: [{ ...payload, id: vectorId, vector }],
distance_metric: this.distanceMetric as any, distance_metric: this.distanceMetric as any,
}); });
} else { } else {
await this.ns.write({ await ns.write({
patch_rows: [{ ...payload, id: vectorId }], patch_rows: [{ ...payload, id: vectorId }],
}); });
} }
} }
async delete(vectorId: string): Promise<void> { async delete(vectorId: string): Promise<void> {
await this.ns.write({ deletes: [vectorId] }); const ns = await this.getNs();
await ns.write({ deletes: [vectorId] });
} }
async deleteCol(): Promise<void> { async deleteCol(): Promise<void> {
await this.ns.deleteAll(); const ns = await this.getNs();
await ns.deleteAll();
} }
async list( async list(
@@ -149,8 +191,9 @@ export class TurbopufferDB implements VectorStore {
const tpufFilters = this.convertFilters(filters); const tpufFilters = this.convertFilters(filters);
if (tpufFilters !== null) queryParams.filters = tpufFilters; if (tpufFilters !== null) queryParams.filters = tpufFilters;
const ns = await this.getNs();
try { try {
const result = await this.ns.query(queryParams); const result = await ns.query(queryParams);
const rows = this.parseRows(result.rows ?? []); const rows = this.parseRows(result.rows ?? []);
return [rows, rows.length]; return [rows, rows.length];
} catch (err) { } catch (err) {
@@ -161,9 +204,10 @@ export class TurbopufferDB implements VectorStore {
async getUserId(): Promise<string> { async getUserId(): Promise<string> {
try { try {
const migrationsNs = await this.getMigrationsNs();
let rows: any[] = []; let rows: any[] = [];
try { try {
const result = await this.migrationsNs.query({ const result = await migrationsNs.query({
rank_by: ["id", "asc"] as any, rank_by: ["id", "asc"] as any,
top_k: 1, top_k: 1,
include_attributes: true, include_attributes: true,
@@ -181,7 +225,7 @@ export class TurbopufferDB implements VectorStore {
const randomId = const randomId =
Math.random().toString(36).slice(2, 15) + Math.random().toString(36).slice(2, 15) +
Math.random().toString(36).slice(2, 15); Math.random().toString(36).slice(2, 15);
await this.migrationsNs.write({ await migrationsNs.write({
upsert_rows: [{ id: "1", vector: [0.0], user_id: randomId }], upsert_rows: [{ id: "1", vector: [0.0], user_id: randomId }],
distance_metric: "cosine_distance" as any, distance_metric: "cosine_distance" as any,
}); });
@@ -194,7 +238,8 @@ export class TurbopufferDB implements VectorStore {
async setUserId(userId: string): Promise<void> { async setUserId(userId: string): Promise<void> {
try { try {
await this.migrationsNs.write({ const migrationsNs = await this.getMigrationsNs();
await migrationsNs.write({
upsert_rows: [{ id: "1", vector: [0.0], user_id: userId }], upsert_rows: [{ id: "1", vector: [0.0], user_id: userId }],
distance_metric: "cosine_distance" as any, distance_metric: "cosine_distance" as any,
}); });
@@ -1,4 +1,4 @@
import { Index, QueryResult, Vector } from "@upstash/vector"; import type { Index, QueryResult, Vector } from "@upstash/vector";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
@@ -6,36 +6,59 @@ interface UpstashVectorConfig extends VectorStoreConfig {
collectionName: string; collectionName: string;
url?: string; url?: string;
token?: string; token?: string;
client?: Index<Record<string, unknown>>; /** Pre-configured Upstash Vector client instance (typed as `any` to keep
* the optional driver's types out of the published type declarations). */
client?: any;
} }
type UpstashMetadata = Record<string, unknown>; type UpstashMetadata = Record<string, unknown>;
export class UpstashVector implements VectorStore { export class UpstashVector implements VectorStore {
private readonly client: Index<UpstashMetadata>; private client!: Index<UpstashMetadata>;
private readonly config: UpstashVectorConfig;
private readonly collectionName: string; private readonly collectionName: string;
constructor(config: UpstashVectorConfig) { constructor(config: UpstashVectorConfig) {
if (!config.collectionName) { if (!config.collectionName) {
throw new Error("collectionName is required for Upstash Vector."); throw new Error("collectionName is required for Upstash Vector.");
} }
if (!config.client && !(config.url && config.token)) {
if (config.client) {
this.client = config.client;
} else if (config.url && config.token) {
this.client = new Index({
url: config.url,
token: config.token,
});
} else {
throw new Error("Either a client or url and token must be provided."); throw new Error("Either a client or url and token must be provided.");
} }
this.config = config;
this.collectionName = config.collectionName; this.collectionName = config.collectionName;
} }
/**
* Lazily construct (or reuse) the Upstash Vector client, importing the
* optional `@upstash/vector` peer only when the store is first used so
* consumers that never touch Upstash Vector don't need it installed.
*/
private async ensureClient(): Promise<void> {
if (this.client) return;
const config = this.config;
if (config.client) {
this.client = config.client;
} else {
let sdk: any;
try {
sdk = await import("@upstash/vector");
} catch {
throw new Error(
"The '@upstash/vector' package is required to use the Upstash Vector store. Install it with: npm install @upstash/vector",
);
}
this.client = new sdk.Index({
url: config.url,
token: config.token,
});
}
}
async initialize(): Promise<void> { async initialize(): Promise<void> {
return; await this.ensureClient();
} }
async insert( async insert(
@@ -43,6 +66,7 @@ export class UpstashVector implements VectorStore {
ids: string[], ids: string[],
payloads: Record<string, any>[], payloads: Record<string, any>[],
): Promise<void> { ): Promise<void> {
await this.initialize();
const upsertData = vectors.map((vector, idx) => { const upsertData = vectors.map((vector, idx) => {
return { return {
id: ids[idx], id: ids[idx],
@@ -59,6 +83,7 @@ export class UpstashVector implements VectorStore {
topK: number = 5, topK: number = 5,
filters?: SearchFilters, filters?: SearchFilters,
): Promise<VectorStoreResult[]> { ): Promise<VectorStoreResult[]> {
await this.initialize();
const response = await this.client.query<UpstashMetadata>( const response = await this.client.query<UpstashMetadata>(
{ {
vector: query, vector: query,
@@ -77,6 +102,7 @@ export class UpstashVector implements VectorStore {
topK: number = 5, topK: number = 5,
filters?: SearchFilters, filters?: SearchFilters,
): Promise<VectorStoreResult[] | null> { ): Promise<VectorStoreResult[] | null> {
await this.initialize();
try { try {
const response = await this.client.query<UpstashMetadata>( const response = await this.client.query<UpstashMetadata>(
{ {
@@ -96,6 +122,7 @@ export class UpstashVector implements VectorStore {
} }
async get(vectorId: string): Promise<VectorStoreResult | null> { async get(vectorId: string): Promise<VectorStoreResult | null> {
await this.initialize();
const response = await this.client.fetch<UpstashMetadata>([vectorId], { const response = await this.client.fetch<UpstashMetadata>([vectorId], {
includeMetadata: true, includeMetadata: true,
namespace: this.collectionName, namespace: this.collectionName,
@@ -117,6 +144,7 @@ export class UpstashVector implements VectorStore {
vector: number[], vector: number[],
payload: Record<string, any>, payload: Record<string, any>,
): Promise<void> { ): Promise<void> {
await this.initialize();
// Upstash's `update` can't set the vector and metadata in one call (its // Upstash's `update` can't set the vector and metadata in one call (its
// payload is a discriminated union of vector | data | metadata), so a // payload is a discriminated union of vector | data | metadata), so a
// single `upsert` replaces both atomically, the same way insert() writes. // single `upsert` replaces both atomically, the same way insert() writes.
@@ -131,10 +159,12 @@ export class UpstashVector implements VectorStore {
} }
async delete(vectorId: string): Promise<void> { async delete(vectorId: string): Promise<void> {
await this.initialize();
await this.client.delete(vectorId, { namespace: this.collectionName }); await this.client.delete(vectorId, { namespace: this.collectionName });
} }
async deleteCol(): Promise<void> { async deleteCol(): Promise<void> {
await this.initialize();
await this.client.reset({ namespace: this.collectionName }); await this.client.reset({ namespace: this.collectionName });
} }
@@ -142,6 +172,7 @@ export class UpstashVector implements VectorStore {
filters?: SearchFilters, filters?: SearchFilters,
topK: number = 100, topK: number = 100,
): Promise<[VectorStoreResult[], number]> { ): Promise<[VectorStoreResult[], number]> {
await this.initialize();
const results: VectorStoreResult[] = []; const results: VectorStoreResult[] = [];
let cursor = "0"; let cursor = "0";
+29 -6
View File
@@ -1,10 +1,12 @@
import weaviate, { Filters, type WeaviateClient } from "weaviate-client"; import type { WeaviateClient } from "weaviate-client";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
interface WeaviateConfig extends VectorStoreConfig { interface WeaviateConfig extends VectorStoreConfig {
client?: WeaviateClient; /** Pre-configured Weaviate client instance (typed as `any` to keep the
* optional driver's types out of the published type declarations). */
client?: any;
clusterUrl?: string; clusterUrl?: string;
apiKey?: string; apiKey?: string;
additionalHeaders?: Record<string, string>; additionalHeaders?: Record<string, string>;
@@ -28,6 +30,7 @@ const RETURN_PROPERTIES = [
export class WeaviateDB implements VectorStore { export class WeaviateDB implements VectorStore {
private _config: WeaviateConfig; private _config: WeaviateConfig;
private _client!: WeaviateClient; private _client!: WeaviateClient;
private _sdk: any;
private _col!: any; private _col!: any;
private _userId: string; private _userId: string;
private _initPromise?: Promise<void>; private _initPromise?: Promise<void>;
@@ -42,9 +45,23 @@ export class WeaviateDB implements VectorStore {
return (this._initPromise ??= this._doInitialize()); return (this._initPromise ??= this._doInitialize());
} }
private async _doInitialize(): Promise<void> { // Loaded dynamically: weaviate-client is an optional peer dependency, so a static
const { client, clusterUrl, apiKey, additionalHeaders, collectionName } = // value import would break `import { Memory } from "mem0ai/oss"` for everyone else.
this._config; private async ensureClient(): Promise<void> {
if (this._client) return;
let sdk: any;
try {
sdk = await import("weaviate-client");
} catch {
throw new Error(
"The 'weaviate-client' package is required to use the Weaviate vector store. Install it with: npm install weaviate-client",
);
}
this._sdk = sdk;
const { client, clusterUrl, apiKey, additionalHeaders } = this._config;
const weaviate = sdk.default;
if (client) { if (client) {
this._client = client; this._client = client;
@@ -79,6 +96,12 @@ export class WeaviateDB implements VectorStore {
headers: additionalHeaders, headers: additionalHeaders,
}); });
} }
}
private async _doInitialize(): Promise<void> {
await this.ensureClient();
const { collectionName } = this._config;
const weaviate = this._sdk.default;
const exists = await this._client.collections.exists(collectionName); const exists = await this._client.collections.exists(collectionName);
if (!exists) { if (!exists) {
@@ -101,7 +124,7 @@ export class WeaviateDB implements VectorStore {
const conditions = (["user_id", "agent_id", "run_id"] as const) const conditions = (["user_id", "agent_id", "run_id"] as const)
.filter((key) => filters[key] != null) .filter((key) => filters[key] != null)
.map((key) => this._col.filter.byProperty(key).equal(filters[key])); .map((key) => this._col.filter.byProperty(key).equal(filters[key]));
return conditions.length ? Filters.and(...conditions) : undefined; return conditions.length ? this._sdk.Filters.and(...conditions) : undefined;
} }
async insert( async insert(
@@ -50,8 +50,10 @@ beforeEach(() => {
describe("ElasticsearchDB", () => { describe("ElasticsearchDB", () => {
describe("constructor", () => { describe("constructor", () => {
it("creates client with self-hosted host:port", () => { // The client is built lazily on first use, so await initialize() before
new ElasticsearchDB({ // asserting how it was constructed.
it("creates client with self-hosted host:port", async () => {
const store = new ElasticsearchDB({
collectionName: "mem0", collectionName: "mem0",
embeddingModelDims: 768, embeddingModelDims: 768,
host: "localhost", host: "localhost",
@@ -59,6 +61,7 @@ describe("ElasticsearchDB", () => {
username: "user", username: "user",
password: "pass", password: "pass",
}); });
await store.initialize();
expect(mockClient).toHaveBeenCalledWith( expect(mockClient).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@@ -68,14 +71,15 @@ describe("ElasticsearchDB", () => {
); );
}); });
it("creates client with cloud config", () => { it("creates client with cloud config", async () => {
new ElasticsearchDB({ const store = new ElasticsearchDB({
collectionName: "mem0", collectionName: "mem0",
embeddingModelDims: 1536, embeddingModelDims: 1536,
cloudId: cloudId:
"my-cloud:dXMtZWFzdDQuZ2NwLmVsYXN0aWMtY2xvdWQuY29tOjQ0MyQxMjM0NTY3ODkw", "my-cloud:dXMtZWFzdDQuZ2NwLmVsYXN0aWMtY2xvdWQuY29tOjQ0MyQxMjM0NTY3ODkw",
apiKey: "base64-key", apiKey: "base64-key",
}); });
await store.initialize();
expect(mockClient).toHaveBeenCalledWith( expect(mockClient).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@@ -98,12 +102,13 @@ describe("ElasticsearchDB", () => {
expect(mockClient).toHaveBeenCalledTimes(1); expect(mockClient).toHaveBeenCalledTimes(1);
}); });
it("defaults to port 9200 and https", () => { it("defaults to port 9200 and https", async () => {
new ElasticsearchDB({ const store = new ElasticsearchDB({
collectionName: "mem0", collectionName: "mem0",
embeddingModelDims: 384, embeddingModelDims: 384,
host: "es.example.com", host: "es.example.com",
}); });
await store.initialize();
expect(mockClient).toHaveBeenCalledWith( expect(mockClient).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@@ -3606,6 +3606,7 @@ describe("Neptune Analytics – backward compat with mocked client", () => {
profile: "dev-profile", profile: "dev-profile",
maxAttempts: 3, maxAttempts: 3,
}); });
await store.initialize();
// The client is now constructed lazily on first use, not in the constructor. // The client is now constructed lazily on first use, not in the constructor.
await store.search([1, 2, 3], 1); await store.search([1, 2, 3], 1);