use super::{simd, RerankError, Result};
const MAX_SIMHASH_BITS: u8 = 12;
const DEFAULT_SEED: u64 = 0x7a5d_12c3_8e91_b6f0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FdeConfig {
simhash_bits: u8,
repetitions: usize,
seed: u64,
fill_empty_doc_clusters: bool,
}
impl Default for FdeConfig {
fn default() -> Self {
Self {
simhash_bits: 2,
repetitions: 4,
seed: DEFAULT_SEED,
fill_empty_doc_clusters: true,
}
}
}
impl FdeConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn simhash_bits(&self) -> u8 {
self.simhash_bits
}
#[must_use]
pub const fn repetitions(&self) -> usize {
self.repetitions
}
#[must_use]
pub const fn seed(&self) -> u64 {
self.seed
}
#[must_use]
pub const fn fills_empty_doc_clusters(&self) -> bool {
self.fill_empty_doc_clusters
}
pub fn with_simhash_bits(mut self, simhash_bits: u8) -> Result<Self> {
if simhash_bits > MAX_SIMHASH_BITS {
return Err(RerankError::InvalidFdeConfig {
reason: "simhash_bits must be <= 12",
});
}
self.simhash_bits = simhash_bits;
Ok(self)
}
pub fn with_repetitions(mut self, repetitions: usize) -> Result<Self> {
if repetitions == 0 {
return Err(RerankError::InvalidFdeConfig {
reason: "repetitions must be >= 1",
});
}
self.repetitions = repetitions;
Ok(self)
}
#[must_use]
pub const fn with_seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
#[must_use]
pub const fn with_empty_doc_cluster_fill(mut self, fill: bool) -> Self {
self.fill_empty_doc_clusters = fill;
self
}
#[must_use]
pub const fn buckets(&self) -> usize {
1usize << self.simhash_bits
}
#[must_use]
pub fn encoded_len(&self, dim: usize) -> usize {
self.repetitions * self.buckets() * dim
}
pub fn encode_query(&self, tokens: &[Vec<f32>]) -> Result<FixedDimEncoding> {
let dim = validate_tokens(tokens)?;
let buckets = self.buckets();
let mut values = vec![0.0; self.encoded_len(dim)];
for repetition in 0..self.repetitions {
for token in tokens {
let bucket = simhash_bucket(token, repetition, self.simhash_bits, self.seed);
let offset = block_offset(repetition, bucket, buckets, dim);
add_into(&mut values[offset..offset + dim], token);
}
}
Ok(FixedDimEncoding {
values,
repetitions: self.repetitions,
buckets,
dim,
})
}
pub fn encode_document(&self, tokens: &[Vec<f32>]) -> Result<FixedDimEncoding> {
let dim = validate_tokens(tokens)?;
let buckets = self.buckets();
let mut values = vec![0.0; self.encoded_len(dim)];
let mut counts = vec![0usize; self.repetitions * buckets];
for repetition in 0..self.repetitions {
for token in tokens {
let bucket = simhash_bucket(token, repetition, self.simhash_bits, self.seed);
counts[repetition * buckets + bucket] += 1;
let offset = block_offset(repetition, bucket, buckets, dim);
add_into(&mut values[offset..offset + dim], token);
}
}
for repetition in 0..self.repetitions {
for bucket in 0..buckets {
let count = counts[repetition * buckets + bucket];
if count > 0 {
let offset = block_offset(repetition, bucket, buckets, dim);
scale_in_place(&mut values[offset..offset + dim], 1.0 / count as f32);
}
}
}
if self.fill_empty_doc_clusters {
fill_empty_document_buckets(&mut values, &counts, self.repetitions, buckets, dim);
}
Ok(FixedDimEncoding {
values,
repetitions: self.repetitions,
buckets,
dim,
})
}
pub fn score(&self, query: &[Vec<f32>], document: &[Vec<f32>]) -> Result<f32> {
let query = self.encode_query(query)?;
let document = self.encode_document(document)?;
query.score(&document)
}
pub fn rank<I: Clone>(
&self,
query: &[Vec<f32>],
docs: &[(I, Vec<Vec<f32>>)],
) -> Result<Vec<(I, f32)>> {
let query = self.encode_query(query)?;
let mut results = Vec::with_capacity(docs.len());
for (id, tokens) in docs {
let document = self.encode_document(tokens)?;
results.push((id.clone(), query.score(&document)?));
}
super::sort_scored_desc(&mut results);
Ok(results)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FixedDimEncoding {
values: Vec<f32>,
repetitions: usize,
buckets: usize,
dim: usize,
}
impl FixedDimEncoding {
#[must_use]
pub fn as_slice(&self) -> &[f32] {
&self.values
}
#[must_use]
pub fn into_vec(self) -> Vec<f32> {
self.values
}
#[must_use]
pub const fn repetitions(&self) -> usize {
self.repetitions
}
#[must_use]
pub const fn buckets(&self) -> usize {
self.buckets
}
#[must_use]
pub const fn token_dim(&self) -> usize {
self.dim
}
#[must_use]
pub fn len(&self) -> usize {
self.values.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn score(&self, other: &Self) -> Result<f32> {
self.check_compatible(other)?;
Ok(simd::dot(&self.values, &other.values) / self.repetitions as f32)
}
fn check_compatible(&self, other: &Self) -> Result<()> {
if self.values.len() != other.values.len() {
return Err(RerankError::DimensionMismatch {
expected: self.values.len(),
got: other.values.len(),
});
}
if self.repetitions != other.repetitions {
return Err(RerankError::DimensionMismatch {
expected: self.repetitions,
got: other.repetitions,
});
}
if self.buckets != other.buckets {
return Err(RerankError::DimensionMismatch {
expected: self.buckets,
got: other.buckets,
});
}
if self.dim != other.dim {
return Err(RerankError::DimensionMismatch {
expected: self.dim,
got: other.dim,
});
}
Ok(())
}
}
fn validate_tokens(tokens: &[Vec<f32>]) -> Result<usize> {
let Some(first) = tokens.first() else {
return Err(RerankError::InvalidFdeConfig {
reason: "token list must be non-empty",
});
};
let dim = first.len();
if dim == 0 {
return Err(RerankError::InvalidFdeConfig {
reason: "token dimension must be >= 1",
});
}
for token in tokens {
if token.len() != dim {
return Err(RerankError::DimensionMismatch {
expected: dim,
got: token.len(),
});
}
}
Ok(dim)
}
fn simhash_bucket(token: &[f32], repetition: usize, bits: u8, seed: u64) -> usize {
let mut bucket = 0usize;
for bit in 0..bits {
let mut sum = 0.0;
for (dim, value) in token.iter().enumerate() {
let sign = projection_sign(seed, repetition, bit, dim);
sum += sign * value;
}
if sum >= 0.0 {
bucket |= 1usize << bit;
}
}
bucket
}
fn projection_sign(seed: u64, repetition: usize, bit: u8, dim: usize) -> f32 {
let mut x = seed;
x ^= (repetition as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15);
x ^= (bit as u64).wrapping_mul(0xbf58_476d_1ce4_e5b9);
x ^= (dim as u64).wrapping_mul(0x94d0_49bb_1331_11eb);
if splitmix64(x) & 1 == 0 {
-1.0
} else {
1.0
}
}
fn splitmix64(mut x: u64) -> u64 {
x = x.wrapping_add(0x9e37_79b9_7f4a_7c15);
x = (x ^ (x >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
x = (x ^ (x >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
x ^ (x >> 31)
}
fn block_offset(repetition: usize, bucket: usize, buckets: usize, dim: usize) -> usize {
(repetition * buckets + bucket) * dim
}
fn add_into(accumulator: &mut [f32], token: &[f32]) {
for (acc, value) in accumulator.iter_mut().zip(token) {
*acc += value;
}
}
fn scale_in_place(values: &mut [f32], factor: f32) {
for value in values {
*value *= factor;
}
}
fn fill_empty_document_buckets(
values: &mut [f32],
counts: &[usize],
repetitions: usize,
buckets: usize,
dim: usize,
) {
for repetition in 0..repetitions {
for bucket in 0..buckets {
if counts[repetition * buckets + bucket] > 0 {
continue;
}
let Some(nearest) = nearest_non_empty_bucket(counts, repetition, bucket, buckets)
else {
continue;
};
let dst = block_offset(repetition, bucket, buckets, dim);
let src = block_offset(repetition, nearest, buckets, dim);
for offset in 0..dim {
values[dst + offset] = values[src + offset];
}
}
}
}
fn nearest_non_empty_bucket(
counts: &[usize],
repetition: usize,
bucket: usize,
buckets: usize,
) -> Option<usize> {
(0..buckets)
.filter(|candidate| counts[repetition * buckets + candidate] > 0)
.min_by_key(|candidate| {
(
(bucket ^ candidate).count_ones(),
bucket.abs_diff(*candidate),
*candidate,
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encoded_len_matches_configuration() {
let config = FdeConfig::new()
.with_simhash_bits(3)
.unwrap()
.with_repetitions(5)
.unwrap();
assert_eq!(config.buckets(), 8);
assert_eq!(config.encoded_len(7), 5 * 8 * 7);
}
#[test]
fn rejects_invalid_configuration() {
assert!(matches!(
FdeConfig::new().with_repetitions(0),
Err(RerankError::InvalidFdeConfig { .. })
));
assert!(matches!(
FdeConfig::new().with_simhash_bits(13),
Err(RerankError::InvalidFdeConfig { .. })
));
}
#[test]
fn rejects_mixed_token_dimensions() {
let tokens = vec![vec![1.0, 0.0], vec![1.0]];
assert!(matches!(
FdeConfig::new().encode_query(&tokens),
Err(RerankError::DimensionMismatch {
expected: 2,
got: 1
})
));
}
#[test]
fn single_bucket_single_token_matches_dot_product() {
let config = FdeConfig::new()
.with_simhash_bits(0)
.unwrap()
.with_repetitions(1)
.unwrap();
let query = vec![vec![1.0, 2.0, 3.0]];
let document = vec![vec![4.0, 5.0, 6.0]];
let score = config.score(&query, &document).unwrap();
assert_eq!(score, 32.0);
}
#[test]
fn fixed_vectors_score_if_compatible() {
let config = FdeConfig::new()
.with_simhash_bits(0)
.unwrap()
.with_repetitions(2)
.unwrap();
let query = config.encode_query(&[vec![1.0, 0.0]]).unwrap();
let document = config.encode_document(&[vec![0.5, 0.5]]).unwrap();
assert_eq!(query.repetitions(), 2);
assert_eq!(query.buckets(), 1);
assert_eq!(query.token_dim(), 2);
assert_eq!(query.score(&document).unwrap(), 0.5);
}
#[test]
fn rank_sorts_by_proxy_score() {
let config = FdeConfig::new()
.with_simhash_bits(0)
.unwrap()
.with_repetitions(1)
.unwrap();
let query = vec![vec![1.0, 0.0]];
let docs = vec![
("weak", vec![vec![0.2, 0.0]]),
("strong", vec![vec![0.9, 0.0]]),
];
let ranked = config.rank(&query, &docs).unwrap();
assert_eq!(ranked[0].0, "strong");
assert!(ranked[0].1 > ranked[1].1);
}
}