Skip to content

Commit f83d17f

Browse files
committed
added projected eigen distance function
1 parent ab967c1 commit f83d17f

4 files changed

Lines changed: 133 additions & 1 deletion

File tree

diskann-quantization/src/multi_vector/distance/fallback.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use diskann_vector::distance::InnerProduct;
99
use diskann_vector::{DistanceFunctionMut, PureDistanceFunction};
1010

1111
use super::max_sim::{Chamfer, MaxSim};
12+
use super::projected_eigen::ProjectedEigen;
1213
use crate::multi_vector::{MatRef, MaxSimError, Repr, Standard};
1314

1415
/////////////////
@@ -100,6 +101,50 @@ impl FallbackKernel {
100101
f(i, min_dist);
101102
}
102103
}
104+
105+
/// Core kernel for computing per-query-vector projected-eigen scores.
106+
///
107+
/// For each `query` vector, sums the negated squared inner product
108+
/// against every document vector, then calls `f(index, score)` with the
109+
/// result. If there are no vectors in the `doc`, the kernel returns
110+
/// immediately.
111+
///
112+
/// The callback can be used to aggregate scores as needed - as is the
113+
/// case with [`ProjectedEigen`].
114+
///
115+
/// # Arguments
116+
///
117+
/// * `query` - The query multi-vector (wrapped as [`QueryMatRef`])
118+
/// * `doc` - The document multi-vector
119+
/// * `f` - Callback invoked with `(query_index, score)` for each query vector
120+
#[inline]
121+
pub(crate) fn projected_eigen_kernel<F, T: Copy>(
122+
query: QueryMatRef<'_, Standard<T>>,
123+
doc: MatRef<'_, Standard<T>>,
124+
mut f: F,
125+
) where
126+
F: FnMut(usize, f32),
127+
InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
128+
{
129+
// Early exit if no doc vectors - callback should never be invoked
130+
if doc.num_vectors() == 0 {
131+
return;
132+
}
133+
134+
for (i, q_vec) in query.rows().enumerate() {
135+
let mut sum = 0.0f32;
136+
137+
for d_vec in doc.rows() {
138+
// `InnerProduct::evaluate` returns the negated inner product;
139+
// squaring discards the sign, so negate the squared value to
140+
// obtain `-IP(q, d)²`.
141+
let ip = InnerProduct::evaluate(q_vec, d_vec);
142+
sum += -(ip * ip);
143+
}
144+
145+
f(i, sum);
146+
}
147+
}
103148
}
104149

105150
////////////
@@ -159,6 +204,27 @@ where
159204
}
160205
}
161206

207+
/////////////////////
208+
// ProjectedEigen //
209+
/////////////////////
210+
211+
impl<T: Copy> PureDistanceFunction<QueryMatRef<'_, Standard<T>>, MatRef<'_, Standard<T>>, f32>
212+
for ProjectedEigen
213+
where
214+
InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
215+
{
216+
#[inline(always)]
217+
fn evaluate(query: QueryMatRef<'_, Standard<T>>, doc: MatRef<'_, Standard<T>>) -> f32 {
218+
let mut sum = 0.0f32;
219+
220+
FallbackKernel::projected_eigen_kernel(query, doc, |_i, score| {
221+
sum += score;
222+
});
223+
224+
sum
225+
}
226+
}
227+
162228
#[cfg(test)]
163229
mod tests {
164230
use super::*;
@@ -185,6 +251,17 @@ mod tests {
185251
.fold(f32::MAX, f32::min)
186252
}
187253

254+
/// Naive implementation of projected-eigen for a single query vector
255+
/// against all doc vectors: `\sum_{j} -IP(q, d_{j})^2`.
256+
fn naive_projected_eigen_single(query_vec: &[f32], doc: &MatRef<'_, Standard<f32>>) -> f32 {
257+
doc.rows()
258+
.map(|d_vec| {
259+
let ip: f32 = query_vec.iter().zip(d_vec.iter()).map(|(a, b)| a * b).sum();
260+
-(ip * ip)
261+
})
262+
.sum()
263+
}
264+
188265
/// Generate deterministic test data.
189266
fn make_test_data(len: usize, ceil: usize, shift: usize) -> Vec<f32> {
190267
(0..len).map(|v| ((v + shift) % ceil) as f32).collect()
@@ -286,6 +363,22 @@ mod tests {
286363
nd,
287364
dim
288365
);
366+
367+
// Test ProjectedEigen
368+
let projected = ProjectedEigen::evaluate(query, doc);
369+
let expected_projected: f32 = query
370+
.rows()
371+
.map(|q_vec| naive_projected_eigen_single(q_vec, &doc))
372+
.sum();
373+
374+
assert!(
375+
(projected - expected_projected).abs()
376+
< 1e-6 * expected_projected.abs().max(1.0),
377+
"ProjectedEigen mismatch for ({},{},{})",
378+
nq,
379+
nd,
380+
dim
381+
);
289382
}
290383
}
291384

