Skip to content

Commit 2bfaed1

Browse files
committed
fix(knowledge): enumerate a small filtered source so both legs rank inside it
1 parent 7bac82a commit 2bfaed1

2 files changed

Lines changed: 70 additions & 18 deletions

File tree

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2233,6 +2233,50 @@ describe('filters on a resolved scope', () => {
22332233
expect(probes()).toHaveLength(0)
22342234
})
22352235

2236+
it('enumerates a small source and ranks its keyword matches over every chunk it holds', async () => {
2237+
mockResolveTinKeywordQuery.mockResolvedValue('"releas"')
2238+
queueTableRows(schemaMock.knowledgeConnector, [
2239+
{ id: 'src-small', accessMode: 'admin', connectorType: 'slack', githubRepository: false },
2240+
{
2241+
id: 'src-large',
2242+
accessMode: 'admin',
2243+
connectorType: 'google_drive',
2244+
githubRepository: false,
2245+
},
2246+
])
2247+
dbChainMockFns.execute.mockImplementation(async (query) => {
2248+
const statement = JSON.stringify(render(query))
2249+
if (statement.includes('EXPLAIN'))
2250+
return [
2251+
{
2252+
'QUERY PLAN': [
2253+
{ Plan: { 'Plan Rows': statement.includes('src-small') ? 900 : 1_000_000 } },
2254+
],
2255+
},
2256+
]
2257+
if (statement.includes(') reached')) return [{ n: 250_000 }]
2258+
if (statement.includes('AS saturated'))
2259+
return [{ id: 'doc-small', connectorId: 'src-small', saturated: false }]
2260+
if (statement.includes('ranked_tin_chunks')) return [{ ranked: 0, candidates: [] }]
2261+
return []
2262+
})
2263+
await retrieveKnowledgeSearch({
2264+
...params,
2265+
accessProvider: provider,
2266+
searchMode: 'hybrid',
2267+
query: 'release',
2268+
filters: { source: 'slack' },
2269+
})
2270+
/** The confined set is enumerated once, and the keyword leg ranks inside it, never a base-wide window. */
2271+
const probes = statements().filter((query) => query.sql.includes('AS saturated'))
2272+
expect(probes).toHaveLength(1)
2273+
expect(probes[0].sql).not.toContain('WITH reach')
2274+
expect(statements().filter((query) => query.sql.includes('ranked_tin_chunks'))).toHaveLength(0)
2275+
expect(
2276+
statements().filter((query) => query.sql.includes('WITH matched_keyword_chunks'))
2277+
).toHaveLength(1)
2278+
})
2279+
22362280
it('tests the date through the document inside an on-row walk when the filtered set is unbounded', async () => {
22372281
traversedRows = [{ id: 'a' }]
22382282
rerankRows = [hit('a', 'src-a')]

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

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,19 +1153,24 @@ const indexDocumentCounts = new LRUCache<string, number>({
11531153
})
11541154

11551155
/**
1156-
* The planner's estimate of the bases' documents changed since a time, from the statistics on
1157-
* the date index: whether the filtered set is worth enumerating needs its order of magnitude.
1156+
* The planner's estimate of the documents a filter leaves in the bases — a date filter from the
1157+
* statistics on its index, a source filter from its connectors' — so whether the filtered set is
1158+
* worth enumerating is decided from its order of magnitude, without reading a row.
11581159
*/
1159-
async function estimateDocumentsModifiedAfter(
1160+
async function estimateFilteredDocuments(
11601161
knowledgeBaseIds: string[],
1161-
modifiedAfter: string
1162+
filters: WorkspaceSearchFilters,
1163+
plan: SearchAccessPlan
11621164
): Promise<number> {
11631165
const [row] = await db.execute<{ 'QUERY PLAN': Array<{ Plan: { 'Plan Rows': number } }> }>(sql`
11641166
EXPLAIN (FORMAT JSON) SELECT 1 FROM ${document}
11651167
WHERE ${and(
11661168
inArray(document.knowledgeBaseId, knowledgeBaseIds),
11671169
isNull(document.deletedAt),
1168-
gte(document.sourceModifiedAt, new Date(modifiedAfter))
1170+
filters.modifiedAfter
1171+
? gte(document.sourceModifiedAt, new Date(filters.modifiedAfter))
1172+
: undefined,
1173+
filters.source ? planSourceCondition(plan) : undefined
11691174
)}`)
11701175
return Number(row?.['QUERY PLAN']?.[0]?.Plan?.['Plan Rows'] ?? 0)
11711176
}
@@ -1282,7 +1287,7 @@ export async function resolvePermittedDocuments(params: {
12821287
* change; the filtered set still has to be enumerated, so under one the probe always runs.
12831288
*/
12841289
const remembered =
1285-
key && !(params.accessPlan && params.filters?.modifiedAfter)
1290+
key && !(params.accessPlan && (params.filters?.modifiedAfter || params.filters?.source))
12861291
? saturatedReach.get(key)
12871292
: undefined
12881293
if (remembered) {
@@ -1301,7 +1306,9 @@ export async function resolvePermittedDocuments(params: {
13011306
params.access,
13021307
params.budget,
13031308
'permitted_documents',
1304-
params.accessPlan && params.filters?.modifiedAfter ? 'direct' : 'reach-first'
1309+
params.accessPlan && (params.filters?.modifiedAfter || params.filters?.source)
1310+
? 'direct'
1311+
: 'reach-first'
13051312
)
13061313
} catch (error) {
13071314
if (!params.budget?.isTimeout(error)) throw error
@@ -1666,14 +1673,14 @@ async function selectVectorResults(params: SearchParams): Promise<SearchResult[]
16661673
)
16671674
if (
16681675
params.permitted?.kind === 'bounded' &&
1669-
(!walksASource || params.filters?.modifiedAfter)
1676+
(!walksASource || params.filters?.modifiedAfter || params.filters?.source)
16701677
) {
16711678
/**
16721679
* A bounded permitted set is ranked exactly without walking the graph first: the walk
16731680
* post-filters, so when the caller reads a small share of the index it spends its whole
16741681
* uninterruptible tuple budget and still returns almost none of their neighbours. A
1675-
* member's indexed source is otherwise walked instead, but not under a date filter: the
1676-
* walk cannot see the date, and the set the filter admits is small by construction.
1682+
* member's indexed source is otherwise walked instead, but not under a filter: the walk
1683+
* cannot see the date, and a filtered set is small by construction.
16771684
*/
16781685
selected = await rankPermittedExactly(params.permitted.documents.map((entry) => entry.id))
16791686
} else if (plan && !(params.permitted?.kind === 'unbounded' && params.permitted.broad)) {
@@ -2404,20 +2411,21 @@ export async function retrieveKnowledgeSearch(
24042411
* bounded scope with their own exhaustive ordering.
24052412
*/
24062413
/**
2407-
* The row does not carry the document's date, so a date filter's documents are enumerated off
2408-
* the date index and ranked exactly while the planner estimates the window within what the
2409-
* probe may enumerate; a wider window is walked instead, with the date tested through the
2410-
* document, since a window that wide holds most of the query's neighbours anyway.
2414+
* A filter that leaves few documents is enumerated and ranked exactly inside them, both legs:
2415+
* the row does not carry the document's date, and a keyword ranking of the whole base may hold
2416+
* few of a small source's matches. A filter that leaves many is ranked as the scope is — the
2417+
* source confined on the row, the date tested through the document — since a set that large
2418+
* holds most of the query's neighbours anyway. The planner's estimate decides which.
24112419
*/
2412-
const enumerateDated =
2413-
accessPlan && params.filters?.modifiedAfter
2420+
const enumerateFiltered =
2421+
accessPlan && (params.filters?.modifiedAfter || params.filters?.source)
24142422
? (await measureSearchStage('permitted_documents', () =>
2415-
estimateDocumentsModifiedAfter(knowledgeBaseIds, params.filters!.modifiedAfter!)
2423+
estimateFilteredDocuments(knowledgeBaseIds, params.filters!, accessPlan)
24162424
)) <= VECTOR_PROBE_DOCUMENT_LIMIT
24172425
: false
24182426
const permitted =
24192427
access.kind === 'user' && params.accessProvider && !params.filters?.documentIds?.length
2420-
? accessPlan && !enumerateDated
2428+
? accessPlan && !enumerateFiltered
24212429
? /**
24222430
* With readability decided on the projection row, a resolved scope never needs its
24232431
* readable documents enumerated ahead of ranking: its reach alone chooses between one

0 commit comments

Comments
 (0)