diff --git a/.vscode/settings.json b/.vscode/settings.json index 82165a17..ab66c194 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,10 +2,13 @@ "cSpell.words": [ "Dockerized", "Langchain", + "Milvus", "Ollama", "openai", "Qdrant", - "Weaviate" + "vectordbs", + "Weaviate", + "Zilliz" ], "eslint.experimental.useFlatConfig": true } \ No newline at end of file diff --git a/README.md b/README.md index 6e3df0df..c3eb429c 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ Some cool features of AnythingLLM - [Weaviate](https://weaviate.io) - [QDrant](https://qdrant.tech) - [Milvus](https://milvus.io) +- [Zilliz](https://zilliz.com) ### Technical Overview diff --git a/docker/.env.example b/docker/.env.example index 8d33a809..f3eba241 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -99,6 +99,11 @@ GID='1000' # MILVUS_USERNAME= # MILVUS_PASSWORD= +# Enable all below if you are using vector database: Zilliz Cloud. +# VECTOR_DB="zilliz" +# ZILLIZ_ENDPOINT="https://sample.api.gcp-us-west1.zillizcloud.com" +# ZILLIZ_API_TOKEN=api-token-here + # CLOUD DEPLOYMENT VARIRABLES ONLY # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting. diff --git a/frontend/src/components/VectorDBSelection/ZillizCloudOptions/index.jsx b/frontend/src/components/VectorDBSelection/ZillizCloudOptions/index.jsx new file mode 100644 index 00000000..5a26b437 --- /dev/null +++ b/frontend/src/components/VectorDBSelection/ZillizCloudOptions/index.jsx @@ -0,0 +1,38 @@ +export default function ZillizCloudOptions({ settings }) { + return ( +
+
+
+ + +
+ +
+ + +
+
+
+ ); +} diff --git a/frontend/src/media/vectordbs/zilliz.png b/frontend/src/media/vectordbs/zilliz.png new file mode 100644 index 00000000..e755b0f1 Binary files /dev/null and b/frontend/src/media/vectordbs/zilliz.png differ diff --git a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx index f49054b9..02887b86 100644 --- a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx +++ b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx @@ -9,6 +9,7 @@ import LanceDbLogo from "@/media/vectordbs/lancedb.png"; import WeaviateLogo from "@/media/vectordbs/weaviate.png"; import QDrantLogo from "@/media/vectordbs/qdrant.png"; import MilvusLogo from "@/media/vectordbs/milvus.png"; +import ZillizLogo from "@/media/vectordbs/zilliz.png"; import PreLoader from "@/components/Preloader"; import ChangeWarningModal from "@/components/ChangeWarning"; import { MagnifyingGlass } from "@phosphor-icons/react"; @@ -19,6 +20,7 @@ import QDrantDBOptions from "@/components/VectorDBSelection/QDrantDBOptions"; import WeaviateDBOptions from "@/components/VectorDBSelection/WeaviateDBOptions"; import VectorDBItem from "@/components/VectorDBSelection/VectorDBItem"; import MilvusDBOptions from "@/components/VectorDBSelection/MilvusDBOptions"; +import ZillizCloudOptions from "@/components/VectorDBSelection/ZillizCloudOptions"; export default function GeneralVectorDatabase() { const [saving, setSaving] = useState(false); @@ -33,7 +35,6 @@ export default function GeneralVectorDatabase() { useEffect(() => { async function fetchKeys() { const _settings = await System.keys(); - console.log(_settings); setSettings(_settings); setSelectedVDB(_settings?.VectorDB || "lancedb"); setHasEmbeddings(_settings?.HasExistingEmbeddings || false); @@ -66,6 +67,14 @@ export default function GeneralVectorDatabase() { options: , description: "100% cloud-based vector database for enterprise use cases.", }, + { + name: "Zilliz Cloud", + value: "zilliz", + logo: ZillizLogo, + options: , + description: + "Cloud hosted vector database built for enterprise with SOC 2 compliance.", + }, { name: "QDrant", value: "qdrant", diff --git a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx index 3b004638..ae573027 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx @@ -10,6 +10,7 @@ import TogetherAILogo from "@/media/llmprovider/togetherai.png"; import LMStudioLogo from "@/media/llmprovider/lmstudio.png"; import LocalAiLogo from "@/media/llmprovider/localai.png"; import MistralLogo from "@/media/llmprovider/mistral.jpeg"; +import ZillizLogo from "@/media/vectordbs/zilliz.png"; import ChromaLogo from "@/media/vectordbs/chroma.png"; import PineconeLogo from "@/media/vectordbs/pinecone.png"; import LanceDbLogo from "@/media/vectordbs/lancedb.png"; @@ -139,6 +140,13 @@ const VECTOR_DB_PRIVACY = { ], logo: MilvusLogo, }, + zilliz: { + name: "Zilliz Cloud", + description: [ + "Your vectors and document text are stored on your Zilliz cloud cluster.", + ], + logo: ZillizLogo, + }, lancedb: { name: "LanceDB", description: [ diff --git a/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx index 37e0e5b7..af0b5662 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx @@ -6,6 +6,7 @@ import LanceDbLogo from "@/media/vectordbs/lancedb.png"; import WeaviateLogo from "@/media/vectordbs/weaviate.png"; import QDrantLogo from "@/media/vectordbs/qdrant.png"; import MilvusLogo from "@/media/vectordbs/milvus.png"; +import ZillizLogo from "@/media/vectordbs/zilliz.png"; import System from "@/models/system"; import paths from "@/utils/paths"; import PineconeDBOptions from "@/components/VectorDBSelection/PineconeDBOptions"; @@ -14,6 +15,7 @@ import QDrantDBOptions from "@/components/VectorDBSelection/QDrantDBOptions"; import WeaviateDBOptions from "@/components/VectorDBSelection/WeaviateDBOptions"; import LanceDBOptions from "@/components/VectorDBSelection/LanceDBOptions"; import MilvusOptions from "@/components/VectorDBSelection/MilvusDBOptions"; +import ZillizCloudOptions from "@/components/VectorDBSelection/ZillizCloudOptions"; import showToast from "@/utils/toast"; import { useNavigate } from "react-router-dom"; import VectorDBItem from "@/components/VectorDBSelection/VectorDBItem"; @@ -68,6 +70,14 @@ export default function VectorDatabaseConnection({ options: , description: "100% cloud-based vector database for enterprise use cases.", }, + { + name: "Zilliz Cloud", + value: "zilliz", + logo: ZillizLogo, + options: , + description: + "Cloud hosted vector database built for enterprise with SOC 2 compliance.", + }, { name: "QDrant", value: "qdrant", diff --git a/server/.env.example b/server/.env.example index 26c51927..23e20bb1 100644 --- a/server/.env.example +++ b/server/.env.example @@ -96,6 +96,11 @@ VECTOR_DB="lancedb" # MILVUS_USERNAME= # MILVUS_PASSWORD= +# Enable all below if you are using vector database: Zilliz Cloud. +# VECTOR_DB="zilliz" +# ZILLIZ_ENDPOINT="https://sample.api.gcp-us-west1.zillizcloud.com" +# ZILLIZ_API_TOKEN=api-token-here + # CLOUD DEPLOYMENT VARIRABLES ONLY # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting. # STORAGE_DIR= # absolute filesystem path with no trailing slash diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index 1c4069ac..90de463f 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -63,6 +63,12 @@ const SystemSettings = { MilvusPassword: !!process.env.MILVUS_PASSWORD, } : {}), + ...(vectorDB === "zilliz" + ? { + ZillizEndpoint: process.env.ZILLIZ_ENDPOINT, + ZillizApiToken: process.env.ZILLIZ_API_TOKEN, + } + : {}), LLMProvider: llmProvider, ...(llmProvider === "openai" ? { diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index 2eed9057..b72bb797 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -19,6 +19,9 @@ function getVectorDbClass() { case "milvus": const { Milvus } = require("../vectorDbProviders/milvus"); return Milvus; + case "zilliz": + const { Zilliz } = require("../vectorDbProviders/zilliz"); + return Zilliz; default: throw new Error("ENV: No VECTOR_DB value found in environment!"); } diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index f44b040b..9e89047f 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -199,6 +199,16 @@ const KEY_MAPPING = { checks: [isNotEmpty], }, + // Zilliz Cloud Options + ZillizEndpoint: { + envKey: "ZILLIZ_ENDPOINT", + checks: [isValidURL], + }, + ZillizApiToken: { + envKey: "ZILLIZ_API_TOKEN", + checks: [isNotEmpty], + }, + // Together Ai Options TogetherAiApiKey: { envKey: "TOGETHER_AI_API_KEY", @@ -316,6 +326,7 @@ function supportedVectorDB(input = "") { "weaviate", "qdrant", "milvus", + "zilliz", ]; return supported.includes(input) ? null diff --git a/server/utils/vectorDbProviders/zilliz/index.js b/server/utils/vectorDbProviders/zilliz/index.js new file mode 100644 index 00000000..b8493e1c --- /dev/null +++ b/server/utils/vectorDbProviders/zilliz/index.js @@ -0,0 +1,365 @@ +const { + DataType, + MetricType, + IndexType, + MilvusClient, +} = require("@zilliz/milvus2-sdk-node"); +const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter"); +const { v4: uuidv4 } = require("uuid"); +const { storeVectorResult, cachedVectorInformation } = require("../../files"); +const { + toChunks, + getLLMProvider, + getEmbeddingEngineSelection, +} = require("../../helpers"); + +// Zilliz is basically a copy of Milvus DB class with a different constructor +// to connect to the cloud +const Zilliz = { + name: "Zilliz", + connect: async function () { + if (process.env.VECTOR_DB !== "zilliz") + throw new Error("Zilliz::Invalid ENV settings"); + + const client = new MilvusClient({ + address: process.env.ZILLIZ_ENDPOINT, + token: process.env.ZILLIZ_API_TOKEN, + }); + + const { isHealthy } = await client.checkHealth(); + if (!isHealthy) + throw new Error( + "Zilliz::Invalid Heartbeat received - is the instance online?" + ); + + return { client }; + }, + heartbeat: async function () { + await this.connect(); + return { heartbeat: Number(new Date()) }; + }, + totalVectors: async function () { + const { client } = await this.connect(); + const { collection_names } = await client.listCollections(); + const total = collection_names.reduce(async (acc, collection_name) => { + const statistics = await client.getCollectionStatistics({ + collection_name, + }); + return Number(acc) + Number(statistics?.data?.row_count ?? 0); + }, 0); + return total; + }, + namespaceCount: async function (_namespace = null) { + const { client } = await this.connect(); + const statistics = await client.getCollectionStatistics({ + collection_name: _namespace, + }); + return Number(statistics?.data?.row_count ?? 0); + }, + namespace: async function (client, namespace = null) { + if (!namespace) throw new Error("No namespace value provided."); + const collection = await client + .getCollectionStatistics({ collection_name: namespace }) + .catch(() => null); + return collection; + }, + hasNamespace: async function (namespace = null) { + if (!namespace) return false; + const { client } = await this.connect(); + return await this.namespaceExists(client, namespace); + }, + namespaceExists: async function (client, namespace = null) { + if (!namespace) throw new Error("No namespace value provided."); + const { value } = await client + .hasCollection({ collection_name: namespace }) + .catch((e) => { + console.error("Zilliz::namespaceExists", e.message); + return { value: false }; + }); + return value; + }, + deleteVectorsInNamespace: async function (client, namespace = null) { + await client.dropCollection({ collection_name: namespace }); + return true; + }, + // Zilliz requires a dimension aspect for collection creation + // we pass this in from the first chunk to infer the dimensions like other + // providers do. + getOrCreateCollection: async function (client, namespace, dimensions = null) { + const isExists = await this.namespaceExists(client, namespace); + if (!isExists) { + if (!dimensions) + throw new Error( + `Zilliz:getOrCreateCollection Unable to infer vector dimension from input. Open an issue on Github for support.` + ); + + await client.createCollection({ + collection_name: namespace, + fields: [ + { + name: "id", + description: "id", + data_type: DataType.VarChar, + max_length: 255, + is_primary_key: true, + }, + { + name: "vector", + description: "vector", + data_type: DataType.FloatVector, + dim: dimensions, + }, + { + name: "metadata", + decription: "metadata", + data_type: DataType.JSON, + }, + ], + }); + await client.createIndex({ + collection_name: namespace, + field_name: "vector", + index_type: IndexType.AUTOINDEX, + metric_type: MetricType.COSINE, + }); + await client.loadCollectionSync({ + collection_name: namespace, + }); + } + }, + addDocumentToNamespace: async function ( + namespace, + documentData = {}, + fullFilePath = null + ) { + const { DocumentVectors } = require("../../../models/vectors"); + try { + let vectorDimension = null; + const { pageContent, docId, ...metadata } = documentData; + if (!pageContent || pageContent.length == 0) return false; + + console.log("Adding new vectorized document into namespace", namespace); + const cacheResult = await cachedVectorInformation(fullFilePath); + if (cacheResult.exists) { + const { client } = await this.connect(); + const { chunks } = cacheResult; + const documentVectors = []; + vectorDimension = chunks[0][0].values.length || null; + + await this.getOrCreateCollection(client, namespace, vectorDimension); + for (const chunk of chunks) { + // Before sending to Pinecone and saving the records to our db + // we need to assign the id of each chunk that is stored in the cached file. + const newChunks = chunk.map((chunk) => { + const id = uuidv4(); + documentVectors.push({ docId, vectorId: id }); + return { id, vector: chunk.values, metadata: chunk.metadata }; + }); + const insertResult = await client.insert({ + collection_name: namespace, + data: newChunks, + }); + + if (insertResult?.status.error_code !== "Success") { + throw new Error( + `Error embedding into Zilliz! Reason:${insertResult?.status.reason}` + ); + } + } + await DocumentVectors.bulkInsert(documentVectors); + await client.flushSync({ collection_names: [namespace] }); + return true; + } + + const textSplitter = new RecursiveCharacterTextSplitter({ + chunkSize: + getEmbeddingEngineSelection()?.embeddingMaxChunkLength || 1_000, + chunkOverlap: 20, + }); + const textChunks = await textSplitter.splitText(pageContent); + + console.log("Chunks created from document:", textChunks.length); + const LLMConnector = getLLMProvider(); + const documentVectors = []; + const vectors = []; + const vectorValues = await LLMConnector.embedChunks(textChunks); + + if (!!vectorValues && vectorValues.length > 0) { + for (const [i, vector] of vectorValues.entries()) { + if (!vectorDimension) vectorDimension = vector.length; + const vectorRecord = { + id: uuidv4(), + values: vector, + // [DO NOT REMOVE] + // LangChain will be unable to find your text if you embed manually and dont include the `text` key. + metadata: { ...metadata, text: textChunks[i] }, + }; + + vectors.push(vectorRecord); + documentVectors.push({ docId, vectorId: vectorRecord.id }); + } + } else { + throw new Error( + "Could not embed document chunks! This document will not be recorded." + ); + } + + if (vectors.length > 0) { + const chunks = []; + const { client } = await this.connect(); + await this.getOrCreateCollection(client, namespace, vectorDimension); + + console.log("Inserting vectorized chunks into Zilliz."); + for (const chunk of toChunks(vectors, 100)) { + chunks.push(chunk); + const insertResult = await client.insert({ + collection_name: namespace, + data: chunk.map((item) => ({ + id: item.id, + vector: item.values, + metadata: chunk.metadata, + })), + }); + + if (insertResult?.status.error_code !== "Success") { + throw new Error( + `Error embedding into Zilliz! Reason:${insertResult?.status.reason}` + ); + } + } + await storeVectorResult(chunks, fullFilePath); + await client.flushSync({ collection_names: [namespace] }); + } + + await DocumentVectors.bulkInsert(documentVectors); + return true; + } catch (e) { + console.error(e); + console.error("addDocumentToNamespace", e.message); + return false; + } + }, + deleteDocumentFromNamespace: async function (namespace, docId) { + const { DocumentVectors } = require("../../../models/vectors"); + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) return; + const knownDocuments = await DocumentVectors.where({ docId }); + if (knownDocuments.length === 0) return; + + const vectorIds = knownDocuments.map((doc) => doc.vectorId); + const queryIn = vectorIds.map((v) => `'${v}'`).join(","); + await client.deleteEntities({ + collection_name: namespace, + expr: `id in [${queryIn}]`, + }); + + const indexes = knownDocuments.map((doc) => doc.id); + await DocumentVectors.deleteIds(indexes); + + // Even after flushing Zilliz can take some time to re-calc the count + // so all we can hope to do is flushSync so that the count can be correct + // on a later call. + await client.flushSync({ collection_names: [namespace] }); + return true; + }, + performSimilaritySearch: async function ({ + namespace = null, + input = "", + LLMConnector = null, + similarityThreshold = 0.25, + }) { + if (!namespace || !input || !LLMConnector) + throw new Error("Invalid request to performSimilaritySearch."); + + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) { + return { + contextTexts: [], + sources: [], + message: "Invalid query - no documents found for workspace!", + }; + } + + const queryVector = await LLMConnector.embedTextInput(input); + const { contextTexts, sourceDocuments } = await this.similarityResponse( + client, + namespace, + queryVector, + similarityThreshold + ); + + const sources = sourceDocuments.map((metadata, i) => { + return { ...metadata, text: contextTexts[i] }; + }); + return { + contextTexts, + sources: this.curateSources(sources), + message: false, + }; + }, + similarityResponse: async function ( + client, + namespace, + queryVector, + similarityThreshold = 0.25 + ) { + const result = { + contextTexts: [], + sourceDocuments: [], + scores: [], + }; + const response = await client.search({ + collection_name: namespace, + vectors: queryVector, + }); + response.results.forEach((match) => { + if (match.score < similarityThreshold) return; + result.contextTexts.push(match.metadata.text); + result.sourceDocuments.push(match); + result.scores.push(match.score); + }); + return result; + }, + "namespace-stats": async function (reqBody = {}) { + const { namespace = null } = reqBody; + if (!namespace) throw new Error("namespace required"); + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) + throw new Error("Namespace by that name does not exist."); + const stats = await this.namespace(client, namespace); + return stats + ? stats + : { message: "No stats were able to be fetched from DB for namespace" }; + }, + "delete-namespace": async function (reqBody = {}) { + const { namespace = null } = reqBody; + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) + throw new Error("Namespace by that name does not exist."); + + const statistics = await this.namespace(client, namespace); + await this.deleteVectorsInNamespace(client, namespace); + const vectorCount = Number(statistics?.data?.row_count ?? 0); + return { + message: `Namespace ${namespace} was deleted along with ${vectorCount} vectors.`, + }; + }, + curateSources: function (sources = []) { + const documents = []; + for (const source of sources) { + const { metadata = {} } = source; + if (Object.keys(metadata).length > 0) { + documents.push({ + ...metadata, + ...(source.hasOwnProperty("pageContent") + ? { text: source.pageContent } + : {}), + }); + } + } + + return documents; + }, +}; + +module.exports.Zilliz = Zilliz;