From cf0b24af0275bf8cb821be15462976c9149fe024 Mon Sep 17 00:00:00 2001 From: Timothy Carambat <rambat1010@gmail.com> Date: Tue, 15 Aug 2023 15:26:44 -0700 Subject: [PATCH] Add Qdrant support for embedding, chat, and conversation (#192) * Add Qdrant support for embedding, chat, and conversation * Change comments --- .vscode/settings.json | 1 + docker/.env.example | 5 + .../Modals/Settings/VectorDbs/index.jsx | 45 ++ frontend/src/media/vectordbs/qdrant.png | Bin 0 -> 15073 bytes server/.env.example | 5 + server/endpoints/system.js | 6 + server/package.json | 1 + server/utils/helpers/index.js | 3 + server/utils/helpers/updateENV.js | 10 +- .../utils/vectorDbProviders/qdrant/index.js | 397 ++++++++++++++++++ server/yarn.lock | 28 +- 11 files changed, 499 insertions(+), 2 deletions(-) create mode 100644 frontend/src/media/vectordbs/qdrant.png create mode 100644 server/utils/vectorDbProviders/qdrant/index.js diff --git a/.vscode/settings.json b/.vscode/settings.json index c8c7ea995..dde2d134b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "openai", + "Qdrant", "Weaviate" ] } \ No newline at end of file diff --git a/docker/.env.example b/docker/.env.example index 77550b6f0..70c61ef96 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -37,6 +37,11 @@ PINECONE_INDEX= # WEAVIATE_ENDPOINT="http://localhost:8080" # WEAVIATE_API_KEY= +# Enable all below if you are using vector database: Qdrant. +# VECTOR_DB="qdrant" +# QDRANT_ENDPOINT="http://localhost:6333" +# QDRANT_API_KEY= + # CLOUD DEPLOYMENT VARIRABLES ONLY # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting. # NO_DEBUG="true" diff --git a/frontend/src/components/Modals/Settings/VectorDbs/index.jsx b/frontend/src/components/Modals/Settings/VectorDbs/index.jsx index b1a5a97b5..ec25e0a7e 100644 --- a/frontend/src/components/Modals/Settings/VectorDbs/index.jsx +++ b/frontend/src/components/Modals/Settings/VectorDbs/index.jsx @@ -4,6 +4,7 @@ import ChromaLogo from "../../../../media/vectordbs/chroma.png"; import PineconeLogo from "../../../../media/vectordbs/pinecone.png"; import LanceDbLogo from "../../../../media/vectordbs/lancedb.png"; import WeaviateLogo from "../../../../media/vectordbs/weaviate.png"; +import QDrantLogo from "../../../../media/vectordbs/qdrant.png"; const noop = () => false; export default function VectorDBSelection({ @@ -80,6 +81,15 @@ export default function VectorDBSelection({ image={PineconeLogo} onClick={updateVectorChoice} /> + <VectorDBOption + name="QDrant" + value="qdrant" + link="qdrant.tech" + description="Open source local and distributed cloud vector database." + checked={vectorDB === "qdrant"} + image={QDrantLogo} + onClick={updateVectorChoice} + /> <VectorDBOption name="Weaviate" value="weaviate" @@ -181,6 +191,41 @@ export default function VectorDBSelection({ </p> </div> )} + {vectorDB === "qdrant" && ( + <> + <div> + <label className="block mb-2 text-sm font-medium text-gray-800 dark:text-slate-200"> + QDrant API Endpoint + </label> + <input + type="url" + name="QdrantEndpoint" + disabled={!canDebug} + className="bg-gray-50 border border-gray-500 text-gray-900 placeholder-gray-500 text-sm rounded-lg dark:bg-stone-700 focus:border-stone-500 block w-full p-2.5 dark:text-slate-200 dark:placeholder-stone-500 dark:border-slate-200" + placeholder="http://localhost:6633" + defaultValue={settings?.QdrantEndpoint} + required={true} + autoComplete="off" + spellCheck={false} + /> + </div> + <div> + <label className="block mb-2 text-sm font-medium text-gray-800 dark:text-slate-200"> + Api Key + </label> + <input + type="password" + name="QdrantApiKey" + disabled={!canDebug} + className="bg-gray-50 border border-gray-500 text-gray-900 placeholder-gray-500 text-sm rounded-lg dark:bg-stone-700 focus:border-stone-500 block w-full p-2.5 dark:text-slate-200 dark:placeholder-stone-500 dark:border-slate-200" + placeholder="wOeqxsYP4....1244sba" + defaultValue={settings?.QdrantApiKey} + autoComplete="off" + spellCheck={false} + /> + </div> + </> + )} {vectorDB === "weaviate" && ( <> <div> diff --git a/frontend/src/media/vectordbs/qdrant.png b/frontend/src/media/vectordbs/qdrant.png new file mode 100644 index 0000000000000000000000000000000000000000..d63e720c5402f3abd7246ffdd0ad6f853063c2a5 GIT binary patch literal 15073 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 literal 0 HcmV?d00001 diff --git a/server/.env.example b/server/.env.example index 606dd8988..ff92295ec 100644 --- a/server/.env.example +++ b/server/.env.example @@ -36,6 +36,11 @@ PINECONE_INDEX= # WEAVIATE_ENDPOINT="http://localhost:8080" # WEAVIATE_API_KEY= +# Enable all below if you are using vector database: Qdrant. +# VECTOR_DB="qdrant" +# QDRANT_ENDPOINT="http://localhost:6333" +# QDRANT_API_KEY= + # CLOUD DEPLOYMENT VARIRABLES ONLY # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting. diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 73041e183..d0b48cc62 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -78,6 +78,12 @@ function systemEndpoints(app) { WeaviateApiKey: process.env.WEAVIATE_API_KEY, } : {}), + ...(vectorDB === "qdrant" + ? { + QdrantEndpoint: process.env.QDRANT_ENDPOINT, + QdrantApiKey: process.env.QDRANT_API_KEY, + } + : {}), LLMProvider: llmProvider, ...(llmProvider === "openai" ? { diff --git a/server/package.json b/server/package.json index b2f5bdc8a..15bbff6f6 100644 --- a/server/package.json +++ b/server/package.json @@ -18,6 +18,7 @@ "@azure/openai": "^1.0.0-beta.3", "@googleapis/youtube": "^9.0.0", "@pinecone-database/pinecone": "^0.1.6", + "@qdrant/js-client-rest": "^1.4.0", "archiver": "^5.3.1", "bcrypt": "^5.1.0", "body-parser": "^1.20.2", diff --git a/server/utils/helpers/index.js b/server/utils/helpers/index.js index b7fb5ae00..b077606ad 100644 --- a/server/utils/helpers/index.js +++ b/server/utils/helpers/index.js @@ -13,6 +13,9 @@ function getVectorDbClass() { case "weaviate": const { Weaviate } = require("../vectorDbProviders/weaviate"); return Weaviate; + case "qdrant": + const { QDrant } = require("../vectorDbProviders/qdrant"); + return QDrant; 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 9f00ec423..d08f25c7a 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -47,6 +47,14 @@ const KEY_MAPPING = { envKey: "WEAVIATE_API_KEY", checks: [], }, + QdrantEndpoint: { + envKey: "QDRANT_ENDPOINT", + checks: [isValidURL], + }, + QdrantApiKey: { + envKey: "QDRANT_API_KEY", + checks: [], + }, PineConeEnvironment: { envKey: "PINECONE_ENVIRONMENT", @@ -112,7 +120,7 @@ function validOpenAIModel(input = "") { } function supportedVectorDB(input = "") { - const supported = ["chroma", "pinecone", "lancedb", "weaviate"]; + const supported = ["chroma", "pinecone", "lancedb", "weaviate", "qdrant"]; return supported.includes(input) ? null : `Invalid VectorDB type. Must be one of ${supported.join(", ")}.`; diff --git a/server/utils/vectorDbProviders/qdrant/index.js b/server/utils/vectorDbProviders/qdrant/index.js new file mode 100644 index 000000000..0dc39e790 --- /dev/null +++ b/server/utils/vectorDbProviders/qdrant/index.js @@ -0,0 +1,397 @@ +const { QdrantClient } = require("@qdrant/js-client-rest"); +const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter"); +const { storeVectorResult, cachedVectorInformation } = require("../../files"); +const { v4: uuidv4 } = require("uuid"); +const { toChunks, getLLMProvider } = require("../../helpers"); +const { chatPrompt } = require("../../chats"); + +const QDrant = { + name: "QDrant", + connect: async function () { + if (process.env.VECTOR_DB !== "qdrant") + throw new Error("QDrant::Invalid ENV settings"); + + const client = new QdrantClient({ + url: process.env.QDRANT_ENDPOINT, + ...(process.env.QDRANT_API_KEY + ? { apiKey: process.env.QDRANT_API_KEY } + : {}), + }); + + const isAlive = (await client.api("cluster")?.clusterStatus())?.ok || false; + if (!isAlive) + throw new Error( + "QDrant::Invalid Heartbeat received - is the instance online?" + ); + + return { client }; + }, + heartbeat: async function () { + await this.connect(); + return { heartbeat: Number(new Date()) }; + }, + totalIndicies: async function () { + const { client } = await this.connect(); + const { collections } = await client.getCollections(); + var totalVectors = 0; + for (const collection of collections) { + if (!collection || !collection.name) continue; + totalVectors += + (await this.namespace(client, collection.name))?.vectorCount || 0; + } + return totalVectors; + }, + namespaceCount: async function (_namespace = null) { + const { client } = await this.connect(); + const namespace = await this.namespace(client, _namespace); + return namespace?.vectorCount || 0; + }, + similarityResponse: async function (_client, namespace, queryVector) { + const { client } = await this.connect(); + const result = { + contextTexts: [], + sourceDocuments: [], + }; + + const responses = await client.search(namespace, { + vector: queryVector, + limit: 4, + }); + + responses.forEach((response) => { + result.contextTexts.push(response?.payload?.text || ""); + result.sourceDocuments.push({ + ...(response?.payload || {}), + id: response.id, + }); + }); + + return result; + }, + namespace: async function (client, namespace = null) { + if (!namespace) throw new Error("No namespace value provided."); + const collection = await client.getCollection(namespace).catch(() => null); + if (!collection) return null; + + return { + name: namespace, + ...collection, + vectorCount: collection.vectors_count, + }; + }, + 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 collection = await client.getCollection(namespace).catch((e) => { + console.error("QDrant::namespaceExists", e.message); + return null; + }); + return !!collection; + }, + deleteVectorsInNamespace: async function (client, namespace = null) { + await client.deleteCollection(namespace); + return true; + }, + getOrCreateCollection: async function (client, namespace) { + if (await this.namespaceExists(client, namespace)) { + return await client.getCollection(namespace); + } + await client.createCollection(namespace, { + vectors: { + size: 1536, //TODO: Fixed to OpenAI models - when other embeddings exist make variable. + distance: "Cosine", + }, + }); + return await client.getCollection(namespace); + }, + addDocumentToNamespace: async function ( + namespace, + documentData = {}, + fullFilePath = null + ) { + const { DocumentVectors } = require("../../../models/vectors"); + try { + 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 collection = await this.getOrCreateCollection(client, namespace); + if (!collection) + throw new Error("Failed to create new QDrant collection!", { + namespace, + }); + + const { chunks } = cacheResult; + const documentVectors = []; + + for (const chunk of chunks) { + const submission = { + ids: [], + vectors: [], + payloads: [], + }; + + // Before sending to Qdrant and saving the records to our db + // we need to assign the id of each chunk that is stored in the cached file. + chunk.forEach((chunk) => { + const id = uuidv4(); + const { id: _id, ...payload } = chunk.payload; + documentVectors.push({ docId, vectorId: id }); + submission.ids.push(id); + submission.vectors.push(chunk.vector); + submission.payloads.push(payload); + }); + + const additionResult = await client.upsert(namespace, { + wait: true, + batch: { ...submission }, + }); + if (additionResult?.status !== "completed") + throw new Error("Error embedding into QDrant", additionResult); + } + + await DocumentVectors.bulkInsert(documentVectors); + return true; + } + + // If we are here then we are going to embed and store a novel document. + // We have to do this manually as opposed to using LangChains `Qdrant.fromDocuments` + // because we then cannot atomically control our namespace to granularly find/remove documents + // from vectordb. + const textSplitter = new RecursiveCharacterTextSplitter({ + chunkSize: 1000, + 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); + const submission = { + ids: [], + vectors: [], + payloads: [], + }; + + if (!!vectorValues && vectorValues.length > 0) { + for (const [i, vector] of vectorValues.entries()) { + const vectorRecord = { + id: uuidv4(), + vector: vector, + // [DO NOT REMOVE] + // LangChain will be unable to find your text if you embed manually and dont include the `text` key. + // https://github.com/hwchase17/langchainjs/blob/2def486af734c0ca87285a48f1a04c057ab74bdf/langchain/src/vectorstores/pinecone.ts#L64 + payload: { ...metadata, text: textChunks[i] }, + }; + + submission.ids.push(vectorRecord.id); + submission.vectors.push(vectorRecord.vector); + submission.payloads.push(vectorRecord.payload); + + vectors.push(vectorRecord); + documentVectors.push({ docId, vectorId: vectorRecord.id }); + } + } else { + console.error( + "Could not use OpenAI to embed document chunks! This document will not be recorded." + ); + } + + const { client } = await this.connect(); + const collection = await this.getOrCreateCollection(client, namespace); + if (!collection) + throw new Error("Failed to create new QDrant collection!", { + namespace, + }); + + if (vectors.length > 0) { + const chunks = []; + + console.log("Inserting vectorized chunks into QDrant collection."); + for (const chunk of toChunks(vectors, 500)) chunks.push(chunk); + + const additionResult = await client.upsert(namespace, { + wait: true, + batch: { + ids: submission.ids, + vectors: submission.vectors, + payloads: submission.payloads, + }, + }); + if (additionResult?.status !== "completed") + throw new Error("Error embedding into QDrant", additionResult); + + await storeVectorResult(chunks, fullFilePath); + } + + await DocumentVectors.bulkInsert(documentVectors); + return true; + } catch (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 = '${docId}'`); + if (knownDocuments.length === 0) return; + + const vectorIds = knownDocuments.map((doc) => doc.vectorId); + await client.delete(namespace, { + wait: true, + points: vectorIds, + }); + + const indexes = knownDocuments.map((doc) => doc.id); + await DocumentVectors.deleteIds(indexes); + return true; + }, + query: async function (reqBody = {}) { + const { namespace = null, input, workspace = {} } = reqBody; + if (!namespace || !input) throw new Error("Invalid request body"); + + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) { + return { + response: null, + sources: [], + message: "Invalid query - no documents found for workspace!", + }; + } + + const LLMConnector = getLLMProvider(); + const queryVector = await LLMConnector.embedTextInput(input); + const { contextTexts, sourceDocuments } = await this.similarityResponse( + client, + namespace, + queryVector + ); + const prompt = { + role: "system", + content: `${chatPrompt(workspace)} + Context: + ${contextTexts + .map((text, i) => { + return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`; + }) + .join("")}`, + }; + const memory = [prompt, { role: "user", content: input }]; + const responseText = await LLMConnector.getChatCompletion(memory, { + temperature: workspace?.openAiTemp ?? 0.7, + }); + + return { + response: responseText, + sources: this.curateSources(sourceDocuments), + message: false, + }; + }, + // This implementation of chat uses the chat history and modifies the system prompt at execution + // this is improved over the regular langchain implementation so that chats do not directly modify embeddings + // because then multi-user support will have all conversations mutating the base vector collection to which then + // the only solution is replicating entire vector databases per user - which will very quickly consume space on VectorDbs + chat: async function (reqBody = {}) { + const { + namespace = null, + input, + workspace = {}, + chatHistory = [], + } = reqBody; + if (!namespace || !input) throw new Error("Invalid request body"); + + const { client } = await this.connect(); + if (!(await this.namespaceExists(client, namespace))) { + return { + response: null, + sources: [], + message: "Invalid query - no documents found for workspace!", + }; + } + + const LLMConnector = getLLMProvider(); + const queryVector = await LLMConnector.embedTextInput(input); + const { contextTexts, sourceDocuments } = await this.similarityResponse( + client, + namespace, + queryVector + ); + const prompt = { + role: "system", + content: `${chatPrompt(workspace)} + Context: + ${contextTexts + .map((text, i) => { + return `[CONTEXT ${i}]:\n${text}\n[END CONTEXT ${i}]\n\n`; + }) + .join("")}`, + }; + const memory = [prompt, ...chatHistory, { role: "user", content: input }]; + const responseText = await LLMConnector.getChatCompletion(memory, { + temperature: workspace?.openAiTemp ?? 0.7, + }); + + return { + response: responseText, + sources: this.curateSources(sourceDocuments), + message: false, + }; + }, + "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 details = await this.namespace(client, namespace); + await this.deleteVectorsInNamespace(client, namespace); + return { + message: `Namespace ${namespace} was deleted along with ${details?.vectorCount} vectors.`, + }; + }, + reset: async function () { + const { client } = await this.connect(); + const response = await client.getCollections(); + for (const collection of response.collections) { + await client.deleteCollection(collection.name); + } + return { reset: true }; + }, + curateSources: function (sources = []) { + const documents = []; + for (const source of sources) { + if (Object.keys(source).length > 0) { + documents.push({ + ...source, + }); + } + } + + return documents; + }, +}; + +module.exports.QDrant = QDrant; diff --git a/server/yarn.lock b/server/yarn.lock index 2ff2aec4b..6a9e1669e 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -173,6 +173,25 @@ dependencies: cross-fetch "^3.1.5" +"@qdrant/js-client-rest@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@qdrant/js-client-rest/-/js-client-rest-1.4.0.tgz#efd341a9a30b241e7e11f773b581b3102db1adc6" + integrity sha512-I3pCKnaVdqiVpZ9+XtEjCx7IQSJnerXffD/g8mj/fZsOOJH3IFM+nF2izOfVIByufAArW+drGcAPrxHedba99w== + dependencies: + "@qdrant/openapi-typescript-fetch" "^1.2.1" + "@sevinf/maybe" "^0.5.0" + undici "^5.22.1" + +"@qdrant/openapi-typescript-fetch@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@qdrant/openapi-typescript-fetch/-/openapi-typescript-fetch-1.2.1.tgz#6e232899ca0a7fbc769f0c3a229b56f93da39f19" + integrity sha512-oiBJRN1ME7orFZocgE25jrM3knIF/OKJfMsZPBbtMMKfgNVYfps0MokGvSJkBmecj6bf8QoLXWIGlIoaTM4Zmw== + +"@sevinf/maybe@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@sevinf/maybe/-/maybe-0.5.0.tgz#e59fcea028df615fe87d708bb30e1f338e46bb44" + integrity sha512-ARhyoYDnY1LES3vYI0fiG6e9esWfTNcXcO6+MPJJXcnyMV3bim4lnFt45VXouV7y82F4x3YH8nOQ6VztuvUiWg== + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -526,7 +545,7 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -busboy@^1.0.0: +busboy@^1.0.0, busboy@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== @@ -2505,6 +2524,13 @@ undefsafe@^2.0.5: resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== +undici@^5.22.1: + version "5.23.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.23.0.tgz#e7bdb0ed42cebe7b7aca87ced53e6eaafb8f8ca0" + integrity sha512-1D7w+fvRsqlQ9GscLBwcAJinqcZGHUKjbOmXdlE/v8BvEGXjeWAax+341q44EuTcHXXnfyKNbKRq4Lg7OzhMmg== + dependencies: + busboy "^1.6.0" + unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" -- GitLab