@@ -1184,7 +1184,19 @@ 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. */
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 CountedReach {
1192+ broad : boolean
1193+ empty : boolean
1194+ }
1195+
1196+ /**
1197+ * Only breadth is remembered. Emptiness decides completeness, not strategy, so it is counted on
1198+ * every search: the count of a reach of nothing finds nothing and costs almost nothing.
1199+ */
11881200const saturatedReach = new LRUCache < string , { broad : boolean } > ( {
11891201 max : 10_000 ,
11901202 ttl : SATURATED_REACH_TTL_MS ,
@@ -1249,32 +1261,36 @@ async function estimateFilteredDocuments(
12491261}
12501262
12511263/**
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.
1264+ * How far a caller reaches: broad when they reach at least {@link BROAD_REACH_SHARE} of the
1265+ * bases' documents, empty when they reach none. A reach of nothing is a bounded set of nothing: a
1266+ * caller who reads no document in these bases, such as a member with no source of their own yet,
1267+ * has nothing for any leg to rank, where an unbounded set would have each leg scan to its
1268+ * deadline for rows it cannot find. Breadth is counted once against the bound and remembered, so
1269+ * the first search after the window pays for it and the rest do not. A caller whose probe already
1270+ * saturated is known to reach past the probe's limit, so a bound inside that limit is met without
1271+ * counting.
12561272 *
12571273 * The count reads as many index entries as the caller reaches, so on a large index it can cost
12581274 * more than the leg it serves; it gets the probe's share of the deadline, never the whole leg's.
12591275 * A count that runs out of that share answers `null`: the leg keeps its time and its deadline
12601276 * intact, and the caller decides this search alone without remembering anything.
12611277 */
1262- async function reachIsBroad (
1278+ async function countReach (
12631279 knowledgeBaseIds : string [ ] ,
12641280 access : KnowledgeAccessScope ,
12651281 budget : SearchBudget | undefined ,
12661282 plan : SearchAccessPlan | undefined ,
12671283 saturated : boolean
1268- ) : Promise < boolean | null > {
1269- if ( access . kind !== 'user' ) return true
1284+ ) : Promise < CountedReach | null > {
1285+ if ( access . kind !== 'user' ) return { broad : true , empty : false }
12701286 const countBudget = budget ?. capped ( VECTOR_PROBE_BUDGET_MS )
12711287 try {
12721288 const total =
12731289 ( await indexDocumentCounts . fetch ( [ ...knowledgeBaseIds ] . sort ( ) . join ( ',' ) , {
12741290 context : countBudget ,
12751291 } ) ) ?? 0
12761292 const bound = Math . ceil ( total * BROAD_REACH_SHARE )
1277- if ( saturated && bound <= VECTOR_PROBE_DOCUMENT_LIMIT ) return true
1293+ if ( saturated && bound <= VECTOR_PROBE_DOCUMENT_LIMIT ) return { broad : true , empty : false }
12781294 const [ row ] = await runSearchQuery ( countBudget , 'permitted_documents' , ( executor ) =>
12791295 executor . execute < { n : number } > ( sql `
12801296 SELECT count(*) AS n FROM (
@@ -1288,7 +1304,9 @@ async function reachIsBroad(
12881304 LIMIT ${ bound }
12891305 ) reached` )
12901306 )
1291- return Number ( row ?. n ?? 0 ) >= bound
1307+ const reached = Number ( row ?. n ?? 0 )
1308+ /** A count that looked and found nothing: only a bound of zero looks at nothing. */
1309+ return { broad : reached >= bound , empty : bound > 0 && reached === 0 }
12921310 } catch ( error ) {
12931311 if ( ! budget || ! countBudget ?. isTimeout ( error ) ) throw error
12941312 /** Only the count's share was spent; the leg's own deadline still governs. */
@@ -1345,11 +1363,12 @@ export async function resolveReach(
13451363 const remembered = key ? saturatedReach . get ( key ) : undefined
13461364 if ( remembered ) return { kind : 'unbounded' , broad : remembered . broad }
13471365 try {
1348- const broad = await reachIsBroad ( knowledgeBaseIds , access , budget , plan , false )
1366+ const reach = await countReach ( knowledgeBaseIds , access , budget , plan , false )
13491367 /** 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 }
1368+ if ( reach === null ) return { kind : 'unbounded' , broad : true }
1369+ if ( reach . empty ) return { kind : 'bounded' , documents : [ ] }
1370+ if ( key ) saturatedReach . set ( key , { broad : reach . broad } )
1371+ return { kind : 'unbounded' , broad : reach . broad }
13531372 } catch ( error ) {
13541373 /** The leg's own deadline passed during the count: the leg is short, the search is not failed. */
13551374 if ( ! budget ?. isTimeout ( error ) ) throw error
@@ -1408,16 +1427,17 @@ export async function resolvePermittedDocuments(params: {
14081427 }
14091428 if ( probe . kind === 'saturated' ) {
14101429 try {
1411- const counted = await reachIsBroad (
1430+ const reach = await countReach (
14121431 params . knowledgeBaseIds ,
14131432 params . access ,
14141433 params . budget ,
14151434 params . accessPlan ,
14161435 true
14171436 )
14181437 /** A count that ran out of time decides this search only; the next one counts again. */
1419- if ( counted !== null ) {
1420- broad = counted
1438+ if ( reach ?. empty ) probe = { kind : 'documents' , documents : [ ] }
1439+ else if ( reach !== null ) {
1440+ broad = reach . broad
14211441 if ( key ) saturatedReach . set ( key , { broad } )
14221442 }
14231443 } catch ( error ) {
0 commit comments