Skip to content

Commit 981395f

Browse files
committed
change(knowledge): make vector the default search mode, hybrid opt-in
Every existing caller — workflow block, v1 API, copilot, guardrail RAG — keeps its current ranking. Hybrid retrieval is now requested explicitly via `searchMode: 'hybrid'`. Also routes the copilot knowledge tool through the shared `executeKnowledgeSearch` dispatch so all four callers share one retrieval path, and documents `searchMode` on the public v1 search endpoint in the OpenAPI spec.
1 parent 3a6edaf commit 981395f

9 files changed

Lines changed: 38 additions & 64 deletions

File tree

apps/docs/openapi.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6040,7 +6040,7 @@
60406040
"post": {
60416041
"operationId": "searchKnowledgeBase",
60426042
"summary": "Search Knowledge Base",
6043-
"description": "Perform vector similarity search across one or more knowledge bases. Supports semantic search via query text, tag-based filtering, or a combination of both.",
6043+
"description": "Search across one or more knowledge bases. Supports semantic search via query text, tag-based filtering, or a combination of both. Set `searchMode` to `hybrid` to additionally run a full-text keyword leg and fuse it with the semantic results.",
60446044
"tags": ["Knowledge Bases"],
60456045
"x-codeSamples": [
60466046
{
@@ -6095,14 +6095,21 @@
60956095
"items": {
60966096
"$ref": "#/components/schemas/TagFilter"
60976097
}
6098+
},
6099+
"searchMode": {
6100+
"type": "string",
6101+
"enum": ["vector", "hybrid"],
6102+
"default": "vector",
6103+
"description": "Retrieval strategy. `vector` ranks purely on embedding similarity. `hybrid` also runs a full-text keyword search and fuses the two rankings by reciprocal rank, which retrieves exact tokens — error codes, ticket keys, identifiers, rare product names — that embeddings alone rank poorly. Ignored when only tagFilters are provided."
60986104
}
60996105
}
61006106
},
61016107
"example": {
61026108
"workspaceId": "wsp_abc123",
61036109
"knowledgeBaseIds": ["d2c8f4a6-1b3e-4c5d-9e7f-8a0b2c4d6e1f"],
61046110
"query": "How do I reset my password?",
6105-
"topK": 5
6111+
"topK": 5,
6112+
"searchMode": "hybrid"
61066113
}
61076114
}
61086115
}

