Skip to content

Commit 0449d4d

Browse files
authored
Introduce Projected Eigen distance function for multi-vector re-ranking (#1203)
<!-- Thanks for contributing a pull request! Please ensure you have taken a look at the contribution guidelines: https://github.com/microsoft/DiskANN/blob/main/CONTRIBUTING.md --> - [x] Does this PR have a descriptive title that could go in our release notes? - [ ] Does this PR add any new dependencies? - [ ] Does this PR modify any existing APIs? - [ ] Is the change to the API backwards compatible? - [ ] Should this result in any changes to our documentation, either updating existing docs or adding new ones? #### Reference Issues/PRs #1201 <!-- Example: Fixes #1234. See also #3456. Please use keywords (e.g., Fixes) to create link to the issues or pull requests you resolved, so that they will automatically be closed when your pull request is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests --> #### What does this implement/fix? Briefly explain your changes. Introduce a new distance function for multi-vector re-ranking: `ProjectedEigen`. Given a query multivector `Q = [q_1, q_2, .. q_K]` and doc multivector `D = [d_1, d_2, ... d_N]`, `ProjectedEigen` distance between the two (asymmetric) is defined as $$\sum_{i=1}^{i=K} \sum_{j=1}^{j=N} - (IP(q_i, d_j)^2)$$. This PR introduces preliminary support for this distance function inside the `diskann-quantization` crate (similar to `MaxSim`). #### Any other comments?
1 parent ab967c1 commit 0449d4d

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)