Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions apps/sim/lib/knowledge/search/queries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2051,6 +2051,75 @@ describe('permitted-document planner', () => {
expect(reachCounts()).toHaveLength(2)
})

it('reports a caller who reaches nothing as a bounded set of nothing, counted every time', async () => {
dbChainMockFns.execute.mockImplementation(async (query) => {
const statement = render(query).sql
if (statement.includes('EXPLAIN'))
return [{ 'QUERY PLAN': [{ Plan: { 'Plan Rows': 1_000_000 } }] }]
if (statement.includes(') reached')) return [{ n: 0 }]
return []
})
const reachCounts = () => statements().filter((query) => query.sql.includes(') reached'))
const plan = {
connectors: { workspace: [], admin: [], members: [], liveProofRequired: [] },
observers: { confirmed: [], observed: [] },
memberSources: [],
connectorTypes: new Map(),
uploads: true,
}
const budget = () => new SearchBudget('vector', performance.now() + 10_000)
await expect(
resolveReach(['org-index'], scope('reaches-nothing'), budget(), plan)
).resolves.toEqual({ kind: 'bounded', documents: [] })
/** Emptiness decides completeness, so it is never remembered: the next search counts again. */
await expect(
resolveReach(['org-index'], scope('reaches-nothing'), budget(), plan)
).resolves.toEqual({ kind: 'bounded', documents: [] })
expect(reachCounts()).toHaveLength(2)
})

it('reports a saturated probe whose count then finds nothing as a bounded set of nothing', async () => {
dbChainMockFns.execute.mockImplementation(async (query) => {
const statement = render(query).sql
if (isProbeStatement(statement)) return [{ id: null, connectorId: null, saturated: true }]
if (statement.includes('EXPLAIN'))
return [{ 'QUERY PLAN': [{ Plan: { 'Plan Rows': 1_000_000 } }] }]
if (statement.includes(') reached')) return [{ n: 0 }]
return []
})
await expect(
resolvePermittedDocuments({
knowledgeBaseIds: ['org-index'],
access: scope('saturated-then-nothing'),
budget: new SearchBudget('vector', performance.now() + 10_000),
})
).resolves.toEqual({ kind: 'bounded', documents: [] })
})

it('does not read an unanalyzed index as a reach of nothing', async () => {
/** The planner knows no rows yet, so the bound is zero and the count looked at nothing. */
dbChainMockFns.execute.mockImplementation(async (query) => {
const statement = render(query).sql
if (statement.includes('EXPLAIN')) return [{ 'QUERY PLAN': [{ Plan: { 'Plan Rows': 0 } }] }]
if (statement.includes(') reached')) return [{ n: 0 }]
return []
})
await expect(
resolveReach(
['org-index'],
scope('unanalyzed'),
new SearchBudget('vector', performance.now() + 10_000),
{
connectors: { workspace: [], admin: [], members: [], liveProofRequired: [] },
observers: { confirmed: [], observed: [] },
memberSources: [],
connectorTypes: new Map(),
uploads: true,
}
)
).resolves.toEqual({ kind: 'unbounded', broad: true })
})

it('reports a leg whose own deadline passed during the count as short, not failed', async () => {
const budget = new SearchBudget('vector', performance.now() - 1)
await expect(
Expand Down
54 changes: 37 additions & 17 deletions apps/sim/lib/knowledge/search/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1184,7 +1184,19 @@ export const BROAD_REACH_SHARE = 0.25
*/
const SATURATED_REACH_TTL_MS = 5 * 60 * 1000

/** A saturated reach, and whether it is broad enough to walk the whole graph for. */
/**
* A counted reach: whether it is broad enough to walk the whole graph for, or empty, in which
* case the caller reads nothing in these bases and no leg has anything to rank.
*/
interface CountedReach {
broad: boolean
empty: boolean
}

/**
* Only breadth is remembered. Emptiness decides completeness, not strategy, so it is counted on
* every search: the count of a reach of nothing finds nothing and costs almost nothing.
*/
const saturatedReach = new LRUCache<string, { broad: boolean }>({
max: 10_000,
ttl: SATURATED_REACH_TTL_MS,
Expand Down Expand Up @@ -1249,32 +1261,36 @@ async function estimateFilteredDocuments(
}

/**
* Whether a reach is broad: the caller reaches at least {@link BROAD_REACH_SHARE} of the bases'
* documents. Counted once against that bound and remembered, so the first search after the
* window pays for it and the rest do not. A caller whose probe already saturated is known to
* reach past the probe's limit, so a bound inside that limit is met without counting.
* How far a caller reaches: broad when they reach at least {@link BROAD_REACH_SHARE} of the
* bases' documents, empty when they reach none. A reach of nothing is a bounded set of nothing: a
* caller who reads no document in these bases, such as a member with no source of their own yet,
* has nothing for any leg to rank, where an unbounded set would have each leg scan to its
* deadline for rows it cannot find. Breadth is counted once against the bound and remembered, so
* the first search after the window pays for it and the rest do not. A caller whose probe already
* saturated is known to reach past the probe's limit, so a bound inside that limit is met without
* counting.
*
* The count reads as many index entries as the caller reaches, so on a large index it can cost
* more than the leg it serves; it gets the probe's share of the deadline, never the whole leg's.
* A count that runs out of that share answers `null`: the leg keeps its time and its deadline
* intact, and the caller decides this search alone without remembering anything.
*/
async function reachIsBroad(
async function countReach(
knowledgeBaseIds: string[],
access: KnowledgeAccessScope,
budget: SearchBudget | undefined,
plan: SearchAccessPlan | undefined,
saturated: boolean
): Promise<boolean | null> {
if (access.kind !== 'user') return true
): Promise<CountedReach | null> {
if (access.kind !== 'user') return { broad: true, empty: false }
const countBudget = budget?.capped(VECTOR_PROBE_BUDGET_MS)
try {
const total =
(await indexDocumentCounts.fetch([...knowledgeBaseIds].sort().join(','), {
context: countBudget,
})) ?? 0
const bound = Math.ceil(total * BROAD_REACH_SHARE)
if (saturated && bound <= VECTOR_PROBE_DOCUMENT_LIMIT) return true
if (saturated && bound <= VECTOR_PROBE_DOCUMENT_LIMIT) return { broad: true, empty: false }
const [row] = await runSearchQuery(countBudget, 'permitted_documents', (executor) =>
executor.execute<{ n: number }>(sql`
SELECT count(*) AS n FROM (
Expand All @@ -1288,7 +1304,9 @@ async function reachIsBroad(
LIMIT ${bound}
) reached`)
)
return Number(row?.n ?? 0) >= bound
const reached = Number(row?.n ?? 0)
/** A count that looked and found nothing: only a bound of zero looks at nothing. */
return { broad: reached >= bound, empty: bound > 0 && reached === 0 }
} catch (error) {
if (!budget || !countBudget?.isTimeout(error)) throw error
/** Only the count's share was spent; the leg's own deadline still governs. */
Expand Down Expand Up @@ -1345,11 +1363,12 @@ export async function resolveReach(
const remembered = key ? saturatedReach.get(key) : undefined
if (remembered) return { kind: 'unbounded', broad: remembered.broad }
try {
const broad = await reachIsBroad(knowledgeBaseIds, access, budget, plan, false)
const reach = await countReach(knowledgeBaseIds, access, budget, plan, false)
/** A count that ran out of time decides this search only; the next one counts again. */
if (broad === null) return { kind: 'unbounded', broad: true }
if (key) saturatedReach.set(key, { broad })
return { kind: 'unbounded', broad }
if (reach === null) return { kind: 'unbounded', broad: true }
if (reach.empty) return { kind: 'bounded', documents: [] }
if (key) saturatedReach.set(key, { broad: reach.broad })
return { kind: 'unbounded', broad: reach.broad }
} catch (error) {
/** The leg's own deadline passed during the count: the leg is short, the search is not failed. */
if (!budget?.isTimeout(error)) throw error
Expand Down Expand Up @@ -1408,16 +1427,17 @@ export async function resolvePermittedDocuments(params: {
}
if (probe.kind === 'saturated') {
try {
const counted = await reachIsBroad(
const reach = await countReach(
params.knowledgeBaseIds,
params.access,
params.budget,
params.accessPlan,
true
)
/** A count that ran out of time decides this search only; the next one counts again. */
if (counted !== null) {
broad = counted
if (reach?.empty) probe = { kind: 'documents', documents: [] }
else if (reach !== null) {
broad = reach.broad
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
if (key) saturatedReach.set(key, { broad })
}
} catch (error) {
Expand Down
Loading