Files
2023-10-15 15:43:51 -07:00

404 lines
12 KiB
JavaScript

const { DocumentVectors } = require('../../../backend/models/documentVectors');
const {
OrganizationConnection,
} = require('../../../backend/models/organizationConnection');
const {
OrganizationWorkspace,
} = require('../../../backend/models/organizationWorkspace');
const { Queue } = require('../../../backend/models/queue');
const {
WorkspaceDocument,
} = require('../../../backend/models/workspaceDocument');
const {
cachedVectorInformation,
storeVectorResult,
} = require('../../../backend/utils/storage');
const {
selectConnector,
} = require('../../../backend/utils/vectordatabases/providers');
const { toChunks } = require('../../../backend/utils/vectordatabases/utils');
const { v4 } = require('uuid');
const { InngestClient } = require('../../utils/inngest');
const { vectorSpaceMetric } = require('../../utils/telemetryHelpers');
const migrateOrganization = InngestClient.createFunction(
{ name: 'Migrate all vector data from one vector db to another' },
{ event: 'organization/migrate' },
async ({ event, step: _step, logger }) => {
var result = {};
const { organization, destinationOrganization, jobId } = event.data;
try {
const skipped = [];
const orgConnector = await OrganizationConnection.get({
organization_id: Number(organization.id),
});
const destinationConnector = await OrganizationConnection.get({
organization_id: Number(destinationOrganization.id),
});
if (!orgConnector || !destinationConnector) {
const result = {
message: `Job failed with error`,
error:
'The organization or destination organization does not have a connected vector database.',
details: e,
};
await Queue.updateJob(jobId, Queue.status.failed, result);
return { result };
}
const destinationVectorDb = selectConnector(destinationConnector);
var count = 1;
const workspaces = await OrganizationWorkspace.where({
organization_id: Number(organization.id),
});
for (const workspace of workspaces) {
await Queue.updateJob(jobId, Queue.status.pending, {
message: `Working on namespace ${count} of ${workspaces.length}`,
});
const migratedWorkspace = await createMigrationWorkspace(
destinationVectorDb,
workspace,
destinationOrganization
);
if (!migratedWorkspace) {
skipped.push(workspace.fname);
count++;
continue;
}
const documentsToClone = await WorkspaceDocument.where({
workspace_id: Number(workspace.id),
});
console.log(
`Migrating ${documentsToClone.length} documents to ${destinationOrganization.name}:${migratedWorkspace.fname}`
);
for (const document of documentsToClone) {
const newDocId = v4();
const cacheInfo = await cachedVectorInformation(
WorkspaceDocument.vectorFilename(document)
);
if (!cacheInfo.exists) {
console.error(
`No vector cache file was found for ${document.name} - cannot clone. Skipping.`
);
continue;
}
const { document: migratedDocument } = await WorkspaceDocument.create(
{
id: newDocId,
name: document.name,
workspaceId: migratedWorkspace.id,
organizationId: migratedWorkspace.organization_id,
}
);
if (!migratedDocument) {
console.error(
`Failed to migrate document for ${document.name}. Skipping.`
);
continue;
}
const newFragments = [];
const newCacheInfo = [];
for (const chunks of toChunks(cacheInfo.chunks, 500)) {
const chunk = chunks.map((chunk) => {
const vectorDbId = v4();
const { metadata, values } = chunk;
newFragments.push({
docId: newDocId,
vectorId: vectorDbId,
documentId: migratedDocument.id,
workspaceId: migratedDocument.workspace_id,
organizationId: migratedDocument.organization_id,
});
newCacheInfo.push({
vectorDbId: vectorDbId,
values,
metadata,
});
return {
metadata,
values,
id: vectorDbId,
};
});
console.log(
`Need to insert ${chunk.length} vectors into ${destinationConnector.type}`
);
await upsertVectors(
destinationVectorDb,
migratedWorkspace.fname,
chunk
);
}
await DocumentVectors.createMany(newFragments);
await storeVectorResult(
newCacheInfo,
WorkspaceDocument.vectorFilename(migratedDocument)
);
}
count++;
}
result = {
message: `Organization ${organization.name} vectors migrated to ${destinationOrganization.name}.`,
details: {
skippedNamespaces: skipped,
},
};
await Queue.updateJob(jobId, Queue.status.complete, result);
await vectorSpaceMetric();
return { result };
} catch (e) {
const result = {
message: `Job failed with error`,
error: e.message,
details: e,
};
await Queue.updateJob(jobId, Queue.status.failed, result);
return { result };
}
}
);
// Pinecone, which has a default empty namespace needs to be able to be ported.
// since this namespace will exist for every pinecone instance we need to skip
// the check + creation of workspace for the empty workspace and allow it to pass-through
async function createMigrationWorkspace(
vectorDBClient,
workspace,
destinationOrganization
) {
if (
vectorDBClient.name === 'pinecone' &&
vectorDBClient.isStarterTier() &&
workspace.fname !== ''
) {
console.log(
`Destination Pinecone vector DB does not support namespace - so must skip upsert for ${workspace.fname}`
);
return null;
}
if (vectorDBClient.name === 'pinecone' && workspace.fname === '') {
const { workspace: migratedWorkspace } = await OrganizationWorkspace.create(
'(default)',
destinationOrganization.id,
''
);
return migratedWorkspace;
}
// Otherwise migrate over as normal
const existsInDestination = await namespaceExists(
vectorDBClient,
workspace.fname
);
if (existsInDestination) return null;
const collection = await createVectorSpace(vectorDBClient, workspace.fname);
const { workspace: migratedWorkspace } = await OrganizationWorkspace.create(
collection.name,
destinationOrganization.id
);
return migratedWorkspace;
}
async function namespaceExists(vectorDBClient, namespace) {
if (vectorDBClient.name === 'pinecone') {
try {
const { pineconeIndex } = await vectorDBClient.connect();
return await vectorDBClient.namespaceExists(pineconeIndex, namespace);
} catch (e) {
return null;
}
}
if (vectorDBClient.name === 'chroma') {
try {
return await vectorDBClient.namespaceExists(null, namespace);
} catch (e) {
return null;
}
}
if (vectorDBClient.name === 'qdrant') {
try {
const { client } = await vectorDBClient.connect();
return await vectorDBClient.namespaceExists(client, namespace);
} catch (e) {
return null;
}
}
if (vectorDBClient.name === 'weaviate') {
try {
const className = vectorDBClient.camelCase(namespace);
return await vectorDBClient.namespaceExists(null, className);
} catch (e) {
return null;
}
}
return null;
}
async function createVectorSpace(vectorDBClient, namespace) {
if (vectorDBClient.name === 'chroma') {
try {
const { client } = await vectorDBClient.connect();
const collection = await client.createCollection({
name: namespace,
});
return { name: namespace, ...collection };
} catch (e) {
return null;
}
}
if (vectorDBClient.name === 'qdrant') {
try {
const { client } = await vectorDBClient.connect();
const collectionCreated = await client.createCollection(namespace, {
vectors: {
size: 1536, // TODO: Fixed to OpenAI models - when other embeddings exist make variable.
distance: 'Cosine',
},
});
return collectionCreated ? { name: namespace } : null;
} catch (e) {
return null;
}
}
if (vectorDBClient.name === 'weaviate') {
try {
const className = vectorDBClient.camelCase(namespace);
const { client } = await vectorDBClient.connect();
const collectionCreated = await client.schema
.classCreator()
.withClass({
class: className,
description: `Class created by VectorAdmin named ${className}`,
vectorizer: 'none',
})
.do();
return collectionCreated?.class ? { name: className } : null;
} catch (e) {
return null;
}
}
return { name: namespace };
}
// Data will always be an object[] with the following schema:
// metadata: {object with key value pairs}
// values: number[]
// id: string
// This function will take a known format and re-format it to be able to
// be easily inserted into the correct vector database.
async function upsertVectors(vectorDBClient, namespace, data = []) {
if (vectorDBClient.name === 'pinecone') {
try {
const { pineconeIndex } = await vectorDBClient.connect();
const upserts = data; // data array is fine in default format.
await pineconeIndex.upsert({
upsertRequest: {
vectors: upserts,
namespace: namespace,
},
});
return true;
} catch (e) {
console.error(e);
return false;
}
}
if (vectorDBClient.name === 'chroma') {
try {
const { client } = await vectorDBClient.connect();
const collection = await client.getCollection({ name: namespace });
const upserts = {
ids: [],
embeddings: [],
metadatas: [],
documents: [],
};
data.forEach((chunk) => {
upserts.ids.push(chunk.id);
upserts.embeddings.push(chunk.values);
upserts.metadatas.push(chunk.metadata);
upserts.documents.push(chunk.metadata?.text || '');
});
await collection.add(upserts);
return true;
} catch (e) {
console.error(e);
return false;
}
}
if (vectorDBClient.name === 'qdrant') {
try {
const { client } = await vectorDBClient.connect();
const upserts = {
ids: [],
vectors: [],
payloads: [],
};
data.forEach((chunk) => {
upserts.ids.push(chunk.id);
upserts.vectors.push(chunk.values);
upserts.payloads.push(chunk.metadata);
});
await client.upsert(namespace, {
wait: true,
batch: upserts,
});
return true;
} catch (e) {
return null;
}
}
if (vectorDBClient.name === 'weaviate') {
try {
const { client } = await vectorDBClient.connect();
const weaviateVectors = [];
data.forEach((chunk) => {
weaviateVectors.push({
id: chunk.id,
class: namespace,
vector: chunk.values,
properties: vectorDBClient.flattenObjectForWeaviate(chunk.metadata),
});
});
await vectorDBClient.addVectors(client, weaviateVectors);
return true;
} catch (e) {
return null;
}
}
}
module.exports = {
migrateOrganization,
};