mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
feat(core): Add a searchByText opt-in to createVectorStoreNode (no-changelog) (#39027)
This commit is contained in:
parent
a378ba34cd
commit
fb66e3f37f
|
|
@ -125,6 +125,7 @@ interface VectorStoreNodeConstructorArgs<T extends VectorStore> {
|
|||
populateVectorStore: Function; // Store documents in vector store (accepts batches in v1.1+)
|
||||
getVectorStoreClient: Function; // Get vector store instance
|
||||
releaseVectorStoreClient?: Function; // Clean up resources
|
||||
searchByText?: true; // Text-search modes call similaritySearchWithScore and skip embedQuery
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -109,6 +109,32 @@ describe('handleLoadOperation', () => {
|
|||
expect(result).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should search by text when searchByText is set', async () => {
|
||||
mockVectorStore.similaritySearchWithScore.mockResolvedValue([
|
||||
[{ pageContent: 'test content 1', metadata: { test: 'metadata 1' } } as Document, 0.95],
|
||||
[{ pageContent: 'test content 2', metadata: { test: 'metadata 2' } } as Document, 0.85],
|
||||
[{ pageContent: 'test content 3', metadata: { test: 'metadata 3' } } as Document, 0.75],
|
||||
]);
|
||||
|
||||
const result = await handleLoadOperation(
|
||||
mockContext,
|
||||
{ ...mockArgs, searchByText: true },
|
||||
mockEmbeddings,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockVectorStore.similaritySearchWithScore).toHaveBeenCalledWith('test search query', 3, {
|
||||
testFilter: 'value',
|
||||
});
|
||||
expect(mockEmbeddings.embedQuery).not.toHaveBeenCalled();
|
||||
expect(mockVectorStore.similaritySearchVectorWithScore).not.toHaveBeenCalled();
|
||||
expect(result.map((item) => (item.json.document as IDataObject).pageContent)).toEqual([
|
||||
'test content 1',
|
||||
'test content 2',
|
||||
'test content 3',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should include document metadata when includeDocumentMetadata is true', async () => {
|
||||
const result = await handleLoadOperation(mockContext, mockArgs, mockEmbeddings, 0);
|
||||
|
||||
|
|
|
|||
|
|
@ -141,6 +141,33 @@ describe('handleRetrieveAsToolExecuteOperation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should search by text when searchByText is set', async () => {
|
||||
mockVectorStore.similaritySearchWithScore.mockResolvedValue([
|
||||
[{ pageContent: 'test content 1', metadata: { test: 'metadata 1' } } as Document, 0.95],
|
||||
[{ pageContent: 'test content 2', metadata: { test: 'metadata 2' } } as Document, 0.85],
|
||||
[{ pageContent: 'test content 3', metadata: { test: 'metadata 3' } } as Document, 0.75],
|
||||
]);
|
||||
|
||||
const result = await handleRetrieveAsToolExecuteOperation(
|
||||
mockContext,
|
||||
{ ...mockArgs, searchByText: true },
|
||||
mockEmbeddings,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(mockVectorStore.similaritySearchWithScore).toHaveBeenCalledWith('test search query', 3, {
|
||||
testFilter: 'value',
|
||||
});
|
||||
expect(mockEmbeddings.embedQuery).not.toHaveBeenCalled();
|
||||
expect(mockVectorStore.similaritySearchVectorWithScore).not.toHaveBeenCalled();
|
||||
const response = result[0].json.response as Array<{ type: string; text: string }>;
|
||||
expect(response.map((entry) => JSON.parse(entry.text).pageContent)).toEqual([
|
||||
'test content 1',
|
||||
'test content 2',
|
||||
'test content 3',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw error when input data does not contain query', async () => {
|
||||
inputData[0].json = { notQuery: 'some value' };
|
||||
|
||||
|
|
|
|||
|
|
@ -180,6 +180,34 @@ describe('handleRetrieveAsToolOperation', () => {
|
|||
expect(mockArgs.releaseVectorStoreClient).toHaveBeenCalledWith(mockVectorStore);
|
||||
});
|
||||
|
||||
it('should search by text when searchByText is set', async () => {
|
||||
mockVectorStore.similaritySearchWithScore.mockResolvedValue([
|
||||
[{ pageContent: 'test content 1', metadata: { test: 'metadata 1' } } as Document, 0.95],
|
||||
[{ pageContent: 'test content 2', metadata: { test: 'metadata 2' } } as Document, 0.85],
|
||||
[{ pageContent: 'test content 3', metadata: { test: 'metadata 3' } } as Document, 0.75],
|
||||
]);
|
||||
const result = await handleRetrieveAsToolOperation(
|
||||
mockContext,
|
||||
{ ...mockArgs, searchByText: true },
|
||||
mockEmbeddings,
|
||||
0,
|
||||
);
|
||||
const tool = result.response as DynamicTool;
|
||||
|
||||
const toolResult = await tool.invoke({ input: 'test query' });
|
||||
|
||||
expect(mockVectorStore.similaritySearchWithScore).toHaveBeenCalledWith('test query', 3, {
|
||||
testFilter: 'value',
|
||||
});
|
||||
expect(mockEmbeddings.embedQuery).not.toHaveBeenCalled();
|
||||
expect(mockVectorStore.similaritySearchVectorWithScore).not.toHaveBeenCalled();
|
||||
expect(toolResult).toHaveLength(3);
|
||||
expect(JSON.parse(toolResult[0].text)).toEqual({
|
||||
pageContent: 'test content 1',
|
||||
metadata: { test: 'metadata 1' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should include metadata in results when includeDocumentMetadata is true', async () => {
|
||||
const result = await handleRetrieveAsToolOperation(mockContext, mockArgs, mockEmbeddings, 0);
|
||||
const tool = result.response as DynamicTool;
|
||||
|
|
|
|||
|
|
@ -39,11 +39,15 @@ export async function handleLoadOperation<T extends VectorStore = VectorStore>(
|
|||
true,
|
||||
) as boolean;
|
||||
|
||||
// Embed the prompt to prepare for vector similarity search
|
||||
const embeddedPrompt = await embeddings.embedQuery(prompt);
|
||||
|
||||
// Get the most similar documents to the embedded prompt
|
||||
let docs = await vectorStore.similaritySearchVectorWithScore(embeddedPrompt, topK, filter);
|
||||
// Get the most similar documents to the prompt
|
||||
let docs =
|
||||
args.searchByText === true
|
||||
? await vectorStore.similaritySearchWithScore(prompt, topK, filter)
|
||||
: await vectorStore.similaritySearchVectorWithScore(
|
||||
await embeddings.embedQuery(prompt),
|
||||
topK,
|
||||
filter,
|
||||
);
|
||||
|
||||
// If reranker is used, rerank the documents
|
||||
if (useReranker && docs.length > 0) {
|
||||
|
|
|
|||
|
|
@ -50,11 +50,15 @@ export async function handleRetrieveAsToolExecuteOperation<T extends VectorStore
|
|||
);
|
||||
assertParamIsBoolean('includeDocumentMetadata', includeDocumentMetadata, context.getNode());
|
||||
|
||||
// Embed the query to prepare for vector similarity search
|
||||
const embeddedQuery = await embeddings.embedQuery(query);
|
||||
|
||||
// Get the most similar documents to the embedded query
|
||||
let docs = await vectorStore.similaritySearchVectorWithScore(embeddedQuery, topK, filter);
|
||||
// Get the most similar documents to the query
|
||||
let docs =
|
||||
args.searchByText === true
|
||||
? await vectorStore.similaritySearchWithScore(query, topK, filter)
|
||||
: await vectorStore.similaritySearchVectorWithScore(
|
||||
await embeddings.embedQuery(query),
|
||||
topK,
|
||||
filter,
|
||||
);
|
||||
|
||||
// If reranker is used, rerank the documents
|
||||
if (useReranker && docs.length > 0) {
|
||||
|
|
|
|||
|
|
@ -61,15 +61,15 @@ export async function handleRetrieveAsToolOperation<T extends VectorStore = Vect
|
|||
);
|
||||
|
||||
try {
|
||||
// Embed the input query
|
||||
const embeddedPrompt = await embeddings.embedQuery(queryString);
|
||||
|
||||
// Search for similar documents
|
||||
let documents = await vectorStore.similaritySearchVectorWithScore(
|
||||
embeddedPrompt,
|
||||
topK,
|
||||
filter,
|
||||
);
|
||||
let documents =
|
||||
args.searchByText === true
|
||||
? await vectorStore.similaritySearchWithScore(queryString, topK, filter)
|
||||
: await vectorStore.similaritySearchVectorWithScore(
|
||||
await embeddings.embedQuery(queryString),
|
||||
topK,
|
||||
filter,
|
||||
);
|
||||
|
||||
// If reranker is used, rerank the documents
|
||||
if (useReranker && documents.length > 0) {
|
||||
|
|
|
|||
|
|
@ -2,17 +2,13 @@ import type { Document } from '@langchain/core/documents';
|
|||
import type { Embeddings } from '@langchain/core/embeddings';
|
||||
import type { VectorStore } from '@langchain/core/vectorstores';
|
||||
import type {
|
||||
ICredentialTestFunction,
|
||||
IExecuteFunctions,
|
||||
INodeCredentialDescription,
|
||||
INodeProperties,
|
||||
ILoadOptionsFunctions,
|
||||
INodeListSearchResult,
|
||||
INodeType,
|
||||
Icon,
|
||||
ISupplyDataFunctions,
|
||||
ThemeIconColor,
|
||||
IDataObject,
|
||||
NodeParameterValueType,
|
||||
IBuilderHint,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
|
@ -35,24 +31,7 @@ export interface NodeMeta {
|
|||
|
||||
export interface VectorStoreNodeConstructorArgs<T extends VectorStore = VectorStore> {
|
||||
meta: NodeMeta;
|
||||
methods?: {
|
||||
listSearch?: {
|
||||
[key: string]: (
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
) => Promise<INodeListSearchResult>;
|
||||
};
|
||||
actionHandler?: {
|
||||
[functionName: string]: (
|
||||
this: ILoadOptionsFunctions,
|
||||
payload: IDataObject | string | undefined,
|
||||
) => Promise<NodeParameterValueType>;
|
||||
};
|
||||
credentialTest?: {
|
||||
[functionName: string]: ICredentialTestFunction;
|
||||
};
|
||||
};
|
||||
methods?: INodeType['methods'];
|
||||
|
||||
sharedFields: INodeProperties[];
|
||||
insertFields?: INodeProperties[];
|
||||
|
|
@ -60,6 +39,13 @@ export interface VectorStoreNodeConstructorArgs<T extends VectorStore = VectorSt
|
|||
retrieveFields?: INodeProperties[];
|
||||
updateFields?: INodeProperties[];
|
||||
hidden?: true;
|
||||
/**
|
||||
* The text-search operations call `similaritySearchWithScore(prompt, k, filter)` instead of
|
||||
* embedding the prompt and calling `similaritySearchVectorWithScore`. The store must override
|
||||
* `similaritySearchWithScore`: the base class version calls `embeddings.embedQuery` first,
|
||||
* which a backend that embeds server-side cannot do.
|
||||
*/
|
||||
searchByText?: true;
|
||||
|
||||
/**
|
||||
* Optional function called once before any documents are inserted.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user