Skip to content

Implement Backups & Restore #342

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 9, 2025
4 changes: 1 addition & 3 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ jobs:
[
{ runner: 'npm', jest_env: 'node' },
{ runner: 'npm', jest_env: 'edge' },
{ runner: 'bun', jest_env: 'node', bun_version: '1.0.0' },
{ runner: 'bun', jest_env: 'node', bun_version: '1.0.36' },
{ runner: 'bun', jest_env: 'node', bun_version: '1.1.11' },
{ runner: 'bun', jest_env: 'node', bun_version: '1.2' },
]

steps:
Expand Down
219 changes: 219 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,225 @@ const list = await pc.listCollections();
// }
```

## Backups

A backup is a static copy of a serverless index that only consumes storage. It is a non-queryable representation of a set of records. You can create a backup of a serverless index, and you can create a new serverless index from a backup. You can optionally apply new `tags` and `deletionProtection` configurations to the index. You can read more about [backups here](https://docs.pinecone.io/guides/manage-data/backups-overview).

### Create a backup

You can create a new backup from an existing index using the index name:

```typescript
import { Pinecone } from '@pinecone-database/pinecone';
const pc = new Pinecone();
const backup = await pc.createBackup({
indexName: 'my-index',
name: 'my-index-backup-1',
description: 'weekly backup',
});
console.log(backup);
// {
// backupId: '11450b9f-96e5-47e5-9186-03f346b1f385',
// sourceIndexName: 'my-index',
// sourceIndexId: 'b480770b-600d-4c4e-bf19-799c933ae2bf',
// name: 'my-index-backup-1',
// description: 'weekly backup',
// status: 'Initializing',
// cloud: 'aws',
// region: 'us-east-1',
// dimension: 1024,
// metric: 'cosine',
// recordCount: 500,
// namespaceCount: 4,
// sizeBytes: 78294,
// tags: {},
// createdAt: '2025-05-07T03:11:11.722238160Z'
// }
```

### Create a new index from a backup

You can restore a serverless index by creating a new index from a backup. Optionally, you can provide
new `tags` or `deletionProtection` values when restoring an index. Creating an index from a backup intiates
a new restore job, which can be used to view the progress of the index restoration through `describeRestoreJob`.

```typescript
import { Pinecone } from '@pinecone-database/pinecone';
const pc = new Pinecone();
const response = await pc.createIndexFromBackup({
backupId: '11450b9f-96e5-47e5-9186-03f346b1f385',
name: 'my-index-restore-1',
});
console.log(response);
// {
// restoreJobId: '4d4c8693-10fd-4204-a57b-1e3e626fca07',
// indexId: 'deb7688b-9f21-4c16-8eb7-f0027abd27fe'
// }
```

### Describe and list backups

You can use a `backupId` and the `describeBackup` method to describe a specific backup:

```typescript
import { Pinecone } from '@pinecone-database/pinecone';
const pc = new Pinecone();
const backup = await pc.describeBackup('11450b9f-96e5-47e5-9186-03f346b1f385');
console.log(backup);
// {
// backupId: '11450b9f-96e5-47e5-9186-03f346b1f385',
// sourceIndexName: 'my-index',
// sourceIndexId: 'b480770b-600d-4c4e-bf19-799c933ae2bf',
// name: 'my-index-backup-1',
// description: 'weekly backup',
// status: 'Initializing',
// cloud: 'aws',
// region: 'us-east-1',
// dimension: 1024,
// metric: 'cosine',
// recordCount: 500,
// namespaceCount: 4,
// sizeBytes: 78294,
// tags: {},
// createdAt: '2025-05-07T03:11:11.722238160Z'
// }
```

`listBackups` lists all the backups for a specific index, or your entire project. If an `indexName` is provided, only

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as before, isn't indexName a required field since its a path parameter?

the backups for that index will be listed.

```typescript
import { Pinecone } from '@pinecone-database/pinecone';
const pc = new Pinecone();

