Skip to content

Commit e72dea2

Browse files
committed
fix(knowledge): align search fixtures with shared retrieval
1 parent 5c2e6c7 commit e72dea2

2 files changed

Lines changed: 65 additions & 50 deletions

File tree

apps/sim/app/api/knowledge/search/utils.test.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ describe('Knowledge Search Utils', () => {
217217
Array.from({ length: 201 }, (_, index) => ({ id: `candidate-${index}` }))
218218
)
219219
queueTableRows(schemaMock.embedding, [makeResult('second', 0.2), makeResult('first', 0.1)])
220+
queueTableRows(schemaMock.embedding, [makeResult('second', 0.2), makeResult('first', 0.1)])
220221

221222
const results = await handleTagAndVectorSearch({
222223
knowledgeBaseIds: ['kb-1', 'kb-2'],
@@ -231,9 +232,8 @@ describe('Knowledge Search Utils', () => {
231232

232233
expect(results.map((row) => row.id)).toEqual(['first', 'second'])
233234
expect(dbChainMockFns.select).toHaveBeenCalledTimes(3)
234-
expect(dbChainMockFns.as).toHaveBeenCalledWith('ranked_embeddings')
235235
expect(Object.keys(dbChainMockFns.select.mock.calls[0][0])).toEqual(['id'])
236-
expect(dbChainMockFns.limit).toHaveBeenNthCalledWith(1, 201)
236+
expect(dbChainMockFns.limit).toHaveBeenNthCalledWith(1, 400)
237237
expect(dbChainMockFns.select.mock.calls[1][0]).toHaveProperty('distance')
238238
expect(dbChainMockFns.limit).toHaveBeenCalledWith(2)
239239
})
@@ -463,11 +463,7 @@ describe('Knowledge Search Utils', () => {
463463
queryVector: JSON.stringify([0.1, 0.2, 0.3]),
464464
})
465465

466-
/**
467-
* A single global LIMIT would let the lexically strongest base consume
468-
* every slot, so an exact-token hit in a smaller base never reaches
469-
* fusion. The vector leg already fans out here; both legs must match.
470-
*/
466+
/** Keyword retrieval preserves its existing per-base lexical candidate selection. */
471467
expect(dbChainMockFns.select).toHaveBeenCalledTimes(knowledgeBaseIds.length)
472468
})
473469

@@ -542,6 +538,7 @@ describe('Knowledge Search Utils', () => {
542538
})
543539

544540
it('runs a single retrieval leg in vector mode', async () => {
541+
dbChainMockFns.execute.mockResolvedValue([{ id: 'vector-hit' }])
545542
queueTableRows(schemaMock.embedding, [{ id: 'vector-hit' }])
546543
queueTableRows(schemaMock.embedding, [makeResult('vector-hit')])
547544

@@ -551,22 +548,22 @@ describe('Knowledge Search Utils', () => {
551548
topK: 10,
552549
searchMode: 'vector',
553550
query: 'PROJ-1234',
554-
queryVector: JSON.stringify([0.1, 0.2, 0.3]),
551+
queryVector: { vector: JSON.stringify(TEST_EMBEDDING), dimensions: 1536 },
555552
})
556553

557554
expect(results.map((r) => r.id)).toEqual(['vector-hit'])
558-
expect(dbChainMockFns.select).toHaveBeenCalledTimes(3)
559-
expect(dbChainMockFns.as).toHaveBeenCalledWith('ranked_embeddings')
555+
expect(dbChainMockFns.select).toHaveBeenCalledTimes(2)
560556
})
561557

562558
it('runs both legs and fuses them in hybrid mode', async () => {
563559
/**
564-
* Chains dequeue in creation order: keyword ranking, the budgeted vector
565-
* probe, keyword hydration, then vector ranking and hydration in one query.
560+
* The raw vector probe does not consume a table chain. Keyword ranking and
561+
* hydration complete before vector exact ranking and content hydration.
566562
*/
563+
dbChainMockFns.execute.mockResolvedValue([{ id: 'vector-hit' }])
567564
queueTableRows(schemaMock.embedding, [{ id: 'keyword-hit', keywordRank: 0.9 }])
568-
queueTableRows(schemaMock.embedding, [{ id: 'vector-hit' }])
569565
queueTableRows(schemaMock.embedding, [makeResult('keyword-hit')])
566+
queueTableRows(schemaMock.embedding, [{ id: 'vector-hit' }])
570567
queueTableRows(schemaMock.embedding, [makeResult('vector-hit')])
571568

572569
const results = await executeKnowledgeSearch({
@@ -575,15 +572,16 @@ describe('Knowledge Search Utils', () => {
575572
topK: 10,
576573
searchMode: 'hybrid',
577574
query: 'PROJ-1234',
578-
queryVector: JSON.stringify([0.1, 0.2, 0.3]),
575+
queryVector: { vector: JSON.stringify(TEST_EMBEDDING), dimensions: 1536 },
579576
})
580577

581578
expect(results.map((r) => r.id).sort()).toEqual(['keyword-hit', 'vector-hit'])
582-
expect(dbChainMockFns.select).toHaveBeenCalledTimes(5)
579+
expect(dbChainMockFns.select).toHaveBeenCalledTimes(4)
583580
})
584581

585582
it('propagates unexpected keyword errors after the vector leg finishes', async () => {
586583
/** The failing ranking chain is still built first and takes the first queued set. */
584+
dbChainMockFns.execute.mockResolvedValue([{ id: 'vector-hit' }])
587585
queueTableRows(schemaMock.embedding, [{ id: 'never-ranked', keywordRank: 0 }])
588586
queueTableRows(schemaMock.embedding, [{ id: 'vector-hit' }])
589587
queueTableRows(schemaMock.embedding, [makeResult('vector-hit')])
@@ -600,10 +598,10 @@ describe('Knowledge Search Utils', () => {
600598
topK: 10,
601599
searchMode: 'hybrid',
602600
query: 'PROJ-1234',
603-
queryVector: JSON.stringify([0.1, 0.2, 0.3]),
601+
queryVector: { vector: JSON.stringify(TEST_EMBEDDING), dimensions: 1536 },
604602
})
605603
).rejects.toBe(failure)
606-
expect(dbChainMockFns.as).toHaveBeenCalledWith('ranked_embeddings')
604+
expect(dbChainMockFns.select).toHaveBeenCalledTimes(3)
607605
})
608606

609607
it('skips both query legs when only tag filters are provided', async () => {

apps/sim/lib/knowledge/__integration__/search-latency.integration.ts

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** Real Assistant tool, application authorization, PostgreSQL/pgvector, and result processing. */
1+
/** Real search adapters, application authorization, PostgreSQL/pgvector, and result processing. */
22
import { readFileSync, statSync, writeFileSync } from 'node:fs'
33
import type { Principal } from '@sim/auth/principal'
44
import { db } from '@sim/db'
@@ -9,7 +9,6 @@ import {
99
document,
1010
embedding,
1111
knowledgeBase,
12-
knowledgeBaseTagDefinitions,
1312
knowledgeConnector,
1413
knowledgeConnectorMember,
1514
knowledgeDocumentObservation,
@@ -103,6 +102,8 @@ function readFixtureReport(file: string) {
103102
const reused = reuseFile ? readFixtureReport(reuseFile) : undefined
104103
const ids = reused?.fixture ?? createKnowledgeAclFixtureIds()
105104
const unrelated = reused?.unrelatedFixture ?? createKnowledgeAclFixtureIds()
105+
const fullWidthFixture = createKnowledgeAclFixtureIds()
106+
const fullWidthChunkCount = 5000
106107
const organizationChatId = generateId()
107108
function topicVector(topic = 0) {
108109
const vector = Array.from({ length: dimensions }, (_, index) =>
@@ -124,6 +125,7 @@ const report: Record<string, unknown> = {
124125
fixtureVersion: 2,
125126
chunkCount,
126127
unrelatedChunkCount,
128+
fullWidthChunkCount,
127129
dimensions,
128130
candidateDimensions,
129131
chunksPerDocument,
@@ -133,7 +135,7 @@ const report: Record<string, unknown> = {
133135
vectors:
134136
'Normalized 512-dimensional topic/noise geometry with permuted copies across 1536 dimensions; verifies prefix candidate ranking, not semantic embedding quality',
135137
cache: evictSharedBuffers
136-
? 'Organization samples evict PostgreSQL shared buffers before each request; operating-system cache is not cleared'
138+
? 'Selected workspace and organization samples evict PostgreSQL shared buffers; operating-system cache is not cleared'
137139
: 'First and repeated samples; no claim of a cold operating-system cache',
138140
layout: reused
139141
? 'Reused fixture; physical layout is inherited from its original report'
@@ -383,17 +385,22 @@ async function searchDashboard(
383385

384386
async function searchWorkspaceKb(
385387
query = 'Orion deployment',
386-
options: { principal?: Principal; tagFilters?: KnowledgeSearchTagFilter[] } = {}
388+
options: {
389+
principal?: Principal
390+
tagFilters?: KnowledgeSearchTagFilter[]
391+
fixture?: typeof ids
392+
} = {}
387393
) {
394+
const fixture = options.fixture ?? ids
388395
const result = await searchKnowledge.execute({
389396
principal: options.principal ?? {
390397
kind: 'workspace_api_key',
391-
workspaceId: ids.workspaceId,
398+
workspaceId: fixture.workspaceId,
392399
keyId: 'fixture-search-key',
393400
},
394401
input: {
395-
workspaceId: ids.workspaceId,
396-
knowledgeBaseIds: [ids.knowledgeBaseId],
402+
workspaceId: fixture.workspaceId,
403+
knowledgeBaseIds: [fixture.knowledgeBaseId],
397404
query,
398405
topK: 15,
399406
searchMode: 'vector',
@@ -683,6 +690,32 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu
683690
logger.info('Synthetic corpora loaded', { chunkCount, unrelatedChunkCount })
684691
for (const index of indexes) await db.execute(sql.raw(index.indexdef))
685692
}
693+
/** A non-shortenable model must populate its own full-width projection through the write trigger. */
694+
await seedKnowledgeAclFixture(fullWidthFixture, { connectorType: 'google_drive' })
695+
await db
696+
.update(knowledgeBase)
697+
.set({ embeddingModel: 'text-embedding-ada-002' })
698+
.where(eq(knowledgeBase.id, fullWidthFixture.knowledgeBaseId))
699+
await db.execute(sql`
700+
WITH source AS MATERIALIZED (
701+
SELECT id, content, embedding FROM embedding
702+
WHERE knowledge_base_id = ${ids.knowledgeBaseId} ORDER BY id LIMIT ${fullWidthChunkCount}
703+
), documents AS (
704+
INSERT INTO document
705+
(id, knowledge_base_id, connector_id, external_id, filename, file_url, file_size,
706+
mime_type, processing_status, acl, acl_verified_at)
707+
SELECT ${fullWidthFixture.workspaceId} || '-doc-' || id, ${fullWidthFixture.knowledgeBaseId},
708+
${fullWidthFixture.connectorId}, id, 'Full-width deployment guide',
709+
'https://fixture.invalid/full-width', 12000, 'text/plain', 'completed',
710+
ARRAY['pub']::text[], statement_timestamp() FROM source RETURNING id
711+
) INSERT INTO embedding
712+
(id, knowledge_base_id, document_id, chunk_index, chunk_hash, content, content_length,
713+
token_count, start_offset, end_offset, embedding)
714+
SELECT ${fullWidthFixture.workspaceId} || '-chunk-' || source.id,
715+
${fullWidthFixture.knowledgeBaseId}, documents.id, 0, source.id, source.content,
716+
3000, 750, 0, 3000, source.embedding
717+
FROM source JOIN documents ON documents.id = ${fullWidthFixture.workspaceId} || '-doc-' || source.id
718+
`)
686719
await db.execute(sql`ANALYZE document`)
687720
await db.execute(sql`ANALYZE embedding`)
688721
await db.execute(sql`ANALYZE embedding_search`)
@@ -710,9 +743,12 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu
710743
db.$client.options.debug = previousDebug
711744
vi.unstubAllGlobals()
712745
saveReport()
713-
for (const fixture of process.env.KNOWLEDGE_SEARCH_PERFORMANCE_KEEP_DATABASE === 'true'
714-
? []
715-
: [ids, unrelated]) {
746+
for (const fixture of [
747+
fullWidthFixture,
748+
...(process.env.KNOWLEDGE_SEARCH_PERFORMANCE_KEEP_DATABASE === 'true'
749+
? []
750+
: [ids, unrelated]),
751+
]) {
716752
await db.delete(workspace).where(eq(workspace.id, fixture.workspaceId))
717753
await db.delete(organization).where(eq(organization.id, fixture.organizationId))
718754
await db.delete(user).where(eq(user.id, fixture.aliceId))
@@ -1281,18 +1317,12 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu
12811317
report[`${label}.recall`] = { neighbors: expected.length, recall }
12821318
saveReport()
12831319
}
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())
1320+
const fullWidth = await sample('workspace-kb.full-width', () =>
1321+
searchWorkspaceKb('Orion deployment', { fixture: fullWidthFixture })
1322+
)
12891323
expectCompleteVectorSearch(fullWidth.diagnostics)
12901324
expect(fullWidth.diagnostics.vectorCandidateDimensions).toBe(dimensions)
12911325
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))
12961326

12971327
const workflowId = generateId()
12981328
const scheduled: Principal = {
@@ -1346,17 +1376,11 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu
13461376
for (const diagnostics of completed) expectCompleteVectorSearch(diagnostics)
13471377
}
13481378

1349-
await db.insert(knowledgeBaseTagDefinitions).values({
1350-
id: generateId(),
1351-
knowledgeBaseId: ids.knowledgeBaseId,
1352-
tagSlot: 'tag1',
1353-
displayName: 'Fixture group',
1354-
})
13551379
await db.execute(sql`UPDATE embedding SET tag1 = 'selected' WHERE knowledge_base_id = ${ids.knowledgeBaseId}
13561380
AND document_id IN (SELECT id FROM document WHERE knowledge_base_id = ${ids.knowledgeBaseId} AND external_id::int < 600)`)
13571381
const tagged = await sample('workspace-kb.tagged', () =>
13581382
searchWorkspaceKb('Orion deployment', {
1359-
tagFilters: [{ tagName: 'Fixture group', operator: 'eq', value: 'selected' }],
1383+
tagFilters: [{ tagName: 'Fixture', operator: 'eq', value: 'selected' }],
13601384
})
13611385
)
13621386
expectCompleteVectorSearch(tagged.diagnostics)
@@ -1393,16 +1417,9 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu
13931417
expectCompleteVectorSearch(denied.diagnostics)
13941418
expect(denied.result.data.results).toEqual([])
13951419
} finally {
1396-
await db
1397-
.delete(knowledgeBaseTagDefinitions)
1398-
.where(eq(knowledgeBaseTagDefinitions.knowledgeBaseId, ids.knowledgeBaseId))
13991420
await db.execute(
14001421
sql`UPDATE embedding SET tag1 = NULL WHERE knowledge_base_id = ${ids.knowledgeBaseId} AND tag1 = 'selected'`
14011422
)
1402-
await db
1403-
.update(knowledgeBase)
1404-
.set({ embeddingModel: 'text-embedding-3-small' })
1405-
.where(eq(knowledgeBase.id, ids.knowledgeBaseId))
14061423
await db
14071424
.update(knowledgeConnector)
14081425
.set({ accessRewritePending: false })

0 commit comments

Comments
 (0)