Skip to content

Commit e9b6816

Browse files
fix(knowledge): skip global readiness checks for ordinary KBs
1 parent e2cdee6 commit e9b6816

2 files changed

Lines changed: 125 additions & 84 deletions

File tree

‎apps/sim/lib/knowledge/search/queries.test.ts‎

Lines changed: 120 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,26 @@ describe('workspace-scoped vector retrieval', () => {
438438
vi.useRealTimers()
439439
})
440440

441+
it.each([false, undefined])(
442+
'retrieves ordinary KB results without global projection readiness (searchIndexOnly=%s)',
443+
async (searchIndexOnly) => {
444+
queueTableRows(schemaMock.embedding, [...ranked].reverse())
445+
const result = await retrieveKnowledgeSearch({
446+
...params,
447+
searchIndexOnly,
448+
searchMode: 'vector',
449+
query: 'What is the capital of France?',
450+
})
451+
expect(result.retrieval).toEqual({ status: 'complete', timedOutLegs: [] })
452+
expect(result.rows.map((row) => row.id)).toEqual(['near', 'far'])
453+
expect(statements().filter((query) => query.sql.includes('AS unfilled'))).toHaveLength(0)
454+
expect(statements().some((query) => isPageStatement(query.sql))).toBe(true)
455+
const walk = statements().find((query) => isWalk(query.sql))!
456+
expect(JSON.stringify(walk)).toContain('required_clause')
457+
expect(JSON.stringify(walk)).toContain(String(schemaMock.document.acl))
458+
}
459+
)
460+
441461
it.each([handleVectorOnlySearch, handleTagAndVectorSearch])(
442462
'does not acquire a connection or start SQL after the KB retrieval deadline',
443463
async (search) => {
@@ -749,7 +769,7 @@ describe('workspace-scoped vector retrieval', () => {
749769
statements()
750770
.filter((query) => query.sql.includes('statement_timeout'))
751771
.map((query) => query.params[0])
752-
).toEqual(['100', '100', '40', '20'])
772+
).toEqual(['100', '40', '20'])
753773
})
754774

