Skip to content

Commit a5d9596

Browse files
authored
Merge pull request FlowiseAI#1108 from vinodkiran/FEATURE/redis-vectorstore
Feature/RedisVectorStore
2 parents ee7644d + 4988627 commit a5d9596

File tree

8 files changed

+406
-22
lines changed

8 files changed

+406
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { INodeParams, INodeCredential } from '../src/Interface'
2+
3+
class RedisCacheUrlApi implements INodeCredential {
4+
label: string
5+
name: string
6+
version: number
7+
description: string
8+
inputs: INodeParams[]
9+
10+
constructor() {
11+
this.label = 'Redis Cache URL'
12+
this.name = 'redisCacheUrlApi'
13+
this.version = 1.0
14+
this.inputs = [
15+
{
16+
label: 'Redis URL',
17+
name: 'redisUrl',
18+
type: 'string',
19+
default: '127.0.0.1'
20+
}
21+
]
22+
}
23+
}
24+
25+
module.exports = { credClass: RedisCacheUrlApi }

packages/components/nodes/cache/RedisCache/RedisCache.ts

+18-11
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class RedisCache implements INode {
3030
name: 'credential',
3131
type: 'credential',
3232
optional: true,
33-
credentialNames: ['redisCacheApi']
33+
credentialNames: ['redisCacheApi', 'redisCacheUrlApi']
3434
}
3535
this.inputs = [
3636
{
@@ -48,17 +48,24 @@ class RedisCache implements INode {
4848
const ttl = nodeData.inputs?.ttl as string
4949

5050
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
51-
const username = getCredentialParam('redisCacheUser', credentialData, nodeData)
52-
const password = getCredentialParam('redisCachePwd', credentialData, nodeData)
53-
const portStr = getCredentialParam('redisCachePort', credentialData, nodeData)
54-
const host = getCredentialParam('redisCacheHost', credentialData, nodeData)
51+
const redisUrl = getCredentialParam('redisUrl', credentialData, nodeData)
5552

56-
const client = new Redis({
57-
port: portStr ? parseInt(portStr) : 6379,
58-
host,
59-
username,
60-
password
61-
})
53+
let client: Redis
54+
if (!redisUrl || redisUrl === '') {
55+
const username = getCredentialParam('redisCacheUser', credentialData, nodeData)
56+
const password = getCredentialParam('redisCachePwd', credentialData, nodeData)
57+
const portStr = getCredentialParam('redisCachePort', credentialData, nodeData)
58+
const host = getCredentialParam('redisCacheHost', credentialData, nodeData)
59+
60+
client = new Redis({
61+
port: portStr ? parseInt(portStr) : 6379,
62+
host,
63+
username,
64+
password
65+
})
66+
} else {
67+
client = new Redis(redisUrl)
68+
}
6269

6370
const redisClient = new LangchainRedisCache(client)
6471

packages/components/nodes/cache/RedisCache/RedisEmbeddingsCache.ts

+19-11
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class RedisEmbeddingsCache implements INode {
3030
name: 'credential',
3131
type: 'credential',
3232
optional: true,
33-
credentialNames: ['redisCacheApi']
33+
credentialNames: ['redisCacheApi', 'redisCacheUrlApi']
3434
}
3535
this.inputs = [
3636
{
@@ -63,17 +63,25 @@ class RedisEmbeddingsCache implements INode {
6363
const underlyingEmbeddings = nodeData.inputs?.embeddings as Embeddings
6464

6565
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
66-
const username = getCredentialParam('redisCacheUser', credentialData, nodeData)
67-
const password = getCredentialParam('redisCachePwd', credentialData, nodeData)
68-
const portStr = getCredentialParam('redisCachePort', credentialData, nodeData)
69-
const host = getCredentialParam('redisCacheHost', credentialData, nodeData)
66+
const redisUrl = getCredentialParam('redisUrl', credentialData, nodeData)
67+
68+
let client: Redis
69+
if (!redisUrl || redisUrl === '') {
70+
const username = getCredentialParam('redisCacheUser', credentialData, nodeData)
71+
const password = getCredentialParam('redisCachePwd', credentialData, nodeData)
72+
const portStr = getCredentialParam('redisCachePort', credentialData, nodeData)
73+
const host = getCredentialParam('redisCacheHost', credentialData, nodeData)
74+
75+
client = new Redis({
76+
port: portStr ? parseInt(portStr) : 6379,
77+
host,
78+
username,
79+
password
80+
})
81+
} else {
82+
client = new Redis(redisUrl)
83+
}
7084

71-
const client = new Redis({
72-
port: portStr ? parseInt(portStr) : 6379,
73-
host,
74-
username,
75-
password
76-
})
7785
ttl ??= '3600'
7886
let ttlNumber = parseInt(ttl, 10)
7987
const redisStore = new RedisByteStore({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import {
2+
getBaseClasses,
3+
getCredentialData,
4+
getCredentialParam,
5+
ICommonObject,
6+
INodeData,
7+
INodeOutputsValue,
8+
INodeParams
9+
} from '../../../src'
10+
11+
import { Embeddings } from 'langchain/embeddings/base'
12+
import { VectorStore } from 'langchain/vectorstores/base'
13+
import { Document } from 'langchain/document'
14+
import { createClient, SearchOptions } from 'redis'
15+
import { RedisVectorStore } from 'langchain/vectorstores/redis'
16+
import { escapeSpecialChars, unEscapeSpecialChars } from './utils'
17+
18+
export abstract class RedisSearchBase {
19+
label: string
20+
name: string
21+
version: number
22+
description: string
23+
type: string
24+
icon: string
25+
category: string
26+
baseClasses: string[]
27+
inputs: INodeParams[]
28+
credential: INodeParams
29+
outputs: INodeOutputsValue[]
30+
redisClient: ReturnType<typeof createClient>
31+
32+
protected constructor() {
33+
this.type = 'Redis'
34+
this.icon = 'redis.svg'
35+
this.category = 'Vector Stores'
36+
this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever']
37+
this.credential = {
38+
label: 'Connect Credential',
39+
name: 'credential',
40+
type: 'credential',
41+
credentialNames: ['redisCacheUrlApi', 'redisCacheApi']
42+
}
43+
this.inputs = [
44+
{
45+
label: 'Embeddings',
46+
name: 'embeddings',
47+
type: 'Embeddings'
48+
},
49+
{
50+
label: 'Index Name',
51+
name: 'indexName',
52+
placeholder: '<VECTOR_INDEX_NAME>',
53+
type: 'string'
54+
},
55+
{
56+
label: 'Replace Index?',
57+
name: 'replaceIndex',
58+
description: 'Selecting this option will delete the existing index and recreate a new one',
59+
default: false,
60+
type: 'boolean'
61+
},
62+
{
63+
label: 'Content Field',
64+
name: 'contentKey',
65+
description: 'Name of the field (column) that contains the actual content',
66+
type: 'string',
67+
default: 'content',
68+
additionalParams: true,
69+
optional: true
70+
},
71+
{
72+
label: 'Metadata Field',
73+
name: 'metadataKey',
74+
description: 'Name of the field (column) that contains the metadata of the document',
75+
type: 'string',
76+
default: 'metadata',
77+
additionalParams: true,
78+
optional: true
79+
},
80+
{
81+
label: 'Vector Field',
82+
name: 'vectorKey',
83+
description: 'Name of the field (column) that contains the vector',
84+
type: 'string',
85+
default: 'content_vector',
86+
additionalParams: true,
87+
optional: true
88+
},
89+
{
90+
label: 'Top K',
91+
name: 'topK',
92+
description: 'Number of top results to fetch. Default to 4',
93+
placeholder: '4',
94+
type: 'number',
95+
additionalParams: true,
96+
optional: true
97+
}
98+
]
99+
this.outputs = [
100+
{
101+
label: 'Redis Retriever',
102+
name: 'retriever',
103+
baseClasses: this.baseClasses
104+
},
105+
{
106+
label: 'Redis Vector Store',
107+
name: 'vectorStore',
108+
baseClasses: [this.type, ...getBaseClasses(RedisVectorStore)]
109+
}
110+
]
111+
}
112+
113+
abstract constructVectorStore(
114+
embeddings: Embeddings,
115+
indexName: string,
116+
replaceIndex: boolean,
117+
docs: Document<Record<string, any>>[] | undefined
118+
): Promise<VectorStore>
119+
120+
async init(nodeData: INodeData, _: string, options: ICommonObject, docs: Document<Record<string, any>>[] | undefined): Promise<any> {
121+
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
122+
const indexName = nodeData.inputs?.indexName as string
123+
let contentKey = nodeData.inputs?.contentKey as string
124+
let metadataKey = nodeData.inputs?.metadataKey as string
125+
let vectorKey = nodeData.inputs?.vectorKey as string
126+
const embeddings = nodeData.inputs?.embeddings as Embeddings
127+
const topK = nodeData.inputs?.topK as string
128+
const replaceIndex = nodeData.inputs?.replaceIndex as boolean
129+
const k = topK ? parseFloat(topK) : 4
130+
const output = nodeData.outputs?.output as string
131+
132+
let redisUrl = getCredentialParam('redisUrl', credentialData, nodeData)
133+
if (!redisUrl || redisUrl === '') {
134+
const username = getCredentialParam('redisCacheUser', credentialData, nodeData)
135+
const password = getCredentialParam('redisCachePwd', credentialData, nodeData)
136+
const portStr = getCredentialParam('redisCachePort', credentialData, nodeData)
137+
const host = getCredentialParam('redisCacheHost', credentialData, nodeData)
138+
139+
redisUrl = 'redis://' + username + ':' + password + '@' + host + ':' + portStr
140+
}
141+
142+
this.redisClient = createClient({ url: redisUrl })
143+
await this.redisClient.connect()
144+
145+
const vectorStore = await this.constructVectorStore(embeddings, indexName, replaceIndex, docs)
146+
if (!contentKey || contentKey === '') contentKey = 'content'
147+
if (!metadataKey || metadataKey === '') metadataKey = 'metadata'
148+
if (!vectorKey || vectorKey === '') vectorKey = 'content_vector'
149+
150+
const buildQuery = (query: number[], k: number, filter?: string[]): [string, SearchOptions] => {
151+
const vectorScoreField = 'vector_score'
152+
153+
let hybridFields = '*'
154+
// if a filter is set, modify the hybrid query
155+
if (filter && filter.length) {
156+
// `filter` is a list of strings, then it's applied using the OR operator in the metadata key
157+
hybridFields = `@${metadataKey}:(${filter.map(escapeSpecialChars).join('|')})`
158+
}
159+
160+
const baseQuery = `${hybridFields} => [KNN ${k} @${vectorKey} $vector AS ${vectorScoreField}]`
161+
const returnFields = [metadataKey, contentKey, vectorScoreField]
162+
163+
const options: SearchOptions = {
164+
PARAMS: {
165+
vector: Buffer.from(new Float32Array(query).buffer)
166+
},
167+
RETURN: returnFields,
168+
SORTBY: vectorScoreField,
169+
DIALECT: 2,
170+
LIMIT: {
171+
from: 0,
172+
size: k
173+
}
174+
}
175+
176+
return [baseQuery, options]
177+
}
178+
179+
vectorStore.similaritySearchVectorWithScore = async (
180+
query: number[],
181+
k: number,
182+
filter?: string[]
183+
): Promise<[Document, number][]> => {
184+
const results = await this.redisClient.ft.search(indexName, ...buildQuery(query, k, filter))
185+
const result: [Document, number][] = []
186+
187+
if (results.total) {
188+
for (const res of results.documents) {
189+
if (res.value) {
190+
const document = res.value
191+
if (document.vector_score) {
192+
const metadataString = unEscapeSpecialChars(document[metadataKey] as string)
193+
result.push([
194+
new Document({
195+
pageContent: document[contentKey] as string,
196+
metadata: JSON.parse(metadataString)
197+
}),
198+
Number(document.vector_score)
199+
])
200+
}
201+
}
202+
}
203+
}
204+
return result
205+
}
206+
207+
if (output === 'retriever') {
208+
return vectorStore.asRetriever(k)
209+
} else if (output === 'vectorStore') {
210+
;(vectorStore as any).k = k
211+
return vectorStore
212+
}
213+
return vectorStore
214+
}
215+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { ICommonObject, INode, INodeData } from '../../../src/Interface'
2+
import { Embeddings } from 'langchain/embeddings/base'
3+
import { VectorStore } from 'langchain/vectorstores/base'
4+
import { RedisVectorStore, RedisVectorStoreConfig } from 'langchain/vectorstores/redis'
5+
import { Document } from 'langchain/document'
6+
7+
import { RedisSearchBase } from './RedisSearchBase'
8+
9+
class RedisExisting_VectorStores extends RedisSearchBase implements INode {
10+
constructor() {
11+
super()
12+
this.label = 'Redis Load Existing Index'
13+
this.name = 'RedisIndex'
14+
this.version = 1.0
15+
this.description = 'Load existing index from Redis (i.e: Document has been upserted)'
16+
17+
// Remove deleteIndex from inputs as it is not applicable while fetching data from Redis
18+
let input = this.inputs.find((i) => i.name === 'deleteIndex')
19+
if (input) this.inputs.splice(this.inputs.indexOf(input), 1)
20+
}
21+
22+
async constructVectorStore(
23+
embeddings: Embeddings,
24+
indexName: string,
25+
// eslint-disable-next-line unused-imports/no-unused-vars
26+
replaceIndex: boolean,
27+
_: Document<Record<string, any>>[]
28+
): Promise<VectorStore> {
29+
const storeConfig: RedisVectorStoreConfig = {
30+
redisClient: this.redisClient,
31+
indexName: indexName
32+
}
33+
34+
return new RedisVectorStore(embeddings, storeConfig)
35+
}
36+
37+
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
38+
return super.init(nodeData, _, options, undefined)
39+
}
40+
}
41+
42+
module.exports = { nodeClass: RedisExisting_VectorStores }

0 commit comments

Comments
 (0)