mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
feat(Redis Vector Store Node): Redis vector store node implementation (#19428)
Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
parent
353b992668
commit
f178a59702
|
|
@ -0,0 +1,524 @@
|
|||
import { mock } from 'jest-mock-extended';
|
||||
import { NodeOperationError, type ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
|
||||
// Mock external modules that are not needed for these unit tests
|
||||
jest.mock('@langchain/redis', () => {
|
||||
const state: any = { ctorArgs: undefined };
|
||||
class RedisVectorStore {
|
||||
static fromDocuments = jest.fn();
|
||||
constructor(...args: any[]) {
|
||||
state.ctorArgs = args;
|
||||
}
|
||||
}
|
||||
return { RedisVectorStore, __state: state };
|
||||
});
|
||||
jest.mock('@utils/sharedFields', () => ({ metadataFilterField: {} }), { virtual: true });
|
||||
jest.mock(
|
||||
'@utils/helpers',
|
||||
() => ({ getMetadataFiltersValues: jest.fn(), logAiEvent: jest.fn() }),
|
||||
{ virtual: true },
|
||||
);
|
||||
jest.mock('@utils/N8nBinaryLoader', () => ({ N8nBinaryLoader: class {} }), { virtual: true });
|
||||
jest.mock('@utils/N8nJsonLoader', () => ({ N8nJsonLoader: class {} }), { virtual: true });
|
||||
jest.mock('@utils/logWrapper', () => ({ logWrapper: (fn: any) => fn }), { virtual: true });
|
||||
// Mock the vector store node factory to avoid deep imports but preserve passed methods
|
||||
jest.mock('../shared/createVectorStoreNode/createVectorStoreNode', () => ({
|
||||
createVectorStoreNode: (config: any) =>
|
||||
class BaseNode {
|
||||
async getVectorStoreClient(...args: any[]) {
|
||||
return config.getVectorStoreClient.apply(config, args);
|
||||
}
|
||||
async populateVectorStore(...args: any[]) {
|
||||
return config.populateVectorStore.apply(config, args);
|
||||
}
|
||||
},
|
||||
}));
|
||||
jest.mock('redis', () => ({ createClient: jest.fn() }));
|
||||
|
||||
import { createClient } from 'redis';
|
||||
|
||||
import * as RedisNode from './VectorStoreRedis.node';
|
||||
|
||||
const MockCreateClient = createClient as jest.MockedFunction<typeof createClient>;
|
||||
|
||||
describe('VectorStoreRedis.node', () => {
|
||||
const helpers = mock<ILoadOptionsFunctions['helpers']>();
|
||||
const loadOptionsFunctions = mock<ILoadOptionsFunctions>({ helpers });
|
||||
loadOptionsFunctions.logger = {
|
||||
info: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
verbose: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const baseCredentials = {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
ssl: false,
|
||||
user: 'default',
|
||||
password: 'pass',
|
||||
database: 0,
|
||||
} as any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
// Reset cached client
|
||||
RedisNode.redisConfig.client = null as any;
|
||||
RedisNode.redisConfig.connectionString = '';
|
||||
});
|
||||
|
||||
describe('getRedisClient', () => {
|
||||
it('creates and reuses client for same configuration', async () => {
|
||||
const mockClient = {
|
||||
on: jest.fn(),
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
disconnect: jest.fn().mockResolvedValue(undefined),
|
||||
quit: jest.fn().mockResolvedValue(undefined),
|
||||
} as any;
|
||||
|
||||
MockCreateClient.mockReturnValue(mockClient);
|
||||
|
||||
const context = {
|
||||
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
|
||||
} as any;
|
||||
|
||||
const client1 = await RedisNode.getRedisClient(context);
|
||||
const client2 = await RedisNode.getRedisClient(context);
|
||||
|
||||
expect(MockCreateClient).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient.connect).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient.disconnect).not.toHaveBeenCalled();
|
||||
expect(client1).toBe(mockClient);
|
||||
expect(client2).toBe(mockClient);
|
||||
});
|
||||
|
||||
it('disconnects previous client and creates a new one when configuration changes', async () => {
|
||||
const mockClient1 = {
|
||||
on: jest.fn(),
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
disconnect: jest.fn().mockResolvedValue(undefined),
|
||||
quit: jest.fn().mockResolvedValue(undefined),
|
||||
} as any;
|
||||
const mockClient2 = {
|
||||
on: jest.fn(),
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
disconnect: jest.fn().mockResolvedValue(undefined),
|
||||
quit: jest.fn().mockResolvedValue(undefined),
|
||||
} as any;
|
||||
|
||||
MockCreateClient.mockImplementationOnce(() => mockClient1).mockImplementationOnce(
|
||||
() => mockClient2,
|
||||
);
|
||||
|
||||
const context = {
|
||||
getCredentials: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(baseCredentials)
|
||||
.mockResolvedValueOnce({ ...baseCredentials, port: 6380 }),
|
||||
} as any;
|
||||
|
||||
const client1 = await RedisNode.getRedisClient(context);
|
||||
const client2 = await RedisNode.getRedisClient(context);
|
||||
|
||||
expect(MockCreateClient).toHaveBeenCalledTimes(2);
|
||||
expect(mockClient1.disconnect).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient2.connect).toHaveBeenCalledTimes(1);
|
||||
expect(client1).toBe(mockClient1);
|
||||
expect(client2).toBe(mockClient2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listIndexes', () => {
|
||||
it('returns mapped indexes when FT._LIST succeeds', async () => {
|
||||
const mockClient = {
|
||||
on: jest.fn(),
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
disconnect: jest.fn(),
|
||||
quit: jest.fn(),
|
||||
ft: { _list: jest.fn().mockResolvedValue(['Idx1', 'Idx2']) },
|
||||
} as any;
|
||||
|
||||
MockCreateClient.mockReturnValue(mockClient);
|
||||
|
||||
(loadOptionsFunctions as any).getCredentials = jest.fn().mockResolvedValue(baseCredentials);
|
||||
|
||||
const results = await (RedisNode.listIndexes as any).call(loadOptionsFunctions as any);
|
||||
|
||||
expect(mockClient.ft._list).toHaveBeenCalled();
|
||||
expect(results).toEqual({
|
||||
results: [
|
||||
{ name: 'Idx1', value: 'Idx1' },
|
||||
{ name: 'Idx2', value: 'Idx2' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty results when FT._LIST fails', async () => {
|
||||
const mockClient = {
|
||||
on: jest.fn(),
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
disconnect: jest.fn(),
|
||||
quit: jest.fn(),
|
||||
ft: { _list: jest.fn().mockRejectedValue(new Error('no module')) },
|
||||
} as any;
|
||||
|
||||
MockCreateClient.mockReturnValue(mockClient);
|
||||
|
||||
const failureCredentials = { ...baseCredentials, port: 6380 };
|
||||
(loadOptionsFunctions as any).getCredentials = jest
|
||||
.fn()
|
||||
.mockResolvedValue(failureCredentials);
|
||||
|
||||
const results = await (RedisNode.listIndexes as any).call(loadOptionsFunctions as any);
|
||||
|
||||
expect(results).toEqual({ results: [] });
|
||||
});
|
||||
|
||||
it('returns empty results when FT._LIST returns unexpected data type', async () => {
|
||||
const mockClient = {
|
||||
on: jest.fn(),
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
disconnect: jest.fn(),
|
||||
quit: jest.fn(),
|
||||
ft: { _list: jest.fn().mockResolvedValue({ unexpected: 'object' }) },
|
||||
} as any;
|
||||
|
||||
MockCreateClient.mockReturnValue(mockClient);
|
||||
|
||||
(loadOptionsFunctions as any).getCredentials = jest.fn().mockResolvedValue(baseCredentials);
|
||||
|
||||
const results = await (RedisNode.listIndexes as any).call(loadOptionsFunctions as any);
|
||||
|
||||
expect(results).toEqual({ results: [] });
|
||||
expect(loadOptionsFunctions.logger.warn).toHaveBeenCalledWith(
|
||||
'FT._LIST returned unexpected data type',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVectorStoreClient', () => {
|
||||
it('constructs ExtendedRedisVectorSearch with correct options and passes filter tokens', async () => {
|
||||
const mockClient = {
|
||||
on: jest.fn(),
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
disconnect: jest.fn(),
|
||||
quit: jest.fn(),
|
||||
sendCommand: jest
|
||||
.fn()
|
||||
.mockImplementation(async ([cmd]) =>
|
||||
cmd === 'FT.INFO' ? await Promise.resolve(undefined) : await Promise.resolve([]),
|
||||
),
|
||||
} as any;
|
||||
|
||||
// Adapt to new client.ft.info usage
|
||||
mockClient.ft = { ...(mockClient.ft || {}), info: jest.fn().mockResolvedValue(undefined) };
|
||||
|
||||
(MockCreateClient as any).mockReturnValue(mockClient);
|
||||
|
||||
// Provide a base class method that ExtendedRedisVectorSearch will call via super
|
||||
const RedisVectorStoreMod: any = jest.requireMock('@langchain/redis');
|
||||
RedisVectorStoreMod.RedisVectorStore.prototype.similaritySearchVectorWithScore = jest
|
||||
.fn()
|
||||
.mockResolvedValue('ok');
|
||||
|
||||
const context: any = {
|
||||
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
|
||||
getNodeParameter: (name: string) => {
|
||||
const map: Record<string, any> = {
|
||||
redisIndex: 'myIndex',
|
||||
'options.keyPrefix': 'doc',
|
||||
'options.metadataKey': 'm',
|
||||
'options.contentKey': 'c',
|
||||
'options.vectorKey': 'v',
|
||||
'options.metadataFilter': 'a,b',
|
||||
};
|
||||
return map[name];
|
||||
},
|
||||
getNode: () => ({ name: 'VectorStoreRedis' }),
|
||||
logger: loadOptionsFunctions.logger,
|
||||
} as any;
|
||||
|
||||
const embeddings: any = {};
|
||||
const instance = new RedisNode.VectorStoreRedis();
|
||||
const client = await (instance as any).getVectorStoreClient(
|
||||
context,
|
||||
undefined,
|
||||
embeddings,
|
||||
0,
|
||||
);
|
||||
|
||||
// Ensure FT.INFO is called to validate index
|
||||
expect(mockClient.ft.info).toHaveBeenCalledWith('myIndex');
|
||||
|
||||
// The base class constructor should have been called with embeddings and options
|
||||
const state = RedisVectorStoreMod.__state;
|
||||
expect(state.ctorArgs[0]).toBe(embeddings);
|
||||
expect(state.ctorArgs[1]).toMatchObject({
|
||||
redisClient: mockClient,
|
||||
indexName: 'myIndex',
|
||||
keyPrefix: 'doc',
|
||||
metadataKey: 'm',
|
||||
contentKey: 'c',
|
||||
vectorKey: 'v',
|
||||
});
|
||||
|
||||
// Call the overridden method and ensure behavior is as expected
|
||||
const res = await client.similaritySearchVectorWithScore([1, 2], 3);
|
||||
expect(res).toBe('ok');
|
||||
// Validate filter tokens got captured on the instance
|
||||
expect(client.defaultFilter).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('trims and removes empty metadata filter tokens', async () => {
|
||||
const mockClient = {
|
||||
on: jest.fn(),
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
disconnect: jest.fn(),
|
||||
quit: jest.fn(),
|
||||
ft: { info: jest.fn().mockResolvedValue(undefined) },
|
||||
} as any;
|
||||
|
||||
(MockCreateClient as any).mockReturnValue(mockClient);
|
||||
|
||||
const RedisVectorStoreMod: any = jest.requireMock('@langchain/redis');
|
||||
RedisVectorStoreMod.RedisVectorStore.prototype.similaritySearchVectorWithScore = jest
|
||||
.fn()
|
||||
.mockResolvedValue('ok');
|
||||
|
||||
const context: any = {
|
||||
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
|
||||
getNodeParameter: (name: string) => {
|
||||
const map: Record<string, any> = {
|
||||
redisIndex: 'idx2',
|
||||
'options.keyPrefix': '',
|
||||
'options.metadataKey': '',
|
||||
'options.contentKey': '',
|
||||
'options.vectorKey': '',
|
||||
'options.metadataFilter': 'tag1, tag2 , ,tag3',
|
||||
};
|
||||
return map[name];
|
||||
},
|
||||
getNode: () => ({ name: 'VectorStoreRedis' }),
|
||||
logger: loadOptionsFunctions.logger,
|
||||
} as any;
|
||||
|
||||
const node = new RedisNode.VectorStoreRedis();
|
||||
const client = await (node as any).getVectorStoreClient(context, undefined, {}, 0);
|
||||
|
||||
// Ensure trimming/removal works
|
||||
expect(client.defaultFilter).toEqual(['tag1', 'tag2', 'tag3']);
|
||||
});
|
||||
|
||||
it('omits optional keys when empty/whitespace and handles empty filter as null', async () => {
|
||||
const mockClient = {
|
||||
on: jest.fn(),
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
disconnect: jest.fn(),
|
||||
quit: jest.fn(),
|
||||
ft: { info: jest.fn().mockResolvedValue(undefined) },
|
||||
} as any;
|
||||
|
||||
(MockCreateClient as any).mockReturnValue(mockClient);
|
||||
|
||||
const RedisVectorStoreMod: any = jest.requireMock('@langchain/redis');
|
||||
RedisVectorStoreMod.RedisVectorStore.prototype.similaritySearchVectorWithScore = jest
|
||||
.fn()
|
||||
.mockResolvedValue('ok');
|
||||
|
||||
const context: any = {
|
||||
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
|
||||
getNodeParameter: (name: string) => {
|
||||
const map: Record<string, any> = {
|
||||
redisIndex: 'myIndex',
|
||||
'options.keyPrefix': ' ',
|
||||
'options.metadataKey': ' ',
|
||||
'options.contentKey': '',
|
||||
'options.vectorKey': ' \t',
|
||||
'options.metadataFilter': '',
|
||||
};
|
||||
return map[name];
|
||||
},
|
||||
getNode: () => ({ name: 'VectorStoreRedis' }),
|
||||
logger: loadOptionsFunctions.logger,
|
||||
} as any;
|
||||
|
||||
const embeddings: any = {};
|
||||
const node = new RedisNode.VectorStoreRedis();
|
||||
const instance = await (node as any).getVectorStoreClient(context, undefined, embeddings, 0);
|
||||
|
||||
// Ensure FT.INFO is called to validate index
|
||||
expect(mockClient.ft.info).toHaveBeenCalledWith('myIndex');
|
||||
|
||||
const opts = RedisVectorStoreMod.__state.ctorArgs[1];
|
||||
expect(opts).toMatchObject({ redisClient: mockClient, indexName: 'myIndex' });
|
||||
expect(opts).not.toHaveProperty('keyPrefix');
|
||||
expect(opts).not.toHaveProperty('metadataKey');
|
||||
expect(opts).not.toHaveProperty('contentKey');
|
||||
expect(opts).not.toHaveProperty('vectorKey');
|
||||
|
||||
const res = await instance.similaritySearchVectorWithScore([0], 1);
|
||||
expect(res).toBeDefined();
|
||||
expect(instance.defaultFilter).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined filter when filter string contains only whitespace and commas', async () => {
|
||||
const mockClient = {
|
||||
on: jest.fn(),
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
disconnect: jest.fn(),
|
||||
quit: jest.fn(),
|
||||
ft: { info: jest.fn().mockResolvedValue(undefined) },
|
||||
} as any;
|
||||
|
||||
(MockCreateClient as any).mockReturnValue(mockClient);
|
||||
|
||||
const RedisVectorStoreMod: any = jest.requireMock('@langchain/redis');
|
||||
RedisVectorStoreMod.RedisVectorStore.prototype.similaritySearchVectorWithScore = jest
|
||||
.fn()
|
||||
.mockResolvedValue('ok');
|
||||
|
||||
const context: any = {
|
||||
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
|
||||
getNodeParameter: (name: string) => {
|
||||
const map: Record<string, any> = {
|
||||
redisIndex: 'myIndex',
|
||||
'options.keyPrefix': '',
|
||||
'options.metadataKey': '',
|
||||
'options.contentKey': '',
|
||||
'options.vectorKey': '',
|
||||
'options.metadataFilter': ' , , , ',
|
||||
};
|
||||
return map[name];
|
||||
},
|
||||
getNode: () => ({ name: 'VectorStoreRedis' }),
|
||||
logger: loadOptionsFunctions.logger,
|
||||
} as any;
|
||||
|
||||
const node = new RedisNode.VectorStoreRedis();
|
||||
const instance = await (node as any).getVectorStoreClient(context, undefined, {}, 0);
|
||||
|
||||
// Filter with only whitespace and commas should result in undefined
|
||||
expect(instance.defaultFilter).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws NodeOperationError when index is missing', async () => {
|
||||
const mockClient = {
|
||||
on: jest.fn(),
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
disconnect: jest.fn(),
|
||||
quit: jest.fn(),
|
||||
ft: { info: jest.fn().mockRejectedValue(new Error('no such index')) },
|
||||
} as any;
|
||||
(MockCreateClient as any).mockReturnValue(mockClient);
|
||||
|
||||
const context: any = {
|
||||
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
|
||||
getNodeParameter: (name: string) => (name === 'redisIndex' ? 'idx' : ''),
|
||||
getNode: () => ({ name: 'VectorStoreRedis' }),
|
||||
};
|
||||
|
||||
const node = new RedisNode.VectorStoreRedis();
|
||||
await expect((node as any).getVectorStoreClient(context, undefined, {}, 0)).rejects.toEqual(
|
||||
new NodeOperationError(context.getNode(), 'Index idx not found', {
|
||||
itemIndex: 0,
|
||||
description: 'Please check that the index exists in your Redis instance',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('populateVectorStore', () => {
|
||||
it('drops index and deletes the documents when overwrite is true; passes TTL and batch size', async () => {
|
||||
const mockClient = {
|
||||
on: jest.fn(),
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
disconnect: jest.fn(),
|
||||
quit: jest.fn(),
|
||||
ft: { dropIndex: jest.fn().mockResolvedValue(undefined) },
|
||||
} as any;
|
||||
(MockCreateClient as any).mockReturnValue(mockClient);
|
||||
|
||||
const RedisVectorStoreMod: any = jest.requireMock('@langchain/redis');
|
||||
RedisVectorStoreMod.RedisVectorStore.fromDocuments = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const context: any = {
|
||||
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
|
||||
getNodeParameter: (name: string) => {
|
||||
const map: Record<string, any> = {
|
||||
redisIndex: 'myIndex',
|
||||
'options.overwriteDocuments': true,
|
||||
'options.keyPrefix': 'doc',
|
||||
'options.metadataKey': 'm',
|
||||
'options.contentKey': 'c',
|
||||
'options.vectorKey': 'v',
|
||||
'options.ttl': 60,
|
||||
embeddingBatchSize: 123,
|
||||
};
|
||||
return map[name];
|
||||
},
|
||||
getNode: () => ({ name: 'VectorStoreRedis' }),
|
||||
logger: loadOptionsFunctions.logger,
|
||||
} as any;
|
||||
|
||||
const node = new RedisNode.VectorStoreRedis();
|
||||
await (node as any).populateVectorStore(
|
||||
context,
|
||||
{},
|
||||
[{ pageContent: 'hello', metadata: {} }],
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockClient.ft.dropIndex).toHaveBeenCalledWith('myIndex', { DD: true });
|
||||
|
||||
expect(RedisVectorStoreMod.RedisVectorStore.fromDocuments).toHaveBeenCalledWith(
|
||||
[{ pageContent: 'hello', metadata: {} }],
|
||||
{},
|
||||
{
|
||||
redisClient: mockClient,
|
||||
indexName: 'myIndex',
|
||||
keyPrefix: 'doc',
|
||||
metadataKey: 'm',
|
||||
contentKey: 'c',
|
||||
vectorKey: 'v',
|
||||
ttl: 60,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('logs and throws NodeOperationError on failure', async () => {
|
||||
const mockClient = {
|
||||
on: jest.fn(),
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
disconnect: jest.fn(),
|
||||
quit: jest.fn(),
|
||||
sendCommand: jest.fn().mockResolvedValue(undefined),
|
||||
} as any;
|
||||
(MockCreateClient as any).mockReturnValue(mockClient);
|
||||
|
||||
const RedisVectorStoreMod: any = jest.requireMock('@langchain/redis');
|
||||
RedisVectorStoreMod.RedisVectorStore.fromDocuments = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('fail'));
|
||||
|
||||
const context: any = {
|
||||
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
|
||||
getNodeParameter: (name: string) => (name === 'redisIndex' ? 'idx' : ''),
|
||||
getNode: () => ({ name: 'VectorStoreRedis' }),
|
||||
logger: loadOptionsFunctions.logger,
|
||||
} as any;
|
||||
|
||||
const node = new RedisNode.VectorStoreRedis();
|
||||
await expect((node as any).populateVectorStore(context, {}, [], 0)).rejects.toEqual(
|
||||
new NodeOperationError(context.getNode(), 'Error: fail', {
|
||||
itemIndex: 0,
|
||||
description: 'Please check your index/schema and parameters',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(loadOptionsFunctions.logger.info).toHaveBeenCalledWith(
|
||||
'Error while populating the store: fail',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,412 @@
|
|||
import type { EmbeddingsInterface } from '@langchain/core/embeddings';
|
||||
import { RedisVectorStore } from '@langchain/redis';
|
||||
import type { RedisVectorStoreConfig } from '@langchain/redis/dist/vectorstores';
|
||||
import {
|
||||
type IExecuteFunctions,
|
||||
type ILoadOptionsFunctions,
|
||||
type INodeProperties,
|
||||
type ISupplyDataFunctions,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
import type { RedisClientOptions } from 'redis';
|
||||
import { createClient } from 'redis';
|
||||
|
||||
import { createVectorStoreNode } from '../shared/createVectorStoreNode/createVectorStoreNode';
|
||||
|
||||
/**
|
||||
* Constants for the name of the credentials and Node parameters.
|
||||
*/
|
||||
const REDIS_CREDENTIALS = 'redis';
|
||||
const REDIS_INDEX_NAME = 'redisIndex';
|
||||
const REDIS_KEY_PREFIX = 'keyPrefix';
|
||||
const REDIS_OVERWRITE_DOCUMENTS = 'overwriteDocuments';
|
||||
const REDIS_METADATA_KEY = 'metadataKey';
|
||||
const REDIS_METADATA_FILTER = 'metadataFilter';
|
||||
const REDIS_CONTENT_KEY = 'contentKey';
|
||||
const REDIS_EMBEDDING_KEY = 'vectorKey';
|
||||
const REDIS_TTL = 'ttl';
|
||||
|
||||
const redisIndexRLC: INodeProperties = {
|
||||
displayName: 'Redis Index',
|
||||
name: REDIS_INDEX_NAME,
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'redisIndexSearch',
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const metadataFilterField: INodeProperties = {
|
||||
displayName: 'Metadata Filter',
|
||||
name: REDIS_METADATA_FILTER,
|
||||
type: 'string',
|
||||
description:
|
||||
'The comma-separated list of words by which to apply additional full-text metadata filtering',
|
||||
placeholder: 'Item1,Item2,Item3',
|
||||
default: '',
|
||||
};
|
||||
|
||||
const metadataKeyField: INodeProperties = {
|
||||
displayName: 'Metadata Key',
|
||||
name: REDIS_METADATA_KEY,
|
||||
type: 'string',
|
||||
description: 'The hash key to be used to store the metadata of the document',
|
||||
placeholder: 'metadata',
|
||||
default: '',
|
||||
};
|
||||
|
||||
const contentKeyField: INodeProperties = {
|
||||
displayName: 'Content Key',
|
||||
name: REDIS_CONTENT_KEY,
|
||||
type: 'string',
|
||||
description: 'The hash key to be used to store the content of the document',
|
||||
placeholder: 'content',
|
||||
default: '',
|
||||
};
|
||||
|
||||
const embeddingKeyField: INodeProperties = {
|
||||
displayName: 'Embedding Key',
|
||||
name: REDIS_EMBEDDING_KEY,
|
||||
type: 'string',
|
||||
description: 'The hash key to be used to store the embedding of the document',
|
||||
placeholder: 'content_vector',
|
||||
default: '',
|
||||
};
|
||||
|
||||
const overwriteDocuments: INodeProperties = {
|
||||
displayName: 'Overwrite Documents',
|
||||
name: REDIS_OVERWRITE_DOCUMENTS,
|
||||
type: 'boolean',
|
||||
description: 'Whether existing documents and the index should be overwritten',
|
||||
default: false,
|
||||
};
|
||||
|
||||
const keyPrefixField: INodeProperties = {
|
||||
displayName: 'Key Prefix',
|
||||
name: REDIS_KEY_PREFIX,
|
||||
type: 'string',
|
||||
description: 'Prefix for Redis keys storing the documents',
|
||||
placeholder: 'doc',
|
||||
default: '',
|
||||
};
|
||||
|
||||
const ttlField: INodeProperties = {
|
||||
displayName: 'Time-To-Live',
|
||||
name: REDIS_TTL,
|
||||
description: 'Time-to-live for the documents in seconds',
|
||||
placeholder: '0',
|
||||
type: 'number',
|
||||
default: '',
|
||||
};
|
||||
|
||||
const sharedFields: INodeProperties[] = [redisIndexRLC];
|
||||
|
||||
const insertFields: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
keyPrefixField,
|
||||
overwriteDocuments,
|
||||
metadataKeyField,
|
||||
contentKeyField,
|
||||
embeddingKeyField,
|
||||
ttlField,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const retrieveFields: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
metadataFilterField,
|
||||
keyPrefixField,
|
||||
metadataKeyField,
|
||||
contentKeyField,
|
||||
embeddingKeyField,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const redisConfig = {
|
||||
client: null as ReturnType<typeof createClient> | null,
|
||||
connectionString: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Type used for cleaner, more intentional typing.
|
||||
*/
|
||||
type IFunctionsContext = IExecuteFunctions | ISupplyDataFunctions | ILoadOptionsFunctions;
|
||||
|
||||
/**
|
||||
* Get the Redis client.
|
||||
* @param context - The context.
|
||||
* @returns the Redis client for the node.
|
||||
*/
|
||||
export async function getRedisClient(context: IFunctionsContext) {
|
||||
const credentials = await context.getCredentials(REDIS_CREDENTIALS);
|
||||
|
||||
// Create client configuration object
|
||||
const config: RedisClientOptions = {
|
||||
socket: {
|
||||
host: (credentials.host as string) || 'localhost',
|
||||
port: (credentials.port as number) || 6379,
|
||||
tls: credentials.ssl === true,
|
||||
},
|
||||
username: credentials.user as string,
|
||||
password: credentials.password as string,
|
||||
database: credentials.database as number,
|
||||
clientInfoTag: 'n8n',
|
||||
};
|
||||
|
||||
if (!redisConfig.client || redisConfig.connectionString !== JSON.stringify(config)) {
|
||||
if (redisConfig.client) {
|
||||
await redisConfig.client.disconnect();
|
||||
}
|
||||
|
||||
redisConfig.connectionString = JSON.stringify(config);
|
||||
redisConfig.client = createClient(config);
|
||||
|
||||
if (redisConfig.client) {
|
||||
redisConfig.client.on('error', (error: Error) => {
|
||||
context.logger.error(`[Redis client] ${error.message}`, { error });
|
||||
});
|
||||
|
||||
await redisConfig.client.connect();
|
||||
}
|
||||
}
|
||||
|
||||
return redisConfig.client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if a value is a string array.
|
||||
* @param value - The value to check.
|
||||
* @returns True if the value is a string array, false otherwise.
|
||||
*/
|
||||
function isStringArray(value: unknown): value is string[] {
|
||||
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the complete list of indexes from Redis.
|
||||
* @returns The list of indexes.
|
||||
*/
|
||||
export async function listIndexes(this: ILoadOptionsFunctions) {
|
||||
const client = await getRedisClient(this);
|
||||
|
||||
if (client === null) {
|
||||
return { results: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all indexes using FT._LIST command
|
||||
const indexes = await client.ft._list();
|
||||
|
||||
// Validate that indexes is actually a string array
|
||||
if (!isStringArray(indexes)) {
|
||||
this.logger.warn('FT._LIST returned unexpected data type');
|
||||
return { results: [] };
|
||||
}
|
||||
|
||||
const results = indexes.map((index) => ({
|
||||
name: index,
|
||||
value: index,
|
||||
}));
|
||||
|
||||
return { results };
|
||||
} catch (error) {
|
||||
this.logger.info('Failed to get Redis indexes: ' + error.message);
|
||||
return { results: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a parameter from the context.
|
||||
* @param key - The key of the parameter.
|
||||
* @param context - The context.
|
||||
* @param itemIndex - The index.
|
||||
* @returns The value.
|
||||
*/
|
||||
export function getParameter(key: string, context: IFunctionsContext, itemIndex: number): string {
|
||||
return context.getNodeParameter(key, itemIndex, '', {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a parameter from the context as a number.
|
||||
* @param key - The key of the parameter.
|
||||
* @param context - The context.
|
||||
* @param itemIndex - The index.
|
||||
* @returns The value.
|
||||
*/
|
||||
export function getParameterAsNumber(
|
||||
key: string,
|
||||
context: IFunctionsContext,
|
||||
itemIndex: number,
|
||||
): number {
|
||||
return context.getNodeParameter(key, itemIndex, '', {
|
||||
extractValue: true,
|
||||
}) as number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended RedisVectorStore class to handle custom filtering.
|
||||
*
|
||||
* This wrapper is necessary because when used as a retriever, the similaritySearchVectorWithScore should
|
||||
* use a processed filter
|
||||
*/
|
||||
class ExtendedRedisVectorSearch extends RedisVectorStore {
|
||||
defaultFilter?: string[];
|
||||
|
||||
constructor(embeddings: EmbeddingsInterface, options: RedisVectorStoreConfig, filter?: string[]) {
|
||||
super(embeddings, options);
|
||||
this.defaultFilter = filter;
|
||||
}
|
||||
|
||||
async similaritySearchVectorWithScore(query: number[], k: number) {
|
||||
return await super.similaritySearchVectorWithScore(query, k, this.defaultFilter);
|
||||
}
|
||||
}
|
||||
|
||||
const getIndexName = getParameter.bind(null, REDIS_INDEX_NAME);
|
||||
const getKeyPrefix = getParameter.bind(null, `options.${REDIS_KEY_PREFIX}`);
|
||||
const getOverwrite = getParameter.bind(null, `options.${REDIS_OVERWRITE_DOCUMENTS}`);
|
||||
const getContentKey = getParameter.bind(null, `options.${REDIS_CONTENT_KEY}`);
|
||||
const getMetadataFilter = getParameter.bind(null, `options.${REDIS_METADATA_FILTER}`);
|
||||
const getMetadataKey = getParameter.bind(null, `options.${REDIS_METADATA_KEY}`);
|
||||
const getEmbeddingKey = getParameter.bind(null, `options.${REDIS_EMBEDDING_KEY}`);
|
||||
const getTtl = getParameterAsNumber.bind(null, `options.${REDIS_TTL}`);
|
||||
|
||||
export class VectorStoreRedis extends createVectorStoreNode({
|
||||
meta: {
|
||||
displayName: 'Redis Vector Store',
|
||||
name: 'vectorStoreRedis',
|
||||
description: 'Work with your data in a Redis vector index',
|
||||
icon: { light: 'file:redis.svg', dark: 'file:redis.dark.svg' },
|
||||
docsUrl:
|
||||
'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.vectorstoreredis/',
|
||||
credentials: [
|
||||
{
|
||||
name: REDIS_CREDENTIALS,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
operationModes: ['load', 'insert', 'retrieve', 'update', 'retrieve-as-tool'],
|
||||
},
|
||||
methods: { listSearch: { redisIndexSearch: listIndexes } },
|
||||
retrieveFields,
|
||||
loadFields: retrieveFields,
|
||||
insertFields,
|
||||
sharedFields,
|
||||
async getVectorStoreClient(context, _filter, embeddings, itemIndex) {
|
||||
const client = await getRedisClient(context);
|
||||
const indexField = getIndexName(context, itemIndex).trim();
|
||||
const keyPrefixField = getKeyPrefix(context, itemIndex).trim();
|
||||
const metadataField = getMetadataKey(context, itemIndex).trim();
|
||||
const contentField = getContentKey(context, itemIndex).trim();
|
||||
const embeddingField = getEmbeddingKey(context, itemIndex).trim();
|
||||
const filter = getMetadataFilter(context, itemIndex).trim();
|
||||
|
||||
if (client === null) {
|
||||
throw new NodeOperationError(context.getNode(), 'Redis client not initialized', {
|
||||
itemIndex,
|
||||
description: 'Please check your Redis connection details',
|
||||
});
|
||||
}
|
||||
|
||||
// Check if index exists by trying to get info about it
|
||||
try {
|
||||
await client.ft.info(indexField);
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(context.getNode(), `Index ${indexField} not found`, {
|
||||
itemIndex,
|
||||
description: 'Please check that the index exists in your Redis instance',
|
||||
});
|
||||
}
|
||||
|
||||
// Process filter: split by comma, trim, and remove empty strings
|
||||
// If no valid filter terms exist, pass undefined instead of empty array
|
||||
const filterTerms = filter
|
||||
? filter
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s)
|
||||
: [];
|
||||
|
||||
return new ExtendedRedisVectorSearch(
|
||||
embeddings,
|
||||
{
|
||||
redisClient: client,
|
||||
indexName: indexField,
|
||||
...(keyPrefixField ? { keyPrefix: keyPrefixField } : {}),
|
||||
...(metadataField ? { metadataKey: metadataField } : {}),
|
||||
...(contentField ? { contentKey: contentField } : {}),
|
||||
...(embeddingField ? { vectorKey: embeddingField } : {}),
|
||||
},
|
||||
filterTerms.length > 0 ? filterTerms : undefined,
|
||||
);
|
||||
},
|
||||
async populateVectorStore(context, embeddings, documents, itemIndex) {
|
||||
const client = await getRedisClient(context);
|
||||
|
||||
if (client === null) {
|
||||
throw new NodeOperationError(context.getNode(), 'Redis client not initialized', {
|
||||
itemIndex,
|
||||
description: 'Please check your Redis connection details',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const indexField = getIndexName(context, itemIndex).trim();
|
||||
const overwrite = getOverwrite(context, itemIndex);
|
||||
const keyPrefixField = getKeyPrefix(context, itemIndex).trim();
|
||||
const metadataField = getMetadataKey(context, itemIndex).trim();
|
||||
const contentField = getContentKey(context, itemIndex).trim();
|
||||
const embeddingField = getEmbeddingKey(context, itemIndex).trim();
|
||||
const ttl = getTtl(context, itemIndex);
|
||||
|
||||
if (overwrite) {
|
||||
await client.ft.dropIndex(indexField, { DD: true });
|
||||
}
|
||||
|
||||
await ExtendedRedisVectorSearch.fromDocuments(documents, embeddings, {
|
||||
redisClient: client,
|
||||
indexName: indexField,
|
||||
...(keyPrefixField ? { keyPrefix: keyPrefixField } : {}),
|
||||
...(metadataField ? { metadataKey: metadataField } : {}),
|
||||
...(contentField ? { contentKey: contentField } : {}),
|
||||
...(embeddingField ? { vectorKey: embeddingField } : {}),
|
||||
...(ttl ? { ttl } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
context.logger.info(`Error while populating the store: ${error.message}`);
|
||||
throw new NodeOperationError(context.getNode(), `Error: ${error.message}`, {
|
||||
itemIndex,
|
||||
description: 'Please check your index/schema and parameters',
|
||||
});
|
||||
}
|
||||
},
|
||||
}) {}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
|
||||
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
|
||||
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
|
||||
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
|
||||
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
|
||||
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
|
||||
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
|
||||
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
|
||||
]>
|
||||
<svg version="1.1" id="Layer_1" xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 156.0529938 144"
|
||||
style="enable-background:new 0 0 156.0529938 144;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<metadata>
|
||||
<sfw xmlns="&ns_sfw;">
|
||||
<slices></slices>
|
||||
<sliceSourceBounds bottomLeftOrigin="true" height="143.9999773" width="156.053" x="0" y="-143.9999773"></sliceSourceBounds>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<path class="st0" d="M147.6701355,79.4482651c-10.7946014,13.6011963-22.4527664,29.1454239-45.769104,29.1454239
|
||||
c-20.826828,0-28.5858688-18.3700104-29.1312943-33.2931747c4.5630341,9.6490402,13.4846039,17.4628448,27.4041595,17.1012726
|
||||
c26.7706146-0.8635712,45.1214371-25.0434799,45.1214371-47.0644608C145.2953339,18.998497,125.6491547,0,91.5382156,0
|
||||
C67.1424179,0,36.9175339,9.2833567,17.0554695,23.9640141c-0.2158928,15.1124401,8.2038975,34.7586136,11.2263851,32.5996933
|
||||
c17.2190762-12.3804779,30.8731899-20.3501434,44.1166077-24.3462524C52.7944527,54.0785789,5.7588773,104.8390808,0,113.7750931
|
||||
c0.6476761,8.2038956,10.7946014,30.2248917,15.7601175,30.2248917c1.5112448,0,2.8065958-0.8635712,4.3178396-2.3748169
|
||||
c14.1801414-15.9326324,25.7396431-30.2172928,36.0214996-43.985733
|
||||
c1.443779,20.1794739,11.3667984,44.8493042,39.1089249,44.8493042c24.8275833,0,49.4392776-17.9190445,60.665657-58.2908478
|
||||
C157.1693878,79.2323685,151.1244202,75.3463135,147.6701355,79.4482651z M119.3882904,46.848568
|
||||
c0,12.7376289-12.5217438,18.9985008-23.9640198,18.9985008c-6.1162338,0-10.8146286-1.6060715-14.5303345-3.6929359
|
||||
c6.8368607-10.3524857,13.6045761-20.9680901,20.8757477-32.3321495
|
||||
C114.5905914,31.9920235,119.3882904,39.118515,119.3882904,46.848568z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
|
||||
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
|
||||
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
|
||||
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
|
||||
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
|
||||
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
|
||||
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
|
||||
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
|
||||
]>
|
||||
<svg version="1.1" id="Layer_1" xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 156.0529938 144"
|
||||
style="enable-background:new 0 0 156.0529938 144;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FF4438;}
|
||||
</style>
|
||||
<metadata>
|
||||
<sfw xmlns="&ns_sfw;">
|
||||
<slices></slices>
|
||||
<sliceSourceBounds bottomLeftOrigin="true" height="144" width="156.0530246" x="0" y="-144"></sliceSourceBounds>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<path class="st0" d="M147.670166,79.4482727c-10.7946014,13.6012039-22.452774,29.1454315-45.7691193,29.1454315
|
||||
c-20.8268356,0-28.5858765-18.3700104-29.1313019-33.2931824c4.5630417,9.6490479,13.4846115,17.4628525,27.4041672,17.1012802
|
||||
c26.7706146-0.8635712,45.1214371-25.0434799,45.1214371-47.0644722C145.2953491,18.9985008,125.6491776,0,91.5382309,0
|
||||
C67.1424255,0,36.9175415,9.2833586,17.0554714,23.9640179c-0.2158909,15.1124439,8.2038994,34.7586212,11.226387,32.5997009
|
||||
c17.21908-12.3804817,30.8731976-20.3501434,44.1166153-24.3462563C52.7944603,54.0785866,5.7588782,104.8391037,0,113.775116
|
||||
C0.6476762,121.9790115,10.7946024,144,15.7601204,144c1.5112438,0,2.8065968-0.8635712,4.3178406-2.3748169
|
||||
c14.1801453-15.9326324,25.7396469-30.2172928,36.0215034-43.985733
|
||||
c1.443779,20.1794739,11.366806,44.8493042,39.1089325,44.8493042c24.8275833,0,49.4392776-17.9190369,60.6656723-58.2908554
|
||||
C157.1694183,79.2323837,151.1244354,75.3463287,147.670166,79.4482727z M119.3883057,46.8485756
|
||||
c0,12.7376328-12.5217361,18.9985008-23.9640198,18.9985008c-6.1162338,0-10.8146286-1.6060638-14.5303345-3.6929359
|
||||
c6.8368607-10.3524857,13.6045761-20.9680939,20.8757477-32.3321533
|
||||
C114.5906143,31.9920292,119.3883057,39.1185226,119.3883057,46.8485756z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -137,6 +137,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/VectorStoreRedis/VectorStoreRedis.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",
|
||||
|
|
@ -227,7 +228,7 @@
|
|||
"pdf-parse": "1.1.1",
|
||||
"pg": "8.12.0",
|
||||
"proxy-from-env": "^1.1.0",
|
||||
"redis": "4.6.12",
|
||||
"redis": "4.6.14",
|
||||
"sanitize-html": "2.12.1",
|
||||
"sqlite3": "5.1.7",
|
||||
"temp": "0.9.4",
|
||||
|
|
|
|||
141
pnpm-lock.yaml
141
pnpm-lock.yaml
|
|
@ -1105,7 +1105,7 @@ importers:
|
|||
version: 0.3.4(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)
|
||||
'@langchain/community':
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.50(8ac6ecc2064042e5620199e694862b5d)
|
||||
version: 0.3.50(90a6aa8f17697e0046031fe5da1489d1)
|
||||
'@langchain/core':
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))
|
||||
|
|
@ -1168,7 +1168,7 @@ importers:
|
|||
version: link:../json-schema-to-zod
|
||||
'@n8n/typeorm':
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.20-13(@sentry/node@9.42.1)(ioredis@5.3.2)(mysql2@3.15.0)(pg@8.12.0)(redis@4.6.12)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
|
||||
version: 0.3.20-13(@sentry/node@9.42.1)(ioredis@5.3.2)(mysql2@3.15.0)(pg@8.12.0)(redis@4.6.14)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
|
||||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../typescript-config
|
||||
|
|
@ -1260,8 +1260,8 @@ importers:
|
|||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
redis:
|
||||
specifier: 4.6.12
|
||||
version: 4.6.12
|
||||
specifier: 4.6.14
|
||||
version: 4.6.14
|
||||
sanitize-html:
|
||||
specifier: 2.12.1
|
||||
version: 2.12.1
|
||||
|
|
@ -6356,10 +6356,6 @@ packages:
|
|||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
|
||||
'@redis/client@1.5.13':
|
||||
resolution: {integrity: sha512-epkUM9D0Sdmt93/8Ozk43PNjLi36RZzG+d/T1Gdu5AI8jvghonTeLYV69WVWdilvFo+PYxbP0TZ0saMvr6nscQ==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@redis/client@1.5.16':
|
||||
resolution: {integrity: sha512-X1a3xQ5kEMvTib5fBrHKh6Y+pXbeKXqziYuxOUo1ojQNECg4M5Etd1qqyhMap+lFUOAh8S7UYevgJHOm4A+NOg==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -14535,9 +14531,6 @@ packages:
|
|||
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
redis@4.6.12:
|
||||
resolution: {integrity: sha512-41Xuuko6P4uH4VPe5nE3BqXHB7a9lkFL0J29AlxKaIfD6eWO8VO/5PDF9ad2oS+mswMsfFxaM5DlE3tnXT+P8Q==}
|
||||
|
||||
redis@4.6.14:
|
||||
resolution: {integrity: sha512-GrNg/e33HtsQwNXL7kJT+iNFPSwE1IPmd7wzV3j4f2z0EYxZfZE7FVTmUysgAtqQQtg5NXF5SNLR9OdO/UHOfw==}
|
||||
|
||||
|
|
@ -19713,14 +19706,14 @@ snapshots:
|
|||
'@jest/test-result': 29.6.2
|
||||
'@jest/transform': 29.6.2
|
||||
'@jest/types': 29.6.1
|
||||
'@types/node': 20.19.1
|
||||
'@types/node': 20.19.11
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.8.0
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-changed-files: 29.5.0
|
||||
jest-config: 29.6.2(@types/node@20.19.1)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2))
|
||||
jest-config: 29.6.2(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2))
|
||||
jest-haste-map: 29.6.2
|
||||
jest-message-util: 29.6.2
|
||||
jest-regex-util: 29.4.3
|
||||
|
|
@ -19748,14 +19741,14 @@ snapshots:
|
|||
'@jest/test-result': 29.6.2
|
||||
'@jest/transform': 29.6.2
|
||||
'@jest/types': 29.6.1
|
||||
'@types/node': 20.19.1
|
||||
'@types/node': 20.19.11
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.8.0
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-changed-files: 29.5.0
|
||||
jest-config: 29.6.2(@types/node@20.19.1)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
|
||||
jest-config: 29.6.2(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
|
||||
jest-haste-map: 29.6.2
|
||||
jest-message-util: 29.6.2
|
||||
jest-regex-util: 29.4.3
|
||||
|
|
@ -19780,7 +19773,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@jest/fake-timers': 29.6.2
|
||||
'@jest/types': 29.6.1
|
||||
'@types/node': 20.19.10
|
||||
'@types/node': 20.19.11
|
||||
jest-mock: 29.6.2
|
||||
|
||||
'@jest/expect-utils@29.6.2':
|
||||
|
|
@ -19798,7 +19791,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@jest/types': 29.6.1
|
||||
'@sinonjs/fake-timers': 10.0.2
|
||||
'@types/node': 20.19.10
|
||||
'@types/node': 20.19.11
|
||||
jest-message-util: 29.6.2
|
||||
jest-mock: 29.6.2
|
||||
jest-util: 29.6.2
|
||||
|
|
@ -19894,7 +19887,7 @@ snapshots:
|
|||
'@jest/schemas': 29.6.3
|
||||
'@types/istanbul-lib-coverage': 2.0.4
|
||||
'@types/istanbul-reports': 3.0.1
|
||||
'@types/node': 20.19.10
|
||||
'@types/node': 20.19.11
|
||||
'@types/yargs': 17.0.19
|
||||
chalk: 4.1.2
|
||||
|
||||
|
|
@ -19980,7 +19973,7 @@ snapshots:
|
|||
- aws-crt
|
||||
- encoding
|
||||
|
||||
'@langchain/community@0.3.50(8ac6ecc2064042e5620199e694862b5d)':
|
||||
'@langchain/community@0.3.50(90a6aa8f17697e0046031fe5da1489d1)':
|
||||
dependencies:
|
||||
'@browserbasehq/stagehand': 1.9.0(@playwright/test@1.54.2)(bufferutil@4.0.9)(deepmerge@4.3.1)(dotenv@16.6.1)(encoding@0.1.13)(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))(utf-8-validate@5.0.10)(zod@3.25.67)
|
||||
'@ibm-cloud/watsonx-ai': 1.1.2
|
||||
|
|
@ -20040,7 +20033,7 @@ snapshots:
|
|||
pdf-parse: 1.1.1
|
||||
pg: 8.12.0
|
||||
playwright: 1.54.2
|
||||
redis: 4.6.12
|
||||
redis: 4.6.14
|
||||
weaviate-client: 3.6.2(encoding@0.1.13)
|
||||
web-auth-library: 1.0.3
|
||||
ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||
|
|
@ -20374,36 +20367,6 @@ snapshots:
|
|||
esprima-next: 5.8.4
|
||||
recast: 0.22.0
|
||||
|
||||
'@n8n/typeorm@0.3.20-13(@sentry/node@9.42.1)(ioredis@5.3.2)(mysql2@3.15.0)(pg@8.12.0)(redis@4.6.12)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@n8n/p-retry': 6.2.0-2
|
||||
'@sqltools/formatter': 1.2.5
|
||||
app-root-path: 3.1.0
|
||||
async-mutex: 0.5.0
|
||||
buffer: 6.0.3
|
||||
chalk: 4.1.2
|
||||
dayjs: 1.11.10
|
||||
debug: 4.4.1(supports-color@8.1.1)
|
||||
dotenv: 16.6.1
|
||||
glob: 10.4.5
|
||||
mkdirp: 2.1.3
|
||||
reflect-metadata: 0.2.2
|
||||
sha.js: 2.4.12
|
||||
tarn: 3.0.2
|
||||
tslib: 2.8.1
|
||||
uuid: 9.0.1
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
'@sentry/node': 9.42.1
|
||||
ioredis: 5.3.2
|
||||
mysql2: 3.15.0
|
||||
pg: 8.12.0
|
||||
redis: 4.6.12
|
||||
sqlite3: 5.1.7
|
||||
ts-node: 10.9.2(@types/node@20.19.11)(typescript@5.9.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@n8n/typeorm@0.3.20-13(@sentry/node@9.42.1)(ioredis@5.3.2)(mysql2@3.15.0)(pg@8.12.0)(redis@4.6.14)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@n8n/p-retry': 6.2.0-2
|
||||
|
|
@ -20981,54 +20944,28 @@ snapshots:
|
|||
|
||||
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
||||
|
||||
'@redis/bloom@1.2.0(@redis/client@1.5.13)':
|
||||
dependencies:
|
||||
'@redis/client': 1.5.13
|
||||
|
||||
'@redis/bloom@1.2.0(@redis/client@1.5.16)':
|
||||
dependencies:
|
||||
'@redis/client': 1.5.16
|
||||
|
||||
'@redis/client@1.5.13':
|
||||
dependencies:
|
||||
cluster-key-slot: 1.1.2
|
||||
generic-pool: 3.9.0
|
||||
yallist: 4.0.0
|
||||
|
||||
'@redis/client@1.5.16':
|
||||
dependencies:
|
||||
cluster-key-slot: 1.1.2
|
||||
generic-pool: 3.9.0
|
||||
yallist: 4.0.0
|
||||
|
||||
'@redis/graph@1.1.1(@redis/client@1.5.13)':
|
||||
dependencies:
|
||||
'@redis/client': 1.5.13
|
||||
|
||||
'@redis/graph@1.1.1(@redis/client@1.5.16)':
|
||||
dependencies:
|
||||
'@redis/client': 1.5.16
|
||||
|
||||
'@redis/json@1.0.6(@redis/client@1.5.13)':
|
||||
dependencies:
|
||||
'@redis/client': 1.5.13
|
||||
|
||||
'@redis/json@1.0.6(@redis/client@1.5.16)':
|
||||
dependencies:
|
||||
'@redis/client': 1.5.16
|
||||
|
||||
'@redis/search@1.1.6(@redis/client@1.5.13)':
|
||||
dependencies:
|
||||
'@redis/client': 1.5.13
|
||||
|
||||
'@redis/search@1.1.6(@redis/client@1.5.16)':
|
||||
dependencies:
|
||||
'@redis/client': 1.5.16
|
||||
|
||||
'@redis/time-series@1.0.5(@redis/client@1.5.13)':
|
||||
dependencies:
|
||||
'@redis/client': 1.5.13
|
||||
|
||||
'@redis/time-series@1.0.5(@redis/client@1.5.16)':
|
||||
dependencies:
|
||||
'@redis/client': 1.5.16
|
||||
|
|
@ -22365,7 +22302,7 @@ snapshots:
|
|||
|
||||
'@types/jsdom@20.0.1':
|
||||
dependencies:
|
||||
'@types/node': 20.19.1
|
||||
'@types/node': 20.19.11
|
||||
'@types/tough-cookie': 4.0.2
|
||||
parse5: 7.1.2
|
||||
|
||||
|
|
@ -22637,7 +22574,7 @@ snapshots:
|
|||
|
||||
'@types/through@0.0.30':
|
||||
dependencies:
|
||||
'@types/node': 20.19.10
|
||||
'@types/node': 20.19.11
|
||||
|
||||
'@types/tough-cookie@4.0.2': {}
|
||||
|
||||
|
|
@ -27842,7 +27779,7 @@ snapshots:
|
|||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@29.6.2(@types/node@20.19.1)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)):
|
||||
jest-config@29.6.2(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)):
|
||||
dependencies:
|
||||
'@babel/core': 7.26.10
|
||||
'@jest/test-sequencer': 29.6.2
|
||||
|
|
@ -27867,43 +27804,12 @@ snapshots:
|
|||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.1
|
||||
'@types/node': 20.19.11
|
||||
ts-node: 10.9.2(@types/node@20.17.57)(typescript@5.9.2)
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@29.6.2(@types/node@20.19.1)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2)):
|
||||
dependencies:
|
||||
'@babel/core': 7.26.10
|
||||
'@jest/test-sequencer': 29.6.2
|
||||
'@jest/types': 29.6.1
|
||||
babel-jest: 29.6.2(@babel/core@7.26.10)
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.8.0
|
||||
deepmerge: 4.3.1
|
||||
glob: 7.2.3
|
||||
graceful-fs: 4.2.11
|
||||
jest-circus: 29.6.2
|
||||
jest-environment-node: 29.6.2
|
||||
jest-get-type: 29.4.3
|
||||
jest-regex-util: 29.4.3
|
||||
jest-resolve: 29.6.2
|
||||
jest-runner: 29.6.2
|
||||
jest-util: 29.6.2
|
||||
jest-validate: 29.6.2
|
||||
micromatch: 4.0.8
|
||||
parse-json: 5.2.0
|
||||
pretty-format: 29.7.0
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.1
|
||||
ts-node: 10.9.2(@types/node@20.19.11)(typescript@5.9.2)
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@29.6.2(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2)):
|
||||
dependencies:
|
||||
'@babel/core': 7.26.10
|
||||
|
|
@ -28179,7 +28085,7 @@ snapshots:
|
|||
jest-util@29.5.0:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.1
|
||||
'@types/node': 20.19.1
|
||||
'@types/node': 20.19.11
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.8.0
|
||||
graceful-fs: 4.2.11
|
||||
|
|
@ -28188,7 +28094,7 @@ snapshots:
|
|||
jest-util@29.6.2:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.1
|
||||
'@types/node': 20.19.10
|
||||
'@types/node': 20.19.11
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.8.0
|
||||
graceful-fs: 4.2.11
|
||||
|
|
@ -31187,15 +31093,6 @@ snapshots:
|
|||
dependencies:
|
||||
redis-errors: 1.2.0
|
||||
|
||||
redis@4.6.12:
|
||||
dependencies:
|
||||
'@redis/bloom': 1.2.0(@redis/client@1.5.13)
|
||||
'@redis/client': 1.5.13
|
||||
'@redis/graph': 1.1.1(@redis/client@1.5.13)
|
||||
'@redis/json': 1.0.6(@redis/client@1.5.13)
|
||||
'@redis/search': 1.1.6(@redis/client@1.5.13)
|
||||
'@redis/time-series': 1.0.5(@redis/client@1.5.13)
|
||||
|
||||
redis@4.6.14:
|
||||
dependencies:
|
||||
'@redis/bloom': 1.2.0(@redis/client@1.5.16)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user