Skip to content

Commit 72e4068

Browse files
committed
fix(knowledge): count emptiness on every search and honor it on the probe path
Emptiness decides completeness, not strategy, so it is never remembered: a member who gains a readable document is answered on their next search, and the count of a reach of nothing costs almost nothing. A saturated probe whose count then finds nothing answers the same bounded empty set.
1 parent acff580 commit 72e4068

2 files changed

Lines changed: 37 additions & 35 deletions

File tree

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2051,7 +2051,7 @@ describe('permitted-document planner', () => {
20512051
expect(reachCounts()).toHaveLength(2)
20522052
})
20532053

2054-
it('reports a caller who reaches nothing as a bounded set of nothing, and remembers it', async () => {
2054+
it('reports a caller who reaches nothing as a bounded set of nothing, counted every time', async () => {
20552055
dbChainMockFns.execute.mockImplementation(async (query) => {
20562056
const statement = render(query).sql
20572057
if (statement.includes('EXPLAIN'))
@@ -2071,21 +2071,29 @@ describe('permitted-document planner', () => {
20712071
await expect(
20722072
resolveReach(['org-index'], scope('reaches-nothing'), budget(), plan)
20732073
).resolves.toEqual({ kind: 'bounded', documents: [] })
2074-
expect(reachCounts()).toHaveLength(1)
2075-
/** Remembered like any reach: the next search neither counts nor probes. */
2074+
/** Emptiness decides completeness, so it is never remembered: the next search counts again. */
20762075
await expect(
20772076
resolveReach(['org-index'], scope('reaches-nothing'), budget(), plan)
20782077
).resolves.toEqual({ kind: 'bounded', documents: [] })
2078+
expect(reachCounts()).toHaveLength(2)
2079+
})
2080+
2081+
it('reports a saturated probe whose count then finds nothing as a bounded set of nothing', async () => {
2082+
dbChainMockFns.execute.mockImplementation(async (query) => {
2083+
const statement = render(query).sql
2084+
if (isProbeStatement(statement)) return [{ id: null, connectorId: null, saturated: true }]
2085+
if (statement.includes('EXPLAIN'))
2086+
return [{ 'QUERY PLAN': [{ Plan: { 'Plan Rows': 1_000_000 } }] }]
2087+
if (statement.includes(') reached')) return [{ n: 0 }]
2088+
return []
2089+
})
20792090
await expect(
20802091
resolvePermittedDocuments({
20812092
knowledgeBaseIds: ['org-index'],
2082-
access: scope('reaches-nothing'),
2083-
budget: budget(),
2084-
accessPlan: plan,
2093+
access: scope('saturated-then-nothing'),
2094+
budget: new SearchBudget('vector', performance.now() + 10_000),
20852095
})
20862096
).resolves.toEqual({ kind: 'bounded', documents: [] })
2087-
expect(reachCounts()).toHaveLength(1)
2088-
expect(probes()).toBe(0)
20892097
})
20902098

20912099
it('does not read an unanalyzed index as a reach of nothing', async () => {

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

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,12 +1188,16 @@ const SATURATED_REACH_TTL_MS = 5 * 60 * 1000
11881188
* A counted reach: whether it is broad enough to walk the whole graph for, or empty, in which
11891189
* case the caller reads nothing in these bases and no leg has anything to rank.
11901190
*/
1191-
interface RememberedReach {
1191+
interface CountedReach {
11921192
broad: boolean
11931193
empty: boolean
11941194
}
11951195

1196-
const saturatedReach = new LRUCache<string, RememberedReach>({
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+
*/
1200+
const saturatedReach = new LRUCache<string, { broad: boolean }>({
11971201
max: 10_000,
11981202
ttl: SATURATED_REACH_TTL_MS,
11991203
})
@@ -1258,10 +1262,13 @@ async function estimateFilteredDocuments(
12581262

12591263
/**
12601264
* 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.
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.
12651272
*
12661273
* The count reads as many index entries as the caller reaches, so on a large index it can cost
12671274
* more than the leg it serves; it gets the probe's share of the deadline, never the whole leg's.
@@ -1274,7 +1281,7 @@ async function countReach(
12741281
budget: SearchBudget | undefined,
12751282
plan: SearchAccessPlan | undefined,
12761283
saturated: boolean
1277-
): Promise<RememberedReach | null> {
1284+
): Promise<CountedReach | null> {
12781285
if (access.kind !== 'user') return { broad: true, empty: false }
12791286
const countBudget = budget?.capped(VECTOR_PROBE_BUDGET_MS)
12801287
try {
@@ -1354,31 +1361,21 @@ export async function resolveReach(
13541361
): Promise<PermittedDocuments> {
13551362
const key = reachKey(knowledgeBaseIds, access, plan)
13561363
const remembered = key ? saturatedReach.get(key) : undefined
1357-
if (remembered) return permittedFromReach(remembered)
1364+
if (remembered) return { kind: 'unbounded', broad: remembered.broad }
13581365
try {
13591366
const reach = await countReach(knowledgeBaseIds, access, budget, plan, false)
13601367
/** A count that ran out of time decides this search only; the next one counts again. */
13611368
if (reach === null) return { kind: 'unbounded', broad: true }
1362-
if (key) saturatedReach.set(key, reach)
1363-
return permittedFromReach(reach)
1369+
if (reach.empty) return { kind: 'bounded', documents: [] }
1370+
if (key) saturatedReach.set(key, { broad: reach.broad })
1371+
return { kind: 'unbounded', broad: reach.broad }
13641372
} catch (error) {
13651373
/** The leg's own deadline passed during the count: the leg is short, the search is not failed. */
13661374
if (!budget?.isTimeout(error)) throw error
13671375
return { kind: 'unbounded', broad: true }
13681376
}
13691377
}
13701378

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-
13821379
/**
13831380
* Resolve the permitted set with the candidate predicate both legs apply, so restricting a leg
13841381
* to it never admits a document the leg would otherwise refuse. Tag filters stay chunk-level in
@@ -1406,10 +1403,6 @@ export async function resolvePermittedDocuments(params: {
14061403
params.accessPlan && (dateFilterCondition(params.filters) || params.filters?.source)
14071404
)
14081405
const remembered = key && !filteredDirectly ? saturatedReach.get(key) : undefined
1409-
if (remembered?.empty) {
1410-
annotateSearchDiagnostics({ permittedDocuments: 'bounded', permittedDocumentCount: 0 })
1411-
return { kind: 'bounded', documents: [] }
1412-
}
14131406
if (remembered) {
14141407
probe = { kind: 'saturated' }
14151408
broad = remembered.broad
@@ -1442,9 +1435,10 @@ export async function resolvePermittedDocuments(params: {
14421435
true
14431436
)
14441437
/** A count that ran out of time decides this search only; the next one counts again. */
1445-
if (reach !== null) {
1438+
if (reach?.empty) probe = { kind: 'documents', documents: [] }
1439+
else if (reach !== null) {
14461440
broad = reach.broad
1447-
if (key) saturatedReach.set(key, reach)
1441+
if (key) saturatedReach.set(key, { broad })
14481442
}
14491443
} catch (error) {
14501444
/** The leg's own deadline passed during the count: the leg is short, the search is not failed. */

0 commit comments

Comments
 (0)