feat(vector-stores): add S3 Vectors provider to TypeScript OSS SDK (#5822)

Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
Rod Boev
2026-07-06 20:08:13 +05:30
committed by GitHub
co-authored by kartik-mem0
parent 580d390e4d
commit 6dc4606dcf
11 changed files with 2082 additions and 8 deletions
@@ -138,6 +138,11 @@ jest.mock("../src/vector_stores/pgvector", () => ({
.fn()
.mockImplementation((config) => ({ type: "pgvector", config })),
}));
jest.mock("../src/vector_stores/s3_vectors", () => ({
S3Vectors: jest
.fn()
.mockImplementation((config) => ({ type: "s3-vectors", config })),
}));
jest.mock("../src/storage/SupabaseHistoryManager", () => ({
SupabaseHistoryManager: jest
.fn()
@@ -260,6 +265,8 @@ describe("VectorStoreFactory", () => {
["vectorize"],
["azure-ai-search"],
["pgvector"],
["s3-vectors"],
["s3_vectors"],
])("creates vector store for provider '%s'", (provider) => {
expect(() =>
VectorStoreFactory.create(provider, dummyVSConfig),
+3 -4
View File
@@ -14,7 +14,6 @@ describe("tsup.config.ts externals", () => {
beforeAll(() => {
const pkgPath = path.resolve(__dirname, "../../../package.json");
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
// Filter out @types/* packages — they are type-only and not bundled at runtime
peerDeps = Object.keys(pkg.peerDependencies || {}).filter(
(dep) => !dep.startsWith("@types/"),
);
@@ -22,16 +21,16 @@ describe("tsup.config.ts externals", () => {
const tsupConfigPath = path.resolve(__dirname, "../../../tsup.config.ts");
const tsupContent = fs.readFileSync(tsupConfigPath, "utf-8");
// Extract strings from the external array (supports double, single, and backtick quotes)
const externalMatch = tsupContent.match(
/const external\s*=\s*\[([\s\S]*?)\];/,
);
if (!externalMatch) {
throw new Error("Could not find external array in tsup.config.ts");
}
const matches = externalMatch[1].match(/["'`]([^"'`]+)["'`]/g);
externalDeps = (matches || []).map((m) => m.replace(/["'`]/g, ""));
externalDeps = (matches || []).map((value) => value.replace(/["'`]/g, ""));
});
it("should have every peerDependency in the external array", () => {
@@ -720,7 +720,358 @@ describe("AzureAISearch – backward compat with mocked client", () => {
});
// ───────────────────────────────────────────────────────────────────────────
// 6. Vectorize — mock Cloudflare client, test idempotent init
// 6. S3 Vectors — mock AWS client, test interface + init
// ───────────────────────────────────────────────────────────────────────────
describe("S3 Vectors – backward compat with mocked client", () => {
function createMockS3VectorsClient(options?: {
queryDistance?: number;
queryDistanceMetric?: "cosine" | "euclidean";
}) {
const buckets = new Set<string>();
const indexes = new Map<
string,
{
dimension: number;
distanceMetric: string;
}
>();
const vectors = new Map<
string,
{
key: string;
data: { float32: number[] };
metadata: Record<string, any>;
}
>();
const vectorMapKey = (indexName: string, key: string) =>
`${indexName}:${key}`;
return {
send: jest.fn().mockImplementation(async (command: any) => {
const name = command.constructor.name;
const input = command.input;
switch (name) {
case "GetVectorBucketCommand":
if (!buckets.has(input.vectorBucketName)) {
const error: any = new Error("Bucket not found");
error.name = "NotFoundException";
throw error;
}
return {};
case "CreateVectorBucketCommand":
if (buckets.has(input.vectorBucketName)) {
const error: any = new Error("Bucket exists");
error.name = "ConflictException";
throw error;
}
buckets.add(input.vectorBucketName);
return {};
case "GetIndexCommand":
if (!indexes.has(input.indexName)) {
const error: any = new Error("Index not found");
error.name = "NotFoundException";
throw error;
}
return {};
case "CreateIndexCommand":
if (indexes.has(input.indexName)) {
const error: any = new Error("Index exists");
error.name = "ConflictException";
throw error;
}
indexes.set(input.indexName, {
dimension: input.dimension,
distanceMetric: input.distanceMetric,
});
return {};
case "PutVectorsCommand":
for (const vector of input.vectors || []) {
vectors.set(vectorMapKey(input.indexName, vector.key), {
key: vector.key,
data: vector.data,
metadata: vector.metadata || {},
});
}
return {};
case "QueryVectorsCommand":
return {
distanceMetric: options?.queryDistanceMetric ?? "cosine",
vectors: [
{
key: "doc-1",
metadata: { user_id: "u1", topic: "alpha" },
distance: options?.queryDistance ?? 0.25,
},
],
};
case "GetVectorsCommand":
return {
vectors: (input.keys || [])
.map((key: string) =>
vectors.get(vectorMapKey(input.indexName, key)),
)
.filter(Boolean),
};
case "DeleteVectorsCommand":
for (const key of input.keys || []) {
vectors.delete(vectorMapKey(input.indexName, key));
}
return {};
case "DeleteIndexCommand":
indexes.delete(input.indexName);
for (const key of Array.from(vectors.keys())) {
if (key.startsWith(`${input.indexName}:`)) {
vectors.delete(key);
}
}
return {};
case "ListVectorsCommand":
return {
vectors: Array.from(vectors.values())
.filter((entry) =>
vectorMapKey(input.indexName, entry.key).startsWith(
`${input.indexName}:`,
),
)
.map((entry) => ({
key: entry.key,
metadata: entry.metadata,
})),
};
default:
throw new Error(`Unexpected S3Vectors command: ${name}`);
}
}),
};
}
function findCommandInput(
client: { send: jest.Mock },
commandName: string,
): Record<string, any> | undefined {
const match = client.send.mock.calls.find(
([command]) => command.constructor.name === commandName,
);
return match?.[0]?.input;
}
it("implements full VectorStore interface", () => {
const { S3Vectors } = require("../src/vector_stores/s3_vectors");
const store = new S3Vectors({
client: createMockS3VectorsClient(),
vectorBucketName: "test-bucket",
collectionName: "test-index",
embeddingModelDims: 3,
});
expect(typeof store.insert).toBe("function");
expect(typeof store.search).toBe("function");
expect(typeof store.get).toBe("function");
expect(typeof store.update).toBe("function");
expect(typeof store.delete).toBe("function");
expect(typeof store.deleteCol).toBe("function");
expect(typeof store.list).toBe("function");
expect(typeof store.getUserId).toBe("function");
expect(typeof store.setUserId).toBe("function");
expect(typeof store.initialize).toBe("function");
});
it("initialize() is idempotent (same promise returned)", async () => {
const { S3Vectors } = require("../src/vector_stores/s3_vectors");
const mockClient = createMockS3VectorsClient();
const store = new S3Vectors({
client: mockClient,
vectorBucketName: "test-bucket",
collectionName: "test-index",
embeddingModelDims: 3,
});
const p1 = store.initialize();
const p2 = store.initialize();
const p3 = store.initialize();
await Promise.all([p1, p2, p3]);
expect(
mockClient.send.mock.calls.filter(
([command]) => command.constructor.name === "CreateVectorBucketCommand",
),
).toHaveLength(1);
expect(
mockClient.send.mock.calls.filter(
([command]) => command.constructor.name === "CreateIndexCommand",
),
).toHaveLength(1);
expect(
mockClient.send.mock.calls.filter(
([command]) => command.constructor.name === "GetVectorBucketCommand",
),
).toHaveLength(1);
expect(
mockClient.send.mock.calls.filter(
([command]) => command.constructor.name === "GetIndexCommand",
),
).toHaveLength(1);
});
it("shapes S3 write requests and normalizes search results", async () => {
const { S3Vectors } = require("../src/vector_stores/s3_vectors");
const mockClient = createMockS3VectorsClient();
const store = new S3Vectors({
client: mockClient,
vectorBucketName: "test-bucket",
collectionName: "test-index",
embeddingModelDims: 3,
});
await store.initialize();
await store.insert(
[[0.1, 0.2, 0.3]],
["doc-1"],
[{ user_id: "u1", topic: "alpha" }],
);
const putInput = findCommandInput(mockClient, "PutVectorsCommand");
expect(putInput).toMatchObject({
vectorBucketName: "test-bucket",
indexName: "test-index",
vectors: [
{
key: "doc-1",
data: { float32: [0.1, 0.2, 0.3] },
metadata: { user_id: "u1", topic: "alpha" },
},
],
});
const results = await store.search([0.1, 0.2, 0.3], 5, { user_id: "u1" });
const queryInput = findCommandInput(mockClient, "QueryVectorsCommand");
expect(queryInput).toMatchObject({
vectorBucketName: "test-bucket",
indexName: "test-index",
queryVector: { float32: [0.1, 0.2, 0.3] },
topK: 5,
filter: { user_id: { $eq: "u1" } },
returnMetadata: true,
returnDistance: true,
});
expect(results).toEqual([
{
id: "doc-1",
payload: { user_id: "u1", topic: "alpha" },
score: 0.75,
},
]);
});
it("normalizes euclidean distances without collapsing scores to zero", async () => {
const { S3Vectors } = require("../src/vector_stores/s3_vectors");
const store = new S3Vectors({
client: createMockS3VectorsClient({
queryDistance: 1.5,
queryDistanceMetric: "euclidean",
}),
vectorBucketName: "test-bucket",
collectionName: "test-index",
embeddingModelDims: 3,
distanceMetric: "cosine",
});
const [result] = await store.search([0.1, 0.2, 0.3], 1);
expect(result).toEqual({
id: "doc-1",
payload: { user_id: "u1", topic: "alpha" },
score: 0.4,
});
});
it("normalizes empty in and nin operands before search hits AWS", async () => {
const { S3Vectors } = require("../src/vector_stores/s3_vectors");
const mockClient = createMockS3VectorsClient();
const store = new S3Vectors({
client: mockClient,
vectorBucketName: "test-bucket",
collectionName: "test-index",
embeddingModelDims: 3,
});
const baselineQueryCalls = mockClient.send.mock.calls.filter(
([command]) => command.constructor.name === "QueryVectorsCommand",
).length;
const emptyInResults = await store.search([0.1, 0.2, 0.3], 5, {
topic: { in: [] },
});
expect(emptyInResults).toEqual([]);
expect(
mockClient.send.mock.calls.filter(
([command]) => command.constructor.name === "QueryVectorsCommand",
),
).toHaveLength(baselineQueryCalls);
await store.search([0.1, 0.2, 0.3], 5, {
topic: { nin: [] },
});
const queryInput = findCommandInput(mockClient, "QueryVectorsCommand");
expect(queryInput).toMatchObject({
vectorBucketName: "test-bucket",
indexName: "test-index",
queryVector: { float32: [0.1, 0.2, 0.3] },
topK: 5,
returnMetadata: true,
returnDistance: true,
});
expect(queryInput.filter).toBeUndefined();
});
it("applies client-side list filters, including empty $nin operands", async () => {
const { S3Vectors } = require("../src/vector_stores/s3_vectors");
const mockClient = createMockS3VectorsClient();
const store = new S3Vectors({
client: mockClient,
vectorBucketName: "test-bucket",
collectionName: "test-index",
embeddingModelDims: 3,
});
await store.initialize();
await store.insert(
[
[0.1, 0.2, 0.3],
[0.3, 0.2, 0.1],
],
["doc-1", "doc-2"],
[
{ user_id: "u1", topic: "alpha", tags: ["keep"] },
{ user_id: "u2", topic: "beta", tags: ["skip"] },
],
);
const [allRows, allCount] = await store.list({ topic: { nin: [] } }, 10);
expect(allCount).toBe(2);
expect(allRows.map((row) => row.id)).toEqual(["doc-1", "doc-2"]);
const [filteredRows, filteredCount] = await store.list(
{
$and: [{ topic: { nin: ["beta"] } }, { tags: { in: ["keep"] } }],
},
10,
);
expect(filteredCount).toBe(1);
expect(filteredRows).toEqual([
{
id: "doc-1",
payload: { user_id: "u1", topic: "alpha", tags: ["keep"] },
},
]);
});
});
// ───────────────────────────────────────────────────────────────────────────
// 7. Vectorize — mock Cloudflare client, test idempotent init
// ───────────────────────────────────────────────────────────────────────────
describe("Vectorize – backward compat with mocked client", () => {
let VectorizeDB: any;