SHARE
Teams that run retrieval on Qdrant, or are evaluating vector databases, should read the 1.20 release notes. It is not a paper, but three changes directly affect retrieval quality and machine cost. In one sentence: keyword search no longer requires converting text into sparse vectors, and vector storage gains a format with the same bytes but lower error. Key numbers: nested filters on 100,000 points ran 7 to 38 times faster; the new 16-bit turbo16 format has 7.5 times lower reconstruction error than float16; the 8-bit turbo8 takes half the space of float16 and searches 1.1 to 2.4 times faster.
What changed in this release?
Ranked by impact on retrieval teams:
| Change | Before | In 1.20 | Who should care |
|---|---|---|---|
| BM25 directly on full-text indexes | Convert text to sparse vectors and track average document length yourself | Index computes statistics; query directly | Keyword and hybrid search teams |
| Code tokenizer | getUserName indexed as one token | Split on underscores and case changes | Code search teams |
| Per-element indexes for nested filters | Indexes only narrowed candidates; payloads read to verify | Answered from indexes alone | Data with arrays of objects |
| turbo16 and turbo8 datatypes | float16, or self-quantized uint8 | Rotate then quantize, automatically | Large vector sets, tight memory |
| Memory tiers and internal auth | ID tracker always in RAM; internal port on public address | Can live on disk; bind to private interface | Ops and upgrade owners |
Why move BM25 from sparse vectors to the full-text index?
BM25, the standard keyword ranking formula, scores a document from how often the query term appears, how rare the term is in the corpus, and how much longer the document is than average. Previously, every document and query had to be converted into a sparse vector, with the average length computed and baked in by hand.
The problem is that the average goes stale. A hand-computed illustration: the length term is k×(1−b+b×length÷avg), with common values k=1.2 and b=0.75; a 10-word document contains the query term twice.
| Average length | Length term | Term-frequency score 2×2.2÷(2+term) |
|---|---|---|
| 5 (current) | 1.2×(0.25+0.75×2)=2.1 | 4.4÷4.1≈1.07 |
| 10 (stale value from build time) | 1.2×(0.25+0.75×1)=1.2 | 4.4÷3.2≈1.38 |
The same document scores about 28% higher only because the average was not updated, and fixing it meant re-converting every document. In 1.20 the full-text index computes these statistics itself, so text becomes BM25-searchable once indexed, with no model inference. k and b can be set per query without rebuilding. Existing sparse-BM25 users can migrate without re-uploading: create a scoring-enabled full-text index with the same text processing and switch to text queries.
What does the code tokenizer fix?
The default tokenizer splits on spaces and punctuation, so getUserName is a single token and searches for "user name" or "get_user_name" miss it. The code tokenizer splits identifiers on underscores and case changes, including acronyms, so all three forms match. It also indexes the full identifier, so hashmap still finds HashMap. Combined with BM25, code search can be ranked without embeddings.
Why were nested filters slow?
A nested filter finds points where a single array element satisfies all conditions. Illustration: product X has two offers, shop A at 50 and shop B at 200. The filter is "shop B and price below 100." The old shop index says X has a shop-B offer, and the price index says X has an offer below 100, so X becomes a candidate even though the conditions come from different offers. Qdrant had to read X's payload to reject it. A per-element index records each offer separately: the shop match is offer 2, the price match is offer 1, so X is rejected without reading the payload. On 100,000 points with three nested filters, even with all payloads in memory (the best case for reading them), per-element indexes were 7 to 38 times faster. The search API is unchanged.
turbo16: same 16 bits, why less error?
float16 has relative precision: larger values get coarser spacing. With 10 mantissa bits, adjacent representable numbers between 16 and 32 differ by 2^4÷2^10≈0.016, while near 0.01 the spacing is about 0.00001. Some embedding models produce a few very large coordinates (the post names text-embedding-ada-002, CLIP, and Cohere), and their rounding error hurts overall precision. turbo16 first applies the TurboQuant rotation, which Qdrant's docs describe as a fast random rotation: multiplying by a random orthogonal matrix preserves lengths and distances but mixes every coordinate into all others, so one large coordinate is spread across many. In two dimensions, (4, 0) rotated by 45 degrees becomes about (2.83, 2.83). Each rotated coordinate is then rounded to one uniform scale recorded as a 16-bit integer (65,536 steps); if the scale spanned −1 to 1 (illustrative), steps would be about 0.00003 for every coordinate.
Across 13 datasets, turbo16 reconstruction error was 7.5 times lower than float16. On 12 datasets with exact search, turbo16 recall@10 matched float32 within 0.001 everywhere, while float16 dropped to 0.995 on ada-002 and CLIP. With HNSW, a graph-based approximate nearest-neighbor index, turbo16 median latency was 0.81 times float16 on x86 and 0.83 times on ARM, so the same bytes buy better precision and speed.
turbo8: what does halving again cost?

