Skip to content

An embedding wider than 2000 dims gets a halfvec index that picks the candidates, and they are still scored and ordered at full precision - #43

Merged
gedaiu merged 1 commit into
re-devfrom
perf/wide-embeddings-halfvec-index
Oct 27, 2026
Merged

gedaiu merged 1 commit into
re-devfrom
perf/wide-embeddings-halfvec-index

Conversation

@gedaiu

@gedaiu gedaiu commented Oct 27, 2026

Copy link
Copy Markdown

Every vector search on a knowledge base with 3072-dim embeddings (text-embedding-3-large) scans the whole table. pgvector's HNSW holds vector up to 2000 dims, so vector_index never built an index for these, and the comment there said halfvec would wait until someone needs it. We need it: in our deployment chunks, entities.profile_embedding and name_vectors are all 3072 dims with no HNSW index. Name lookups run up to 35 s, reading name_vectors (3.5 GB) from disk, and the database sits at its CPU limit.

Above 2000 dims the build job now makes the same partial expression index on col::halfvec(N) with halfvec_cosine_ops (halfvec goes to 4000). The index only picks the candidates. Each search selects its distance with vector_index::distance, which stays col::vector(N), while ORDER BY uses the new vector_index::index_order, which matches the index expression. The outer RESORT that already exists then orders the candidates by full-precision distance. Half precision can therefore only affect which rows make the candidate list, never a score or the order returned. At 2000 dims or fewer index_order equals distance, so the SQL is unchanged there.

The three nearest-neighbour searches (documents::vector_search, name_vectors::nearest, entity-profile neighbours in resolution.rs) use index_order. name_vectors::nearest also now runs in a transaction with relaxed_order. It never set it, which was harmless without an index, but with one a small base sharing the table would come back short (rule 3).

a_wide_embedding_is_ranked_at_full_precision builds a 2001-dim index, checks it is a halfvec one, forces the planner onto it and confirms through idx_scan that the search used it. It places two chunks whose distances tie in half precision and differ in full precision, with the farther one holding the smaller id. Scoring the candidates with the half-precision expression puts that one first and fails the test. A small base beside the big one still fills its limit.

On a deployment that already holds 3072-dim rows, the next write of each kind queues its build. The builds are CONCURRENTLY and serial, so searches keep their current (slow) path until each index is valid.

🤖 Generated with Claude Code

… candidates, and they are still scored and ordered at full precision

Signed-off-by: Bogdan Szabo <bogdan@re-cinq.com>
@gedaiu
gedaiu merged commit f47da57 into re-dev Oct 27, 2026
6 checks passed
gedaiu added a commit to re-cinq/lore that referenced this pull request Oct 27, 2026
…s database (#3727)

Utopia was slow because its database sat at its 4-CPU limit: every
vector search scanned the whole table. Utopia only indexed embeddings up
to 2000 dims, and ours are 3072. Name lookups took up to 35 s.

This pins `0.1.0-re.25` (digest `sha256:abab8151…`), which adds
re-cinq/utopia#43. Embeddings wider than 2000 dims get a halfvec HNSW
index that only picks the candidates, which are still scored and ordered
at full precision. There is no new migration. After the rollout, the
next write of each kind queues its index build (CONCURRENTLY, one at a
time). The chunks index (617k rows) can take a while, and searches keep
the old path until it is valid.

It also raises the Utopia database from 4Gi to 8Gi, with
`shared_buffers` 1GB → 2GB and `effective_cache_size` 3GB → 6GB, so the
indexes stay in memory. Changing `shared_buffers` restarts the single
CNPG instance, so expect a short Utopia outage at apply.

Related: re-cinq/utopia#43

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant