Skip to content

Commit 7ccefe2

Browse files
committed
improvement(knowledge): stop re-probing known indexing providers per access batch
The search source overview loops over access batches and, for each one, ran the indexing probe in full. That probe's result is only ever read back as set membership when projecting `isSyncing`, so a provider type already found cannot change the answer — every later batch re-paid an EXISTS scan over `document` carrying the full ACL predicate for nothing. Mirror the searchable probe's existing guard: skip the indexing probe once every configured provider type is accounted for, and exclude already-found types from the query on later batches. Both queries build on the same configured condition and the probe only adds narrowing predicates, so the resulting set is unchanged.
1 parent 87625de commit 7ccefe2

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

‎apps/sim/lib/knowledge/application/search-source-overview.test.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ vi.mock('@/lib/knowledge/read-access', () => ({
2424
}))
2525

2626
import { readSearchSourceOverview } from '@/lib/knowledge/application/search-source-overview'
27+
import { MAX_SEARCH_SOURCE_PROVIDER_TYPES } from '@/lib/knowledge/constants'
2728

2829
const principal = { kind: 'session', userId: 'reader', sessionId: 'session' } as const
2930
const input = { organizationId: 'org-1', workspaceId: null }
@@ -36,6 +37,14 @@ const searchableProbeCount = () =>
3637
dbChainMockFns.limit.mock.calls.filter(([rows]) => rows === 1).length -
3738
AUTHORIZATION_SINGLE_ROW_READS
3839

40+
/** The configured-provider list is read once, before the batches, under the same bound. */
41+
const CONFIGURED_PROVIDER_READS = 1
42+
43+
/** Every other provider-bounded read in this use case is the indexing probe. */
44+
const indexingProbeCount = () =>
45+
dbChainMockFns.limit.mock.calls.filter(([rows]) => rows === MAX_SEARCH_SOURCE_PROVIDER_TYPES)
46+
.length - CONFIGURED_PROVIDER_READS
47+
3948
function yieldBatches(count: number) {
4049
mocks.batches.mockImplementation(async function* () {
4150
for (let index = 0; index < count; index += 1) yield sql`batch-${sql.raw(String(index))}`
@@ -73,4 +82,31 @@ describe('readSearchSourceOverview', () => {
7382
expect(result.hasSearchableDocuments).toBe(false)
7483
expect(searchableProbeCount()).toBe(3)
7584
})
85+
86+
it('stops probing for indexing once every configured provider type is known', async () => {
87+
yieldBatches(3)
88+
queueTableRows(member, [{ role: 'owner' }])
89+
queueTableRows(knowledgeConnector, [{ connectorType: 'gmail' }])
90+
queueTableRows(knowledgeConnector, [{ connectorType: 'gmail' }])
91+
92+
const result = await readSearchSourceOverview.execute({ principal, input })
93+
94+
expect(result.providers).toEqual([{ connectorType: 'gmail', isSyncing: true }])
95+
expect(indexingProbeCount()).toBe(1)
96+
})
97+
98+
it('keeps probing every batch while a configured provider type is still unaccounted for', async () => {
99+
yieldBatches(3)
100+
queueTableRows(member, [{ role: 'owner' }])
101+
queueTableRows(knowledgeConnector, [{ connectorType: 'gmail' }, { connectorType: 'notion' }])
102+
queueTableRows(knowledgeConnector, [{ connectorType: 'gmail' }])
103+
104+
const result = await readSearchSourceOverview.execute({ principal, input })
105+
106+
expect(result.providers).toEqual([
107+
{ connectorType: 'gmail', isSyncing: true },
108+
{ connectorType: 'notion', isSyncing: false },
109+
])
110+
expect(indexingProbeCount()).toBe(3)
111+
})
76112
})

‎apps/sim/lib/knowledge/application/search-source-overview.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,30 @@ export const readSearchSourceOverview = instrumentSourceOverviewUseCase(
117117
/** One searchable document is the whole answer, so later batches skip the probe entirely. */
118118
const probesSearchable: boolean = probesSources && !hasSearchableDocuments
119119
if (probesSearchable) searchableProbes += 1
120+
/**
121+
* A provider type is only read back as membership of `indexingTypes`, so once every
122+
* configured type is in the set no later batch can change the answer.
123+
*/
124+
const probesIndexing: boolean =
125+
probesSources && providers.some(({ connectorType }) => !indexingTypes.has(connectorType))
120126
/** Annotated so the searchable probe's guard does not infer through its own result. */
121127
const [indexing, searchable]: [{ connectorType: string }[], { id: string }[]] =
122128
await Promise.all([
123-
probesSources
129+
probesIndexing
124130
? measureSearchStage('source_overview.indexing', () =>
125131
configuredProvidersQuery()
126132
.where(
127133
and(
128134
configured,
129135
syncingEnabled,
136+
/**
137+
* The probe narrows the configured set the provider list came from, so a
138+
* type already found stays found; excluding it only drops repeated work.
139+
* An empty set adds no predicate rather than a no-op one.
140+
*/
141+
indexingTypes.size > 0
142+
? notInArray(knowledgeConnector.connectorType, [...indexingTypes])
143+
: undefined,
130144
or(
131145
inArray(knowledgeConnector.status, ['pending', 'syncing']),
132146
and(

0 commit comments

Comments
 (0)