11/** Real Assistant tool, application authorization, PostgreSQL/pgvector, and result processing. */
22import { readFileSync , statSync , writeFileSync } from 'node:fs'
3+ import type { Principal } from '@sim/auth/principal'
34import { db } from '@sim/db'
45import {
56 copilotChats ,
89 document ,
910 embedding ,
1011 knowledgeBase ,
12+ knowledgeBaseTagDefinitions ,
1113 knowledgeConnector ,
1214 knowledgeConnectorMember ,
1315 knowledgeDocumentObservation ,
@@ -39,6 +41,7 @@ import {
3941 seedKnowledgeAclFixture ,
4042 seedKnowledgeMemberFixture ,
4143} from '@/lib/knowledge/__integration__/seed-source-access-fixture'
44+ import { type KnowledgeSearchTagFilter , searchKnowledge } from '@/lib/knowledge/application/search'
4245import {
4346 SearchBudget ,
4447 SearchDeadlineError ,
@@ -124,7 +127,7 @@ const report: Record<string, unknown> = {
124127 dimensions,
125128 candidateDimensions,
126129 chunksPerDocument,
127- sql : 'Captured from the real Assistant tool ; no hand-written search query' ,
130+ sql : 'Captured from real search application adapters ; no hand-written retrieval query' ,
128131 providers :
129132 'Embedding and source-permission HTTP responses are controlled; internal search and authorization code is real' ,
130133 vectors :
@@ -197,16 +200,21 @@ function explainNodes(node: ExplainNode): ExplainNode[] {
197200}
198201
199202/** Broad ranking must stop the ordered ANN scan instead of sorting every accessible chunk. */
200- function assertIndexedCandidates ( plan : ExplainNode , candidateLimit : number ) {
203+ function assertIndexedCandidates (
204+ plan : ExplainNode ,
205+ candidateLimit : number ,
206+ width = candidateDimensions
207+ ) {
208+ const indexName =
209+ width === 1536
210+ ? 'embedding_search_cosine_hnsw_idx'
211+ : `embedding_search_${ width } _cosine_hnsw_idx`
201212 const nodes = explainNodes ( plan )
202213 const initial = nodes . find ( ( node ) => node [ 'Subplan Name' ] === 'CTE initial_candidates' )
203214 expect ( initial ) . toBeDefined ( )
204215 const candidateNodes = explainNodes ( initial ! )
205216 expect (
206- candidateNodes . some (
207- ( node ) =>
208- node [ 'Index Name' ] === 'embedding_search_512_cosine_hnsw_idx' && node [ 'Actual Loops' ] > 0
209- )
217+ candidateNodes . some ( ( node ) => node [ 'Index Name' ] === indexName && node [ 'Actual Loops' ] > 0 )
210218 ) . toBe ( true )
211219 expect ( candidateNodes . some ( ( node ) => node [ 'Node Type' ] === 'Sort' ) ) . toBe ( false )
212220 expect (
@@ -272,7 +280,7 @@ async function prepareOrganizationSample(label: string) {
272280
273281const diagnosticSchema = z
274282 . object ( {
275- surface : z . enum ( [ 'dashboard' , 'copilot' ] ) ,
283+ surface : z . enum ( [ 'dashboard' , 'copilot' , 'workflow' , 'api' ] ) ,
276284 outcome : z . enum ( [ 'success' , 'partial' ] ) ,
277285 elapsedMs : z . number ( ) ,
278286 vectorBudgetMs : z . number ( ) . positive ( ) ,
@@ -301,7 +309,12 @@ const resultSchema = z.object({
301309 success : z . literal ( true ) ,
302310 data : z . object ( {
303311 results : z . array (
304- z . object ( { documentId : z . string ( ) , content : z . string ( ) , knowledgeBaseId : z . string ( ) } )
312+ z . object ( {
313+ documentId : z . string ( ) ,
314+ content : z . string ( ) ,
315+ knowledgeBaseId : z . string ( ) ,
316+ embeddingId : z . string ( ) . optional ( ) ,
317+ } )
305318 ) ,
306319 } ) ,
307320} )
@@ -368,6 +381,29 @@ async function searchDashboard(
368381 }
369382}
370383
384+ async function searchWorkspaceKb (
385+ query = 'Orion deployment' ,
386+ options : { principal ?: Principal ; tagFilters ?: KnowledgeSearchTagFilter [ ] } = { }
387+ ) {
388+ const result = await searchKnowledge . execute ( {
389+ principal : options . principal ?? {
390+ kind : 'workspace_api_key' ,
391+ workspaceId : ids . workspaceId ,
392+ keyId : 'fixture-search-key' ,
393+ } ,
394+ input : {
395+ workspaceId : ids . workspaceId ,
396+ knowledgeBaseIds : [ ids . knowledgeBaseId ] ,
397+ query,
398+ topK : 15 ,
399+ searchMode : 'vector' ,
400+ surface : 'workflow' ,
401+ tagFilters : options . tagFilters ,
402+ } ,
403+ } )
404+ return resultSchema . parse ( { success : true , data : result } )
405+ }
406+
371407/** Allow either index or filtered plans, but require successful retrieval within the real surface budget. */
372408function expectCompleteVectorSearch ( diagnostics : z . infer < typeof diagnosticSchema > ) {
373409 const budget = diagnostics . surface === 'dashboard' ? 3000 : 8000
@@ -478,10 +514,12 @@ async function sample(
478514 } )
479515 saveReport ( )
480516 if ( query . query . includes ( 'WITH visible_search_documents' ) ) {
481- expect ( query . query ) . toContain ( '"embedding_search"."vector_512"' )
482- expect ( diagnostics . vectorCandidateDimensions ) . toBe ( candidateDimensions )
517+ const width = diagnostics . vectorCandidateDimensions !
518+ expect ( query . query ) . toContain (
519+ `"embedding_search"."${ width === 1536 ? 'vector' : `vector_${ width } ` } "`
520+ )
483521 expect ( diagnostics . vectorCandidateLimit ) . toBeGreaterThan ( 0 )
484- assertIndexedCandidates ( parsedPlan [ 0 ] . Plan , diagnostics . vectorCandidateLimit ! )
522+ assertIndexedCandidates ( parsedPlan [ 0 ] . Plan , diagnostics . vectorCandidateLimit ! , width )
485523 }
486524 if ( query . query . includes ( 'WITH visible_keyword_documents' ) ) {
487525 assertScalarKeywordSorts ( parsedPlan [ 0 ] . Plan )
@@ -495,7 +533,7 @@ async function sample(
495533 return { result, plans, diagnostics }
496534}
497535
498- describe . skipIf ( ! enabled ) ( 'Assistant search latency on a realistic indexed corpus' , ( ) => {
536+ describe . skipIf ( ! enabled ) ( 'Knowledge search latency on a realistic indexed corpus' , ( ) => {
499537 beforeAll ( async ( ) => {
500538 if (
501539 [ chunkCount , unrelatedChunkCount ] . some (
@@ -526,7 +564,7 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu
526564 . object ( {
527565 input : z . array ( z . string ( ) ) . length ( 1 ) ,
528566 encoding_format : z . literal ( 'base64' ) ,
529- model : z . literal ( 'text-embedding-3-small' ) ,
567+ model : z . enum ( [ 'text-embedding-3-small' , 'text-embedding-ada-002' ] ) ,
530568 } )
531569 . parse ( JSON . parse ( String ( init ?. body ) ) )
532570 embeddingCalls += body . input . length
@@ -1201,6 +1239,182 @@ describe.skipIf(!enabled)('Assistant search latency on a realistic indexed corpu
12011239 for ( const diagnostics of completed ) expectCompleteVectorSearch ( diagnostics )
12021240 } , 180_000 )
12031241
1242+ it ( 'uses compact indexed ranking for workspace KBs with stale estimates and private neighbors' , async ( ) => {
1243+ const originalAcl = `u:${ ids . aliceId } @fixture.test`
1244+ await db . execute ( sql `ALTER TABLE document SET (autovacuum_enabled = false)` )
1245+ try {
1246+ /** Analyze a narrow scope, then grow it without updating the planner's ACL histogram. */
1247+ await db . execute ( sql `UPDATE document SET acl = CASE WHEN external_id::int % 10 = 1
1248+ THEN ARRAY['pub'] ELSE ARRAY[${ originalAcl } ] END
1249+ WHERE knowledge_base_id = ${ ids . knowledgeBaseId } ` )
1250+ await db . execute ( sql `ANALYZE document` )
1251+ await db . execute ( sql `UPDATE document SET acl = CASE WHEN external_id::int % 5 <> 0
1252+ THEN ARRAY['pub'] ELSE ARRAY[${ originalAcl } ] END
1253+ WHERE knowledge_base_id = ${ ids . knowledgeBaseId } ` )
1254+ for ( const topic of [ 0 , 11 , 23 ] ) {
1255+ const label = `workspace-kb.topic.${ topic } `
1256+ await prepareOrganizationSample ( label )
1257+ const { result, plans, diagnostics } = await sample ( label , ( ) =>
1258+ searchWorkspaceKb ( `Topic ${ topic } deployment` )
1259+ )
1260+ expectCompleteVectorSearch ( diagnostics )
1261+ expect ( diagnostics . accessScopeKind ) . toBe ( 'workspace' )
1262+ expect ( diagnostics . vectorRanking ) . toBe ( 'candidate-rerank' )
1263+ expect ( result . data . results ) . toHaveLength ( 15 )
1264+ expect ( plans . some ( ( plan ) => plan . kind === 'vector' ) ) . toBe ( true )
1265+ for ( const row of result . data . results ) {
1266+ expect ( row . knowledgeBaseId ) . toBe ( ids . knowledgeBaseId )
1267+ expect ( Number ( row . documentId . split ( '-doc-' ) [ 1 ] ) % 5 ) . not . toBe ( 0 )
1268+ }
1269+ const expected = await db . execute < { id : string } > ( sql `
1270+ SELECT embedding.id FROM embedding JOIN document ON document.id = embedding.document_id
1271+ WHERE embedding.knowledge_base_id = ${ ids . knowledgeBaseId } AND embedding.enabled
1272+ AND document.acl = ARRAY['pub']::text[]
1273+ ORDER BY (embedding.embedding <=> ${ JSON . stringify ( topicVector ( topic ) ) } ::vector) + 0, embedding.id
1274+ LIMIT 15
1275+ ` )
1276+ const expectedIds = new Set ( expected . map ( ( { id } ) => id ) )
1277+ const recall =
1278+ result . data . results . filter ( ( row ) => expectedIds . has ( row . embeddingId ! ) ) . length /
1279+ expected . length
1280+ expect ( recall ) . toBeGreaterThanOrEqual ( 0.95 )
1281+ report [ `${ label } .recall` ] = { neighbors : expected . length , recall }
1282+ saveReport ( )
1283+ }
1284+ await db
1285+ . update ( knowledgeBase )
1286+ . set ( { embeddingModel : 'text-embedding-ada-002' } )
1287+ . where ( eq ( knowledgeBase . id , ids . knowledgeBaseId ) )
1288+ const fullWidth = await sample ( 'workspace-kb.full-width' , ( ) => searchWorkspaceKb ( ) )
1289+ expectCompleteVectorSearch ( fullWidth . diagnostics )
1290+ expect ( fullWidth . diagnostics . vectorCandidateDimensions ) . toBe ( dimensions )
1291+ expect ( fullWidth . result . data . results ) . toHaveLength ( 15 )
1292+ await db
1293+ . update ( knowledgeBase )
1294+ . set ( { embeddingModel : 'text-embedding-3-small' } )
1295+ . where ( eq ( knowledgeBase . id , ids . knowledgeBaseId ) )
1296+
1297+ const workflowId = generateId ( )
1298+ const scheduled : Principal = {
1299+ kind : 'delegated' ,
1300+ serviceId : 'executor' ,
1301+ workspaceId : ids . workspaceId ,
1302+ delegationId : generateId ( ) ,
1303+ audience : 'sim:knowledge' ,
1304+ issuedAt : new Date ( ) ,
1305+ expiresAt : new Date ( Date . now ( ) + 60_000 ) ,
1306+ delegationContext : {
1307+ kind : 'workflow_execution' ,
1308+ workflowId,
1309+ principal : {
1310+ kind : 'system' ,
1311+ serviceId : 'schedule' ,
1312+ workspaceId : ids . workspaceId ,
1313+ workflowId,
1314+ } ,
1315+ currentWorkflow : { workflowId, mode : 'deployment' , deploymentVersionId : generateId ( ) } ,
1316+ } ,
1317+ }
1318+ const scheduledResult = await sample ( 'workspace-kb.scheduled' , ( ) =>
1319+ searchWorkspaceKb ( 'Orion deployment' , { principal : scheduled } )
1320+ )
1321+ expectCompleteVectorSearch ( scheduledResult . diagnostics )
1322+ expect ( scheduledResult . diagnostics . accessScopeKind ) . toBe ( 'workspace' )
1323+ expect ( scheduledResult . result . data . results ) . toHaveLength ( 15 )
1324+
1325+ for ( const concurrency of [ 2 , 8 ] ) {
1326+ const label = `workspace-kb.concurrent.${ concurrency } `
1327+ await prepareOrganizationSample ( label )
1328+ diagnosticLog ?. mockClear ( )
1329+ const started = performance . now ( )
1330+ const results = await Promise . all (
1331+ Array . from ( { length : concurrency } , ( _ , index ) =>
1332+ searchWorkspaceKb ( `Topic ${ index * 3 } deployment` )
1333+ )
1334+ )
1335+ const completed = diagnosticLog ! . mock . calls
1336+ . filter ( ( [ message ] ) => message === 'Knowledge search completed' )
1337+ . map ( ( [ , metadata ] ) => diagnosticSchema . parse ( metadata ) )
1338+ report [ label ] = {
1339+ milliseconds : performance . now ( ) - started ,
1340+ resultCounts : results . map ( ( result ) => result . data . results . length ) ,
1341+ diagnostics : completed ,
1342+ }
1343+ saveReport ( )
1344+ expect ( completed ) . toHaveLength ( concurrency )
1345+ for ( const result of results ) expect ( result . data . results ) . toHaveLength ( 15 )
1346+ for ( const diagnostics of completed ) expectCompleteVectorSearch ( diagnostics )
1347+ }
1348+
1349+ await db . insert ( knowledgeBaseTagDefinitions ) . values ( {
1350+ id : generateId ( ) ,
1351+ knowledgeBaseId : ids . knowledgeBaseId ,
1352+ tagSlot : 'tag1' ,
1353+ displayName : 'Fixture group' ,
1354+ } )
1355+ await db . execute ( sql `UPDATE embedding SET tag1 = 'selected' WHERE knowledge_base_id = ${ ids . knowledgeBaseId }
1356+ AND document_id IN (SELECT id FROM document WHERE knowledge_base_id = ${ ids . knowledgeBaseId } AND external_id::int < 600)` )
1357+ const tagged = await sample ( 'workspace-kb.tagged' , ( ) =>
1358+ searchWorkspaceKb ( 'Orion deployment' , {
1359+ tagFilters : [ { tagName : 'Fixture group' , operator : 'eq' , value : 'selected' } ] ,
1360+ } )
1361+ )
1362+ expectCompleteVectorSearch ( tagged . diagnostics )
1363+ expect ( tagged . diagnostics . vectorRanking ) . toBe ( 'candidate-rerank' )
1364+ expect ( tagged . result . data . results ) . toHaveLength ( 15 )
1365+ for ( const row of tagged . result . data . results ) {
1366+ const ordinal = Number ( row . documentId . split ( '-doc-' ) [ 1 ] )
1367+ expect ( ordinal ) . toBeLessThan ( 600 )
1368+ expect ( ordinal % 5 ) . not . toBe ( 0 )
1369+ }
1370+ const expectedTagged = await db . execute < { id : string } > ( sql `
1371+ SELECT embedding.id FROM embedding JOIN document ON document.id = embedding.document_id
1372+ WHERE embedding.knowledge_base_id = ${ ids . knowledgeBaseId } AND embedding.enabled
1373+ AND document.acl = ARRAY['pub']::text[] AND embedding.tag1 = 'selected'
1374+ ORDER BY (embedding.embedding <=> ${ JSON . stringify ( queryVector ) } ::vector) + 0, embedding.id LIMIT 15
1375+ ` )
1376+ const taggedIds = new Set ( expectedTagged . map ( ( { id } ) => id ) )
1377+ const taggedRecall =
1378+ tagged . result . data . results . filter ( ( row ) => taggedIds . has ( row . embeddingId ! ) ) . length /
1379+ expectedTagged . length
1380+ expect ( taggedRecall ) . toBeGreaterThanOrEqual ( 0.95 )
1381+ report [ 'workspace-kb.tagged.recall' ] = {
1382+ neighbors : expectedTagged . length ,
1383+ recall : taggedRecall ,
1384+ }
1385+ saveReport ( )
1386+
1387+ /** Workspace credentials cannot keep reading a source after its access rewrite begins. */
1388+ await db
1389+ . update ( knowledgeConnector )
1390+ . set ( { accessRewritePending : true } )
1391+ . where ( eq ( knowledgeConnector . id , ids . connectorId ) )
1392+ const denied = await sample ( 'workspace-kb.revoked' , ( ) => searchWorkspaceKb ( ) )
1393+ expectCompleteVectorSearch ( denied . diagnostics )
1394+ expect ( denied . result . data . results ) . toEqual ( [ ] )
1395+ } finally {
1396+ await db
1397+ . delete ( knowledgeBaseTagDefinitions )
1398+ . where ( eq ( knowledgeBaseTagDefinitions . knowledgeBaseId , ids . knowledgeBaseId ) )
1399+ await db . execute (
1400+ sql `UPDATE embedding SET tag1 = NULL WHERE knowledge_base_id = ${ ids . knowledgeBaseId } AND tag1 = 'selected'`
1401+ )
1402+ await db
1403+ . update ( knowledgeBase )
1404+ . set ( { embeddingModel : 'text-embedding-3-small' } )
1405+ . where ( eq ( knowledgeBase . id , ids . knowledgeBaseId ) )
1406+ await db
1407+ . update ( knowledgeConnector )
1408+ . set ( { accessRewritePending : false } )
1409+ . where ( eq ( knowledgeConnector . id , ids . connectorId ) )
1410+ await db . execute (
1411+ sql `UPDATE document SET acl = ARRAY[${ originalAcl } ] WHERE knowledge_base_id = ${ ids . knowledgeBaseId } `
1412+ )
1413+ await db . execute ( sql `ALTER TABLE document RESET (autovacuum_enabled)` )
1414+ await db . execute ( sql `ANALYZE document` )
1415+ }
1416+ } , 180_000 )
1417+
12041418 it ( 'checks live reader access on every search, including after revocation' , async ( ) => {
12051419 await seedSearchReaderFixture ( ids )
12061420 const allowed = await sample ( 'live.allowed' , ( ) => search ( ) )
0 commit comments