Skip to content

Commit 04f17dc

Browse files
committed
fix(knowledge): enumerate a small filtered source so both legs rank inside it
1 parent a35fac6 commit 04f17dc

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
@@ -2236,6 +2236,50 @@ describe('filters on a resolved scope', () => {
22362236
expect(probes()).toHaveLength(0)
22372237
})
22382238

2239+
it('enumerates a small source and ranks its keyword matches over every chunk it holds', async () => {
2240+
mockResolveTinKeywordQuery.mockResolvedValue('"releas"')
2241+
queueTableRows(schemaMock.knowledgeConnector, [
2242+
{ id: 'src-small', accessMode: 'admin', connectorType: 'slack', githubRepository: false },
2243+
{
2244+
id: 'src-large',
2245+
accessMode: 'admin',
2246+
connectorType: 'google_drive',
2247+
githubRepository: false,
2248+
},
2249+
])
2250+
dbChainMockFns.execute.mockImplementation(async (query) => {
2251+
const statement = JSON.stringify(render(query))
2252+
if (statement.includes('EXPLAIN'))
2253+
return [
2254+
{
2255+
'QUERY PLAN': [
2256+
{ Plan: { 'Plan Rows': statement.includes('src-small') ? 900 : 1_000_000 } },
2257+
],
2258+
},
2259+
]
2260+
if (statement.includes(') reached')) return [{ n: 250_000 }]
2261+
if (statement.includes('AS saturated'))
2262+
return [{ id: 'doc-small', connectorId: 'src-small', saturated: false }]
2263+
if (statement.includes('ranked_tin_chunks')) return [{ ranked: 0, candidates: [] }]
2264+
return []
2265+
})
2266+
await retrieveKnowledgeSearch({
2267+
...params,
2268+
accessProvider: provider,
2269+
searchMode: 'hybrid',
2270+
query: 'release',
2271+
filters: { source: 'slack' },
2272+
})
2273+
/** The confined set is enumerated once, and the keyword leg ranks inside it, never a base-wide window. */
2274+
const probes = statements().filter((query) => query.sql.includes('AS saturated'))
2275+
expect(probes).toHaveLength(1)
2276+
expect(probes[0].sql).not.toContain('WITH reach')
2277+
expect(statements().filter((query) => query.sql.includes('ranked_tin_chunks'))).toHaveLength(0)
2278+
expect(
2279+
statements().filter((query) => query.sql.includes('WITH matched_keyword_chunks'))
2280+
).toHaveLength(1)
2281+
})
2282+
22392283
it('tests the date through the document inside an on-row walk when the filtered set is unbounded', async () => {
22402284
traversedRows = [{ id: 'a' }]
22412285
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)) {
@@ -2408,20 +2415,21 @@ export async function retrieveKnowledgeSearch(
24082415
* bounded scope with their own exhaustive ordering.
24092416
*/
24102417
/**
2411-
* The row does not carry the document's date, so a date filter's documents are enumerated off
2412-
* the date index and ranked exactly while the planner estimates the window within what the
2413-
* probe may enumerate; a wider window is walked instead, with the date tested through the
2414-
* document, since a window that wide holds most of the query's neighbours anyway.
2418+
* A filter that leaves few documents is enumerated and ranked exactly inside them, both legs:
2419+
* the row does not carry the document's date, and a keyword ranking of the whole base may hold
2420+
* few of a small source's matches. A filter that leaves many is ranked as the scope is — the
2421+
* source confined on the row, the date tested through the document — since a set that large
2422+
* holds most of the query's neighbours anyway. The planner's estimate decides which.
24152423
*/
2416-
const enumerateDated =
2417-
accessPlan && params.filters?.modifiedAfter
2424+
const enumerateFiltered =
2425+
accessPlan && (params.filters?.modifiedAfter || params.filters?.source)
24182426
? (await measureSearchStage('permitted_documents', () =>
2419-
estimateDocumentsModifiedAfter(knowledgeBaseIds, params.filters!.modifiedAfter!)
2427+
estimateFilteredDocuments(knowledgeBaseIds, params.filters!, accessPlan)
24202428
)) <= VECTOR_PROBE_DOCUMENT_LIMIT
24212429
: false
24222430
const permitted =
24232431
access.kind === 'user' && params.accessProvider && !params.filters?.documentIds?.length
2424-
? accessPlan && !enumerateDated
2432+
? accessPlan && !enumerateFiltered
24252433
? /**
24262434
* With readability decided on the projection row, a resolved scope never needs its
24272435
* readable documents enumerated ahead of ranking: its reach alone chooses between one

0 commit comments

Comments
 (0)