apps/sim/app/api/knowledge/search/route.test.ts

Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,11 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vites
2222
const {
2323
mockGetDocumentTagDefinitions,
2424
mockExecuteKnowledgeSearch,
25-
mockGetQueryStrategy,
2625
mockGenerateSearchEmbedding,
2726
mockGetDocumentMetadataByIds,
2827
} = vi.hoisted(() => ({
2928
mockGetDocumentTagDefinitions: vi.fn(),
3029
mockExecuteKnowledgeSearch: vi.fn(),
31-
mockGetQueryStrategy: vi.fn(),
3230
mockGenerateSearchEmbedding: vi.fn(),
3331
mockGetDocumentMetadataByIds: vi.fn(),
3432
}))
@@ -66,7 +64,6 @@ vi.mock('@/lib/knowledge/tags/service', () => ({
6664

6765
vi.mock('./utils', () => ({
6866
executeKnowledgeSearch: mockExecuteKnowledgeSearch,
69-
getQueryStrategy: mockGetQueryStrategy,
7067
generateSearchEmbedding: mockGenerateSearchEmbedding,
7168
getDocumentMetadataByIds: mockGetDocumentMetadataByIds,
7269
APIError: class APIError extends Error {
@@ -113,12 +110,6 @@ describe('Knowledge Search API Route', () => {
113110
setEnv({ OPENAI_API_KEY: 'test-api-key' })
114111

115112
mockExecuteKnowledgeSearch.mockClear()
116-
mockGetQueryStrategy.mockClear().mockReturnValue({
117-
useParallel: false,
118-
distanceThreshold: 1.0,
119-
parallelLimit: 15,
120-
singleQueryOptimized: true,
121-
})
122113
mockGenerateSearchEmbedding
123114
.mockClear()
124115
.mockResolvedValue({ embedding: [0.1, 0.2, 0.3, 0.4, 0.5], isBYOK: false })
@@ -207,14 +198,14 @@ describe('Knowledge Search API Route', () => {
207198
expect(mockExecuteKnowledgeSearch).toHaveBeenCalledWith({
208199
knowledgeBaseIds: ['kb-123'],
209200
topK: 10,
210-
searchMode: 'hybrid',
201+
searchMode: 'vector',
211202
query: validSearchData.query,
212203
queryVector: JSON.stringify(mockEmbedding),
213204
structuredFilters: undefined,
214205
})
215206
})
216207

217-
it('should forward the searchMode opt-out to the retrieval layer', async () => {
208+
it('should forward the hybrid searchMode opt-in to the retrieval layer', async () => {
218209
mockGetUserId.mockResolvedValue('user-123')
219210

220211
mockCheckKnowledgeBaseAccess.mockResolvedValue({
@@ -239,12 +230,12 @@ describe('Knowledge Search API Route', () => {
239230
}),
240231
})
241232

242-
const req = createMockRequest('POST', { ...validSearchData, searchMode: 'vector' })
233+
const req = createMockRequest('POST', { ...validSearchData, searchMode: 'hybrid' })
243234
const response = await POST(req)
244235

245236
expect(response.status).toBe(200)
246237
expect(mockExecuteKnowledgeSearch).toHaveBeenCalledWith(
247-
expect.objectContaining({ searchMode: 'vector' })
238+
expect.objectContaining({ searchMode: 'hybrid' })
248239
)
249240
})
250241

@@ -287,7 +278,7 @@ describe('Knowledge Search API Route', () => {
287278
expect(mockExecuteKnowledgeSearch).toHaveBeenCalledWith({
288279
knowledgeBaseIds: ['kb-123', 'kb-456'],
289280
topK: 10,
290-
searchMode: 'hybrid',
281+
searchMode: 'vector',
291282
query: multiKbData.query,
292283
queryVector: JSON.stringify(mockEmbedding),
293284
structuredFilters: undefined,
@@ -796,7 +787,7 @@ describe('Knowledge Search API Route', () => {
796787
expect(mockExecuteKnowledgeSearch).toHaveBeenCalledWith({
797788
knowledgeBaseIds: ['kb-123'],
798789
topK: 10,
799-
searchMode: 'hybrid',
790+
searchMode: 'vector',
800791
structuredFilters: [
801792
{ tagSlot: 'tag1', fieldType: 'text', operator: 'eq', value: 'api', valueTo: undefined },
802793
],
@@ -850,7 +841,7 @@ describe('Knowledge Search API Route', () => {
850841
expect(mockExecuteKnowledgeSearch).toHaveBeenCalledWith({
851842
knowledgeBaseIds: ['kb-123'],
852843
topK: 10,
853-
searchMode: 'hybrid',
844+
searchMode: 'vector',
854845
query: 'test search',
855846
queryVector: JSON.stringify(mockEmbedding),
856847
structuredFilters: [
@@ -1066,13 +1057,6 @@ describe('Knowledge Search API Route', () => {
10661057
},
10671058
])
10681059

1069-
mockGetQueryStrategy.mockReturnValue({
1070-
useParallel: false,
1071-
distanceThreshold: 1.0,
1072-
parallelLimit: 15,
1073-
singleQueryOptimized: true,
1074-
})
1075-
10761060
mockGenerateSearchEmbedding.mockResolvedValue({ embedding: [0.1, 0.2, 0.3], isBYOK: false })
10771061
mockGetDocumentMetadataByIds.mockResolvedValue({
10781062
'doc-active': {
@@ -1142,13 +1126,6 @@ describe('Knowledge Search API Route', () => {
11421126
},
11431127
])
11441128

1145-
mockGetQueryStrategy.mockReturnValue({
1146-
useParallel: false,
1147-
distanceThreshold: 1.0,
1148-
parallelLimit: 15,
1149-
singleQueryOptimized: true,
1150-
})
1151-
11521129
mockGetDocumentMetadataByIds.mockResolvedValue({
11531130
'doc-active-tagged': { filename: 'Active Tagged Document.pdf', sourceUrl: null },
11541131
})
@@ -1214,13 +1191,6 @@ describe('Knowledge Search API Route', () => {
12141191
},
12151192
])
12161193

1217-
mockGetQueryStrategy.mockReturnValue({
1218-
useParallel: false,
1219-
distanceThreshold: 1.0,
1220-
parallelLimit: 15,
1221-
singleQueryOptimized: true,
1222-
})
1223-
12241194
mockGenerateSearchEmbedding.mockResolvedValue({ embedding: [0.1, 0.2, 0.3], isBYOK: false })
12251195
mockGetDocumentMetadataByIds.mockResolvedValue({
12261196
'doc-active-combined': { filename: 'Active Combined Search.pdf', sourceUrl: null },

apps/sim/app/api/v1/knowledge/search/route.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
1313

1414
const {
1515
mockExecuteKnowledgeSearch,
16-
mockGetQueryStrategy,
1716
mockGenerateSearchEmbedding,
1817
mockGetDocumentMetadataByIds,
1918
mockAuthenticateRequest,
@@ -23,7 +22,6 @@ const {
2322
mockRecordSearchEmbeddingUsage,
2423
} = vi.hoisted(() => ({
2524
mockExecuteKnowledgeSearch: vi.fn(),
26-
mockGetQueryStrategy: vi.fn(),
2725
mockGenerateSearchEmbedding: vi.fn(),
2826
mockGetDocumentMetadataByIds: vi.fn(),
2927
mockAuthenticateRequest: vi.fn(),
@@ -48,7 +46,6 @@ const SYSTEM_BILLING_ATTRIBUTION = {
4846

4947
vi.mock('@/app/api/knowledge/search/utils', () => ({
5048
executeKnowledgeSearch: mockExecuteKnowledgeSearch,
51-
getQueryStrategy: mockGetQueryStrategy,
5249
generateSearchEmbedding: mockGenerateSearchEmbedding,
5350
getDocumentMetadataByIds: mockGetDocumentMetadataByIds,
5451
}))
@@ -109,7 +106,6 @@ describe('v1 knowledge search route — per-KB embedding model', () => {
109106
rateLimit: {},
110107
})
111108
mockValidateWorkspaceAccess.mockResolvedValue(null)
112-
mockGetQueryStrategy.mockReturnValue({ distanceThreshold: 0.5 })
113109
mockGenerateSearchEmbedding.mockResolvedValue({
114110
embedding: [0.1, 0.2, 0.3],
115111
isBYOK: false,

apps/sim/blocks/blocks/knowledge.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ export const KnowledgeBlock: BlockConfig = {
9393
title: 'Retrieval Mode',
9494
type: 'dropdown',
9595
options: [
96-
{ label: 'Hybrid (full-text + vector)', id: 'hybrid' },
9796
{ label: 'Vector only', id: 'vector' },
97+
{ label: 'Hybrid (full-text + vector)', id: 'hybrid' },
9898
],
99-
value: () => 'hybrid',
99+
value: () => 'vector',
100100
mode: 'advanced',
101101
condition: { field: 'operation', value: 'search' },
102102
},
@@ -454,7 +454,7 @@ export const KnowledgeBlock: BlockConfig = {
454454
tagFilters: { type: 'string', description: 'Tag filter criteria' },
455455
searchMode: {
456456
type: 'string',
457-
description: 'Retrieval mode: hybrid (full-text + vector) or vector only',
457+
description: 'Retrieval mode: vector only (default) or hybrid (full-text + vector)',
458458
},
459459
rerankerEnabled: { type: 'boolean', description: 'Apply Cohere reranking to search results' },
460460
rerankerModel: { type: 'string', description: 'Cohere rerank model identifier' },

apps/sim/lib/api/contracts/knowledge/search.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@ export const knowledgeSearchTagFilterSchema = z.object({
1010
valueTo: z.union([z.string(), z.number()]).optional(),
1111
})
1212

13-
export const KNOWLEDGE_SEARCH_MODES = ['hybrid', 'vector'] as const
13+
export const KNOWLEDGE_SEARCH_MODES = ['vector', 'hybrid'] as const
1414

15-
/** Shared by the internal and v1 search contracts so both default to hybrid. */
15+
/**
16+
* Shared by the internal and v1 search contracts. Defaults to `vector` so every
17+
* existing caller keeps its current ranking; hybrid is opt-in.
18+
*/
1619
export const knowledgeSearchModeSchema = z
1720
.enum(KNOWLEDGE_SEARCH_MODES)
1821
.optional()
1922
.nullable()
20-
.default('hybrid')
21-
.transform((val) => val ?? 'hybrid')
23+
.default('vector')
24+
.transform((val) => val ?? 'vector')
2225

2326
export const knowledgeSearchBodySchema = z
2427
.object({
@@ -45,9 +48,9 @@ export const knowledgeSearchBodySchema = z
4548
.nullable()
4649
.transform((val) => val || undefined),
4750
/**
48-
* `hybrid` (default) fuses full-text and vector retrieval by reciprocal rank,
49-
* which recovers exact tokens (error codes, ticket keys, identifiers) that
50-
* embeddings alone rank poorly. `vector` opts out to semantic-only retrieval.
51+
* `vector` (default) is semantic-only retrieval. `hybrid` additionally runs a
52+
* full-text leg and fuses the two by reciprocal rank, which recovers exact
53+
* tokens (error codes, ticket keys, identifiers) that embeddings rank poorly.
5154
*/
5255
searchMode: knowledgeSearchModeSchema,
5356
rerankerEnabled: z.boolean().optional().default(false),

apps/sim/lib/api/contracts/v1/knowledge/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ export const v1KnowledgeSearchBodySchema = z
135135
topK: z.number().min(1).max(100).default(10),
136136
tagFilters: z.array(v1SearchTagFilterSchema).optional(),
137137
/**
138-
* `hybrid` (default) fuses full-text and vector retrieval by reciprocal rank;
139-
* `vector` opts out to semantic-only retrieval.
138+
* `vector` (default) is semantic-only retrieval; `hybrid` fuses a full-text
139+
* leg with it by reciprocal rank.
140140
*/
141141
searchMode: knowledgeSearchModeSchema,
142142
})

apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,7 @@ vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({
7474
resolveWorkspaceFileReference: vi.fn(),
7575
}))
7676
vi.mock('@/app/api/knowledge/search/utils', () => ({
77-
getQueryStrategy: vi.fn(),
78-
handleVectorOnlySearch: vi.fn(),
77+
executeKnowledgeSearch: vi.fn(),
7978
}))
8079
vi.mock('@/app/api/knowledge/utils', () => ({
8180
checkDocumentWriteAccess: vi.fn(),

apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import {
5050
} from '@/lib/knowledge/tags/service'
5151
import { StorageService } from '@/lib/uploads'
5252
import { resolveWorkspaceFileReference } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
53-
import { getQueryStrategy, handleVectorOnlySearch } from '@/app/api/knowledge/search/utils'
53+
import { executeKnowledgeSearch } from '@/app/api/knowledge/search/utils'
5454
import {
5555
checkDocumentWriteAccess,
5656
checkKnowledgeBaseAccess,
@@ -264,13 +264,12 @@ export const knowledgeBaseServerTool: BaseServerTool<KnowledgeBaseArgs, Knowledg
264264
await generateSearchEmbedding(args.query, kb.embeddingModel, kb.workspaceId)
265265
const queryVector = JSON.stringify(queryEmbedding)
266266

267-
const strategy = getQueryStrategy(1, topK)
268-
269-
const results = await handleVectorOnlySearch({
267+
const results = await executeKnowledgeSearch({
270268
knowledgeBaseIds: [args.knowledgeBaseId],
271269
topK,
270+
searchMode: 'vector',
271+
query: args.query,
272272
queryVector,
273-
distanceThreshold: strategy.distanceThreshold,
274273
})
275274

276275
await recordSearchEmbeddingUsage({

apps/sim/tools/knowledge/search.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
4747
required: false,
4848
visibility: 'user-only',
4949
description:
50-
"Retrieval mode: 'hybrid' (default) fuses full-text and vector search, 'vector' uses semantic similarity only",
50+
"Retrieval mode: 'vector' (default) uses semantic similarity only, 'hybrid' also runs a full-text leg and fuses both",
5151
},
5252
rerankerEnabled: {
5353
type: 'boolean',
@@ -121,7 +121,7 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
121121
query: params.query,
122122
topK: params.topK ? Math.max(1, Math.min(100, Number(params.topK))) : 10,
123123
...(structuredFilters.length > 0 && { tagFilters: structuredFilters }),
124-
...(params.searchMode === 'vector' && { searchMode: 'vector' }),
124+
...(params.searchMode === 'hybrid' && { searchMode: 'hybrid' }),
125125
...(rerankerEnabled && {
126126
rerankerEnabled: true,
127127
rerankerModel,

0 commit comments

Comments
 (0)