Distance Metrics
How Basis computes vector distance, the L2 → u32 ordering trick, and what to do when you need cosine similarity.
What this page covers
Basis is hard-coded to L2 (Euclidean) distance in the current build. This page explains why, how the distance is encoded for HNSW, and how to normalize inputs when your model emits cosine-similar embeddings.
The Point type
Point is the unit the HNSW graph stores:
struct Point {
id: Uuid,
vec: Vec<f32>,
}
impl MetricPoint for Point {
type Metric = u32;
fn distance(&self, other: &Self) -> u32 {
let mut sum = 0.0f32;
for i in 0..self.vec.len() {
let diff = self.vec[i] - other.vec[i];
sum += diff * diff;
}
sum.sqrt().to_bits()
}
}Two properties matter:
- L2 distance, computed without SIMD or BLAS in the reference build. The loop is the bottleneck for high-dimensional vectors; if you need faster inserts/queries, swap in a SIMD-backed distance function and rebuild.
f32.to_bits()as the metric type. The HNSW crate requiresOrdon the metric type. Casting a non-negativef32tou32viato_bitspreserves the natural ordering because the IEEE-754 representation of non-negative floats is monotonic inu32space.
Why this works for non-negative distances
For f32 values in [0, +∞):
| Float | Bit pattern | u32 |
|---|---|---|
0.0 | 0x0000_0000 | 0 |
1.0 | 0x3F80_0000 | 1 065 353 216 |
42.0 | 0x4228_0000 | 1 109 393 408 |
The mapping is strictly monotonic, so HNSW's smaller-is-closer comparisons remain correct.
Negative distances would break this — they are not possible from L2 by construction, so the encoding is safe here.
Adapting to cosine similarity
The L2 distance between two unit-normalized vectors a and b is related to cosine similarity by:
||a - b||^2 = 2 - 2·cos(a, b)So if your embedding model already emits unit vectors (CLIP, sentence-transformers normalize=True, OpenAI text-embedding-3-*), L2 nearest-neighbor on those vectors is exactly cosine nearest-neighbor.
fn normalize(v: &mut [f32]) {
let norm = v.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
for x in v.iter_mut() {
*x /= norm;
}
}
}
let mut emb = embed("hello world"); // your embedding pipeline
normalize(&mut emb);
basis.add(Uuid::new_v4(), &emb).await?;Dimensionality
Basis does not check that inserted vectors share a dimensionality — the HNSW graph will silently accept mismatched lengths and produce meaningless distances. Validate on the caller side:
const EMBED_DIM: usize = 768;
if vector.len() != EMBED_DIM {
return Err(format!("expected dim {EMBED_DIM}, got {}", vector.len()).into());
}
basis.add(id, &vector).await?;Performance characteristics
| Operation | Complexity | Notes |
|---|---|---|
add | O(M · log N) HNSW + 1 Strand append | Append dominates on slow disk |
search(k) | O(ef · log N) | ef = max(k, 24) |
remove | Forces full HNSW rebuild | See Snapshot and Recovery |
When to pre-quantize
For corpora above 10 M vectors, scalar quantization (f32 → i8) before insertion reduces memory by 4× and speeds the inner loop. Basis does not perform this automatically; apply it before calling add and to query vectors before calling search.