///|
/// Distance between two equal-length vectors under `metric`, using the same
/// convention as search results. Useful for brute-force ground truth and
/// benchmarks.
pub fn distance(
metric : Distance,
a : FixedArray[Float],
b : FixedArray[Float],
) -> Double raise HnswError {
if a.length() != b.length() {
raise DimMismatch(a.length(), b.length())
}
validate_finite_vector(a)
validate_finite_vector(b)
if metric is Cosine {
// Match copy_normalized's f32 rounding without allocating two temporary
// arrays for every pair. Zero vectors keep their original components.
let mut norm_a_sq = 0.0
let mut norm_b_sq = 0.0
for i in 0.. 0.0
let normalize_b = norm_b_sq > 0.0
let inv_a = if normalize_a { 1.0 / norm_a_sq.sqrt() } else { 0.0 }
let inv_b = if normalize_b { 1.0 / norm_b_sq.sqrt() } else { 0.0 }
let mut sum : Float = 0.0
for i in 0.. Unit raise HnswError {
for value in vector {
let v = value.to_double()
if !(v > -@double.infinity && v < @double.infinity) {
raise InvalidParams("vector must contain only finite values")
}
}
}
///|
/// Distance between two vector slices of length `dim`, under `metric`.
/// For `InnerProduct` and `Cosine` the inner product is negated so that
/// smaller always means more similar.
///
/// Accumulation normally happens in `Float`, matching hnswlib. If finite
/// inputs overflow that path, recompute in `Double` so graph ordering still
/// has a finite distance.
fn distance_between(
metric : Distance,
a : FixedArray[Float],
a_off : Int32,
b : FixedArray[Float],
b_off : Int32,
dim : Int32,
) -> Double {
match metric {
L2 => {
let mut s : Float = 0.0
let mut i = 0
while i < dim {
let d = a.unsafe_get(a_off + i) - b.unsafe_get(b_off + i)
s += d * d
i += 1
}
let result = s.to_double()
if result > -@double.infinity && result < @double.infinity {
result
} else {
let mut recovered = 0.0
let mut j = 0
while j < dim {
let d = a.unsafe_get(a_off + j).to_double() -
b.unsafe_get(b_off + j).to_double()
recovered += d * d
j += 1
}
recovered
}
}
InnerProduct | Cosine => {
let mut s : Float = 0.0
let mut i = 0
while i < dim {
s += a.unsafe_get(a_off + i) * b.unsafe_get(b_off + i)
i += 1
}
let result = s.to_double()
if result > -@double.infinity && result < @double.infinity {
-result
} else {
let mut recovered = 0.0
let mut j = 0
while j < dim {
recovered += a.unsafe_get(a_off + j).to_double() *
b.unsafe_get(b_off + j).to_double()
j += 1
}
-recovered
}
}
}
}
///|
/// Copies `src[src_off .. src_off+dim]` into `dst[dst_off .. dst_off+dim]`,
/// L2-normalizing when `normalize` is true and the norm is non-zero. Zero
/// vectors are copied as-is.
fn copy_normalized(
src : FixedArray[Float],
src_off : Int32,
dst : FixedArray[Float],
dst_off : Int32,
dim : Int32,
normalize : Bool,
) -> Double {
let mut norm_sq = 0.0
let mut i = 0
while i < dim {
let v = src.unsafe_get(src_off + i).to_double()
norm_sq += v * v
i += 1
}
if normalize && norm_sq > 0.0 {
let inv = 1.0 / norm_sq.sqrt()
i = 0
while i < dim {
dst[dst_off + i] = Float::from_double(
src.unsafe_get(src_off + i).to_double() * inv,
)
i += 1
}
} else {
i = 0
while i < dim {
dst[dst_off + i] = src.unsafe_get(src_off + i)
i += 1
}
}
norm_sq.sqrt()
}
///|
/// Sorts (distance, id) pairs by distance, ascending. Insertion sort: the
/// inputs here are beam-sized, so O(n^2) is negligible next to the distance
/// computations that produced them.
fn sort_pairs_ascending(
pairs : Array[(Double, Int32)],
) -> Array[(Double, Int32)] {
let arr = pairs
for x in 1..= 0 && arr[y].0 > v.0 {
arr[y + 1] = arr[y]
y -= 1
}
arr[y + 1] = v
}
arr
}