From 4dadd237031e7667c5e43b6d40e278d7d24e6bfb Mon Sep 17 00:00:00 2001 From: gaurav0107 Date: Tue, 30 Jun 2026 02:26:53 +0530 Subject: [PATCH 1/2] feat(milvus): make distance metric type configurable The Milvus node hardcoded the index metric_type to L2 when creating the collection index on upsert, while the similarity-search path already read the metric from indexCreateParams. As a result, collections using COSINE or IP could not be created from Flowise and queries against them failed with metric-type mismatch errors. Add a 'Metric Type' option (L2 / COSINE / IP, default L2) and thread the selected metric into index creation on upsert and into the search-side indexCreateParams so score normalization matches. L2 remains the default, so existing flows are unchanged. Closes #6033 --- .../nodes/vectorstores/Milvus/Milvus.test.ts | 58 ++++++++++++++++++ .../nodes/vectorstores/Milvus/Milvus.ts | 60 +++++++++++++++++-- 2 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 packages/components/nodes/vectorstores/Milvus/Milvus.test.ts diff --git a/packages/components/nodes/vectorstores/Milvus/Milvus.test.ts b/packages/components/nodes/vectorstores/Milvus/Milvus.test.ts new file mode 100644 index 00000000000..450688578a7 --- /dev/null +++ b/packages/components/nodes/vectorstores/Milvus/Milvus.test.ts @@ -0,0 +1,58 @@ +import { MetricType } from '@zilliz/milvus2-sdk-node' + +// getBaseClasses(Milvus) is called in the node constructor; stub the utils module +// so the test does not pull the full src/utils dependency graph. +jest.mock('../../../src/utils', () => ({ + getBaseClasses: jest.fn(() => ['Milvus', 'VectorStore']), + getCredentialData: jest.fn(), + getCredentialParam: jest.fn(), + FLOWISE_CHATID: 'chatId' +})) + +const { nodeClass: Milvus_VectorStores, resolveMilvusMetricType } = require('./Milvus') + +describe('Milvus resolveMilvusMetricType', () => { + it('maps L2 (any case) to MetricType.L2', () => { + expect(resolveMilvusMetricType('L2')).toBe(MetricType.L2) + expect(resolveMilvusMetricType('l2')).toBe(MetricType.L2) + }) + + it('maps COSINE (any case) to MetricType.COSINE', () => { + expect(resolveMilvusMetricType('COSINE')).toBe(MetricType.COSINE) + expect(resolveMilvusMetricType('cosine')).toBe(MetricType.COSINE) + expect(resolveMilvusMetricType(' Cosine ')).toBe(MetricType.COSINE) + }) + + it('maps IP (any case) to MetricType.IP', () => { + expect(resolveMilvusMetricType('IP')).toBe(MetricType.IP) + expect(resolveMilvusMetricType('ip')).toBe(MetricType.IP) + }) + + it('falls back to L2 for empty, undefined, or unknown input (backward compatible)', () => { + expect(resolveMilvusMetricType(undefined)).toBe(MetricType.L2) + expect(resolveMilvusMetricType('')).toBe(MetricType.L2) + expect(resolveMilvusMetricType('HAMMING')).toBe(MetricType.L2) + }) +}) + +describe('Milvus node Metric Type input', () => { + let node: any + + beforeEach(() => { + node = new Milvus_VectorStores() + }) + + it('exposes a metricType options input with L2/COSINE/IP and an L2 default', () => { + const input = node.inputs.find((i: any) => i.name === 'metricType') + expect(input).toBeDefined() + expect(input.type).toBe('options') + expect(input.default).toBe('L2') + const optionNames = input.options.map((o: any) => o.name) + expect(optionNames).toEqual(['L2', 'COSINE', 'IP']) + }) + + it('keeps the metricType input optional so existing flows are unaffected', () => { + const input = node.inputs.find((i: any) => i.name === 'metricType') + expect(input.optional).toBe(true) + }) +}) diff --git a/packages/components/nodes/vectorstores/Milvus/Milvus.ts b/packages/components/nodes/vectorstores/Milvus/Milvus.ts index 527d57bf5d2..17a8f44d5eb 100644 --- a/packages/components/nodes/vectorstores/Milvus/Milvus.ts +++ b/packages/components/nodes/vectorstores/Milvus/Milvus.ts @@ -11,6 +11,25 @@ interface InsertRow { [x: string]: string | number[] } +/** + * Resolve a user-provided metric type string to a Milvus metric type. + * Accepts the values exposed by the node's "Metric Type" input (L2, COSINE, IP), + * case-insensitively, and falls back to L2 for empty or unrecognized input so + * existing flows keep their previous behavior. Returns the string-literal union + * used by @langchain/community's Milvus `indexCreateParams.metric_type`. + */ +const resolveMilvusMetricType = (value?: string): 'L2' | 'COSINE' | 'IP' => { + switch ((value ?? '').trim().toUpperCase()) { + case 'COSINE': + return 'COSINE' + case 'IP': + return 'IP' + case 'L2': + default: + return 'L2' + } +} + class Milvus_VectorStores implements INode { label: string name: string @@ -28,7 +47,7 @@ class Milvus_VectorStores implements INode { constructor() { this.label = 'Milvus' this.name = 'milvus' - this.version = 2.1 + this.version = 2.2 this.type = 'Milvus' this.icon = 'milvus.svg' this.category = 'Vector Stores' @@ -112,6 +131,21 @@ class Milvus_VectorStores implements INode { additionalParams: true, optional: true }, + { + label: 'Metric Type', + name: 'metricType', + description: + 'Distance metric used when creating the index on upsert and when querying. Must match the metric the Milvus collection was created with. Defaults to L2.', + type: 'options', + options: [ + { label: 'L2 (Euclidean)', name: 'L2' }, + { label: 'Cosine Similarity', name: 'COSINE' }, + { label: 'Inner Product (IP)', name: 'IP' } + ], + default: 'L2', + additionalParams: true, + optional: true + }, { label: 'Secure', name: 'secure', @@ -194,6 +228,9 @@ class Milvus_VectorStores implements INode { // partition const partitionName = nodeData.inputs?.milvusPartition ?? '_default' + // metric type + const metricType = resolveMilvusMetricType(nodeData.inputs?.metricType as string) + // init MilvusLibArgs const milVusArgs: MilvusLibArgs = { url: address, @@ -229,7 +266,13 @@ class Milvus_VectorStores implements INode { } try { - const vectorStore = await MilvusUpsert.fromDocuments(finalDocs, embeddings, milVusArgs) + // Mirror MilvusUpsert.fromDocuments, but inject the configured metric type + // into indexCreateParams before the index is created on first upsert. + const vectorStore = new MilvusUpsert(embeddings, milVusArgs) + if (vectorStore.indexCreateParams) { + vectorStore.indexCreateParams.metric_type = metricType + } + await vectorStore.addDocuments(finalDocs) // Avoid Illegal Invocation vectorStore.similaritySearchVectorWithScore = async (query: number[], k: number, filter?: string) => { @@ -276,6 +319,9 @@ class Milvus_VectorStores implements INode { // partition const partitionName = nodeData.inputs?.milvusPartition ?? '_default' + // metric type + const metricType = resolveMilvusMetricType(nodeData.inputs?.metricType as string) + // init MilvusLibArgs const milVusArgs: MilvusLibArgs = { url: address, @@ -308,6 +354,12 @@ class Milvus_VectorStores implements INode { const vectorStore = await Milvus.fromExistingCollection(embeddings, milVusArgs) + // Ensure the search path uses the user-selected metric (and its matching score + // normalization) instead of the LangChain default of L2. + if (vectorStore.indexCreateParams) { + vectorStore.indexCreateParams.metric_type = metricType + } + // Avoid Illegal Invocation vectorStore.similaritySearchVectorWithScore = async (query: number[], k: number, filter?: string) => { return await similaritySearchVectorWithScore(query, k, vectorStore, milvusFilter, filter) @@ -471,7 +523,7 @@ class MilvusUpsert extends Milvus { field_name: this.vectorField, index_name: `myindex_${Date.now().toString()}`, index_type: IndexType.AUTOINDEX, - metric_type: MetricType.L2 + metric_type: this.indexCreateParams?.metric_type ?? MetricType.L2 }) if (resp.error_code !== ErrorCode.SUCCESS) { throw new Error(`Error creating index`) @@ -491,4 +543,4 @@ class MilvusUpsert extends Milvus { } } -module.exports = { nodeClass: Milvus_VectorStores } +module.exports = { nodeClass: Milvus_VectorStores, resolveMilvusMetricType } From 5e153b0c35258e84a3e641eea0908d4c9dcfd1ff Mon Sep 17 00:00:00 2001 From: gaurav0107 Date: Tue, 30 Jun 2026 02:36:00 +0530 Subject: [PATCH 2/2] refactor(milvus): set indexCreateParams via spread and cast metric type Address review feedback: build indexCreateParams with object spread so the selected metric type is always applied even if the property is initially undefined (instead of being silently dropped by a truthiness guard), and cast the metric to MetricType at the createIndex call site for strict type-checking configurations. --- .../components/nodes/vectorstores/Milvus/Milvus.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/components/nodes/vectorstores/Milvus/Milvus.ts b/packages/components/nodes/vectorstores/Milvus/Milvus.ts index 17a8f44d5eb..7ecbfbb5a1c 100644 --- a/packages/components/nodes/vectorstores/Milvus/Milvus.ts +++ b/packages/components/nodes/vectorstores/Milvus/Milvus.ts @@ -269,8 +269,9 @@ class Milvus_VectorStores implements INode { // Mirror MilvusUpsert.fromDocuments, but inject the configured metric type // into indexCreateParams before the index is created on first upsert. const vectorStore = new MilvusUpsert(embeddings, milVusArgs) - if (vectorStore.indexCreateParams) { - vectorStore.indexCreateParams.metric_type = metricType + vectorStore.indexCreateParams = { + ...vectorStore.indexCreateParams, + metric_type: metricType } await vectorStore.addDocuments(finalDocs) @@ -356,8 +357,9 @@ class Milvus_VectorStores implements INode { // Ensure the search path uses the user-selected metric (and its matching score // normalization) instead of the LangChain default of L2. - if (vectorStore.indexCreateParams) { - vectorStore.indexCreateParams.metric_type = metricType + vectorStore.indexCreateParams = { + ...vectorStore.indexCreateParams, + metric_type: metricType } // Avoid Illegal Invocation @@ -523,7 +525,7 @@ class MilvusUpsert extends Milvus { field_name: this.vectorField, index_name: `myindex_${Date.now().toString()}`, index_type: IndexType.AUTOINDEX, - metric_type: this.indexCreateParams?.metric_type ?? MetricType.L2 + metric_type: (this.indexCreateParams?.metric_type as MetricType) ?? MetricType.L2 }) if (resp.error_code !== ErrorCode.SUCCESS) { throw new Error(`Error creating index`)