candding 0.1.0

Pure candle embeddings for Rust with no ONNX: dense, sparse, multi-vector and rerank models today, image models on the roadmap
Documentation
//! The per-checkpoint reduction of a MaxSim sum: plain, or divided by the query's real row count.

use super::EMPTY_DOCUMENT_SCORE;
use super::MaskedRowFill;
use super::MultiVectorEmbedding;
use super::late_interaction_score;

/// How a checkpoint's reference reduces the MaxSim sum: `Sum` keeps it, `MeanByQueryLength` divides it by the
/// query's real row count, as `BAAI/bge-m3` does.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ScoreReduction {
  Sum,
  MeanByQueryLength,
}

impl ScoreReduction {
  /// Divides `sum` by `real_query_rows` for `MeanByQueryLength`, leaving a zero-row `0.0` unscaled instead of NaN.
  fn apply(self, sum: f32, real_query_rows: usize) -> f32 {
    match (self, real_query_rows) {
      (ScoreReduction::Sum, _) => sum,
      (ScoreReduction::MeanByQueryLength, 0) => sum,
      (ScoreReduction::MeanByQueryLength, rows) => sum / rows as f32,
    }
  }
}

/// `late_interaction_score` with no masks, since catalog output holds only real rows, then the checkpoint's own
/// `reduction`; the empty-document sentinel is returned unscaled, so that document still sorts last.
pub fn late_interaction_score_for(
  reduction: ScoreReduction,
  query: &MultiVectorEmbedding,
  document: &MultiVectorEmbedding,
  masked_document_row_fill: MaskedRowFill,
) -> f32 {
  let sum = late_interaction_score(query, None, document, None, masked_document_row_fill);
  if sum == EMPTY_DOCUMENT_SCORE {
    return sum;
  }
  reduction.apply(sum, query.row_count())
}

#[cfg(test)]
mod tests;