@@ -1184,8 +1184,16 @@ export const BROAD_REACH_SHARE = 0.25
11841184 */
11851185const SATURATED_REACH_TTL_MS = 5 * 60 * 1000
11861186
1187- /** A saturated reach, and whether it is broad enough to walk the whole graph for. */
1188- const saturatedReach = new LRUCache < string , { broad : boolean } > ( {
1187+ /**
1188+ * A counted reach: whether it is broad enough to walk the whole graph for, or empty, in which
1189+ * case the caller reads nothing in these bases and no leg has anything to rank.
1190+ */
1191+ interface RememberedReach {
1192+ broad : boolean
1193+ empty : boolean
1194+ }
1195+
1196+ const saturatedReach = new LRUCache < string , RememberedReach > ( {
11891197 max : 10_000 ,
11901198 ttl : SATURATED_REACH_TTL_MS ,
11911199} )
@@ -1249,32 +1257,33 @@ async function estimateFilteredDocuments(
12491257}
12501258
12511259/**
1252- * Whether a reach is broad: the caller reaches at least {@link BROAD_REACH_SHARE} of the bases'
1253- * documents. Counted once against that bound and remembered, so the first search after the
1254- * window pays for it and the rest do not. A caller whose probe already saturated is known to
1255- * reach past the probe's limit, so a bound inside that limit is met without counting.
1260+ * How far a caller reaches: broad when they reach at least {@link BROAD_REACH_SHARE} of the
1261+ * bases' documents, empty when they reach none. Counted once against that bound and remembered,
1262+ * so the first search after the window pays for it and the rest do not. A caller whose probe
1263+ * already saturated is known to reach past the probe's limit, so a bound inside that limit is
1264+ * met without counting.
12561265 *
12571266 * The count reads as many index entries as the caller reaches, so on a large index it can cost
12581267 * more than the leg it serves; it gets the probe's share of the deadline, never the whole leg's.
12591268 * A count that runs out of that share answers `null`: the leg keeps its time and its deadline
12601269 * intact, and the caller decides this search alone without remembering anything.
12611270 */
1262- async function reachIsBroad (
1271+ async function countReach (
12631272 knowledgeBaseIds : string [ ] ,
12641273 access : KnowledgeAccessScope ,
12651274 budget : SearchBudget | undefined ,
12661275 plan : SearchAccessPlan | undefined ,
12671276 saturated : boolean
1268- ) : Promise < boolean | null > {
1269- if ( access . kind !== 'user' ) return true
1277+ ) : Promise < RememberedReach | null > {
1278+ if ( access . kind !== 'user' ) return { broad : true , empty : false }
12701279 const countBudget = budget ?. capped ( VECTOR_PROBE_BUDGET_MS )
12711280 try {
12721281 const total =
12731282 ( await indexDocumentCounts . fetch ( [ ...knowledgeBaseIds ] . sort ( ) . join ( ',' ) , {
12741283 context : countBudget ,
12751284 } ) ) ?? 0
12761285 const bound = Math . ceil ( total * BROAD_REACH_SHARE )
1277- if ( saturated && bound <= VECTOR_PROBE_DOCUMENT_LIMIT ) return true
1286+ if ( saturated && bound <= VECTOR_PROBE_DOCUMENT_LIMIT ) return { broad : true , empty : false }
12781287 const [ row ] = await runSearchQuery ( countBudget , 'permitted_documents' , ( executor ) =>
12791288 executor . execute < { n : number } > ( sql `
12801289 SELECT count(*) AS n FROM (
@@ -1288,7 +1297,9 @@ async function reachIsBroad(
12881297 LIMIT ${ bound }
12891298 ) reached` )
12901299 )
1291- return Number ( row ?. n ?? 0 ) >= bound
1300+ const reached = Number ( row ?. n ?? 0 )
1301+ /** A count that looked and found nothing: only a bound of zero looks at nothing. */
1302+ return { broad : reached >= bound , empty : bound > 0 && reached === 0 }
12921303 } catch ( error ) {
12931304 if ( ! budget || ! countBudget ?. isTimeout ( error ) ) throw error
12941305 /** Only the count's share was spent; the leg's own deadline still governs. */
@@ -1343,20 +1354,31 @@ export async function resolveReach(
13431354) : Promise < PermittedDocuments > {
13441355 const key = reachKey ( knowledgeBaseIds , access , plan )
13451356 const remembered = key ? saturatedReach . get ( key ) : undefined
1346- if ( remembered ) return { kind : 'unbounded' , broad : remembered . broad }
1357+ if ( remembered ) return permittedFromReach ( remembered )
13471358 try {
1348- const broad = await reachIsBroad ( knowledgeBaseIds , access , budget , plan , false )
1359+ const reach = await countReach ( knowledgeBaseIds , access , budget , plan , false )
13491360 /** A count that ran out of time decides this search only; the next one counts again. */
1350- if ( broad === null ) return { kind : 'unbounded' , broad : true }
1351- if ( key ) saturatedReach . set ( key , { broad } )
1352- return { kind : 'unbounded' , broad }
1361+ if ( reach === null ) return { kind : 'unbounded' , broad : true }
1362+ if ( key ) saturatedReach . set ( key , reach )
1363+ return permittedFromReach ( reach )
13531364 } catch ( error ) {
13541365 /** The leg's own deadline passed during the count: the leg is short, the search is not failed. */
13551366 if ( ! budget ?. isTimeout ( error ) ) throw error
13561367 return { kind : 'unbounded' , broad : true }
13571368 }
13581369}
13591370
1371+ /**
1372+ * A reach of nothing is a bounded set of nothing: a caller who reads no document in these bases,
1373+ * such as a member with no source of their own yet, has nothing for any leg to rank, where an
1374+ * unbounded set would have each leg scan for rows it cannot find.
1375+ */
1376+ function permittedFromReach ( reach : RememberedReach ) : PermittedDocuments {
1377+ return reach . empty
1378+ ? { kind : 'bounded' , documents : [ ] }
1379+ : { kind : 'unbounded' , broad : reach . broad }
1380+ }
1381+
13601382/**
13611383 * Resolve the permitted set with the candidate predicate both legs apply, so restricting a leg
13621384 * to it never admits a document the leg would otherwise refuse. Tag filters stay chunk-level in
@@ -1384,6 +1406,10 @@ export async function resolvePermittedDocuments(params: {
13841406 params . accessPlan && ( dateFilterCondition ( params . filters ) || params . filters ?. source )
13851407 )
13861408 const remembered = key && ! filteredDirectly ? saturatedReach . get ( key ) : undefined
1409+ if ( remembered ?. empty ) {
1410+ annotateSearchDiagnostics ( { permittedDocuments : 'bounded' , permittedDocumentCount : 0 } )
1411+ return { kind : 'bounded' , documents : [ ] }
1412+ }
13871413 if ( remembered ) {
13881414 probe = { kind : 'saturated' }
13891415 broad = remembered . broad
@@ -1408,17 +1434,17 @@ export async function resolvePermittedDocuments(params: {
14081434 }
14091435 if ( probe . kind === 'saturated' ) {
14101436 try {
1411- const counted = await reachIsBroad (
1437+ const reach = await countReach (
14121438 params . knowledgeBaseIds ,
14131439 params . access ,
14141440 params . budget ,
14151441 params . accessPlan ,
14161442 true
14171443 )
14181444 /** A count that ran out of time decides this search only; the next one counts again. */
1419- if ( counted !== null ) {
1420- broad = counted
1421- if ( key ) saturatedReach . set ( key , { broad } )
1445+ if ( reach !== null ) {
1446+ broad = reach . broad
1447+ if ( key ) saturatedReach . set ( key , reach )
14221448 }
14231449 } catch ( error ) {
14241450 /** The leg's own deadline passed during the count: the leg is short, the search is not failed. */
0 commit comments