@@ -9,6 +9,7 @@ use diskann_vector::distance::InnerProduct;
99use diskann_vector:: { DistanceFunctionMut , PureDistanceFunction } ;
1010
1111use super :: max_sim:: { Chamfer , MaxSim } ;
12+ use super :: projected_eigen:: ProjectedEigen ;
1213use 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) ]
163229mod 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}
0 commit comments