@@ -303,5 +396,15 @@ mod tests {
303396

304397
assert_eq!(result, 0.0);
305398
}
399+
400+
#[test]
401+
fn projected_eigen_with_zero_docs_returns_zero() {
402+
let query = make_query(&[1.0, 0.0, 0.0, 1.0], 2, 2);
403+
let doc = make_doc(&[], 0, 2);
404+
405+
// No document vectors means no pairs contribute, so the sum is 0.
406+
let result = ProjectedEigen::evaluate(query, doc);
407+
assert_eq!(result, 0.0);
408+
}
306409
}
307410
}

diskann-quantization/src/multi_vector/distance/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ mod isa;
4747
mod kernel;
4848
mod kernels;
4949
mod max_sim;
50+
mod projected_eigen;
5051

5152
pub use factory::{MaxSimElement, build_max_sim};
5253
pub use fallback::QueryMatRef;
5354
pub use isa::{MaxSimIsa, NotSupported};
5455
pub use kernel::{BoxErase, Erase, MaxSimKernel};
5556
pub use max_sim::{Chamfer, MaxSim, MaxSimError};
57+
pub use projected_eigen::ProjectedEigen;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
//! Projected-eigen distance type for multi-vector representations.
5+
6+
/////////////////////
7+
// ProjectedEigen //
8+
/////////////////////
9+
10+
/// Projected-eigen distance for multi-vector similarity.
11+
///
12+
/// Computes the negated sum of squared inner products over *all*
13+
/// query/document vector pairs:
14+
///
15+
/// ```text
16+
/// ProjectedEigen(Q, D) = \sum_{i} \sum_{j} -IP(q_i, d_j)²
17+
/// ```
18+
///
19+
/// Unlike [`Chamfer`](super::Chamfer), which keeps only the best-matching
20+
/// document vector per query vector, this accumulates a contribution from
21+
/// every pair, so the score reflects the full query–document interaction. As
22+
/// with the other multi-vector distances, lower is better.
23+
///
24+
/// Implements [`PureDistanceFunction`](diskann_vector::PureDistanceFunction)
25+
/// for matrix view types.
26+
#[derive(Debug, Clone, Copy)]
27+
pub struct ProjectedEigen;

diskann-quantization/src/multi_vector/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub(crate) mod matrix;
5858
pub use block_transposed::{BlockTransposed, BlockTransposedMut, BlockTransposedRef};
5959
pub use distance::{
6060
BoxErase, Chamfer, Erase, MaxSim, MaxSimElement, MaxSimError, MaxSimIsa, MaxSimKernel,
61-
NotSupported, QueryMatRef, build_max_sim,
61+
NotSupported, ProjectedEigen, QueryMatRef, build_max_sim,
6262
};
6363
pub use matrix::{
6464
Defaulted, LayoutError, Mat, MatMut, MatRef, NewCloned, NewMut, NewOwned, NewRef, Overflow,

0 commit comments

Comments
 (0)