Qdrant 1.20 Ranks Keyword Matches Inside the Full-Text Index

Qdrant 1.20 ranks keyword matches with BM25 straight from the full-text index, computes its own scoring statistics, adds a code tokenizer, and splits the upgrade into two memory-tier changes

Netics editorial card on Qdrant 1.20, showing BM25 keyword ranking inside the full-text index and the two storage datatypes.
Netics editorial card on Qdrant 1.20, covering BM25 scoring on the full-text index, the code tokenizer, per-element indexes for nested filters and the turbo16 and turbo8 datatypes.

TL;DR

  • Qdrant is an open-source vector database that stores embeddings beside payload fields and answers similarity searches over them.
  • Version 1.20, published on 22 October 2026, ranks keyword matches with BM25 directly on the full-text index, using payload text as its input.
  • Qdrant computes the BM25 statistics itself, average document length included, which keeps scoring accurate as documents are added and changed.
  • A text query ranks any indexed field across the Query API, and the k and b parameters are set per query.
  • A new code tokenizer splits identifiers on underscores and case changes, so getUserName, get_user_name and user name match each other.
  • Per-element indexes answer nested filters from the indexes alone, measured at 7 to 38 times faster on 100,000 points.
  • Turbo16 and Turbo8 shrink vector storage at two precision levels, and turbo16 kept recall@10 within 0.001 of float32 across the 12 search-benchmark datasets.
  • Internal authentication now guards the peer gRPC port by default, and inline storage takes the memory tier of the HNSW index, which makes the upgrade plan the first thing to read.
  • The upgrade needs a decision for clusters that configure only a read-only key.
Qdrant 1.20 release post with the headline features listed, from the Qdrant release post
Capture of the Qdrant 1.20 release post, qdrant.tech/blog/qdrant-1.20.x, taken 29 October 2026.

Keyword search inside a vector database used to be a sidecar. Teams that wanted BM25 fed every document through an embedding model to produce sparse vectors, stored those next to their dense vectors, and paid for the pipeline again whenever the corpus changed. Qdrant 1.20 moves that work into the index the database was already maintaining.

This article deliberately covers five groups relevant to retrieval and upgrade operations from a broader release that also adds snapshot storage on GCS and Azure Blob Storage, audit-log output to stdout or stderr, collection creation times, safer replica removal, local-time log timestamps and substring matching: BM25 ranking on the full-text index, a code tokenizer, per-element indexes for nested filters, two new TurboQuant datatypes, and a pair of security and memory-tier changes that alter how a cluster behaves after the upgrade. The release post describes BM25 ranking, code tokenization, per-element indexes and TurboQuant datatypes as product features. The internal-authentication and memory-tier changes require checks before an upgrade.

BM25 scoring runs on the full-text index and computes its own statistics

The old path forced an inference step on every document and every query. It also made the scoring statistics somebody else's problem: the average document length had to be tracked outside Qdrant, because the value was baked into each stored sparse vector and could not follow the data as it changed. Updating it meant re-encoding the whole corpus.

Netics comparison table of BM25 inputs, sparse vectors against the full-text index
Netics editorial visual: where the BM25 inputs come from in each implementation. Sparse vectors carry the scoring inputs inside every vector; the full-text index derives them from the collection.

Version 1.20 derives those statistics from the collection itself, average document length included, so scoring is correct on the first query and stays correct as documents arrive. That is a smaller change than it sounds. The reason teams choose retrieval augmented generation for a production workload is that the corpus moves; a ranking function whose statistics are frozen in vectors moves with it only when someone rebuilds.

Sparse-vector BM25 remains supported, and the migration path the post describes keeps the data where it is: create the full-text index with the bm25 scoring type, configure the same text processing the sparse model used, then replace the sparse queries with text queries.

The text query ranks any indexed field across the Query API

The API shape is small. A full-text index on a field is created with the text type and the bm25 scoring type, and a query names the field through using while passing the search terms in a text object. Because it is a normal Query API query, it composes: a text query can run inside hybrid search alongside a dense vector, and it can rescore points another query retrieved.

Qdrant indexing documentation page describing payload and full-text index types
Capture of the Qdrant indexing documentation, qdrant.tech/documentation/concepts/indexing, taken 29 October 2026.

The parameter placement is the part worth noticing. k and b are query-time settings, so tuning the length normalisation does not rebuild the index, and a collection can hold one index that serves several query profiles. That is a real operational difference from the sparse-vector path, where a change in the scoring configuration meant re-encoding.

The code tokenizer splits identifiers on underscores and case changes

The default word tokenizer breaks text at spaces and punctuation, which suits prose and fails on source code. getUserName becomes one token, so a search for user name finds nothing. Anyone who has searched a large repository knows the failure mode: the text is present, the index cannot see it.

The new code tokenizer splits identifiers at underscores and at case boundaries, acronyms included, and it also indexes the whole identifier. getUserName, get_user_name and user name all reach the same place, and hashmap still finds HashMap. Combined with BM25 on a full-text index, code search gains ranked results directly from payload text, which is a useful option for a self-hosted documentation or repository search box.

Qdrant nested-filter diagram: point 7 fails and point 9 matches, from the Qdrant 1.20 release post
Qdrant 1.20 diagram from the release post, qdrant.tech/blog/qdrant-1.20.x: pointer 7 has no single offer that satisfies both conditions, point 9 does, which is the case a per-element index answers directly.

Per-element indexes answer nested filters directly from the indexes

