Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions packages/components/nodes/vectorstores/Milvus/Milvus.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
62 changes: 58 additions & 4 deletions packages/components/nodes/vectorstores/Milvus/Milvus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -229,7 +266,14 @@ 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)
vectorStore.indexCreateParams = {
...vectorStore.indexCreateParams,
metric_type: metricType
}
await vectorStore.addDocuments(finalDocs)

// Avoid Illegal Invocation
vectorStore.similaritySearchVectorWithScore = async (query: number[], k: number, filter?: string) => {
Expand Down Expand Up @@ -276,6 +320,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,
Expand Down Expand Up @@ -308,6 +355,13 @@ 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.
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)
Expand Down Expand Up @@ -471,7 +525,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 as MetricType) ?? MetricType.L2
})
if (resp.error_code !== ErrorCode.SUCCESS) {
throw new Error(`Error creating index`)
Expand All @@ -491,4 +545,4 @@ class MilvusUpsert extends Milvus {
}
}

module.exports = { nodeClass: Milvus_VectorStores }
module.exports = { nodeClass: Milvus_VectorStores, resolveMilvusMetricType }