755775
it('applies the scan settings in the deadline statement rather than one of their own', async () => {
@@ -805,49 +825,53 @@ describe('workspace-scoped vector retrieval', () => {
805825
)
806826
})
807827

808-
it('reports incomplete retrieval for 18 expired pool waiters without starting their SQL later', async () => {
809-
vi.useFakeTimers()
810-
const release: Array<() => void> = []
811-
const transactions: Array<Promise<unknown>> = []
812-
vi.spyOn(db, 'transaction').mockImplementation((callback) => {
813-
const transaction = new Promise<void>((resolve) => release.push(resolve)).then(() =>
814-
callback(db as never)
828+
it.each([false, true])(
829+
'expires pool waiters without starting SQL later (searchIndexOnly=%s)',
830+
async (searchIndexOnly) => {
831+
vi.useFakeTimers()
832+
const release: Array<() => void> = []
833+
const transactions: Array<Promise<unknown>> = []
834+
vi.spyOn(db, 'transaction').mockImplementation((callback) => {
835+
const transaction = new Promise<void>((resolve) => release.push(resolve)).then(() =>
836+
callback(db as never)
837+
)
838+
transactions.push(transaction)
839+
return transaction as ReturnType<typeof db.transaction>
840+
})
841+
const pending = Promise.all(
842+
Array.from({ length: 18 }, (_, index) =>
843+
retrieveKnowledgeSearch({
844+
...params,
845+
knowledgeBaseIds: [`kb-${index}`],
846+
searchIndexOnly,
847+
query: 'fixture policy',
848+
searchMode: 'vector',
849+
vectorBudgetMs: 50,
850+
})
851+
)
815852
)
816-
transactions.push(transaction)
817-
return transaction as ReturnType<typeof db.transaction>
818-
})
819-
const pending = Promise.all(
820-
Array.from({ length: 18 }, (_, index) =>
821-
retrieveKnowledgeSearch({
822-
...params,
823-
knowledgeBaseIds: [`kb-${index}`],
824-
query: 'fixture policy',
825-
searchMode: 'vector',
826-
vectorBudgetMs: 50,
853+
await vi.advanceTimersByTimeAsync(60)
854+
const results = await pending
855+
expect(results).toHaveLength(18)
856+
for (const result of results) {
857+
expect(result).toEqual({
858+
rows: [],
859+
retrieval: { status: 'partial', timedOutLegs: ['vector'] },
827860
})
828-
)
829-
)
830-
await vi.advanceTimersByTimeAsync(60)
831-
const results = await pending
832-
expect(results).toHaveLength(18)
833-
for (const result of results) {
834-
expect(result).toEqual({
835-
rows: [],
836-
retrieval: { status: 'partial', timedOutLegs: ['vector'] },
837-
})
838-
}
839-
for (const resume of release) resume()
840-
const settled = await Promise.allSettled(transactions)
841-
/** The searches that miss the projection-fill memo together share one read; each search's own read is refused at its deadline before it starts. */
842-
expect(settled).toHaveLength(1)
843-
for (const transaction of settled) {
844-
expect(transaction.status).toBe('rejected')
845-
if (transaction.status === 'rejected')
846-
expect(transaction.reason).toBeInstanceOf(SearchDeadlineError)
861+
}
862+
for (const resume of release) resume()
863+
const settled = await Promise.allSettled(transactions)
864+
/** Search indexes share the readiness read; ordinary KBs acquire their own candidate reads. */
865+
expect(settled).toHaveLength(searchIndexOnly ? 1 : 18)
866+
for (const transaction of settled) {
867+
expect(transaction.status).toBe('rejected')
868+
if (transaction.status === 'rejected')
869+
expect(transaction.reason).toBeInstanceOf(SearchDeadlineError)
870+
}
871+
expect(dbChainMockFns.select).not.toHaveBeenCalled()
872+
expect(dbChainMockFns.execute).not.toHaveBeenCalled()
847873
}
848-
expect(dbChainMockFns.select).not.toHaveBeenCalled()
849-
expect(dbChainMockFns.execute).not.toHaveBeenCalled()
850-
})
874+
)
851875
})
852876

853877
describe('workspace search filters before ranking', () => {
@@ -943,6 +967,7 @@ describe('hydration follows ranked candidates', () => {
943967
}
944968
const params: SearchParams = {
945969
knowledgeBaseIds: ['org-index'],
970+
searchIndexOnly: true,
946971
topK: 1,
947972
access: identity,
948973
accessProvider: provider,
@@ -1296,6 +1321,7 @@ describe('permitted-document planner', () => {
12961321
}
12971322
const params: SearchParams = {
12981323
knowledgeBaseIds: ['org-index'],
1324+
searchIndexOnly: true,
12991325
topK: 1,
13001326
access: reader,
13011327
accessProvider: provider,
@@ -2278,6 +2304,7 @@ describe('permitted-document planner', () => {
22782304

22792305
const liveSearch = {
22802306
knowledgeBaseIds: ['org-index'],
2307+
searchIndexOnly: true,
22812308
topK: 1,
22822309
searchMode: 'hybrid' as const,
22832310
query: 'release',
@@ -2364,48 +2391,57 @@ describe('permitted-document planner', () => {
23642391
expect(statements().some((query) => isPageStatement(query.sql))).toBe(false)
23652392
})
23662393

2367-
it('excludes a denied source through its documents while the projection is unfilled', async () => {
2368-
queueTableRows(schemaMock.knowledgeConnector, [
2369-
{
2370-
id: 'gated-src',
2371-
accessMode: 'admin',
2372-
connectorType: 'confluence',
2373-
githubRepository: false,
2374-
},
2375-
])
2376-
dbChainMockFns.execute.mockImplementation(async (query) => {
2377-
const statement = render(query).sql
2378-
/** The fill has not reached every row, so a denied source cannot be read off the row. */
2379-
if (statement.includes('AS unfilled')) return [{ unfilled: true }]
2380-
/** The mock renders nested fragments as parameters, so the marker is found in the whole query. */
2381-
const rebuilt = JSON.stringify(query).includes('/* excluded sources */')
2382-
if (isPageStatement(statement))
2383-
return JSON.stringify(render(query).params).includes('"b"')
2384-
? [hit('b', 'other-src')]
2385-
: [hit('a', 'gated-src')]
2386-
if (isWalk(statement))
2387-
return Array.from({ length: 400 }, (_, i) => ({
2388-
id: i === 0 ? (rebuilt ? 'b' : 'a') : `w-${i}`,
2389-
distance: 0.1,
2390-
}))
2391-
return []
2392-
})
2393-
queueTableRows(schemaMock.embedding, [])
2394-
queueTableRows(schemaMock.embedding, [hit('b', 'other-src')])
2395-
const getForConnectors = vi.fn<KnowledgeAccessProvider['getForConnectors']>(async () => reader)
2396-
const result = await retrieveKnowledgeSearch({
2397-
...liveSearch,
2398-
searchMode: 'vector',
2399-
access: reader,
2400-
accessProvider: { ...provider, getForConnectors },
2401-
})
2402-
expect(result.rows.map((row) => row.id)).toEqual(['b'])
2403-
const walks = statements().filter((query) => isWalk(query.sql))
2404-
expect(walks).toHaveLength(2)
2405-
expect(JSON.stringify(walks[0])).not.toContain('/* excluded sources */')
2406-
expect(JSON.stringify(walks[1])).toContain('NOT EXISTS (SELECT 1 FROM')
2407-
expect(JSON.stringify(walks[1])).toContain('/* excluded sources */')
2408-
})
2394+
it.each([false, true])(
2395+
'excludes a denied source through its documents (searchIndexOnly=%s)',
2396+
async (searchIndexOnly) => {
2397+
queueTableRows(schemaMock.knowledgeConnector, [
2398+
{
2399+
id: 'gated-src',
2400+
accessMode: 'admin',
2401+
connectorType: 'confluence',
2402+
githubRepository: false,
2403+
},
2404+
])
2405+
dbChainMockFns.execute.mockImplementation(async (query) => {
2406+
const statement = render(query).sql
2407+
/** The fill has not reached every row, so a denied source cannot be read off the row. */
2408+
if (statement.includes('AS unfilled')) return [{ unfilled: true }]
2409+
/** The mock renders nested fragments as parameters, so the marker is found in the whole query. */
2410+
const rebuilt = JSON.stringify(query).includes('/* excluded sources */')
2411+
if (isPageStatement(statement))
2412+
return JSON.stringify(render(query).params).includes('"b"')
2413+
? [hit('b', 'other-src')]
2414+
: [hit('a', 'gated-src')]
2415+
if (isWalk(statement))
2416+
return Array.from({ length: 400 }, (_, i) => ({
2417+
id: i === 0 ? (rebuilt ? 'b' : 'a') : `w-${i}`,
2418+
distance: 0.1,
2419+
}))
2420+
return []
2421+
})
2422+
queueTableRows(schemaMock.embedding, [])
2423+
queueTableRows(schemaMock.embedding, [hit('b', 'other-src')])
2424+
const getForConnectors = vi.fn<KnowledgeAccessProvider['getForConnectors']>(
2425+
async () => reader
2426+
)
2427+
const result = await retrieveKnowledgeSearch({
2428+
...liveSearch,
2429+
searchIndexOnly,
2430+
searchMode: 'vector',
2431+
access: reader,
2432+
accessProvider: { ...provider, getForConnectors },
2433+
})
2434+
expect(result.rows.map((row) => row.id)).toEqual(['b'])
2435+
expect(statements().filter((query) => query.sql.includes('AS unfilled'))).toHaveLength(
2436+
searchIndexOnly ? 1 : 0
2437+
)
2438+
const walks = statements().filter((query) => isWalk(query.sql))
2439+
expect(walks).toHaveLength(2)
2440+
expect(JSON.stringify(walks[0])).not.toContain('/* excluded sources */')
2441+
expect(JSON.stringify(walks[1])).toContain('NOT EXISTS (SELECT 1 FROM')
2442+
expect(JSON.stringify(walks[1])).toContain('/* excluded sources */')
2443+
}
2444+
)
24092445

24102446
it('hands back the unread slices of a page a denied source made it rebuild', async () => {
24112447
queueTableRows(schemaMock.knowledgeConnector, [
@@ -2488,6 +2524,7 @@ describe('filters on a resolved scope', () => {
24882524
}
24892525
const params: SearchParams = {
24902526
knowledgeBaseIds: ['org-index'],
2527+
searchIndexOnly: true,
24912528
topK: 1,
24922529
access: reader,
24932530
accessProvider: provider,

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,8 @@ export interface SearchParams {
359359
permitted?: PermittedDocuments
360360
/** Connector state resolved once per search, so no candidate re-derives it. */
361361
accessPlan?: SearchAccessPlan
362+
/** Every searched base is a Sim Search index; ordinary KBs use document-backed pages. */
363+
searchIndexOnly?: boolean
362364
}
363365

364366
/** All valid tag slot keys */
@@ -1810,7 +1812,9 @@ async function selectVectorResults(params: SearchParams): Promise<SearchResult[]
18101812
const plan = params.access.kind === 'user' ? params.accessPlan : undefined
18111813
/** Two remembered facts, read together when neither is remembered. */
18121814
const [filled, plannedIndexedSources] = await Promise.all([
1813-
isProjectionFilled('embedding_search', 'vector.projection_filled', params.budget),
1815+
params.searchIndexOnly === true
1816+
? isProjectionFilled('embedding_search', 'vector.projection_filled', params.budget)
1817+
: false,
18141818
plan?.memberSources.length ? indexedVectorSources(params.budget) : undefined,
18151819
])
18161820
/**

0 commit comments

Comments
 (0)