diff --git a/packages/components/credentials/MeilisearchApi.credential.ts b/packages/components/credentials/MeilisearchApi.credential.ts new file mode 100644 index 00000000..64d8367b --- /dev/null +++ b/packages/components/credentials/MeilisearchApi.credential.ts @@ -0,0 +1,32 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class MeilisearchApi implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Meilisearch API' + this.name = 'meilisearchApi' + this.version = 1.0 + this.description = + 'Refer to official guide on how to get an API Key, you need a search API KEY for basic searching functionality, admin API KEY is optional but needed for upsert functionality ' + this.inputs = [ + { + label: 'Meilisearch Search API Key', + name: 'meilisearchSearchApiKey', + type: 'password' + }, + { + label: 'Meilisearch Admin API Key', + name: 'meilisearchAdminApiKey', + type: 'password', + optional: true + } + ] + } +} + +module.exports = { credClass: MeilisearchApi } diff --git a/packages/components/nodes/vectorstores/Meilisearch/Meilisearch.png b/packages/components/nodes/vectorstores/Meilisearch/Meilisearch.png new file mode 100644 index 00000000..7bbb458f Binary files /dev/null and b/packages/components/nodes/vectorstores/Meilisearch/Meilisearch.png differ diff --git a/packages/components/nodes/vectorstores/Meilisearch/Meilisearch.ts b/packages/components/nodes/vectorstores/Meilisearch/Meilisearch.ts new file mode 100644 index 00000000..eecc5e5e --- /dev/null +++ b/packages/components/nodes/vectorstores/Meilisearch/Meilisearch.ts @@ -0,0 +1,174 @@ +import { getCredentialData, getCredentialParam } from '../../../src' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { Meilisearch } from 'meilisearch' +import { MeilisearchRetriever } from './core' +import { flatten } from 'lodash' +import { Document } from '@langchain/core/documents' +import { v4 as uuidv4 } from 'uuid' +import { Embeddings } from '@langchain/core/embeddings' + +class MeilisearchRetriever_node implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + badge: string + outputs: INodeOutputsValue[] + author?: string + + constructor() { + this.label = 'Meilisearch' + this.name = 'meilisearch' + this.version = 1.0 + this.type = 'Meilisearch' + this.icon = 'Meilisearch.png' + this.category = 'Vector Stores' + this.badge = 'NEW' + this.description = `Upsert embedded data and perform similarity search upon query using Meilisearch hybrid search functionality` + this.baseClasses = ['BaseRetriever'] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['meilisearchApi'] + } + this.inputs = [ + { + label: 'Document', + name: 'document', + type: 'Document', + list: true, + optional: true + }, + { + label: 'Embeddings', + name: 'embeddings', + type: 'Embeddings' + }, + { + label: 'Host', + name: 'host', + type: 'string', + description: 'This is the URL for the desired Meilisearch instance' + }, + { + label: 'Index Uid', + name: 'indexUid', + type: 'string', + description: 'UID for the index to answer from' + }, + { + label: 'Top K', + name: 'K', + type: 'number', + description: 'number of top searches to return as context', + additionalParams: true, + optional: true + }, + { + label: 'Semantic Ratio', + name: 'semanticRatio', + type: 'number', + description: 'percentage of sematic reasoning in meilisearch hybrid search', + additionalParams: true, + optional: true + } + ] + this.outputs = [ + { + label: 'Meilisearch Retriever', + name: 'MeilisearchRetriever', + description: 'retrieve answers', + baseClasses: this.baseClasses + } + ] + this.outputs = [ + { + label: 'Meilisearch Retriever', + name: 'retriever', + baseClasses: this.baseClasses + } + ] + } + //@ts-ignore + vectorStoreMethods = { + async upsert(nodeData: INodeData, options: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const meilisearchAdminApiKey = getCredentialParam('meilisearchAdminApiKey', credentialData, nodeData) + const docs = nodeData.inputs?.document as Document[] + const host = nodeData.inputs?.host as string + const indexUid = nodeData.inputs?.indexUid as string + const embeddings = nodeData.inputs?.embeddings as Embeddings + let embeddingDimension: number = 384 + const client = new Meilisearch({ + host: host, + apiKey: meilisearchAdminApiKey + }) + const flattenDocs = docs && docs.length ? flatten(docs) : [] + const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { + if (flattenDocs[i] && flattenDocs[i].pageContent) { + const uniqueId = uuidv4() + const { pageContent, metadata } = flattenDocs[i] + const docEmbedding = await embeddings.embedQuery(pageContent) + embeddingDimension = docEmbedding.length + const documentForIndexing = { + pageContent, + metadata, + objectID: uniqueId, + _vectors: { + ollama: { + embeddings: docEmbedding, + regenerate: false + } + } + } + finalDocs.push(documentForIndexing) + } + } + let index: any + try { + index = await client.getIndex(indexUid) + } catch (error) { + console.error('Error fetching index:', error) + await client.createIndex(indexUid, { primaryKey: 'objectID' }) + } finally { + index = await client.getIndex(indexUid) + } + + try { + await index.updateSettings({ + embedders: { + ollama: { + source: 'userProvided', + dimensions: embeddingDimension + } + } + }) + await index.addDocuments(finalDocs) + } catch (error) { + console.error('Error occurred while adding documents:', error) + } + return + } + } + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const meilisearchSearchApiKey = getCredentialParam('meilisearchSearchApiKey', credentialData, nodeData) + const host = nodeData.inputs?.host as string + const indexUid = nodeData.inputs?.indexUid as string + const K = nodeData.inputs?.K as string + const semanticRatio = nodeData.inputs?.semanticRatio as string + const embeddings = nodeData.inputs?.embeddings as Embeddings + + const hybridsearchretriever = new MeilisearchRetriever(host, meilisearchSearchApiKey, indexUid, K, semanticRatio, embeddings) + return hybridsearchretriever + } +} +module.exports = { nodeClass: MeilisearchRetriever_node } diff --git a/packages/components/nodes/vectorstores/Meilisearch/core.ts b/packages/components/nodes/vectorstores/Meilisearch/core.ts new file mode 100644 index 00000000..7c1063a2 --- /dev/null +++ b/packages/components/nodes/vectorstores/Meilisearch/core.ts @@ -0,0 +1,92 @@ +import { BaseRetriever, type BaseRetrieverInput } from '@langchain/core/retrievers' +import { Document } from '@langchain/core/documents' +import { Meilisearch } from 'meilisearch' +import { Embeddings } from '@langchain/core/embeddings' + +export interface CustomRetrieverInput extends BaseRetrieverInput {} + +export class MeilisearchRetriever extends BaseRetriever { + lc_namespace = ['langchain', 'retrievers'] + private readonly meilisearchSearchApiKey: any + private readonly host: any + private indexUid: string + private K: string + private semanticRatio: string + private embeddings: Embeddings + constructor( + host: string, + meilisearchSearchApiKey: any, + indexUid: string, + K: string, + semanticRatio: string, + embeddings: Embeddings, + fields?: CustomRetrieverInput + ) { + super(fields) + this.meilisearchSearchApiKey = meilisearchSearchApiKey + this.host = host + this.indexUid = indexUid + this.embeddings = embeddings + + if (semanticRatio == '') { + this.semanticRatio = '0.5' + } else { + let semanticRatio_Float = parseFloat(semanticRatio) + if (semanticRatio_Float > 1.0) { + this.semanticRatio = '1.0' + } else if (semanticRatio_Float < 0.0) { + this.semanticRatio = '0.0' + } else { + this.semanticRatio = semanticRatio + } + } + + if (K == '') { + K = '4' + } + this.K = K + } + + async _getRelevantDocuments(query: string): Promise { + // Pass `runManager?.getChild()` when invoking internal runnables to enable tracing + // const additionalDocs = await someOtherRunnable.invoke(params, runManager?.getChild()) + const client = new Meilisearch({ + host: this.host, + apiKey: this.meilisearchSearchApiKey + }) + + const index = await client.index(this.indexUid) + const questionEmbedding = await this.embeddings.embedQuery(query) + // Perform the search + const searchResults = await index.search(query, { + vector: questionEmbedding, + limit: parseInt(this.K), // Optional: Limit the number of results + attributesToRetrieve: ['*'], // Optional: Specify which fields to retrieve + hybrid: { + semanticRatio: parseFloat(this.semanticRatio), + embedder: 'ollama' + } + }) + const hits = searchResults.hits + let documents: Document[] = [ + new Document({ + pageContent: 'mock page', + metadata: {} + }) + ] + try { + documents = hits.map( + (hit: any) => + new Document({ + pageContent: hit.pageContent, + metadata: { + objectID: hit.objectID + } + }) + ) + } catch (e) { + console.error('Error occurred while adding documents:', e) + } + return documents + } +} diff --git a/packages/components/package.json b/packages/components/package.json index e447cdf2..a548490e 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -94,6 +94,7 @@ "lodash": "^4.17.21", "lunary": "^0.6.16", "mammoth": "^1.5.1", + "meilisearch": "^0.41.0", "moment": "^2.29.3", "mongodb": "6.3.0", "mysql2": "^3.9.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29f5a088..29594ad7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -304,6 +304,9 @@ importers: mammoth: specifier: ^1.5.1 version: 1.7.0 + meilisearch: + specifier: ^0.41.0 + version: 0.41.0(encoding@0.1.13) moment: specifier: ^2.29.3 version: 2.30.1 @@ -11698,6 +11701,9 @@ packages: resolution: { integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== } engines: { node: '>= 0.6' } + meilisearch@0.41.0: + resolution: { integrity: sha512-5KcGLxEXD7E+uNO7R68rCbGSHgCqeM3Q3RFFLSsN7ZrIgr8HPDXVAIlP4LHggAZfk0FkSzo8VSXifHCwa2k80g== } + mem-fs-editor@9.7.0: resolution: { integrity: sha512-ReB3YD24GNykmu4WeUL/FDIQtkoyGB6zfJv60yfCo3QjKeimNcTqv2FT83bP0ccs6uu+sm5zyoBlspAzigmsdg== } engines: { node: '>=12.10.0' } @@ -31754,6 +31760,12 @@ snapshots: media-typer@0.3.0: {} + meilisearch@0.41.0(encoding@0.1.13): + dependencies: + cross-fetch: 3.1.8(encoding@0.1.13) + transitivePeerDependencies: + - encoding + mem-fs-editor@9.7.0(mem-fs@2.3.0): dependencies: binaryextensions: 4.19.0