Skip to main content
In this guide, you will:
  • Get introduced briefly to vector search
  • Learn about Approximate Nearest Neighbours (ANN) and Hierarchical Navigable Small World (HNSW)
  • Learn about Quantised Bit (QBit)
  • Use QBit to perform vector search using the DBPedia dataset

Vector search primer

In mathematics and physics, a vector is formally defined as an object that has both a magnitude and direction. This often takes the form of a line segment or an arrow through space and can be used to represent quantities such as velocity, force and acceleration. In computer science, a vector is a finite sequence of numbers. In other words, it’s a data structure that is used to store numeric values. In machine learning, vectors are the same data structures we talk about in computer science, but the numerical values stored in them have a special meaning. When we take a block of text or an image, and distill it down to the key concepts that it represents, this process is called encoding. The resulting output is a machine’s representation of those key concepts in numerical form. This is an embedding, and is stored in a vector. Said differently, when this contextual meaning is embedded in a vector, we can refer to it as an embedding. Vector search is everywhere now. It powers music recommendations, retrieval-augmented generation (RAG) for large language models where external knowledge is fetched to improve answers, and even googling is powered by vector search to some extent. Users often prefer regular databases with ad-hoc vector capabilities over fully specialized vector stores, despite specialized databases’ advantages. ClickHouse supports brute-force vector search as well as methods for approximate nearest neighbour (ANN) search, including HNSW – the current standard for fast vector retrieval.

Understanding embeddings

Let’s look at a simple example to understand how vector search works. Consider embeddings (vector representations) of words: Create the table below with some sample embeddings:
You can search for the most similar words to a given embedding:
The query embedding is closest to “apple” (smallest distance), which makes sense if we look at the two embeddings side by side:

Approximate Nearest Neighbours (ANN)

For large datasets, brute-force search becomes too slow. This is where Approximate Nearest Neighbours methods come in.

Quantisation

Quantisation involves downcasting to smaller numeric types. Smaller numbers mean smaller data, and smaller data means faster distance calculations. ClickHouse’s vectorized query execution engine can fit more values into processor registers per operation, increasing throughput directly. You have two options:
  1. Keep the quantised copy alongside the original column - This doubles storage, but it’s safe as we can always fall back to full precision
  2. Replace the original values entirely (by downcasting on insertion) - This saves space and I/O, but it’s a one-way door

Hierarchical Navigable Small World (HNSW)

HNSW is built from multiple layers of nodes (vectors). Each node is randomly assigned to one or more layers, with the chance of appearing in higher layers decreasing exponentially. When performing a search, we start from a node at the top layer and move greedily towards the closest neighbours. Once no closer node can be found, we descend to the next, denser layer. Because of this layered design, HNSW achieves logarithmic search complexity with respect to the number of nodes.
HNSW limitationThe main bottleneck is memory. ClickHouse uses the usearch implementation of HNSW, which is an in-memory data structure that doesn’t support splitting. As a result, larger datasets require proportionally more RAM.

Comparison of approaches

QBit deep dive

Quantised Bit (QBit)

QBit is a new data structure that can store BFloat16, Float32, and Float64 values by taking advantage of how floating-point numbers are represented – as bits. Instead of storing each number as a whole, QBit splits the values into bit planes: every first bit, every second bit, every third bit, and so on. This approach solves the main limitation of traditional quantisation. There’s no need to store duplicated data or risk making values meaningless. It also avoids the RAM bottlenecks of HNSW, since QBit works directly with the stored data rather than maintaining an in-memory index.
BenefitMost importantly, no upfront decisions are required. Precision and performance can be adjusted dynamically at query time, allowing users to explore the balance between accuracy and speed with minimal friction.
LimitationAlthough QBit speeds up vector search, its computational complexity remains O(n). In other words: if your dataset is small enough for an HNSW index to fit comfortably in RAM, that is still the fastest choice.

The data type

Here’s how to create a QBit column:
When data is inserted into a QBit column, it is transposed so that all first bits line up together, all second bits line up together, and so on. We call these groups. Each group is stored in a separate FixedString(N) column: fixed-length strings of N bytes stored consecutively in memory with no separators between them. All such groups are then bundled together into a single Tuple, which forms the underlying structure of QBit. Example: If we start with a vector of 8×Float64 elements, each group will contain 8 bits. Because a Float64 has 64 bits, we end up with 64 groups (one for each bit). Therefore, the internal layout of QBit(Float64, 8) looks like a Tuple of 64×FixedString(1) columns.
If the original vector length doesn’t divide evenly by 8, the structure is padded with invisible elements to make it align to 8. This ensures compatibility with FixedString, which operates strictly on full bytes.

