Skip to content

Commit 22fed44

Browse files
committed
improvement(knowledge): embed only after every prerequisite holds, refuse a contradicting owner, count the shared fill read
1 parent f138017 commit 22fed44

3 files changed

Lines changed: 47 additions & 46 deletions

File tree

‎apps/sim/lib/knowledge/__integration__/kb-block-search.integration.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,18 +132,20 @@ describe('API-key KB block fan-out', () => {
132132
statements.filter((query) => query.includes(fragment))
133133
/**
134134
* Every statement runs under the leg's deadline: the candidate search applies it with the
135-
* scan settings in one statement, and the probe, the exact ranking, the rerank and
136-
* hydration each open with one of their own.
135+
* scan settings in one statement, and the probe, the exact ranking and hydration each
136+
* open with one of their own. The projection-fill read is shared by the searches that
137+
* miss its memo together, so it appears once.
137138
*/
138-
expect(matching('statement_timeout')).toHaveLength(bases.length * 5)
139+
expect(matching('statement_timeout')).toHaveLength(bases.length * 4 + 1)
139140
/**
140141
* A scope this small leaves the bounded traversal short of its candidate limit, so every
141142
* search probes once and rescues once — never a widening retry loop.
142143
*/
143144
expect(matching('hnsw.iterative_scan')).toHaveLength(bases.length)
144145
expect(matching('AS visible')).toHaveLength(bases.length)
145146
expect(matching(') + 0 LIMIT')).toHaveLength(bases.length)
146-
expect(matching('"embedding_search"."id" = ANY(')).toHaveLength(bases.length)
147+
/** The walk carries each candidate's identities, so a filled projection reads no page. */
148+
expect(matching('"embedding_search"."id" = ANY(')).toHaveLength(0)
147149
/** The probe enumerates visible documents and reports saturation; it never ranks them. */
148150
expect(
149151
statements.filter(

‎apps/sim/lib/knowledge/application/search.ts‎

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -390,51 +390,42 @@ export async function runKnowledgeSearch({
390390
: undefined
391391
const resultSecretRegistry = preparedRegistry ?? input.resultSecretRegistry
392392
input.signal?.throwIfAborted()
393-
/** The embedding is requested the moment admission passes, beside the scope and defaults reads. */
394-
const admittedEmbedding = async () => {
395-
const billingAttribution = await admit()
396-
input.signal?.throwIfAborted()
397-
const queryEmbedding = hasQuery
398-
? await measureSearchStage('embedding', () =>
399-
runWithKnowledgeModelInputProvenance(resultSecretRegistry, () =>
400-
generateSearchEmbedding(
401-
input.query!,
402-
embeddingTarget!,
403-
context.workspaceId,
404-
input.signal
405-
)
406-
)
407-
)
408-
: null
409-
return { billingAttribution, queryEmbedding }
410-
}
411-
const [access, searchDefaults, admitted, tagDefinitions, rerankerCredential] = await Promise.all([
412-
measureSearchStage('access_scope', () => context.access.get()),
413-
measureSearchStage('defaults', () =>
414-
resolveKnowledgeSearchDefaults({
415-
workspaceId: context.workspaceId,
416-
organizationId: context.organizationId,
393+
const [access, searchDefaults, billingAttribution, tagDefinitions, rerankerCredential] =
394+
await Promise.all([
395+
measureSearchStage('access_scope', () => context.access.get()),
396+
measureSearchStage('defaults', () =>
397+
resolveKnowledgeSearchDefaults({
398+
workspaceId: context.workspaceId,
399+
organizationId: context.organizationId,
417400

418-
/** The signed-in person, if any; never the billing owner or a key's creator. */
419-
userId: resolvePrincipalSubjectUserId(principal) ?? undefined,
420-
requestedMode: input.searchMode,
421-
})
422-
),
423-
admittedEmbedding(),
424-
/** The tag names the results are labelled with depend on the bases alone. */
425-
filters.length === 0
426-
? measureSearchStage('tag_definitions', () =>
427-
getDocumentTagDefinitionsByKnowledgeBaseIds(knowledgeBaseIds)
428-
)
429-
: Promise.resolve(definitionsByKnowledgeBase),
430-
/** A surface may ask to rerank; without a key for the workspace or the platform there is nothing to ask. */
431-
input.rerankerEnabled && hasQuery
432-
? hasRerankerCredential(context.workspaceId, input.rerankerApiKey)
433-
: false,
434-
])
435-
const { billingAttribution, queryEmbedding } = admitted
401+
/** The signed-in person, if any; never the billing owner or a key's creator. */
402+
userId: resolvePrincipalSubjectUserId(principal) ?? undefined,
403+
requestedMode: input.searchMode,
404+
})
405+
),
406+
admit(),
407+
/** The tag names the results are labelled with depend on the bases alone. */
408+
filters.length === 0
409+
? measureSearchStage('tag_definitions', () =>
410+
getDocumentTagDefinitionsByKnowledgeBaseIds(knowledgeBaseIds)
411+
)
412+
: Promise.resolve(definitionsByKnowledgeBase),
413+
/** A surface may ask to rerank; without a key for the workspace or the platform there is nothing to ask. */
414+
input.rerankerEnabled && hasQuery
415+
? hasRerankerCredential(context.workspaceId, input.rerankerApiKey)
416+
: false,
417+
])
436418
definitionsByKnowledgeBase = tagDefinitions
437419
input.signal?.throwIfAborted()
420+
/** Requested only once every prerequisite held: a search refused for any reason spends no model call. */
421+
const queryEmbedding = hasQuery
422+
? await measureSearchStage('embedding', () =>
423+
runWithKnowledgeModelInputProvenance(resultSecretRegistry, () =>
424+
generateSearchEmbedding(input.query!, embeddingTarget!, context.workspaceId, input.signal)
425+
)
426+
)
427+
: null
428+
input.signal?.throwIfAborted()
438429
annotateSearchDiagnostics({
439430
accessScopeKind: access.kind,
440431
searchMode: searchDefaults.searchMode,

‎apps/sim/lib/knowledge/application/workspace-search.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Principal } from '@sim/auth/principal'
22
import { resolvePrincipalSubjectUserId } from '@sim/auth/principal'
3+
import { OrchestrationError } from '@/lib/core/orchestration/types'
34
import { type ResourceOwner, resourceScopeFromOwner } from '@/lib/core/resource-scope'
45
import { requireOrganizationSearchAvailable } from '@/lib/knowledge/access/availability'
56
import { defineAuthorizedKnowledgeUseCase } from '@/lib/knowledge/application/authorized-knowledge-use-case'
@@ -112,6 +113,13 @@ function defineScopedSearchUseCase<
112113
...surface.searchInput(input, context),
113114
knowledgeBaseIds: [index.id],
114115
}
116+
/** An owner the request asserts is the one that was resolved, or the request names none. */
117+
if (
118+
(searchInput.organizationId && searchInput.organizationId !== context.organizationId) ||
119+
(searchInput.workspaceId && searchInput.workspaceId !== context.workspaceId)
120+
) {
121+
throw new OrchestrationError('not_found', 'Knowledge base not found')
122+
}
115123
validateKnowledgeSearchInput(searchInput)
116124
return runKnowledgeSearch({
117125
principal,

0 commit comments

Comments
 (0)