Skip to content

Commit 431966e

Browse files
authored
chore(db): drop unused ANN indexes on embedding (#7955)
* chore(db): drop unused ANN indexes on embedding Approximate retrieval is served by the compact embedding_search projection. The only vector ordering left on embedding is an exact rerank wrapped as (distance) + 0, which the planner cannot match to an index expression, and the binary_quantize expression indexes were never referenced by any query. The last app version that ordered by a bare distance has drained, so these ten indexes were maintained on every chunk write while never being scanned. * fix(knowledge): let the scale benchmark own its ANN index The bulk-seed path required embedding_vector_hnsw_idx to exist, dropped it before seeding and rebuilt it after. With no ANN index left in the schema it threw before measuring anything. The benchmark seeds embedding directly and never populates embedding_search, so it now creates its own HNSW index after the load rather than borrowing a schema one, for both seed paths.
1 parent 1db2a00 commit 431966e

6 files changed

Lines changed: 27815 additions & 93 deletions

File tree

‎apps/sim/lib/knowledge/__integration__/scale.integration.ts‎

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ const rows = Number(process.env.KNOWLEDGE_SCALE_DOCUMENTS ?? 250_000)
4040
const SEED_BATCH_SIZE = 2_000
4141
const PAGE_SIZE = 500
4242
const DIMENSIONS = 1536
43+
/**
44+
* `embedding` carries no ANN index in the schema — production serves approximate
45+
* retrieval from the `embedding_search` projection, which this fixture does not
46+
* populate. The benchmark still measures ANN behaviour over the dense corpus it
47+
* seeds directly, so it owns this index rather than borrowing a schema one, and
48+
* builds it after the load instead of paying index maintenance on every insert.
49+
*/
50+
const BENCHMARK_VECTOR_INDEX = 'embedding_scale_benchmark_hnsw_idx'
51+
const BENCHMARK_VECTOR_INDEX_DEFINITION = `CREATE INDEX IF NOT EXISTS ${BENCHMARK_VECTOR_INDEX} ON public.embedding USING hnsw (embedding vector_cosine_ops) WITH (m='16', ef_construction='64')`
4352
const logger = createLogger('KnowledgeScaleIntegration')
4453
if (reuseReportFile && statSync(reuseReportFile).size > 16 * 1024 * 1024)
4554
throw new Error('Retained scale report must be at most 16 MiB')
@@ -386,7 +395,6 @@ describe.skipIf(!enabled)('knowledge scale: isolated real PostgreSQL, no provide
386395
it.skipIf(metadataOnly)(
387396
'stores a bounded dense corpus and measures ACL/tag-filtered vector and hybrid retrieval',
388397
async () => {
389-
let vectorIndexDefinition: string | undefined
390398
if (bulkSeed) {
391399
const other = await db
392400
.select({ id: embedding.id })
@@ -397,13 +405,6 @@ describe.skipIf(!enabled)('knowledge scale: isolated real PostgreSQL, no provide
397405
throw new Error(
398406
'Bulk scale setup requires a database containing only its own fixture chunks'
399407
)
400-
const [index] = await db.execute(
401-
sql`SELECT indexdef FROM pg_indexes WHERE schemaname = 'public' AND indexname = 'embedding_vector_hnsw_idx'`
402-
)
403-
if (typeof index?.indexdef !== 'string')
404-
throw new Error('Canonical 1536-dimensional HNSW index is missing')
405-
vectorIndexDefinition = index.indexdef
406-
await db.execute(sql`DROP INDEX embedding_vector_hnsw_idx`)
407408
}
408409
if (!reuseReportFile)
409410
await measure('seed.vectors', async () => {
@@ -420,20 +421,17 @@ describe.skipIf(!enabled)('knowledge scale: isolated real PostgreSQL, no provide
420421
}
421422
}
422423
})
423-
if (vectorIndexDefinition) {
424-
const definition = vectorIndexDefinition
425-
await measure('seed.hnswBuild', () =>
426-
db.transaction(async (tx) => {
427-
await tx.execute(sql`SET LOCAL maintenance_work_mem = '2GB'`)
428-
await tx.execute(sql`SET LOCAL max_parallel_maintenance_workers = 2`)
429-
await tx.execute(sql.raw(definition))
430-
})
431-
)
432-
const [restored] = await db.execute(
433-
sql`SELECT indexdef FROM pg_indexes WHERE schemaname = 'public' AND indexname = 'embedding_vector_hnsw_idx'`
434-
)
435-
expect(restored.indexdef).toBe(vectorIndexDefinition)
436-
}
424+
await measure('seed.hnswBuild', () =>
425+
db.transaction(async (tx) => {
426+
await tx.execute(sql`SET LOCAL maintenance_work_mem = '2GB'`)
427+
await tx.execute(sql`SET LOCAL max_parallel_maintenance_workers = 2`)
428+
await tx.execute(sql.raw(BENCHMARK_VECTOR_INDEX_DEFINITION))
429+
})
430+
)
431+
const [built] = await db.execute(
432+
sql`SELECT indexdef FROM pg_indexes WHERE schemaname = 'public' AND indexname = ${BENCHMARK_VECTOR_INDEX}`
433+
)
434+
expect(built?.indexdef).toEqual(expect.stringContaining('USING hnsw'))
437435
await db.execute(sql`ANALYZE embedding`)
438436
await db.execute(sql`ANALYZE document`)
439437
const [count] = await db.execute(

‎apps/sim/lib/knowledge/vector-columns.ts‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,17 @@ export function embeddingVectorValues(
5858
}
5959

6060
/**
61-
* Cosine distance between a chunk's vector and the query vector, in the exact
62-
* form the width's HNSW index was built on.
61+
* Cosine distance between a chunk's vector and the query vector, used for the
62+
* search layer's exact rerank over an already-bounded candidate set.
6363
*
64-
* The 3,072 column is compared through a `halfvec` cast because pgvector
65-
* indexes `vector` only up to 2,000 dimensions, so its index is on that cast
66-
* expression. Postgres matches an expression index by the expression, so a
67-
* plain `<=>` against the column here would silently drop to a sequential scan
68-
* — and the cast belongs here rather than at each call site precisely because
69-
* getting it wrong is invisible in the results and only shows up as latency.
70-
* `packages/db/schema.ts` records what the half-precision comparison costs.
64+
* `embedding` carries no ANN index — approximate retrieval runs against the
65+
* compact `embedding_search` projection — so this expression is never expected
66+
* to match one, and its caller wraps it to keep the planner from trying.
67+
*
68+
* The 3,072 column keeps its `halfvec` cast: the width that made the cast
69+
* necessary is unchanged, and `packages/db/schema.ts` records that the
70+
* half-precision comparison moved no distance measurably for the one model
71+
* that emits this width.
7172
*/
7273
export function embeddingDistance(
7374
dimensions: KbEmbeddingDimensions,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-- Drops the ten unused ANN indexes on "embedding". Approximate retrieval moved to the
2+
-- compact "embedding_search" projection; the only vector ordering left on this table is an
3+
-- exact rerank wrapped as (distance) + 0, which the planner cannot match to an index.
4+
-- The last app version that ordered by a bare distance has drained, so nothing reads these.
5+
COMMIT;--> statement-breakpoint
6+
SET lock_timeout = 0;--> statement-breakpoint
7+
-- migration-safe: DROP INDEX CONCURRENTLY IF EXISTS is idempotent on replay and takes no blocking lock.
8+
DROP INDEX CONCURRENTLY IF EXISTS "embedding_vector_hnsw_idx";--> statement-breakpoint
9+
-- migration-safe: DROP INDEX CONCURRENTLY IF EXISTS is idempotent on replay and takes no blocking lock.
10+
DROP INDEX CONCURRENTLY IF EXISTS "embedding_384_vector_hnsw_idx";--> statement-breakpoint
11+
-- migration-safe: DROP INDEX CONCURRENTLY IF EXISTS is idempotent on replay and takes no blocking lock.
12+
DROP INDEX CONCURRENTLY IF EXISTS "embedding_768_vector_hnsw_idx";--> statement-breakpoint
13+
-- migration-safe: DROP INDEX CONCURRENTLY IF EXISTS is idempotent on replay and takes no blocking lock.
14+
DROP INDEX CONCURRENTLY IF EXISTS "embedding_1024_vector_hnsw_idx";--> statement-breakpoint
15+
-- migration-safe: DROP INDEX CONCURRENTLY IF EXISTS is idempotent on replay and takes no blocking lock.
16+
DROP INDEX CONCURRENTLY IF EXISTS "embedding_3072_vector_hnsw_idx";--> statement-breakpoint
17+
-- migration-safe: DROP INDEX CONCURRENTLY IF EXISTS is idempotent on replay and takes no blocking lock.
18+
DROP INDEX CONCURRENTLY IF EXISTS "embedding_binary_hnsw_idx";--> statement-breakpoint
19+
-- migration-safe: DROP INDEX CONCURRENTLY IF EXISTS is idempotent on replay and takes no blocking lock.
20+
DROP INDEX CONCURRENTLY IF EXISTS "embedding_384_binary_hnsw_idx";--> statement-breakpoint
21+
-- migration-safe: DROP INDEX CONCURRENTLY IF EXISTS is idempotent on replay and takes no blocking lock.
22+
DROP INDEX CONCURRENTLY IF EXISTS "embedding_768_binary_hnsw_idx";--> statement-breakpoint
23+
-- migration-safe: DROP INDEX CONCURRENTLY IF EXISTS is idempotent on replay and takes no blocking lock.
24+
DROP INDEX CONCURRENTLY IF EXISTS "embedding_1024_binary_hnsw_idx";--> statement-breakpoint
25+
-- migration-safe: DROP INDEX CONCURRENTLY IF EXISTS is idempotent on replay and takes no blocking lock.
26+
DROP INDEX CONCURRENTLY IF EXISTS "embedding_3072_binary_hnsw_idx";--> statement-breakpoint
27+
SET lock_timeout = '5s';

0 commit comments

Comments
 (0)