mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(Pinecone Vector Store Node, Supabase Vector Store Node): Add update operation to vector store nodes (#10060)
This commit is contained in:
parent
c6131859f5
commit
7e1eeb4c31
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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];
|
||||||
|
|
Loading…
Reference in a new issue