Skip to content

Commit 7828bde

Browse files
committed
fix(knowledge): preserve recall within bounded reranking
1 parent e72dea2 commit 7828bde

2 files changed

Lines changed: 61 additions & 27 deletions

File tree

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

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ vi.hoisted(() => {
5656
if (process.env.KNOWLEDGE_SEARCH_PERFORMANCE_TEST === 'true') {
5757
Object.assign(process.env, {
5858
OPENAI_API_KEY: 'isolated-embedding-http-fixture',
59+
GEMINI_API_KEY: 'isolated-gemini-http-fixture',
5960
CONFLUENCE_CLIENT_ID: 'isolated-confluence-fixture-client',
6061
CONFLUENCE_CLIENT_SECRET: 'isolated-confluence-fixture-secret',
6162
})
@@ -74,6 +75,7 @@ const unrelatedChunkCount = Number(
7475
const evictSharedBuffers = process.env.KNOWLEDGE_SEARCH_PERFORMANCE_EVICT_BUFFERS === 'true'
7576
const dimensions = 1536
7677
const candidateDimensions = 512
78+
const hybridCandidateLimit = 1600
7779
const chunksPerDocument = 4
7880
const logger = createLogger('SearchLatencyIntegration')
7981
const fixtureSchema = z.object({
@@ -95,14 +97,15 @@ function readFixtureReport(file: string) {
9597
.object({
9698
fixture: fixtureSchema,
9799
unrelatedFixture: fixtureSchema,
100+
fullWidthFixture: fixtureSchema.optional(),
98101
method: z.object({ fixtureVersion: z.literal(2) }),
99102
})
100103
.parse(JSON.parse(readFileSync(file, 'utf8')))
101104
}
102105
const reused = reuseFile ? readFixtureReport(reuseFile) : undefined
103106
const ids = reused?.fixture ?? createKnowledgeAclFixtureIds()
104107
const unrelated = reused?.unrelatedFixture ?? createKnowledgeAclFixtureIds()
105-
const fullWidthFixture = createKnowledgeAclFixtureIds()
108+
const fullWidthFixture = reused?.fullWidthFixture ?? createKnowledgeAclFixtureIds()
106109
const fullWidthChunkCount = 5000
107110
const organizationChatId = generateId()
108111
function topicVector(topic = 0) {
@@ -121,6 +124,7 @@ const captured: CapturedQuery[] = []
121124
const report: Record<string, unknown> = {
122125
fixture: ids,
123126
unrelatedFixture: unrelated,
127+
fullWidthFixture,
124128
method: {
125129
fixtureVersion: 2,
126130
chunkCount,
@@ -565,13 +569,36 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu
565569
? new Response(null, { status: 403 })
566570
: Response.json({ type: 'known', accountId: ids.aliceId })
567571
}
572+
if (
573+
url ===
574+
'https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:batchEmbedContents'
575+
) {
576+
z.object({
577+
requests: z
578+
.array(
579+
z.object({
580+
model: z.literal('models/gemini-embedding-001'),
581+
content: z.object({
582+
parts: z.array(z.object({ text: z.literal('Orion deployment') })),
583+
}),
584+
outputDimensionality: z.literal(dimensions),
585+
})
586+
)
587+
.length(1),
588+
}).parse(JSON.parse(String(init?.body)))
589+
embeddingCalls++
590+
return Response.json({
591+
embeddings: [{ values: queryVector }],
592+
usageMetadata: { promptTokenCount: 4 },
593+
})
594+
}
568595
if (url !== 'https://api.openai.com/v1/embeddings')
569596
throw new Error(`Unexpected outbound request in search fixture: ${new URL(url).origin}`)
570597
const body = z
571598
.object({
572599
input: z.array(z.string()).length(1),
573600
encoding_format: z.literal('base64'),
574-
model: z.enum(['text-embedding-3-small', 'text-embedding-ada-002']),
601+
model: z.literal('text-embedding-3-small'),
575602
})
576603
.parse(JSON.parse(String(init?.body)))
577604
embeddingCalls += body.input.length
@@ -691,12 +718,13 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu
691718
for (const index of indexes) await db.execute(sql.raw(index.indexdef))
692719
}
693720
/** 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`
721+
if (!reused?.fullWidthFixture) {
722+
await seedKnowledgeAclFixture(fullWidthFixture, { connectorType: 'google_drive' })
723+
await db
724+
.update(knowledgeBase)
725+
.set({ embeddingModel: 'gemini-embedding-001' })
726+
.where(eq(knowledgeBase.id, fullWidthFixture.knowledgeBaseId))
727+
await db.execute(sql`
700728
WITH source AS MATERIALIZED (
701729
SELECT id, content, embedding FROM embedding
702730
WHERE knowledge_base_id = ${ids.knowledgeBaseId} ORDER BY id LIMIT ${fullWidthChunkCount}
@@ -716,6 +744,14 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu
716744
3000, 750, 0, 3000, source.embedding
717745
FROM source JOIN documents ON documents.id = ${fullWidthFixture.workspaceId} || '-doc-' || source.id
718746
`)
747+
}
748+
const [fullWidthSize] = await db.execute<{ count: number }>(
749+
sql`SELECT count(*)::int AS count FROM embedding WHERE knowledge_base_id = ${fullWidthFixture.knowledgeBaseId}`
750+
)
751+
expect(fullWidthSize.count).toBe(fullWidthChunkCount)
752+
await db.execute(sql`UPDATE embedding SET tag1 = 'selected' WHERE knowledge_base_id = ${ids.knowledgeBaseId}
753+
AND tag1 IS DISTINCT FROM 'selected'
754+
AND document_id IN (SELECT id FROM document WHERE knowledge_base_id = ${ids.knowledgeBaseId} AND external_id::int < 600)`)
719755
await db.execute(sql`ANALYZE document`)
720756
await db.execute(sql`ANALYZE embedding`)
721757
await db.execute(sql`ANALYZE embedding_search`)
@@ -743,12 +779,9 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu
743779
db.$client.options.debug = previousDebug
744780
vi.unstubAllGlobals()
745781
saveReport()
746-
for (const fixture of [
747-
fullWidthFixture,
748-
...(process.env.KNOWLEDGE_SEARCH_PERFORMANCE_KEEP_DATABASE === 'true'
749-
? []
750-
: [ids, unrelated]),
751-
]) {
782+
for (const fixture of process.env.KNOWLEDGE_SEARCH_PERFORMANCE_KEEP_DATABASE === 'true'
783+
? []
784+
: [ids, unrelated, fullWidthFixture]) {
752785
await db.delete(workspace).where(eq(workspace.id, fixture.workspaceId))
753786
await db.delete(organization).where(eq(organization.id, fixture.organizationId))
754787
await db.delete(user).where(eq(user.id, fixture.aliceId))
@@ -1089,7 +1122,7 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu
10891122
}
10901123
}, 180_000)
10911124

1092-
it.each([200, 396, 400, 1000, 2000])(
1125+
it.each([200, 1000, 1596, 1600, 2000])(
10931126
'keeps a selective scope of %s chunks within both retrieval budgets',
10941127
async (count) => {
10951128
const documentCount = count / chunksPerDocument
@@ -1116,10 +1149,14 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu
11161149
true
11171150
)
11181151
const probe = plans.find((plan) => plan.kind === 'probe')!
1119-
expect(probe.plan[0].Plan['Actual Rows']).toBe(Math.min(count, 400))
1120-
expect(assertIndexedChunkProbe(probe.plan[0].Plan)).toBe(Math.min(documentCount, 100))
1121-
expect(plans.filter((plan) => plan.kind === 'vector')).toHaveLength(count < 400 ? 0 : 1)
1122-
if (count > 400) {
1152+
expect(probe.plan[0].Plan['Actual Rows']).toBe(Math.min(count, hybridCandidateLimit))
1153+
expect(assertIndexedChunkProbe(probe.plan[0].Plan)).toBe(
1154+
Math.min(documentCount, hybridCandidateLimit / chunksPerDocument)
1155+
)
1156+
expect(plans.filter((plan) => plan.kind === 'vector')).toHaveLength(
1157+
count < hybridCandidateLimit ? 0 : 1
1158+
)
1159+
if (count > hybridCandidateLimit) {
11231160
const rerank = plans.find((plan) => plan.kind === 'rerank')!
11241161
const actual = await db.$client.unsafe(rerank.query, rerank.parameters).values()
11251162
const expected = await db.execute<{ id: string }>(sql`SELECT id FROM embedding
@@ -1187,8 +1224,10 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu
11871224
expectCompleteVectorSearch(broad.diagnostics)
11881225
expect(broad.result.data.results).toHaveLength(15)
11891226
const broadProbe = broad.plans.find((plan) => plan.kind === 'probe')!
1190-
expect(broadProbe.plan[0].Plan['Actual Rows']).toBe(400)
1191-
expect(assertIndexedChunkProbe(broadProbe.plan[0].Plan)).toBe(400 / chunksPerDocument)
1227+
expect(broadProbe.plan[0].Plan['Actual Rows']).toBe(hybridCandidateLimit)
1228+
expect(assertIndexedChunkProbe(broadProbe.plan[0].Plan)).toBe(
1229+
hybridCandidateLimit / chunksPerDocument
1230+
)
11921231
const { result, plans, diagnostics } = await sample(`member-scope.${surface}`, () =>
11931232
surface === 'copilot' ? search(ids.bobId) : searchDashboard('Orion deployment', ids.bobId)
11941233
)
@@ -1376,8 +1415,6 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu
13761415
for (const diagnostics of completed) expectCompleteVectorSearch(diagnostics)
13771416
}
13781417

1379-
await db.execute(sql`UPDATE embedding SET tag1 = 'selected' WHERE knowledge_base_id = ${ids.knowledgeBaseId}
1380-
AND document_id IN (SELECT id FROM document WHERE knowledge_base_id = ${ids.knowledgeBaseId} AND external_id::int < 600)`)
13811418
const tagged = await sample('workspace-kb.tagged', () =>
13821419
searchWorkspaceKb('Orion deployment', {
13831420
tagFilters: [{ tagName: 'Fixture', operator: 'eq', value: 'selected' }],
@@ -1417,9 +1454,6 @@ describe.skipIf(!enabled)('Knowledge search latency on a realistic indexed corpu
14171454
expectCompleteVectorSearch(denied.diagnostics)
14181455
expect(denied.result.data.results).toEqual([])
14191456
} finally {
1420-
await db.execute(
1421-
sql`UPDATE embedding SET tag1 = NULL WHERE knowledge_base_id = ${ids.knowledgeBaseId} AND tag1 = 'selected'`
1422-
)
14231457
await db
14241458
.update(knowledgeConnector)
14251459
.set({ accessRewritePending: false })

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const CANDIDATE_HNSW_EF_SEARCH = '1000'
5555
const CANDIDATE_HNSW_SCAN_MEM_MULTIPLIER = '2'
5656
const MIN_VECTOR_RERANK_CANDIDATES = 400
5757
const MAX_VECTOR_RERANK_CANDIDATES = 1600
58-
const VECTOR_RERANK_OVERSAMPLING = 8
58+
const VECTOR_RERANK_OVERSAMPLING = 32
5959

6060
/** How long to stop trying the iterative-scan settings after the server rejected them. */
6161
const HNSW_SETTINGS_UNSUPPORTED_RETRY_MS = 10 * 60 * 1000

0 commit comments

Comments
 (0)