mirror of
https://github.com/Mintplex-Labs/vector-admin.git
synced 2026-08-27 02:21:18 -04:00
404 lines
12 KiB
JavaScript
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,
|
|
};
|