feat(core): Add a searchByText opt-in to createVectorStoreNode (no-changelog) (#39027)

This commit is contained in:
Yen Su 2026-09-21 07:25:39 +00:00 committed by GitHub
parent a378ba34cd
commit fb66e3f37f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 117 additions and 41 deletions

View File

@ -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
}
```

View File

@ -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);

View File

@ -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' };

View File

@ -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;

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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.