1. Home
  2. halfvec & quantization
Guide · storage & dimensions

halfvec, binary quantization & big embeddings

Three ways to make vectors smaller in pgvector — half precision, 1-bit quantization, and fewer dimensions — and what each one actually costs in recall, measured on the same OpenAI embeddings.

Tested on pgvector 0.8.7 · PostgreSQL 18.6Updated 2026-10-277 min read

Rule of thumb: index halfvec whenever you can — on our data it cost -0.2 recall points for a 50% smaller index. It's also how you index 2,001–4,000 dimensions. Binary quantization is a further step for very large tables, and only works with a re-ranking query.

The options at a glance#

RepresentationBytes per 1,536-dim valueIndex limitHNSW index, 100K rowsRecall@10 (ef 40)
vector (float32)6,1522,000 dims781 MB0.947
halfvec (float16)3,0804,000 dims391 MB0.949
bit via binary_quantize20064,000 dims48 MB0.699 alone · 0.985 re-ranked

HNSW index size by representation

100,000 × 1,536 dims. Binary quantization indexes 1 bit per dimension; the full vectors stay in the table for re-ranking.

RepresentationTable sizeIndex sizeIndex build
vector(1536)795 MB781 MB3.4 min
halfvec(1536)400 MB391 MB2.9 min
bit(1536) via binary_quantize(uses vector table)48 MB39 s
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

Recall vs latency: vector, halfvec and binary quantization

100K × 1,536 dims, HNSW m 16 / ef_construction 64. Binary points vary the re-rank candidate count (20 → 640).

  • vector (float32)
  • halfvec (float16)
  • binary + re-rank
VariantSettingRecall@10p50
vectoref 100.8151.4 ms
vectoref 200.8971.9 ms
vectoref 400.9472.9 ms
vectoref 640.9703.9 ms
vectoref 1000.9835.3 ms
vectoref 2000.9909.1 ms
vectoref 4000.99516 ms
halfvecef 100.8191.4 ms
halfvecef 200.8961.8 ms
halfvecef 400.9492.5 ms
halfvecef 640.9683.5 ms
halfvecef 1000.9824.8 ms
halfvecef 2000.9917.8 ms
halfvecef 4000.99514 ms
binary + re-rank20 candidates0.8541.8 ms
binary + re-rank40 candidates0.9182.3 ms
binary + re-rank80 candidates0.9663.5 ms
binary + re-rank160 candidates0.9855.7 ms
binary + re-rank320 candidates0.99310 ms
binary + re-rank640 candidates0.99720 ms
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

halfvec: half the bytes, almost all the recall#

A halfvec stores each element as a 16-bit float — about three significant digits, range ±65,504. Normalised embeddings sit far inside that range, so the rounding barely moves distances. You can either store halfvec columns, or keep full precision in the table and index a cast:

psqlExpression index over a vector column — the query must repeat the cast
demo=# CREATE INDEX ON items USING hnsw ((embedding::halfvec(64)) halfvec_cosine_ops);
CREATE INDEX

demo=# EXPLAIN (COSTS OFF) SELECT id FROM items ORDER BY embedding::halfvec(64) <=> (SELECT embedding FROM items WHERE id = 1)::halfvec(64) LIMIT 10;
                                    QUERY PLAN                                     
-----------------------------------------------------------------------------------
 Limit
   InitPlan 1
     ->  Index Scan using items_pkey on items items_1
           Index Cond: (id = 1)
   ->  Index Scan using items_embedding_idx3 on items
         Order By: ((embedding)::halfvec(64) <=> ((InitPlan 1).col1)::halfvec(64))
(6 rows)
ApproachTableIndexUse when
halfvec column400 MB (100K × 1,536)391 MBYou never need full precision
vector column + halfvec expression index795 MB391 MBYou want exact re-ranking or full-precision exports

Recall and latency for the two were the same within noise: the expression index scored 0.949 at ef_search = 40 (3.0 ms median). The halfvec index also built faster — 2.9 min versus 3.4 min — because two elements fit on each 8 KB page instead of one.

Indexing more than 2,000 dimensions#

OpenAI's text-embedding-3-large returns 3,072 dimensions by default, and pgvector refuses to index it as vector:

outputReal errors from our 3,072-dim table
column cannot have more than 2000 dimensions for hnsw index
column cannot have more than 2000 dimensions for ivfflat index

A halfvec expression index works up to 4,000 dimensions. On 99,000 3,072-dim DBpedia vectors:

ef_searchRecall@10p50p95
200.8892.9 ms4.5 ms
400.9374.1 ms7.0 ms
1000.9797.1 ms12 ms
2000.98912 ms17 ms

Index: 773 MB, built in 5.4 min. Exact search over the same table took 716 ms per query. Above 4,000 dimensions you'll see column cannot have more than 4000 dimensions for hnsw index — use binary quantization or reduce dimensions.

Binary quantization with re-ranking#

binary_quantize() keeps one bit per dimension — 1 if the element is positive. Hamming distance between those bit strings roughly tracks angular distance, so a bit index can shortlist candidates cheaply; the full vectors, still in the table, then put them in the right order:

psqlpsql · PostgreSQL 18.6 · pgvector 0.8.7
demo=# CREATE INDEX ON items USING hnsw ((binary_quantize(embedding)::bit(64)) bit_hamming_ops);
CREATE INDEX

demo=# EXPLAIN (COSTS OFF)
demo-# SELECT id FROM (
demo-#   SELECT id, embedding FROM items
demo-#   ORDER BY binary_quantize(embedding)::bit(64) <~> binary_quantize((SELECT embedding FROM items WHERE id = 1))
demo-#   LIMIT 40
demo-# ) candidates
demo-# ORDER BY embedding <=> (SELECT embedding FROM items WHERE id = 1)
demo-# LIMIT 10;
                                                     QUERY PLAN                                                     
--------------------------------------------------------------------------------------------------------------------
 Limit
   InitPlan 1
     ->  Index Scan using items_pkey on items items_1
           Index Cond: (id = 1)
   ->  Sort
         Sort Key: ((candidates.embedding <=> (InitPlan 1).col1))
         ->  Subquery Scan on candidates
               ->  Limit
                     InitPlan 2
                       ->  Index Scan using items_pkey on items items_2
                             Index Cond: (id = 1)
                     ->  Index Scan using items_binary_quantize_idx on items
                           Order By: ((binary_quantize(embedding))::bit(64) <~> binary_quantize((InitPlan 2).col1))
(13 rows)

Binary quantization: recall vs re-rank candidates

Fetch N candidates by Hamming distance from the bit index, re-order them by exact cosine distance, keep 10.

  • 1,536 dims
  • 3,072 dims
DimsCandidatesRecall@10p50
1536200.8541.8 ms
1536400.9182.3 ms
1536800.9663.5 ms
15361600.9855.7 ms
15363200.99310 ms
15366400.99720 ms
3072400.9302.9 ms
3072800.9704.9 ms
30721600.9868.0 ms
30723200.99316 ms
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data

On its own the bit index scored 0.699 at 1,536 dims — useful only as a filter. Re-ranking 160 candidates gave 0.985 in 5.7 ms; 640 candidates gave 0.997 in 20 ms. At 3,072 dims the extra bits helped only slightly — 0.930 vs 0.918 with 40 candidates, and about the same (0.986) at 160.

  • Index size: 48 MB for 100K × 1,536 — 16× smaller than the vector index. Build: 39 s.
  • The catch: re-ranking reads full vectors from the table, so the table (or at least the hot part) should still be cached.
  • Set hnsw.ef_search ≥ the candidate count or the inner query returns fewer candidates than its LIMIT.

Shortening embeddings#

OpenAI's v3 models are trained so that the first k dimensions form a usable embedding on their own (request them with the dimensions parameter, or truncate and re-normalise). How much does that lose? We truncated the 3,072-dim vectors, re-normalised them, ran exact search, and compared the top-10 with exact search on the full vectors:

Shortening text-embedding-3-large: top-10 overlap with full 3,072 dims

Exact search on truncated, re-normalised vectors vs exact search on the full vectors. 99,000 rows, 1,000 queries.

DimsTop-10 overlapvector byteshalfvec bytes
25658.4%1,032520
51273.2%2,0561,032
76880.0%3,0801,544
102484.1%4,1042,056
153689.0%6,1523,080
204891.9%8,2004,104
3072100.0%12,2966,152
Measured on PostgreSQL 18.6 + pgvector 0.8.7, 4 vCPU / 15 GB, DBpedia × OpenAI text-embedding-3-large.Method & raw data
DimensionsTop-10 overlap with 3,072vector bytesIndexable as
25658.4%1,032vector
51273.2%2,056vector
76880.0%3,080vector
1,02484.1%4,104vector
1,53689.0%6,152vector
2,04891.9%8,200halfvec
3,072100.0%12,296halfvec

This measures agreement with the full model, not retrieval quality on a labelled task — but it tells you how much ranking changes. We also checked that truncating the 3,072-dim output to 1,536 and re-normalising matches what the API returns when you request 1,536 dimensions: across 2,000 articles the cosine similarity between the two averaged 0.99999 (minimum 0.997) — effectively the same vectors.

You can also keep full vectors and index only a prefix with subvector(), re-ranking with the full vector (pattern).

Which to use#

SituationDo this
≤ 2,000 dims, index fits in RAMvector index — or halfvec to halve memory at negligible cost
2,001–4,000 dims (e.g. 3,072)halfvec expression index; or request 1,536 dims from the model
Index doesn't fit in RAMhalfvec first; then binary quantization with re-ranking
> 4,000 dimsBinary quantization with re-ranking, or reduce dimensions

Size your options with the storage calculator, or let the index planner choose.

Frequently asked questions

How do I index text-embedding-3-large (3,072 dimensions) in pgvector?

Index a half-precision cast: CREATE INDEX ON items USING hnsw ((embedding::halfvec(3072)) halfvec_cosine_ops); and query with ORDER BY embedding::halfvec(3072) <=> $1::halfvec(3072). On 99,000 DBpedia articles that index reached recall@10 of 0.937 at ef_search 40 and 0.979 at 100.

Does halfvec reduce accuracy?

Barely. Half precision keeps about three significant digits per element. On our 100K × 1,536 benchmark, a halfvec HNSW index scored 0.949 recall@10 at ef_search 40 versus 0.947 for full precision, while the index shrank from 781 MB to 391 MB.

Is binary quantization worth it in pgvector?

Only with re-ranking. Searching the 1-bit index alone gave recall@10 of 0.699 on our 1,536-dim data. Fetching 160 candidates by Hamming distance and re-ranking them by exact cosine distance raised it to 0.985, with an index 16× smaller than full precision.