Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeOutputsValue, INodeParams } from '../../../src/Interface'
import { applyMatryoshkaTruncation, MATRYOSHKA_TRUNCATE_DIMENSIONS } from '../../../src/matryoshkaEmbeddings'
import { DataSource } from 'typeorm'

class DocStore_VectorStores implements INode {
Expand Down Expand Up @@ -118,24 +119,28 @@ class DocStore_VectorStores implements INode {
const _createEmbeddingsObject = async (componentNodes: ICommonObject, data: ICommonObject, options: ICommonObject): Promise<any> => {
// prepare embedding node data
const embeddingComponent = componentNodes[data.embeddingName]
const embeddingConfig = { ...data.embeddingConfig }
const matryoshkaTruncateDimensions = embeddingConfig[MATRYOSHKA_TRUNCATE_DIMENSIONS]
delete embeddingConfig[MATRYOSHKA_TRUNCATE_DIMENSIONS]
const embeddingNodeData: any = {
inputs: { ...data.embeddingConfig },
inputs: embeddingConfig,
outputs: { output: 'document' },
id: `${embeddingComponent.name}_0`,
label: embeddingComponent.label,
name: embeddingComponent.name,
category: embeddingComponent.category,
inputParams: embeddingComponent.inputs || []
}
if (data.embeddingConfig.credential) {
embeddingNodeData.credential = data.embeddingConfig.credential
if (embeddingConfig.credential) {
embeddingNodeData.credential = embeddingConfig.credential
}

// init embedding object
const embeddingNodeInstanceFilePath = embeddingComponent.filePath as string
const embeddingNodeModule = await import(embeddingNodeInstanceFilePath)
const embeddingNodeInstance = new embeddingNodeModule.nodeClass()
return await embeddingNodeInstance.init(embeddingNodeData, '', options)
const embeddingObj = await embeddingNodeInstance.init(embeddingNodeData, '', options)
return applyMatryoshkaTruncation(embeddingObj, matryoshkaTruncateDimensions)
}

const _createVectorStoreNodeData = (componentNodes: ICommonObject, data: ICommonObject, embeddingObj: any) => {
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './handler'
export * from './headerValidation'
export * from './httpSecurity'
export * from './Interface'
export * from './matryoshkaEmbeddings'
export * from './recordManagerSecurity'
export * from './sanitizeDataSourceOptions'
export * from './speechToText'
Expand Down
37 changes: 37 additions & 0 deletions packages/components/src/matryoshkaEmbeddings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { EmbeddingsInterface } from '@langchain/core/embeddings'
import { applyMatryoshkaTruncation } from './matryoshkaEmbeddings'

const makeEmbeddings = (): EmbeddingsInterface<number[]> => ({
embedDocuments: jest.fn(async () => [
[1, 2, 3, 4],
[5, 6, 7, 8]
]),
embedQuery: jest.fn(async () => [9, 10, 11, 12])
})

describe('applyMatryoshkaTruncation', () => {
it('returns the original embeddings when no truncate dimension is configured', async () => {
const embeddings = makeEmbeddings()
const result = applyMatryoshkaTruncation(embeddings, undefined)

expect(result).toBe(embeddings)
await expect(result.embedQuery('hello')).resolves.toEqual([9, 10, 11, 12])
})

it('truncates document and query embeddings to the configured dimension', async () => {
const embeddings = makeEmbeddings()
const result = applyMatryoshkaTruncation(embeddings, '2')

await expect(result.embedDocuments(['a', 'b'])).resolves.toEqual([
[1, 2],
[5, 6]
])
await expect(result.embedQuery('hello')).resolves.toEqual([9, 10])
})

it('rejects invalid truncate dimensions', () => {
expect(() => applyMatryoshkaTruncation(makeEmbeddings(), '0')).toThrow('Matryoshka truncate dimension must be a positive integer')
expect(() => applyMatryoshkaTruncation(makeEmbeddings(), '2.5')).toThrow('Matryoshka truncate dimension must be a positive integer')
expect(() => applyMatryoshkaTruncation(makeEmbeddings(), 'abc')).toThrow('Matryoshka truncate dimension must be a positive integer')
})
})
35 changes: 35 additions & 0 deletions packages/components/src/matryoshkaEmbeddings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { EmbeddingsInterface } from '@langchain/core/embeddings'

export const MATRYOSHKA_TRUNCATE_DIMENSIONS = 'matryoshkaTruncateDimensions'

const parseTruncateDimension = (value: unknown): number | undefined => {
if (value === undefined || value === null || value === '') return undefined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

According to the general rules, loose equality (== null) should be used as a standard idiom for a 'nullish' check that covers both null and undefined.

Suggested change
if (value === undefined || value === null || value === '') return undefined
if (value == null || value === '') return undefined
References
  1. In JavaScript/TypeScript, use loose equality (== null) as a standard idiom for a 'nullish' check that covers both 'null' and 'undefined'.


const dimension = typeof value === 'number' ? value : Number(value)
if (!Number.isInteger(dimension) || dimension <= 0) {
throw new Error('Matryoshka truncate dimension must be a positive integer')
}

return dimension
}

const truncateVector = (vector: number[], dimension: number): number[] => vector.slice(0, dimension)

export const applyMatryoshkaTruncation = <T extends EmbeddingsInterface<number[]>>(embeddings: T, truncateDimensions: unknown): T => {
const dimension = parseTruncateDimension(truncateDimensions)
if (!dimension) return embeddings

const wrappedEmbeddings = Object.create(embeddings) as T

wrappedEmbeddings.embedDocuments = async (documents: string[]): Promise<number[][]> => {
const vectors = await embeddings.embedDocuments(documents)
return vectors.map((vector) => truncateVector(vector, dimension))
}

wrappedEmbeddings.embedQuery = async (document: string): Promise<number[]> => {
const vector = await embeddings.embedQuery(document)
return truncateVector(vector, dimension)
}

return wrappedEmbeddings
Comment on lines +22 to +34

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Using Object.create(embeddings) to wrap the embeddings instance can cause runtime crashes if the underlying class uses native JavaScript private fields (e.g., #apiKey). Any non-overridden method called on the prototype-derived wrappedEmbeddings object that accesses these private fields will throw a TypeError: Cannot read private member from an object whose class did not declare it.

Since the embedding instance is created fresh for each operation and is not shared globally, directly monkey-patching the methods on the original embeddings instance is a much safer and robust approach that preserves the this context and private field access.

    const originalEmbedDocuments = embeddings.embedDocuments.bind(embeddings)
    const originalEmbedQuery = embeddings.embedQuery.bind(embeddings)

    embeddings.embedDocuments = async (documents: string[]): Promise<number[][]> => {
        const vectors = await originalEmbedDocuments(documents)
        return vectors.map((vector) => truncateVector(vector, dimension))
    }

    embeddings.embedQuery = async (document: string): Promise<number[]> => {
        const vector = await originalEmbedQuery(document)
        return truncateVector(vector, dimension)
    }

    return embeddings

}
31 changes: 26 additions & 5 deletions packages/server/src/services/documentstore/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
getFileFromUpload,
ICommonObject,
IDocument,
applyMatryoshkaTruncation,
MATRYOSHKA_TRUNCATE_DIMENSIONS,
mapExtToInputField,
mapMimeTypeToInputField,
removeFilesFromStorage,
Expand Down Expand Up @@ -1583,21 +1585,40 @@ const _createEmbeddingsObject = async (
): Promise<any> => {
// prepare embedding node data
const embeddingComponent = componentNodes[data.embeddingName]
const embeddingConfig = { ...data.embeddingConfig }
const matryoshkaTruncateDimensions = embeddingConfig[MATRYOSHKA_TRUNCATE_DIMENSIONS]
delete embeddingConfig[MATRYOSHKA_TRUNCATE_DIMENSIONS]
const embeddingNodeData: any = {
inputs: { ...data.embeddingConfig },
inputs: embeddingConfig,
outputs: { output: 'document' },
id: `${embeddingComponent.name}_0`,
label: embeddingComponent.label,
name: embeddingComponent.name,
category: embeddingComponent.category,
inputParams: embeddingComponent.inputs || []
}
if (data.embeddingConfig.credential) {
embeddingNodeData.credential = data.embeddingConfig.credential
if (embeddingConfig.credential) {
embeddingNodeData.credential = embeddingConfig.credential
}

// save to upsert history
if (upsertHistory) upsertHistory['flowData'] = saveUpsertFlowData(embeddingNodeData, upsertHistory)
if (upsertHistory) {
const embeddingHistoryNodeData = matryoshkaTruncateDimensions
? {
...embeddingNodeData,
inputs: { ...embeddingConfig, [MATRYOSHKA_TRUNCATE_DIMENSIONS]: matryoshkaTruncateDimensions },
inputParams: [
...embeddingNodeData.inputParams,
{
label: 'Matryoshka Truncate Dimensions',
name: MATRYOSHKA_TRUNCATE_DIMENSIONS,
type: 'number'
}
]
}
: embeddingNodeData
upsertHistory['flowData'] = saveUpsertFlowData(embeddingHistoryNodeData, upsertHistory)
}

// init embedding object
const embeddingNodeInstanceFilePath = embeddingComponent.filePath as string
Expand All @@ -1607,7 +1628,7 @@ const _createEmbeddingsObject = async (
if (!embeddingObj) {
throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Failed to create EmbeddingObj`)
}
return embeddingObj
return applyMatryoshkaTruncation(embeddingObj, matryoshkaTruncateDimensions)
}

const _createRecordManagerObject = async (
Expand Down
17 changes: 16 additions & 1 deletion packages/ui/src/views/docstore/VectorStoreConfigure.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ import useNotifier from '@/utils/useNotifier'

// const
const steps = ['Embeddings', 'Vector Store', 'Record Manager']
const matryoshkaTruncateDimensionsInput = {
label: 'Matryoshka Truncate Dimensions',
name: 'matryoshkaTruncateDimensions',
type: 'number',
optional: true,
additionalParams: true,
description: 'Optionally keep only the first N dimensions from document and query embeddings.'
}

const addMatryoshkaInput = (nodeData) => {
if (!nodeData.inputParams?.some((inputParam) => inputParam.name === matryoshkaTruncateDimensionsInput.name)) {
nodeData.inputParams = [...(nodeData.inputParams || []), matryoshkaTruncateDimensionsInput]
}
return nodeData
}

const VectorStoreConfigure = () => {
const navigate = useNavigate()
Expand Down Expand Up @@ -123,7 +138,7 @@ const VectorStoreConfigure = () => {
nodeData.inputs = documentStore.embeddingConfig.config
nodeData.credential = documentStore.embeddingConfig.config.credential
}
setSelectedEmbeddingsProvider(nodeData)
setSelectedEmbeddingsProvider(addMatryoshkaInput(nodeData))
setShowEmbeddingsListDialog(false)
}

Expand Down