From 66460f66b0b02ae6f342e52500b29fe8b724e1dc Mon Sep 17 00:00:00 2001 From: Anush Date: Wed, 3 Jan 2024 15:44:51 +0530 Subject: [PATCH] feat(Qdrant Vector Store Node): Qdrant vector store support (#8080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR intends to add [Qdrant](https://qdrant.tech/) as a supported vectorstore node to load and retrieve documents from in a workflow. ## Review / Merge checklist - [x] PR title and summary are descriptive. - [x] Node/credentials documentation to be updated in https://github.com/n8n-io/n8n-docs/pull/1796. --------- Co-authored-by: oleg Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ --- .../credentials/QdrantApi.credentials.ts | 50 +++++++++++ .../VectorStoreQdrant.node.ts | 85 +++++++++++++++++++ .../vector_store/VectorStoreQdrant/qdrant.svg | 21 +++++ .../nodes/vector_store/shared/descriptions.ts | 23 +++++ .../vector_store/shared/methods/listSearch.ts | 19 +++++ packages/@n8n/nodes-langchain/package.json | 3 + pnpm-lock.yaml | 29 ++++++- 7 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 packages/@n8n/nodes-langchain/credentials/QdrantApi.credentials.ts create mode 100644 packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreQdrant/VectorStoreQdrant.node.ts create mode 100644 packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreQdrant/qdrant.svg diff --git a/packages/@n8n/nodes-langchain/credentials/QdrantApi.credentials.ts b/packages/@n8n/nodes-langchain/credentials/QdrantApi.credentials.ts new file mode 100644 index 0000000000..26bb3e9bac --- /dev/null +++ b/packages/@n8n/nodes-langchain/credentials/QdrantApi.credentials.ts @@ -0,0 +1,50 @@ +import type { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class QdrantApi implements ICredentialType { + name = 'qdrantApi'; + + displayName = 'QdrantApi'; + + documentationUrl = 'https://docs.n8n.io/integrations/builtin/credentials/qdrant/'; + + properties: INodeProperties[] = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string', + typeOptions: { password: true }, + required: true, + default: '', + }, + { + displayName: 'Qdrant URL', + name: 'qdrantUrl', + type: 'string', + required: true, + default: '', + }, + ]; + + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + headers: { + 'api-key': '={{$credentials.apiKey}}', + }, + }, + }; + + test: ICredentialTestRequest = { + request: { + baseURL: '={{$credentials.qdrantUrl}}', + headers: { + accept: 'application/json; charset=utf-8', + }, + }, + }; +} diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreQdrant/VectorStoreQdrant.node.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreQdrant/VectorStoreQdrant.node.ts new file mode 100644 index 0000000000..759330539e --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreQdrant/VectorStoreQdrant.node.ts @@ -0,0 +1,85 @@ +import { type INodeProperties } from 'n8n-workflow'; +import type { QdrantLibArgs } from 'langchain/vectorstores/qdrant'; +import { QdrantVectorStore } from 'langchain/vectorstores/qdrant'; +import type { Schemas as QdrantSchemas } from '@qdrant/js-client-rest'; +import { createVectorStoreNode } from '../shared/createVectorStoreNode'; +import { qdrantCollectionRLC } from '../shared/descriptions'; +import { qdrantCollectionsSearch } from '../shared/methods/listSearch'; + +const sharedFields: INodeProperties[] = [qdrantCollectionRLC]; + +const insertFields: INodeProperties[] = [ + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Collection Config', + name: 'collectionConfig', + type: 'json', + default: '', + description: + 'JSON options for creating a collection. Learn more.', + }, + ], + }, +]; + +export const VectorStoreQdrant = createVectorStoreNode({ + meta: { + displayName: 'Qdrant Vector Store', + name: 'vectorStoreQdrant', + description: 'Work with your data in a Qdrant collection', + icon: 'file:qdrant.svg', + docsUrl: + 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.vectorstoreqdrant/', + credentials: [ + { + name: 'qdrantApi', + required: true, + }, + ], + }, + methods: { listSearch: { qdrantCollectionsSearch } }, + insertFields, + sharedFields, + async getVectorStoreClient(context, filter, embeddings, itemIndex) { + const collection = context.getNodeParameter('qdrantCollection', itemIndex, '', { + extractValue: true, + }) as string; + + const credentials = await context.getCredentials('qdrantApi'); + + const config: QdrantLibArgs = { + url: credentials.qdrantUrl as string, + apiKey: credentials.apiKey as string, + collectionName: collection, + }; + + return QdrantVectorStore.fromExistingCollection(embeddings, config); + }, + async populateVectorStore(context, embeddings, documents, itemIndex) { + const collectionName = context.getNodeParameter('qdrantCollection', itemIndex, '', { + extractValue: true, + }) as string; + + // If collection config is not provided, the collection will be created with default settings + // i.e. with the size of the passed embeddings and "Cosine" distance metric + const { collectionConfig } = context.getNodeParameter('options', itemIndex, {}) as { + collectionConfig?: QdrantSchemas['CreateCollection']; + }; + const credentials = await context.getCredentials('qdrantApi'); + + const config: QdrantLibArgs = { + url: credentials.qdrantUrl as string, + apiKey: credentials.apiKey as string, + collectionName, + collectionConfig, + }; + + await QdrantVectorStore.fromDocuments(documents, embeddings, config); + }, +}); diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreQdrant/qdrant.svg b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreQdrant/qdrant.svg new file mode 100644 index 0000000000..be9350cd28 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreQdrant/qdrant.svg @@ -0,0 +1,21 @@ + + + qdrant + + + + + + + + + + + + + + + + + + diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/descriptions.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/descriptions.ts index 573044b922..483e73c1fe 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/descriptions.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/descriptions.ts @@ -45,3 +45,26 @@ export const supabaseTableNameRLC: INodeProperties = { }, ], }; + +export const qdrantCollectionRLC: INodeProperties = { + displayName: 'Qdrant Collection', + name: 'qdrantCollection', + type: 'resourceLocator', + default: { mode: 'list', value: '' }, + required: true, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'qdrantCollectionsSearch', + }, + }, + { + displayName: 'ID', + name: 'id', + type: 'string', + }, + ], +}; diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/methods/listSearch.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/methods/listSearch.ts index 13ee71c273..d606a0db05 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/methods/listSearch.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/methods/listSearch.ts @@ -1,5 +1,6 @@ import { ApplicationError, type IDataObject, type ILoadOptionsFunctions } from 'n8n-workflow'; import { Pinecone } from '@pinecone-database/pinecone'; +import { QdrantClient } from '@qdrant/js-client-rest'; export async function pineconeIndexSearch(this: ILoadOptionsFunctions) { const credentials = await this.getCredentials('pineconeApi'); @@ -49,3 +50,21 @@ export async function supabaseTableNameSearch(this: ILoadOptionsFunctions) { return { results }; } + +export async function qdrantCollectionsSearch(this: ILoadOptionsFunctions) { + const credentials = await this.getCredentials('qdrantApi'); + + const client = new QdrantClient({ + url: credentials.qdrantUrl as string, + apiKey: credentials.apiKey as string, + }); + + const response = await client.getCollections(); + + const results = response.collections.map((collection) => ({ + name: collection.name, + value: collection.name, + })); + + return { results }; +} diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index 12e4dceda9..a909021617 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -33,6 +33,7 @@ "dist/credentials/MotorheadApi.credentials.js", "dist/credentials/OllamaApi.credentials.js", "dist/credentials/PineconeApi.credentials.js", + "dist/credentials/QdrantApi.credentials.js", "dist/credentials/SerpApi.credentials.js", "dist/credentials/WolframAlphaApi.credentials.js", "dist/credentials/XataApi.credentials.js", @@ -93,6 +94,7 @@ "dist/nodes/vector_store/VectorStorePinecone/VectorStorePinecone.node.js", "dist/nodes/vector_store/VectorStorePineconeInsert/VectorStorePineconeInsert.node.js", "dist/nodes/vector_store/VectorStorePineconeLoad/VectorStorePineconeLoad.node.js", + "dist/nodes/vector_store/VectorStoreQdrant/VectorStoreQdrant.node.js", "dist/nodes/vector_store/VectorStoreSupabase/VectorStoreSupabase.node.js", "dist/nodes/vector_store/VectorStoreSupabaseInsert/VectorStoreSupabaseInsert.node.js", "dist/nodes/vector_store/VectorStoreSupabaseLoad/VectorStoreSupabaseLoad.node.js", @@ -118,6 +120,7 @@ "@huggingface/inference": "2.6.4", "@n8n/vm2": "3.9.20", "@pinecone-database/pinecone": "1.1.2", + "@qdrant/js-client-rest": "1.7.0", "@supabase/supabase-js": "2.38.5", "@xata.io/client": "0.25.3", "cohere-ai": "6.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1b794a418..34f857df0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -192,6 +192,9 @@ importers: '@pinecone-database/pinecone': specifier: 1.1.2 version: 1.1.2 + '@qdrant/js-client-rest': + specifier: 1.7.0 + version: 1.7.0(typescript@5.3.2) '@supabase/supabase-js': specifier: 2.38.5 version: 2.38.5 @@ -215,7 +218,7 @@ importers: version: 1.2.0 langchain: specifier: 0.0.198 - version: 0.0.198(@aws-sdk/client-bedrock-runtime@3.454.0)(@getzep/zep-js@0.9.0)(@google-ai/generativelanguage@0.2.1)(@huggingface/inference@2.6.4)(@pinecone-database/pinecone@1.1.2)(@supabase/supabase-js@2.38.5)(@xata.io/client@0.25.3)(cohere-ai@6.2.2)(d3-dsv@2.0.0)(epub2@3.0.1)(html-to-text@9.0.5)(lodash@4.17.21)(mammoth@1.6.0)(pdf-parse@1.1.1)(pg@8.11.3)(redis@4.6.11)(typeorm@0.3.17) + version: 0.0.198(@aws-sdk/client-bedrock-runtime@3.454.0)(@getzep/zep-js@0.9.0)(@google-ai/generativelanguage@0.2.1)(@huggingface/inference@2.6.4)(@pinecone-database/pinecone@1.1.2)(@qdrant/js-client-rest@1.7.0)(@supabase/supabase-js@2.38.5)(@xata.io/client@0.25.3)(cohere-ai@6.2.2)(d3-dsv@2.0.0)(epub2@3.0.1)(html-to-text@9.0.5)(lodash@4.17.21)(mammoth@1.6.0)(pdf-parse@1.1.1)(pg@8.11.3)(redis@4.6.11)(typeorm@0.3.17) lodash: specifier: 4.17.21 version: 4.17.21 @@ -6544,6 +6547,23 @@ packages: resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} dev: false + /@qdrant/js-client-rest@1.7.0(typescript@5.3.2): + resolution: {integrity: sha512-16O0EQfrrybcPVipodxykr6dMUlBzKW7a63cSDUFVgc5a1AWESwERykwjuvW5KqvKdkPcxZ2NssrvgUO1W3MgA==} + engines: {node: '>=18.0.0', pnpm: '>=8'} + peerDependencies: + typescript: ^5.3.0 + dependencies: + '@qdrant/openapi-typescript-fetch': 1.2.1 + '@sevinf/maybe': 0.5.0 + typescript: 5.3.2 + undici: 5.26.4 + dev: false + + /@qdrant/openapi-typescript-fetch@1.2.1: + resolution: {integrity: sha512-oiBJRN1ME7orFZocgE25jrM3knIF/OKJfMsZPBbtMMKfgNVYfps0MokGvSJkBmecj6bf8QoLXWIGlIoaTM4Zmw==} + engines: {node: '>=12.0.0', pnpm: '>=8'} + dev: false + /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} dependencies: @@ -7391,6 +7411,10 @@ packages: - supports-color dev: true + /@sevinf/maybe@0.5.0: + resolution: {integrity: sha512-ARhyoYDnY1LES3vYI0fiG6e9esWfTNcXcO6+MPJJXcnyMV3bim4lnFt45VXouV7y82F4x3YH8nOQ6VztuvUiWg==} + dev: false + /@sideway/address@4.1.4: resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} dependencies: @@ -18585,7 +18609,7 @@ packages: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} dev: false - /langchain@0.0.198(@aws-sdk/client-bedrock-runtime@3.454.0)(@getzep/zep-js@0.9.0)(@google-ai/generativelanguage@0.2.1)(@huggingface/inference@2.6.4)(@pinecone-database/pinecone@1.1.2)(@supabase/supabase-js@2.38.5)(@xata.io/client@0.25.3)(cohere-ai@6.2.2)(d3-dsv@2.0.0)(epub2@3.0.1)(html-to-text@9.0.5)(lodash@4.17.21)(mammoth@1.6.0)(pdf-parse@1.1.1)(pg@8.11.3)(redis@4.6.11)(typeorm@0.3.17): + /langchain@0.0.198(@aws-sdk/client-bedrock-runtime@3.454.0)(@getzep/zep-js@0.9.0)(@google-ai/generativelanguage@0.2.1)(@huggingface/inference@2.6.4)(@pinecone-database/pinecone@1.1.2)(@qdrant/js-client-rest@1.7.0)(@supabase/supabase-js@2.38.5)(@xata.io/client@0.25.3)(cohere-ai@6.2.2)(d3-dsv@2.0.0)(epub2@3.0.1)(html-to-text@9.0.5)(lodash@4.17.21)(mammoth@1.6.0)(pdf-parse@1.1.1)(pg@8.11.3)(redis@4.6.11)(typeorm@0.3.17): resolution: {integrity: sha512-YC0O1g8r61InCWyF5NmiQjdghdq6LKcgMrDZtqLbgDxAe4RoSldonm+5oNXS3yjCISG0j3s5Cty+yB7klqvUpg==} engines: {node: '>=18'} peerDependencies: @@ -18898,6 +18922,7 @@ packages: '@huggingface/inference': 2.6.4 '@langchain/core': 0.0.2 '@pinecone-database/pinecone': 1.1.2 + '@qdrant/js-client-rest': 1.7.0(typescript@5.3.2) '@supabase/supabase-js': 2.38.5 '@xata.io/client': 0.25.3(typescript@5.3.2) binary-extensions: 2.2.0