Nested filters are the awkward case in any document store. A payload holds an array of objects such as offers, each with a shop and a price, and the filter asks for a point where one single offer satisfies both conditions. Indexes on offers[].shop and offers[].price could only shorten the candidate list; Qdrant then read each surviving payload, usually from disk, to check the pairing.

Version 1.20 adds a nested parameter to every payload index type, which builds a per-element index storing each array element separately. When every condition in a nested filter uses a field with a per-element index on the same array, the per-element indexes answer the filter directly. The query API stays as it was, so existing filters speed up as the indexes are created. On three nested filters over 100,000 points, the vendor measured 7 to 38 times faster than regular indexes with payload reads, with every payload already in memory; the gap is likely larger when payloads have to be fetched from disk.

Qdrant TurboQuant diagram mapping float32 vectors to turbo16 and turbo8, from the Qdrant 1.20 release post
Qdrant 1.20 diagram from the release post, qdrant.tech/blog/qdrant-1.20.x: float32 vectors go through the TurboQuant rotation and land on a turbo16 or turbo8 grid.

Turbo16 and Turbo8 shrink vectors at two precision levels

Storage is where the release pays for itself. float16 keeps equal relative precision for every coordinate, so a large coordinate carries a large rounding error, and embeddings with a few very large coordinates suffer for it. turbo16 applies the TurboQuant rotation first, spreading values across coordinates, then stores each vector on a single 16-bit integer grid. Same 2 bytes per dimension, and the vendor measured 7.5 times lower reconstruction error than float16 across 13 datasets.

turbo8 stores each dimension in one byte. In the vendor's 11-dataset benchmark, search ran 1.1 to 2.4 times faster than float16, recall came close to float16 at half the size, and the gain over turbo4 was largest at low dimensions. The 8-bit encoding is also available as TurboQuant quantization through bits8, which compresses about four times, against eight times for bits4; bits4 remains the default.

Netics metric grid of Qdrant 1.20 accuracy and memory figures
Netics editorial visual: the four numbers from the release post that a capacity decision turns on, with the benchmark each one came from.

Internal authentication now guards the peer gRPC port

Distributed Qdrant nodes talk over an internal gRPC API. In 1.20 that API requires authentication by default whenever an API key is configured, and it accepts only read-write keys: the read-only key and JWTs are rejected. The internal port can also be bound to its own address through cluster.p2p.host, so it can sit on a private interface while the public API keeps its current address.

The failure mode is specific and worth writing into the change ticket. A cluster that sets only read_only_api_key stops letting its peers talk to each other after the upgrade, and the fix is either setting api_key or setting enforce_internal_auth: false. Clusters that set only alt_api_key on 1.19.0 or 1.19.1 have to reach 1.19.2 first. Clusters without an API key see no change.

Inline storage takes the memory tier of the HNSW index

Inline storage places the original vectors in the HNSW index file next to the graph links, and until 1.20 Qdrant kept a duplicate copy in regular vector storage. The duplicate is gone, which saves disk. The consequence is that those vectors now inherit the memory tier of the index: a pinned index pins its vectors, and a cold index cools them. A collection that ran with vectors.memory: cold can therefore need more RAM after the upgrade, because the HNSW index defaults to cached.

Qdrant diagram of pinned, cached and cold ID tracker tiers, from the Qdrant 1.20 release post
Qdrant 1.20 diagram from the release post, qdrant.tech/blog/qdrant-1.20.x: the ID tracker moves between RAM, page cache and disk without changing the collection's vectors.

The same release extends memory tiers to the ID tracker, which maps each point ID to its location and version and historically sat in RAM at about 52 bytes per point, roughly 5 GB for 100 million points. pinned stays the default, cold reads from disk on demand, and cached loads the tracker into the page cache when a segment opens.

The upgrade order a 1.20 cluster needs

Read the release as an operations document. The documentation identifies two changes that can break a working cluster: the internal authentication default and the tier inheritance for inline-storage vectors. BM25, code tokenization, per-element indexes and TurboQuant datatypes are available for separate evaluation after those checks.

BM25 on the full-text index is the feature worth testing first. BM25 on the full-text index removes a pipeline and a data-consistency problem, and it costs one index creation to try on a copy of a collection. The nested per-element index can improve an application through index configuration alone, making it the cheapest experiment on the list.

Netics checklist of four checks to run before moving a Qdrant cluster to 1.20
Netics editorial checklist: the four items to settle before a Qdrant cluster moves to 1.20, in the order that avoids a failed rolling upgrade.

Upgrade one minor version at a time, so 1.18 goes to 1.19 and then to 1.20.0, and set hnsw_config.memory and the quantization memory tier before the upgrade to preserve the intended memory placement throughout the change. A stagger test on one node answers most of the questions here, which is cheaper than the same answer from a production incident.

For related operational analysis, see the Netics blog. For this upgrade, a first pass on one collection usually settles the questions above; compare the result with the Qdrant release notes. The release notes carry the numbers this article quotes, and a staging node can confirm the tier behaviour above; [book a Netics review](https://neticslabs.com/#booking) if you want help planning that test.

Sources

Source: Qdrant 1.20 release post, the Qdrant indexing and hybrid-queries documentation, and the v1.20.0 GitHub release — all read on 2026-10-29. The BM25 statistics behaviour, the text query, the code tokenizer, the per-element index benchmark of 7 to 38 times on 100,000 points, the turbo16 7.5x error and 0.001 recall figures, the turbo8 1.1 to 2.4 times speed range, the bits8 compression ratio, the 52 bytes per point ID tracker figure, the internal authentication defaults and the inline-storage tier inheritance all come from those pages.