Blog

A 16× Smaller Index for EmbeddingGemma 2

Google’s EmbeddingGemma 2 offers one way to shrink its stored vectors: keep the first 512, 256 or 128 of its 768 numbers. The card describes the 128-number option as “up to a 6x reduction in vector storage costs”. On two public search benchmarks that option loses 0.056 and 0.065 nDCG@10. Keeping all 768 numbers and storing each in 2 bits is 16× smaller, and its loss is within the 95% interval of zero on both.

EmbeddingGemma 2 came out on October 6: one embedding space for text, images, video and audio, 740M parameters with every encoder loaded and 270M for text alone. Oskar asked whether Google ships compressed vectors for it. The model card, docs and launch post describe the truncation above and nothing else for the vectors. Their one mention of quantization is of the model weights, which brings the text encoder to about 191 MB of RAM on a phone and leaves the index the same size. So I measured the text-only model with the same harness as Perplexity’s pplx-embed-v2-late, also on October 10.

Setup

The benchmarks are BEIR SciFact (5,183 scientific abstracts, 300 test claims) and NFCorpus (3,633 medical documents, 323 queries). Every query was scored against every document by nDCG@10, the standard score for how high the judged-relevant documents land in the top ten. Uncompressed, the model scores 0.8505 on SciFact and 0.3688 on NFCorpus, at 3,072 bytes per document. Differences come with 95% bootstrap intervals over the queries.

The quantizer is remex, which rotates each vector randomly and rounds every coordinate to a fixed codebook (how it works). Only the documents are quantized; queries stay at full precision and are scored against the decoded documents. I ran remex at 1, 2, 3, 4 and 8 bits on all 768 dimensions and on each of Google’s truncated widths, so every byte budget has both kinds of candidate. Each width was also run centered: the corpus’s average vector is subtracted before quantizing and stored once, 3 KB for the whole index. Also measured were sentence-transformers’ built-in int8 and binary formats, and remax, a 1-bit sign code.

nDCG@10 on SciFact against bytes per document for every EmbeddingGemma 2 index measured Google's float32 truncation to 128 dimensions scores 0.795 at 512 bytes; remex 2-bit on all 768 dimensions scores 0.849 at 192 bytes against 0.851 for uncompressed float32 at 3,072 bytes. 0.70 0.72 0.74 0.76 0.78 0.80 0.82 0.84 0.86 32 B 64 B 128 B 256 B 512 B 1 KB 2 KB 4 KB bytes per document (log scale) nDCG@10, SciFact fp16 d=768: 0.851 nDCG@10, 1.5 KB/doc int8 global d=768: 0.854 nDCG@10, 768 B/doc ST int8 d=768: 0.850 nDCG@10, 768 B/doc ST binary d=768 (asym): 0.828 nDCG@10, 96 B/doc ST binary d=768 (sym): 0.805 nDCG@10, 96 B/doc remex 8-bit d=768: 0.849 nDCG@10, 768 B/doc remex 4-bit d=768: 0.848 nDCG@10, 384 B/doc remex 3-bit d=768: 0.849 nDCG@10, 288 B/doc remex 2-bit d=768: 0.849 nDCG@10, 192 B/doc remex 1-bit d=768: 0.820 nDCG@10, 96 B/doc remex 8-bit d=512: 0.851 nDCG@10, 512 B/doc remex 4-bit d=512: 0.846 nDCG@10, 256 B/doc remex 3-bit d=512: 0.850 nDCG@10, 192 B/doc remex 2-bit d=512: 0.839 nDCG@10, 128 B/doc remex 1-bit d=512: 0.805 nDCG@10, 64 B/doc remex 8-bit d=256: 0.837 nDCG@10, 256 B/doc remex 4-bit d=256: 0.830 nDCG@10, 128 B/doc remex 3-bit d=256: 0.828 nDCG@10, 96 B/doc remex 2-bit d=256: 0.811 nDCG@10, 64 B/doc remex 1-bit d=256: 0.721 nDCG@10, 32 B/doc remex 8-bit d=128: 0.794 nDCG@10, 128 B/doc remex 4-bit d=128: 0.785 nDCG@10, 64 B/doc remex 3-bit d=128: 0.759 nDCG@10, 48 B/doc remex 2-bit d=128: 0.708 nDCG@10, 32 B/doc remex centered 4-bit d=256: 0.840 nDCG@10, 128 B/doc remex centered 3-bit d=256: 0.830 nDCG@10, 96 B/doc remex centered 2-bit d=256: 0.820 nDCG@10, 64 B/doc remex centered 1-bit d=256: 0.771 nDCG@10, 32 B/doc remex centered 4-bit d=128: 0.793 nDCG@10, 64 B/doc remex centered 3-bit d=128: 0.788 nDCG@10, 48 B/doc remex centered 2-bit d=128: 0.781 nDCG@10, 32 B/doc remax k=1 d=768 (asym): 0.806 nDCG@10, 96 B/doc remax k=1 d=768 (sym): 0.768 nDCG@10, 96 B/doc remax k=2 d=768 (asym): 0.826 nDCG@10, 192 B/doc remax k=1 d=512 (asym): 0.783 nDCG@10, 64 B/doc remax k=1 d=512 (sym): 0.755 nDCG@10, 64 B/doc remax k=2 d=512 (asym): 0.823 nDCG@10, 128 B/doc fp32 d=128: 0.795 nDCG@10, 512 B/doc fp32 d=256: 0.838 nDCG@10, 1.0 KB/doc fp32 d=512: 0.851 nDCG@10, 2.0 KB/doc fp32 d=768: 0.851 nDCG@10, 3.0 KB/doc remex centered 1-bit d=768: 0.839 nDCG@10, 96 B/doc remex centered 2-bit d=768: 0.848 nDCG@10, 192 B/doc remex centered 3-bit d=768: 0.854 nDCG@10, 288 B/doc remex centered 4-bit d=768: 0.852 nDCG@10, 384 B/doc remex centered 1-bit d=512: 0.827 nDCG@10, 64 B/doc remex centered 2-bit d=512: 0.844 nDCG@10, 128 B/doc remex centered 3-bit d=512: 0.845 nDCG@10, 192 B/doc remex centered 4-bit d=512: 0.849 nDCG@10, 256 B/doc 128 dims, float32: Google's 6× 768 dims 512 dims 256 dims 4-bit, 768 dims 2-bit, 768 dims: 16× 2-bit, 512 dims 1-bit, 768 dims ST binary, 1-bit Google's truncation, float32Google's truncation, float32 remex centered, all 768 dimsremex centered, all 768 dims remex centered, first 512 dimsremex centered, first 512 dims other armsevery other arm (hover) Pareto frontier uncompressed, 768 dims nDCG@10 on NFCorpus against bytes per document for every EmbeddingGemma 2 index measured Google's float32 truncation to 128 dimensions scores 0.304 at 512 bytes; remex 2-bit on all 768 dimensions scores 0.362 at 192 bytes against 0.369 for uncompressed float32 at 3,072 bytes. 0.24 0.26 0.28 0.30 0.32 0.34 0.36 0.38 32 B 64 B 128 B 256 B 512 B 1 KB 2 KB 4 KB bytes per document (log scale) nDCG@10, NFCorpus fp16 d=768: 0.368 nDCG@10, 1.5 KB/doc int8 global d=768: 0.371 nDCG@10, 768 B/doc ST int8 d=768: 0.369 nDCG@10, 768 B/doc ST binary d=768 (asym): 0.347 nDCG@10, 96 B/doc ST binary d=768 (sym): 0.321 nDCG@10, 96 B/doc remex 8-bit d=768: 0.369 nDCG@10, 768 B/doc remex 4-bit d=768: 0.368 nDCG@10, 384 B/doc remex 3-bit d=768: 0.363 nDCG@10, 288 B/doc remex 2-bit d=768: 0.362 nDCG@10, 192 B/doc remex 1-bit d=768: 0.331 nDCG@10, 96 B/doc remex 8-bit d=512: 0.368 nDCG@10, 512 B/doc remex 4-bit d=512: 0.367 nDCG@10, 256 B/doc remex 3-bit d=512: 0.366 nDCG@10, 192 B/doc remex 2-bit d=512: 0.356 nDCG@10, 128 B/doc remex 1-bit d=512: 0.315 nDCG@10, 64 B/doc remex 8-bit d=256: 0.349 nDCG@10, 256 B/doc remex 4-bit d=256: 0.348 nDCG@10, 128 B/doc remex 3-bit d=256: 0.347 nDCG@10, 96 B/doc remex 2-bit d=256: 0.320 nDCG@10, 64 B/doc remex 1-bit d=256: 0.261 nDCG@10, 32 B/doc remex 8-bit d=128: 0.303 nDCG@10, 128 B/doc remex 4-bit d=128: 0.298 nDCG@10, 64 B/doc remex 3-bit d=128: 0.288 nDCG@10, 48 B/doc remex centered 4-bit d=256: 0.348 nDCG@10, 128 B/doc remex centered 3-bit d=256: 0.348 nDCG@10, 96 B/doc remex centered 2-bit d=256: 0.337 nDCG@10, 64 B/doc remex centered 1-bit d=256: 0.310 nDCG@10, 32 B/doc remex centered 4-bit d=128: 0.304 nDCG@10, 64 B/doc remex centered 3-bit d=128: 0.298 nDCG@10, 48 B/doc remex centered 2-bit d=128: 0.292 nDCG@10, 32 B/doc remax k=1 d=768 (asym): 0.330 nDCG@10, 96 B/doc remax k=1 d=768 (sym): 0.295 nDCG@10, 96 B/doc remax k=2 d=768 (asym): 0.348 nDCG@10, 192 B/doc remax k=1 d=512 (asym): 0.313 nDCG@10, 64 B/doc remax k=1 d=512 (sym): 0.267 nDCG@10, 64 B/doc remax k=2 d=512 (asym): 0.337 nDCG@10, 128 B/doc remax k=1 d=256 (asym): 0.252 nDCG@10, 32 B/doc fp32 d=128: 0.304 nDCG@10, 512 B/doc fp32 d=256: 0.349 nDCG@10, 1.0 KB/doc fp32 d=512: 0.368 nDCG@10, 2.0 KB/doc fp32 d=768: 0.369 nDCG@10, 3.0 KB/doc remex centered 1-bit d=768: 0.348 nDCG@10, 96 B/doc remex centered 2-bit d=768: 0.363 nDCG@10, 192 B/doc remex centered 3-bit d=768: 0.367 nDCG@10, 288 B/doc remex centered 4-bit d=768: 0.370 nDCG@10, 384 B/doc remex centered 1-bit d=512: 0.342 nDCG@10, 64 B/doc remex centered 2-bit d=512: 0.360 nDCG@10, 128 B/doc remex centered 3-bit d=512: 0.365 nDCG@10, 192 B/doc remex centered 4-bit d=512: 0.368 nDCG@10, 256 B/doc 128 dims, float32: Google's 6× 768 dims 512 dims 256 dims 4-bit, 768 dims 2-bit, 768 dims: 16× 2-bit, 512 dims 1-bit, 768 dims ST binary, 1-bit Google's truncation, float32Google's truncation, float32 remex centered, all 768 dimsremex centered, all 768 dims remex centered, first 512 dimsremex centered, first 512 dims other armsevery other arm (hover) Pareto frontier uncompressed, 768 dims
Search quality against bytes stored per document, SciFact (left) and NFCorpus (right), log scale. Dark diamonds are Google’s truncation at full precision. Coral circles and blue squares are centered remex at 1 to 4 bits, on all 768 dimensions and on the first 512; at 2 bits and above, plain remex (among the grey dots) lands within 0.006 of each. Grey dots are every other configuration, the thin line is the uncompressed score and the dashed line is the Pareto frontier, the best score reachable at each size. Hover a point for its configuration. Four 16- and 32-byte configurations per corpus score below the plotted range and are left off.

Bits and dimensions

Dropping from 768 to 512 dimensions costs nothing on this model: 0.000 on SciFact, −0.001 on NFCorpus. At a fixed byte budget the two widths then tie. At 192 bytes, 2 bits on 768 dimensions and 3 bits on 512 differ by 0.001 and 0.004, both well inside their intervals. Below 512 dimensions the comparison is against Google’s smallest option. 2 bits on the first 512 dimensions, 128 bytes, beats 128 float32 dimensions at 512 bytes by 0.044 [+0.025, +0.065] on SciFact and 0.053 [+0.036, +0.071] on NFCorpus, at a quarter of the bytes.

Stored asBytesSmallerSciFactNFCorpus
float32, 768 dims3,0721×0.8510.369
float32, 512 dims2,0481.5×+0.000−0.001
float32, 128 dims (Google’s smallest)5126×−0.056−0.065
int8, sentence-transformers, 768 dims7684×0.000+0.001
remex 4-bit, 768 dims3848×−0.003−0.001
remex 2-bit, 768 dims19216×−0.002−0.007
remex 2-bit, 512 dims12824×−0.012−0.012
remex 1-bit centered, 768 dims9632×−0.012−0.021
binary, sentence-transformers, 768 dims9632×−0.022−0.022
remex 1-bit, 768 dims9632×−0.030−0.038
remax 1-bit, 768 dims9632×−0.045−0.039

The first row is the score; the others are changes from it. The int8, 4-bit and 2-bit 768-dimension rows have intervals that include zero on both corpora. All 63 configurations per corpus are in the experiment writeup.

Centering and the 1-bit formats

EmbeddingGemma 2’s document vectors share a large common direction: their average has 85% of a single vector’s length on SciFact and 87% on NFCorpus. Plain remex at 1 bit loses 0.030 and 0.038, and centering recovers about half of that, +0.019 [+0.001, +0.038] and +0.017 [+0.003, +0.031]. At 2 bits and above centering made no measurable difference, so the recommended 2-bit index needs no stored average.

At 1 bit, sentence-transformers’ binary format, which keeps the sign of each raw coordinate with no rotation and no stored average, lands within the noise of centered remex: 0.011 behind on SciFact with an interval spanning zero, 0.001 behind on NFCorpus. remax’s uncentered sign code trails both. What remex adds over the formats sentence-transformers already ships is the 2-, 3- and 4-bit widths in between int8 and binary.

Comparison with pplx-embed-v2-late

pplx-embed-v2-late stores one 128-number vector per token and has no truncation option. Its token vectors share an even larger common direction, an average with 94.5% of a single vector’s length, and plain 1-bit codes lost 0.15 to 0.21 nDCG@10 on SciFact. Centered 2-bit codes, 16× smaller, scored within their interval of zero. That fits the explanation that the shared direction is what costs plain low-bit codes, though the two models differ in more than that: pplx scores each query token against every document token, where EmbeddingGemma 2 compares one vector per document. On both models 2 bits per coordinate is the smallest width whose loss stays inside its interval on every corpus measured, and on EmbeddingGemma 2 that holds without centering.

Using it

remex 1.2.0’s RemexIndex takes the sentence-transformers model directly and applies the card’s query and document prompts:

from sentence_transformers import SentenceTransformer
from remex import RemexIndex

model = SentenceTransformer(
    "google/embeddinggemma-2",
    config_kwargs={"vision_config": None, "audio_config": None})  # text only
index = RemexIndex(model, bits=2)
index.add(documents)
index.search("query text", top_k=10)
index.save("corpus-2bit.npz")

Passing truncate_dim=512 to the model gives the 24× row; center=True on the index is worth adding only at 1 bit; on a full corpus RemexIndex warns about the shared direction at any width, and on this model the warning matters only at 1 bit. The measurements used remex’s Quantizer with random seed 0, where RemexIndex defaults to 42; across two seeds, scores here moved by at most 0.007. The byte counts are the codes alone. A saved RemexIndex also keeps a 4-byte length per document, so the 16× row is about 196 bytes on disk. By default it decodes the codes to float32 once for fast search, so memory at query time is that of a float32 index; low_memory=True scores the codes without decoding them, which I did not time on this model.

Limits

This is text only, while the model’s main pitch is one space across modalities, and the card itself says 128 dimensions “degrades multimodal quality substantially”. Both corpora are small and English. A few thousand documents give each query fewer near neighbours to confuse, which flatters low-bit codes; a corpus of millions will likely lose more at 2 bits than these numbers show. The uncompressed SciFact score of 0.85 is higher than any model I have run on that benchmark, from a 270M encoder, and the card reports MTEB averages rather than SciFact, so I could not check it. If SciFact was in the training data, its vectors may separate relevant from irrelevant documents more cleanly than a new corpus would, which would also flatter compression; NFCorpus, at a typical 0.37, is the safer guide. Encoding both corpora took 1 hour 43 minutes on a 4-core CPU. The scripts, per-query scores and a check that recomputes every number are in oaustegard/experiments.