// list backups for the entire project
const projectBackups = await pc.listBackups({ limit: 2 });
// list backups for a specific index
const myIndexBackups = await pc.listBackups({
indexName: 'my-index',
limit: 2,
});
console.log(myIndexBackups);
// {
// data: [
// {
// backupId: '6a00902c-d118-4ad3-931c-49328c26d558',
// sourceIndexName: 'my-index',
// sourceIndexId: '0888b4d9-0b7b-447e-a403-ab057ceee4d4',
// name: 'my-index-backup-2',
// description: undefined,
// status: 'Ready',
// cloud: 'aws',
// region: 'us-east-1',
// dimension: 5,
// metric: 'cosine',
// recordCount: 200,
// namespaceCount: 2,
// sizeBytes: 67284,
// tags: {},
// createdAt: '2025-05-07T18:34:13.626650Z'
// },
// {
// backupId: '2b362ea3-b7cf-4950-866f-0dff37ab781e',
// sourceIndexName: 'my-index',
// sourceIndexId: '0888b4d9-0b7b-447e-a403-ab057ceee4d4',
// name: 'my-index-backup-1',
// description: undefined,
// status: 'Ready',
// cloud: 'aws',
// region: 'us-east-1',
// dimension: 1024,
// metric: 'cosine',
// recordCount: 500,
// namespaceCount: 4,
// sizeBytes: 78294,
// tags: {},
// createdAt: '2025-05-07T18:33:59.888270Z'
// },
// ],
// pagination: undefined
// }
```

### Describe and list restore jobs

You can use a `restoreJobId` and the `describeRestoreJob` method to describe a specific backup:

```typescript
import { Pinecone } from '@pinecone-database/pinecone';
const pc = new Pinecone();

const restoreJob = await pc.describeRestoreJob(
'4d4c8693-10fd-4204-a57b-1e3e626fca07'
);
console.log(restoreJob);
// {
// restoreJobId: '4d4c8693-10fd-4204-a57b-1e3e626fca07',
// backupId: '11450b9f-96e5-47e5-9186-03f346b1f385',
// targetIndexName: 'my-index-restore-1',
// targetIndexId: 'deb7688b-9f21-4c16-8eb7-f0027abd27fe',
// status: 'Completed',
// createdAt: 2025-05-07T03:38:37.107Z,
// completedAt: 2025-05-07T03:40:23.687Z,
// percentComplete: 100
// }
```

`listRestoreJobs` lists all the restore jobs for your project.

```typescript
import { Pinecone } from '@pinecone-database/pinecone';
const pc = new Pinecone();