turbo8 stores 1 byte per dimension and, unlike uint8, takes regular float vectors and quantizes them for you. Its recall is close to float16, and its largest gain over the 4-bit turbo4 is on low-dimensional data: on SIFT (128d), turbo4 is about 0.8 and turbo8 about 0.97 by the chart, because with fewer dimensions each one carries more information. Across 11 datasets turbo8 searched 1.1 to 2.4 times faster than float16, mostly within 10% of turbo4. Against float32 plus scalar quantization with rescoring, turbo8 had higher recall on 10 of 11 datasets and tied on the other at similar latency, storing about a fifth of the bytes.

The same 8-bit encoding is available as bits8 quantization, compressing about 4 times versus 8 times for the default bits4.
Upgrade pitfalls
Memory tiers: pinned stays in RAM, cached lives on disk and is loaded into the page cache when a segment opens, cold lives on disk and is read on demand. Inline storage now keeps original vectors only in the HNSW index file, saving disk, but the vectors follow the index's memory tier. The index defaults to cached, so with vectors.memory set to cold and nothing else, originals become cached and quantized vectors become pinned in RAM; set hnsw_config.memory before upgrading. Internal requests now require a valid read-write key when api_key is set, and clusters with only a read-only key will lose peer communication unless they add api_key or disable enforcement. Separately, the ID tracker, about 52 bytes per point or roughly 5 GB for 100 million points, can now live on disk.
Limitations I see
All numbers are vendor benchmarks; the 7–38× nested-filter result covers only 100,000 points and three filters. turbo16's precision gain concentrates on models with a few large coordinates; elsewhere float16 is already near float32 and the main benefit is the 17–19% latency drop. BM25 on full-text indexes solves keyword ranking only; fusing keyword and vector scores in hybrid search still needs your own experiments.
How a retrieval team could try it
Check inline-storage memory tiers and key configuration in staging before upgrading. Migrate one sparse-BM25 collection to a full-text index and compare rankings and latency on the same queries. Store a sample of your production embeddings as float16, turbo16, and turbo8 and compare recall@10, latency, and memory. If your data has object arrays, add per-element indexes and load-test real filters.
Verdict
The standout is turbo8 reaching higher recall than float32 plus scalar quantization at a fifth of the storage, a real cost cut for large, memory-bound retrieval services.
| Dimension | Assessment |
|---|---|
| Problem importance | Medium-high: ranking accuracy, nested-filter speed, and vector storage cost are daily pain points |
| Novelty | Medium: rotate-then-quantize and per-element indexes are known ideas; the value is shipping them as defaults |
| Evidence | Medium: many datasets, but vendor-run, and nested filters tested on only 100,000 points |
| Engineering cost | Low to medium: mostly index or config changes; upgrades need care on memory tiers and auth |
| Worth trying | Yes, Qdrant users should evaluate turbo8 and full-text BM25 first |
How did this read for you?
Hoot made a note. Thanks for the feedback.
One tap, no sign-in; tap again to undo. Only anonymous counts are kept, never who you are.
Discussion
Add experiment details, reproduction notes or a different take. Requires a GitHub sign-in.
Loading the discussion…