Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/RedisVectorStore #1108

Merged
merged 4 commits into from
Oct 25, 2023

Conversation

vinodkiran
Copy link
Contributor

No description provided.

@vinodkiran
Copy link
Contributor Author

@HenryHengZJ Can you please take a look? I get the following error and I am stuck. Will continue my debugging/investigation over the weekend.

TypeError: Cannot read private member from an object whose class did not declare it
at __classPrivateFieldGet (...\Flowise\node_modules@redis\client\dist\lib\client\index.js:4:94)
at commandsExecutor (...\Flowise\node_modules@redis\client\dist\lib\client\index.js:188:70)
at Object.Commander.. [as search] (...\Flowise\node_modules@redis\client\dist\lib\commander.js:58:33)
at RedisVectorStore.similaritySearchVectorWithScore (...\Flowise\node_modules\langchain\dist\vectorstores\redis.cjs:149:51)
at RedisVectorStore.similaritySearch (...\Flowise\node_modules\langchain\dist\vectorstores\base.cjs:107:36)

@HenryHengZJ
Copy link
Contributor

we have to implement the similarSearchVectorWithScore function locally, then it works:
image

@HenryHengZJ
Copy link
Contributor

a rough sketch:

import {
    getBaseClasses,
    getCredentialData,
    getCredentialParam,
    ICommonObject,
    INodeData,
    INodeOutputsValue,
    INodeParams
} from '../../../src'

import { Embeddings } from 'langchain/embeddings/base'
import { VectorStore } from 'langchain/vectorstores/base'
import { Document } from 'langchain/document'
import { createClient, SearchOptions } from 'redis'
import { RedisVectorStore } from 'langchain/vectorstores/redis'

export abstract class RedisSearchBase {
    label: string
    name: string
    version: number
    description: string
    type: string
    icon: string
    category: string
    baseClasses: string[]
    inputs: INodeParams[]
    credential: INodeParams
    outputs: INodeOutputsValue[]
    redisClient: ReturnType<typeof createClient>

    protected constructor() {
        this.type = 'Redis'
        this.icon = 'redis.svg'
        this.category = 'Vector Stores'
        this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever']
        this.credential = {
            label: 'Connect Credential',
            name: 'credential',
            type: 'credential',
            credentialNames: ['redisCacheUrlApi', 'redisCacheApi']
        }
        this.inputs = [
            {
                label: 'Embeddings',
                name: 'embeddings',
                type: 'Embeddings'
            },
            {
                label: 'Index Name',
                name: 'indexName',
                placeholder: '<VECTOR_INDEX_NAME>',
                type: 'string'
            },
            {
                label: 'Top K',
                name: 'topK',
                description: 'Number of top results to fetch. Default to 4',
                placeholder: '4',
                type: 'number',
                additionalParams: true,
                optional: true
            }
        ]
        this.outputs = [
            {
                label: 'Redis Retriever',
                name: 'retriever',
                baseClasses: this.baseClasses
            },
            {
                label: 'Redis Vector Store',
                name: 'vectorStore',
                baseClasses: [this.type, ...getBaseClasses(RedisVectorStore)]
            }
        ]
    }

    abstract constructVectorStore(
        embeddings: Embeddings,
        indexName: string,
        docs: Document<Record<string, any>>[] | undefined
    ): Promise<VectorStore>

    async init(nodeData: INodeData, _: string, options: ICommonObject, docs: Document<Record<string, any>>[] | undefined): Promise<any> {
        const credentialData = await getCredentialData(nodeData.credential ?? '', options)
        const indexName = nodeData.inputs?.indexName as string
        const embeddings = nodeData.inputs?.embeddings as Embeddings
        const topK = nodeData.inputs?.topK as string
        const k = topK ? parseFloat(topK) : 4
        const output = nodeData.outputs?.output as string

        let redisUrl = getCredentialParam('redisUrl', credentialData, nodeData)
        if (!redisUrl || redisUrl === '') {
            const username = getCredentialParam('redisCacheUser', credentialData, nodeData)
            const password = getCredentialParam('redisCachePwd', credentialData, nodeData)
            const portStr = getCredentialParam('redisCachePort', credentialData, nodeData)
            const host = getCredentialParam('redisCacheHost', credentialData, nodeData)

            redisUrl = 'redis://' + username + ':' + password + '@' + host + ':' + portStr
        }

        this.redisClient = createClient({ url: redisUrl })
        await this.redisClient.connect()

        const vectorStore = await this.constructVectorStore(embeddings, indexName, docs)

        const contentKey = 'content' // let user specify
        const metadataKey = 'metadata'
        const vectorKey = 'content_vector'

        const escapeSpecialChars = (str: string) => {
            return str.replaceAll('-', '\\-')
        }

        const unEscapeSpecialChars = (str: string) => {
            return str.replaceAll('\\-', '-')
        }

        const buildQuery = (query: number[], k: number, filter?: string[]): [string, SearchOptions] => {
            const vectorScoreField = 'vector_score'

            let hybridFields = '*'
            // if a filter is set, modify the hybrid query
            if (filter && filter.length) {
                // `filter` is a list of strings, then it's applied using the OR operator in the metadata key
                // for example: filter = ['foo', 'bar'] => this will filter all metadata containing either 'foo' OR 'bar'
                hybridFields = `@${metadataKey}:(${filter.map(escapeSpecialChars).join('|')})`
            }

            const baseQuery = `${hybridFields} => [KNN ${k} @${vectorKey} $vector AS ${vectorScoreField}]`
            const returnFields = [metadataKey, contentKey, vectorScoreField]

            const options: SearchOptions = {
                PARAMS: {
                    vector: Buffer.from(new Float32Array(query).buffer)
                },
                RETURN: returnFields,
                SORTBY: vectorScoreField,
                DIALECT: 2,
                LIMIT: {
                    from: 0,
                    size: k
                }
            }

            return [baseQuery, options]
        }

        vectorStore.similaritySearchVectorWithScore = async (
            query: number[],
            k: number,
            filter?: string[]
        ): Promise<[Document, number][]> => {
            const _filter = filter
            const results = await this.redisClient.ft.search(indexName, ...buildQuery(query, k, _filter))
            const result: [Document, number][] = []

            if (results.total) {
                for (const res of results.documents) {
                    if (res.value) {
                        const document = res.value
                        if (document.vector_score) {
                            result.push([
                                new Document({
                                    pageContent: document[contentKey] as string,
                                    metadata: JSON.parse(unEscapeSpecialChars(document.metadata as string))
                                }),
                                Number(document.vector_score)
                            ])
                        }
                    }
                }
            }

            return result
        }

        if (output === 'retriever') {
            return vectorStore.asRetriever(k)
        } else if (output === 'vectorStore') {
            ;(vectorStore as any).k = k
            return vectorStore
        }
        return vectorStore
    }
}

@vinodkiran vinodkiran marked this pull request as ready for review October 22, 2023 06:26
@vinodkiran
Copy link
Contributor Author

we have to implement the similarSearchVectorWithScore function locally, then it works: image

Fantastic!

@vinodkiran vinodkiran closed this Oct 22, 2023
@vinodkiran vinodkiran reopened this Oct 22, 2023
Copy link
Contributor

@HenryHengZJ HenryHengZJ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @vinodkiran !! appreciate the effort!

@HenryHengZJ HenryHengZJ merged commit da31e42 into FlowiseAI:main Oct 25, 2023
2 checks passed
@vinodkiran vinodkiran deleted the FEATURE/redis-vectorstore branch October 25, 2023 13:48
hemati pushed a commit to hemati/Flowise that referenced this pull request Dec 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants