Skip to content

Commit 50d5770

Browse files
committed
improvement(knowledge): exclude a denied source through its documents while the projection is unfilled
1 parent 2f3634c commit 50d5770

2 files changed

Lines changed: 53 additions & 7 deletions

File tree

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2076,6 +2076,48 @@ describe('permitted-document planner', () => {
20762076
expect(JSON.stringify(pages[1].params)).toContain('"b"')
20772077
})
20782078

2079+
it('excludes a denied source through its documents while the projection is unfilled', async () => {
2080+
queueTableRows(schemaMock.knowledgeConnector, [
2081+
{
2082+
id: 'gated-src',
2083+
accessMode: 'admin',
2084+
connectorType: 'confluence',
2085+
githubRepository: false,
2086+
},
2087+
])
2088+
dbChainMockFns.execute.mockImplementation(async (query) => {
2089+
const statement = render(query).sql
2090+
/** The fill has not reached every row, so a denied source cannot be read off the row. */
2091+
if (statement.includes('AS unfilled')) return [{ unfilled: true }]
2092+
const rebuilt = JSON.stringify(query).includes('NOT EXISTS')
2093+
if (isPageStatement(statement))
2094+
return JSON.stringify(render(query).params).includes('"b"')
2095+
? [hit('b', 'other-src')]
2096+
: [hit('a', 'gated-src')]
2097+
if (isWalk(statement))
2098+
return Array.from({ length: 400 }, (_, i) => ({
2099+
id: i === 0 ? (rebuilt ? 'b' : 'a') : `w-${i}`,
2100+
distance: 0.1,
2101+
}))
2102+
return []
2103+
})
2104+
queueTableRows(schemaMock.embedding, [])
2105+
queueTableRows(schemaMock.embedding, [hit('b', 'other-src')])
2106+
const getForConnectors = vi.fn<KnowledgeAccessProvider['getForConnectors']>(async () => reader)
2107+
const result = await retrieveKnowledgeSearch({
2108+
...liveSearch,
2109+
searchMode: 'vector',
2110+
access: reader,
2111+
accessProvider: { ...provider, getForConnectors },
2112+
})
2113+
expect(result.rows.map((row) => row.id)).toEqual(['b'])
2114+
const walks = statements().filter((query) => isWalk(query.sql))
2115+
expect(walks).toHaveLength(2)
2116+
expect(JSON.stringify(walks[1])).toContain('NOT EXISTS')
2117+
expect(JSON.stringify(walks[1])).toContain('gated-src')
2118+
forgetProjectionFilled()
2119+
})
2120+
20792121
it('hands back the unread slices of a page a denied source made it rebuild', async () => {
20802122
queueTableRows(schemaMock.knowledgeConnector, [
20812123
{

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1722,12 +1722,22 @@ async function selectVectorResults(params: SearchParams): Promise<SearchResult[]
17221722
return { candidates, nextOffset: offset + candidates.length }
17231723
}
17241724
if (params.filters?.documentIds?.length) return exactPage()
1725+
const plan = params.access.kind === 'user' ? params.accessPlan : undefined
1726+
const filled = plan
1727+
? ((await projectionFilled.fetch('embedding_search', {
1728+
context: { budget: params.budget, stage: 'vector.projection_filled' },
1729+
})) ?? false)
1730+
: false
17251731
/**
17261732
* A source the caller turned out not to hold is left out where the pool is built: the pool
17271733
* is the page's order now, so a denied source's chunks would otherwise keep their slots.
1734+
* The row's mirrored source decides it once the fill is complete; until then a row the fill
1735+
* has not reached carries no source, so its document is asked instead.
17281736
*/
17291737
const excludedOnRow = excludedSources.length
1730-
? sql`(${embeddingSearch.connectorId} IS NULL OR NOT (${embeddingSearch.connectorId} = ANY(${textArrayLiteral([...excludedSources])})))`
1738+
? filled
1739+
? sql`(${embeddingSearch.connectorId} IS NULL OR NOT (${embeddingSearch.connectorId} = ANY(${textArrayLiteral([...excludedSources])})))`
1740+
: sql`NOT EXISTS (SELECT 1 FROM ${document} WHERE ${document.id} = ${embeddingSearch.documentId} AND ${document.connectorId} = ANY(${textArrayLiteral([...excludedSources])}))`
17311741
: undefined
17321742
const needed = offset + limit
17331743
if (
@@ -1776,12 +1786,6 @@ async function selectVectorResults(params: SearchParams): Promise<SearchResult[]
17761786
)
17771787
}
17781788
let selected: Array<{ id: string }>
1779-
const plan = params.access.kind === 'user' ? params.accessPlan : undefined
1780-
const filled = plan
1781-
? ((await projectionFilled.fetch('embedding_search', {
1782-
context: { budget: params.budget, stage: 'vector.projection_filled' },
1783-
})) ?? false)
1784-
: false
17851789
/**
17861790
* A source the caller is a member of that has its own index is walked on its own, which
17871791
* beats ranking it exactly once it is large enough to have earned that index.

0 commit comments

Comments
 (0)