refactor(ts-sdk): lazy-load optional provider SDKs so importing mem0ai/oss never requires them (#6280)
This commit is contained in:
@@ -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": {
|
||||||
|
|||||||
@@ -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, " ");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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" }),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user