feat(Pinecone Vector Store Node, Supabase Vector Store Node): Add update operation to vector store nodes (#10060)

This commit is contained in:
Eugene 2024-07-22 16:15:43 +02:00 committed by GitHub
parent c6131859f5
commit 7e1eeb4c31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 156 additions and 68 deletions

View file

@ -9,6 +9,15 @@ import { pineconeIndexSearch } from '../shared/methods/listSearch';
const sharedFields: INodeProperties[] = [pineconeIndexRLC]; const sharedFields: INodeProperties[] = [pineconeIndexRLC];
const pineconeNamespaceField: INodeProperties = {
displayName: 'Pinecone Namespace',
name: 'pineconeNamespace',
type: 'string',
description:
'Partition the records in an index into namespaces. Queries and other operations are then limited to one namespace, so different requests can search different subsets of your index.',
default: '',
};
const retrieveFields: INodeProperties[] = [ const retrieveFields: INodeProperties[] = [
{ {
displayName: 'Options', displayName: 'Options',
@ -16,17 +25,7 @@ const retrieveFields: INodeProperties[] = [
type: 'collection', type: 'collection',
placeholder: 'Add Option', placeholder: 'Add Option',
default: {}, default: {},
options: [ options: [pineconeNamespaceField, metadataFilterField],
{
displayName: 'Pinecone Namespace',
name: 'pineconeNamespace',
type: 'string',
description:
'Partition the records in an index into namespaces. Queries and other operations are then limited to one namespace, so different requests can search different subsets of your index.',
default: '',
},
metadataFilterField,
],
}, },
]; ];
@ -45,17 +44,11 @@ const insertFields: INodeProperties[] = [
default: false, default: false,
description: 'Whether to clear the namespace before inserting new data', description: 'Whether to clear the namespace before inserting new data',
}, },
{ pineconeNamespaceField,
displayName: 'Pinecone Namespace',
name: 'pineconeNamespace',
type: 'string',
description:
'Partition the records in an index into namespaces. Queries and other operations are then limited to one namespace, so different requests can search different subsets of your index.',
default: '',
},
], ],
}, },
]; ];
export const VectorStorePinecone = createVectorStoreNode({ export const VectorStorePinecone = createVectorStoreNode({
meta: { meta: {
displayName: 'Pinecone Vector Store', displayName: 'Pinecone Vector Store',
@ -70,6 +63,7 @@ export const VectorStorePinecone = createVectorStoreNode({
required: true, required: true,
}, },
], ],
operationModes: ['load', 'insert', 'retrieve', 'update'],
}, },
methods: { listSearch: { pineconeIndexSearch } }, methods: { listSearch: { pineconeIndexSearch } },
retrieveFields, retrieveFields,

View file

@ -6,6 +6,14 @@ import { metadataFilterField } from '../../../utils/sharedFields';
import { supabaseTableNameRLC } from '../shared/descriptions'; import { supabaseTableNameRLC } from '../shared/descriptions';
import { supabaseTableNameSearch } from '../shared/methods/listSearch'; import { supabaseTableNameSearch } from '../shared/methods/listSearch';
const queryNameField: INodeProperties = {
displayName: 'Query Name',
name: 'queryName',
type: 'string',
default: 'match_documents',
description: 'Name of the query to use for matching documents',
};
const sharedFields: INodeProperties[] = [supabaseTableNameRLC]; const sharedFields: INodeProperties[] = [supabaseTableNameRLC];
const insertFields: INodeProperties[] = [ const insertFields: INodeProperties[] = [
{ {
@ -14,17 +22,10 @@ const insertFields: INodeProperties[] = [
type: 'collection', type: 'collection',
placeholder: 'Add Option', placeholder: 'Add Option',
default: {}, default: {},
options: [ options: [queryNameField],
{
displayName: 'Query Name',
name: 'queryName',
type: 'string',
default: 'match_documents',
description: 'Name of the query to use for matching documents',
},
],
}, },
]; ];
const retrieveFields: INodeProperties[] = [ const retrieveFields: INodeProperties[] = [
{ {
displayName: 'Options', displayName: 'Options',
@ -32,18 +33,12 @@ const retrieveFields: INodeProperties[] = [
type: 'collection', type: 'collection',
placeholder: 'Add Option', placeholder: 'Add Option',
default: {}, default: {},
options: [ options: [queryNameField, metadataFilterField],
{
displayName: 'Query Name',
name: 'queryName',
type: 'string',
default: 'match_documents',
description: 'Name of the query to use for matching documents',
},
metadataFilterField,
],
}, },
]; ];
const updateFields: INodeProperties[] = [...insertFields];
export const VectorStoreSupabase = createVectorStoreNode({ export const VectorStoreSupabase = createVectorStoreNode({
meta: { meta: {
description: 'Work with your data in Supabase Vector Store', description: 'Work with your data in Supabase Vector Store',
@ -58,6 +53,7 @@ export const VectorStoreSupabase = createVectorStoreNode({
required: true, required: true,
}, },
], ],
operationModes: ['load', 'insert', 'retrieve', 'update'],
}, },
methods: { methods: {
listSearch: { supabaseTableNameSearch }, listSearch: { supabaseTableNameSearch },
@ -66,6 +62,7 @@ export const VectorStoreSupabase = createVectorStoreNode({
insertFields, insertFields,
loadFields: retrieveFields, loadFields: retrieveFields,
retrieveFields, retrieveFields,
updateFields,
async getVectorStoreClient(context, filter, embeddings, itemIndex) { async getVectorStoreClient(context, filter, embeddings, itemIndex) {
const tableName = context.getNodeParameter('tableName', itemIndex, '', { const tableName = context.getNodeParameter('tableName', itemIndex, '', {
extractValue: true, extractValue: true,

View file

@ -13,16 +13,21 @@ import type {
ILoadOptionsFunctions, ILoadOptionsFunctions,
INodeListSearchResult, INodeListSearchResult,
Icon, Icon,
INodePropertyOptions,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { Embeddings } from '@langchain/core/embeddings'; import type { Embeddings } from '@langchain/core/embeddings';
import type { Document } from '@langchain/core/documents'; import type { Document } from '@langchain/core/documents';
import { logWrapper } from '../../../utils/logWrapper'; import { logWrapper } from '../../../utils/logWrapper';
import type { N8nJsonLoader } from '../../../utils/N8nJsonLoader'; import { N8nJsonLoader } from '../../../utils/N8nJsonLoader';
import type { N8nBinaryLoader } from '../../../utils/N8nBinaryLoader'; import type { N8nBinaryLoader } from '../../../utils/N8nBinaryLoader';
import { getMetadataFiltersValues, logAiEvent } from '../../../utils/helpers'; import { getMetadataFiltersValues, logAiEvent } from '../../../utils/helpers';
import { getConnectionHintNoticeField } from '../../../utils/sharedFields'; import { getConnectionHintNoticeField } from '../../../utils/sharedFields';
import { processDocument } from './processDocuments'; import { processDocument } from './processDocuments';
type NodeOperationMode = 'insert' | 'load' | 'retrieve' | 'update';
const DEFAULT_OPERATION_MODES: NodeOperationMode[] = ['load', 'insert', 'retrieve'];
interface NodeMeta { interface NodeMeta {
displayName: string; displayName: string;
name: string; name: string;
@ -30,7 +35,9 @@ interface NodeMeta {
docsUrl: string; docsUrl: string;
icon: Icon; icon: Icon;
credentials?: INodeCredentialDescription[]; credentials?: INodeCredentialDescription[];
operationModes?: NodeOperationMode[];
} }
interface VectorStoreNodeConstructorArgs { interface VectorStoreNodeConstructorArgs {
meta: NodeMeta; meta: NodeMeta;
methods?: { methods?: {
@ -42,10 +49,12 @@ interface VectorStoreNodeConstructorArgs {
) => Promise<INodeListSearchResult>; ) => Promise<INodeListSearchResult>;
}; };
}; };
sharedFields: INodeProperties[]; sharedFields: INodeProperties[];
insertFields?: INodeProperties[]; insertFields?: INodeProperties[];
loadFields?: INodeProperties[]; loadFields?: INodeProperties[];
retrieveFields?: INodeProperties[]; retrieveFields?: INodeProperties[];
updateFields?: INodeProperties[];
populateVectorStore: ( populateVectorStore: (
context: IExecuteFunctions, context: IExecuteFunctions,
embeddings: Embeddings, embeddings: Embeddings,
@ -60,15 +69,52 @@ interface VectorStoreNodeConstructorArgs {
) => Promise<VectorStore>; ) => Promise<VectorStore>;
} }
function transformDescriptionForOperationMode( function transformDescriptionForOperationMode(fields: INodeProperties[], mode: NodeOperationMode) {
fields: INodeProperties[],
mode: 'insert' | 'load' | 'retrieve',
) {
return fields.map((field) => ({ return fields.map((field) => ({
...field, ...field,
displayOptions: { show: { mode: [mode] } }, displayOptions: { show: { mode: [mode] } },
})); }));
} }
function isUpdateSupported(args: VectorStoreNodeConstructorArgs): boolean {
return args.meta.operationModes?.includes('update') ?? false;
}
function getOperationModeOptions(args: VectorStoreNodeConstructorArgs): INodePropertyOptions[] {
const enabledOperationModes = args.meta.operationModes ?? DEFAULT_OPERATION_MODES;
const allOptions = [
{
name: 'Get Many',
value: 'load',
description: 'Get many ranked documents from vector store for query',
action: 'Get many ranked documents from vector store for query',
},
{
name: 'Insert Documents',
value: 'insert',
description: 'Insert documents into vector store',
action: 'Insert documents into vector store',
},
{
name: 'Retrieve Documents (For Agent/Chain)',
value: 'retrieve',
description: 'Retrieve documents from vector store to be used with AI nodes',
action: 'Retrieve documents from vector store to be used with AI nodes',
},
{
name: 'Update Documents',
value: 'update',
description: 'Update documents in vector store by ID',
action: 'Update documents in vector store by ID',
},
];
return allOptions.filter(({ value }) =>
enabledOperationModes.includes(value as NodeOperationMode),
);
}
export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) => export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
class VectorStoreNodeType implements INodeType { class VectorStoreNodeType implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -101,11 +147,11 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
const mode = parameters?.mode; const mode = parameters?.mode;
const inputs = [{ displayName: "Embedding", type: "${NodeConnectionType.AiEmbedding}", required: true, maxConnections: 1}] const inputs = [{ displayName: "Embedding", type: "${NodeConnectionType.AiEmbedding}", required: true, maxConnections: 1}]
if (['insert', 'load'].includes(mode)) { if (['insert', 'load', 'update'].includes(mode)) {
inputs.push({ displayName: "", type: "${NodeConnectionType.Main}"}) inputs.push({ displayName: "", type: "${NodeConnectionType.Main}"})
} }
if (mode === 'insert') { if (['insert'].includes(mode)) {
inputs.push({ displayName: "Document", type: "${NodeConnectionType.AiDocument}", required: true, maxConnections: 1}) inputs.push({ displayName: "Document", type: "${NodeConnectionType.AiDocument}", required: true, maxConnections: 1})
} }
return inputs return inputs
@ -127,26 +173,7 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
type: 'options', type: 'options',
noDataExpression: true, noDataExpression: true,
default: 'retrieve', default: 'retrieve',
options: [ options: getOperationModeOptions(args),
{
name: 'Get Many',
value: 'load',
description: 'Get many ranked documents from vector store for query',
action: 'Get many ranked documents from vector store for query',
},
{
name: 'Insert Documents',
value: 'insert',
description: 'Insert documents into vector store',
action: 'Insert documents into vector store',
},
{
name: 'Retrieve Documents (For Agent/Chain)',
value: 'retrieve',
description: 'Retrieve documents from vector store to be used with AI nodes',
action: 'Retrieve documents from vector store to be used with AI nodes',
},
],
}, },
{ {
...getConnectionHintNoticeField([NodeConnectionType.AiRetriever]), ...getConnectionHintNoticeField([NodeConnectionType.AiRetriever]),
@ -185,15 +212,30 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
}, },
}, },
}, },
// ID is always used for update operation
{
displayName: 'ID',
name: 'id',
type: 'string',
default: '',
required: true,
description: 'ID of an embedding entry',
displayOptions: {
show: {
mode: ['update'],
},
},
},
...transformDescriptionForOperationMode(args.loadFields ?? [], 'load'), ...transformDescriptionForOperationMode(args.loadFields ?? [], 'load'),
...transformDescriptionForOperationMode(args.retrieveFields ?? [], 'retrieve'), ...transformDescriptionForOperationMode(args.retrieveFields ?? [], 'retrieve'),
...transformDescriptionForOperationMode(args.updateFields ?? [], 'update'),
], ],
}; };
methods = args.methods; methods = args.methods;
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const mode = this.getNodeParameter('mode', 0) as 'load' | 'insert' | 'retrieve'; const mode = this.getNodeParameter('mode', 0) as NodeOperationMode;
const embeddings = (await this.getInputConnectionData( const embeddings = (await this.getInputConnectionData(
NodeConnectionType.AiEmbedding, NodeConnectionType.AiEmbedding,
@ -208,7 +250,7 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
const filter = getMetadataFiltersValues(this, itemIndex); const filter = getMetadataFiltersValues(this, itemIndex);
const vectorStore = await args.getVectorStoreClient( const vectorStore = await args.getVectorStoreClient(
this, this,
// We'll pass filter to similaritySearchVectorWithScore instaed of getVectorStoreClient // We'll pass filter to similaritySearchVectorWithScore instead of getVectorStoreClient
undefined, undefined,
embeddings, embeddings,
itemIndex, itemIndex,
@ -274,6 +316,60 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
return [resultData]; return [resultData];
} }
if (mode === 'update') {
if (!isUpdateSupported(args)) {
throw new NodeOperationError(
this.getNode(),
'Update operation is not implemented for this Vector Store',
);
}
const items = this.getInputData();
const loader = new N8nJsonLoader(this);
const resultData = [];
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
const itemData = items[itemIndex];
const documentId = this.getNodeParameter('id', itemIndex, '', {
extractValue: true,
}) as string;
const vectorStore = await args.getVectorStoreClient(
this,
undefined,
embeddings,
itemIndex,
);
const { processedDocuments, serializedDocuments } = await processDocument(
loader,
itemData,
itemIndex,
);
if (processedDocuments?.length !== 1) {
throw new NodeOperationError(this.getNode(), 'Single document per item expected');
}
resultData.push(...serializedDocuments);
try {
// Use ids option to upsert instead of insert
await vectorStore.addDocuments(processedDocuments, {
ids: [documentId],
});
void logAiEvent(this, 'n8n.ai.vector.store.updated');
} catch (error) {
throw error;
}
}
return [resultData];
}
throw new NodeOperationError( throw new NodeOperationError(
this.getNode(), this.getNode(),
'Only the "load" and "insert" operation modes are supported with execute', 'Only the "load" and "insert" operation modes are supported with execute',

View file

@ -27,7 +27,7 @@ export function getMetadataFiltersValues(
ctx: IExecuteFunctions, ctx: IExecuteFunctions,
itemIndex: number, itemIndex: number,
): Record<string, never> | undefined { ): Record<string, never> | undefined {
const options = ctx.getNodeParameter('options', itemIndex); const options = ctx.getNodeParameter('options', itemIndex, {});
if (options.metadata) { if (options.metadata) {
const { metadataValues: metadata } = options.metadata as { const { metadataValues: metadata } = options.metadata as {

View file

@ -2145,6 +2145,7 @@ export const eventNamesAiNodes = [
'n8n.ai.llm.generated', 'n8n.ai.llm.generated',
'n8n.ai.llm.error', 'n8n.ai.llm.error',
'n8n.ai.vector.store.populated', 'n8n.ai.vector.store.populated',
'n8n.ai.vector.store.updated',
] as const; ] as const;
export type EventNamesAiNodesType = (typeof eventNamesAiNodes)[number]; export type EventNamesAiNodesType = (typeof eventNamesAiNodes)[number];