@@ -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