The distance calculation

To query with QBit, use the L2DistanceTransposed function with a precision parameter:
The third parameter (16) specifies the precision level in bits.

I/O Optimisation

Before we can calculate distances, the required data must be read from disk and then untransposed (converted back from the grouped bit representation into full vectors). Because QBit stores values bit-transposed by precision level, ClickHouse can read only the top bit planes needed to reconstruct numbers up to the desired precision. In the query above, we use a precision level of 16. Since a Float64 has 64 bits, we only read the first 16 bit planes, skipping 75% of the data. After reading, we reconstruct only the top portion of each number from the loaded bit planes, leaving the unread bits zeroed out.

Calculation optimisation

One might ask whether casting to a smaller type, such as Float32 or BFloat16, could eliminate this unused portion. It does work, but explicit casts are expensive when applied to every row. Instead, we can downcast only the reference vector and treat the QBit data as if it contained narrower values (“forgetting” the existence of some columns), since its layout often corresponds to a truncated version of those types.

BFloat16 Optimization

BFloat16 is a Float32 truncated by half. It keeps the same sign bit and 8-bit exponent, but only the upper 7 bits of the 23-bit mantissa. Because of this, reading the first 16 bit planes from a QBit column effectively reproduces the layout of BFloat16 values. So in this case, we can (and do) safely convert the reference vector to BFloat16.

Float64 Complexity

Float64, however, is a different story. It uses an 11-bit exponent and a 52-bit mantissa, meaning it’s not simply a Float32 with twice the bits. Its structure and exponent bias are completely different. Downcasting a Float64 to a smaller format like Float32 requires an actual IEEE-754 conversion, where each value is rounded to the nearest representable Float32. This rounding step is computationally expensive.
If you’re interested in a deep-dive of performance elements of QBit, see “Let’s vectorize”

Example with DBpedia

Let’s see QBit in action with a real-world example using the DBpedia dataset, which contains 1 million Wikipedia articles represented as Float32 embeddings.

Setup

First, create the table
Insert the data from your command line:
Inserting the data could take a while. Time for a coffee break!
Alternatively, individual SQL statements can be run as shown below to load each of the 25 Parquet files:
Verify that 1 million rows are seen in the dbpedia table:
Next, add a QBit column:

Search query

We’ll look for concepts most related to all space-related search terms: Moon, Apollo 11, Space Shuttle, Astronaut, Rocket:
The query searches the top 1000 semantically similar entries to each of the five concepts. It returns entries that appear in at least three of those results, ranked by how many concepts they match and their minimum distance to any of them (excluding the originals). Using just 5 bits (1 sign + 4 exponent bits, zero mantissa):
Performance: 10 rows in set. Elapsed: 0.271 sec. Processed 8.46 million rows, 4.54 GB (31.19 million rows/s., 16.75 GB/s.) Peak memory usage: 739.82 MiB.
Performance: 10 rows in set. Elapsed: 1.157 sec. Processed 10.00 million rows, 32.76 GB (8.64 million rows/s., 28.32 GB/s.) Peak memory usage: 6.05 GiB.

Key Insight

The results? Not just good. Surprisingly good. It’s not obvious that floating points stripped of their entire mantissa and half their exponent still hold meaningful information. The key insight behind QBit is that vector search still works if we ignore insignificant bits. Memory usage reduced from 6.05 GB to 740 MB while maintaining excellent semantic search quality!

Conclusion

QBit is a column type that stores floats as bit planes. It lets you choose how many bits to read during vector search, tuning recall and performance without changing the data. Each vector search method has its own parameters that decide trade-offs for recall, accuracy, and performance. Normally, these have to be chosen up-front. If you get them wrong, a lot of time and resources are wasted, and changing direction later becomes painful. With QBit, no early decisions are needed. You can adjust precision and speed trade-off directly at query time, exploring the right balance as you go.
Adapted from the blog post by Raufs Dunamalijevs, published October 28, 2025
Last modified on June 23, 2026