const projectRestoreJobs = await pc.listRestoreJobs({ limit: 3 });
console.log(projectRestoreJobs);
// {
// data: [
// {
// restoreJobId: '4d4c8693-10fd-4204-a57b-1e3e626fca07',
// backupId: '11450b9f-96e5-47e5-9186-03f346b1f385',
// targetIndexName: 'my-index-restore-1',
// targetIndexId: 'deb7688b-9f21-4c16-8eb7-f0027abd27fe',
// status: 'Completed',
// createdAt: 2025-05-07T03:38:37.107Z,
// completedAt: 2025-05-07T03:40:23.687Z,
// percentComplete: 100
// },
// {
// restoreJobId: 'c60a62e0-63b9-452a-88af-31d89c56c988',
// backupId: '11450b9f-96e5-47e5-9186-03f346b1f385',
// targetIndexName: 'my-index-restore-2',
// targetIndexId: 'f2c9a846-799f-4b19-81a4-f3096b3d6114',
// status: 'Completed',
// createdAt: 2025-05-07T21:42:38.971Z,
// completedAt: 2025-05-07T21:43:11.782Z,
// percentComplete: 100
// },
// {
// restoreJobId: '792837b7-8001-47bf-9c11-1859826b9c10',
// backupId: '11450b9f-96e5-47e5-9186-03f346b1f385',
// targetIndexName: 'my-index-restore-3',
// targetIndexId: '620dda62-c999-4dd1-b083-6beb087b31e7',
// status: 'Pending',
// createdAt: 2025-05-07T21:48:39.580Z,
// completedAt: 2025-05-07T21:49:12.084Z,
// percentComplete: 45
// }
// ],
// pagination: undefined
// }
```

### Delete backups

You can delete a backup using the backupId and `deleteBackup`.

```typescript
import { Pinecone } from '@pinecone-database/pinecone';
const pc = new Pinecone();
await pc.deleteBackup('6a00902c-d118-4ad3-931c-49328c26d558');
```

## Index operations

Pinecone indexes support operations for working with vector data using methods such as upsert, query, fetch, and delete.
Expand Down
62 changes: 62 additions & 0 deletions src/control/__tests__/createBackup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { createBackup } from '../createBackup';
import { ManageIndexesApi } from '../../pinecone-generated-ts-fetch/db_control';
import type {
BackupModel,
CreateBackupOperationRequest,
} from '../../pinecone-generated-ts-fetch/db_control';
import { PineconeArgumentError } from '../../errors';

const setupCreateBackupResponse = (
createBackupResponse = {} as BackupModel,
isCreateBackupSuccess = true
) => {
const fakeCreateBackup: (
req: CreateBackupOperationRequest
) => Promise<BackupModel> = jest
.fn()
.mockImplementation(() =>
isCreateBackupSuccess
? Promise.resolve(createBackupResponse)
: Promise.reject(createBackupResponse)
);

const MIA = {
createBackup: fakeCreateBackup,
} as ManageIndexesApi;

return MIA;
};

describe('createBackup', () => {
test('calls the openapi create backup endpoint, passing name and description', async () => {
const MIA = setupCreateBackupResponse();
const returned = await createBackup(MIA)({
indexName: 'index-name',
name: 'backup-name',
description: 'backup-description',
});
expect(returned).toEqual({} as BackupModel);
expect(MIA.createBackup).toHaveBeenCalledWith({
indexName: 'index-name',
createBackupRequest: {
name: 'backup-name',
description: 'backup-description',
},
});
});

test('throws an error if indexName is not provided', async () => {
const MIA = setupCreateBackupResponse();
await expect(
createBackup(MIA)({
indexName: '',
name: 'backup-name',
description: 'backup-description',
})
).rejects.toThrow(
new PineconeArgumentError(
'You must pass a non-empty string for `indexName` in order to create a backup'
)
);
});
});
74 changes: 74 additions & 0 deletions src/control/__tests__/createIndexFromBackup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { createIndexFromBackup } from '../createIndexFromBackup';
import { ManageIndexesApi } from '../../pinecone-generated-ts-fetch/db_control';
import type {
BackupModel,
CreateIndexFromBackupOperationRequest,
CreateIndexFromBackupResponse,
} from '../../pinecone-generated-ts-fetch/db_control';
import { PineconeArgumentError } from '../../errors';

const setupCreateIndexFromBackupResponse = (
createIndexFromBackupResponse = {} as BackupModel,
isCreateIndexFromBackupSuccess = true
) => {
const fakeCreateIndexFromBackup: (
req: CreateIndexFromBackupOperationRequest
) => Promise<CreateIndexFromBackupResponse> = jest
.fn()
.mockImplementation(() =>
isCreateIndexFromBackupSuccess
? Promise.resolve(createIndexFromBackupResponse)
: Promise.reject(createIndexFromBackupResponse)
);

const MIA = {
createIndexFromBackup: fakeCreateIndexFromBackup,
} as ManageIndexesApi;

return MIA;
};

describe('createIndexFromBackup', () => {
test('calls the openapi create indexn from backup endpoint, passing backupId, name, tags, and deletionProtection', async () => {
const MIA = setupCreateIndexFromBackupResponse();
const returned = await createIndexFromBackup(MIA)({
backupId: '12345-ajfielkas-123123',
name: 'my-restored-index',
tags: { test: 'test-tag' },
deletionProtection: 'enabled',
});
expect(returned).toEqual({} as CreateIndexFromBackupResponse);
expect(MIA.createIndexFromBackup).toHaveBeenCalledWith({
backupId: '12345-ajfielkas-123123',
createIndexFromBackupRequest: {
name: 'my-restored-index',
tags: { test: 'test-tag' },
deletionProtection: 'enabled',
},
});
});

test('throws an error if backupId or name are not provided', async () => {
const MIA = setupCreateIndexFromBackupResponse();
await expect(
createIndexFromBackup(MIA)({
backupId: '',
name: 'my-restored-index',
})
).rejects.toThrow(
new PineconeArgumentError(
'You must pass a non-empty string for `backupId` in order to create an index from backup'
)
);
await expect(
createIndexFromBackup(MIA)({
backupId: '123-123-123-123',
name: '',
})
).rejects.toThrow(
new PineconeArgumentError(
'You must pass a non-empty string for `name` in order to create an index from backup'
)
);
});
});
Loading
Loading