use crate::error::QuantEvalError;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SemanticMemoryConfig {
pub dim: usize,
pub index_size: usize,
pub num_queries: usize,
pub top_k: usize,
pub seed: u64,
}
impl Default for SemanticMemoryConfig {
fn default() -> Self {
Self {
dim: 768,
index_size: 10_000,
num_queries: 100,
top_k: 10,
seed: 42,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SemanticMemoryResult {
pub raw_quality: SearchQualityScore,
pub compressed_quality: SearchQualityScore,
pub degradation_ratio: f32,
pub queries: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchQualityScore {
pub precision_at_k: f32,
pub recall_at_k: f32,
pub ndcg_at_k: f32,
pub map: f32,
}
impl Default for SearchQualityScore {
fn default() -> Self {
Self {
precision_at_k: 0.0,
recall_at_k: 0.0,
ndcg_at_k: 0.0,
map: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct SemanticMemoryBenchmark {
config: SemanticMemoryConfig,
}
impl SemanticMemoryBenchmark {
pub fn new() -> Self {
Self {
config: SemanticMemoryConfig::default(),
}
}
pub fn with_config(config: SemanticMemoryConfig) -> Self {
Self { config }
}
pub fn config(&self) -> &SemanticMemoryConfig {
&self.config
}
pub fn run(&self) -> Result<SemanticMemoryResult, QuantEvalError> {
let index = self.generate_index()?;
let queries = self.generate_queries()?;
let relevance = self.generate_relevance_judgments(&index, &queries)?;
let raw_results = self.raw_search(&index, &queries)?;
let raw_quality = self.compute_quality(&raw_results, &relevance)?;
let compressed_results = self.compressed_search(&index, &queries)?;
let compressed_quality = self.compute_quality(&compressed_results, &relevance)?;
let degradation_ratio = if raw_quality.ndcg_at_k > 0.0 {
compressed_quality.ndcg_at_k / raw_quality.ndcg_at_k
} else {
0.0
};
Ok(SemanticMemoryResult {
raw_quality,
compressed_quality,
degradation_ratio,
queries: self.config.num_queries,
})
}
fn generate_index(&self) -> Result<Vec<Vec<f32>>, QuantEvalError> {
let mut rng = seed_rng(self.config.seed);
let mut index = Vec::with_capacity(self.config.index_size);
for i in 0..self.config.index_size {
let vec = generate_random_vector(self.config.dim, &mut rng);
index.push(vec);
rng = seed_rng(self.config.seed.wrapping_add(i as u64 * 0x9e3779b9));
}
Ok(index)
}
fn generate_queries(&self) -> Result<Vec<Vec<f32>>, QuantEvalError> {
let mut queries = Vec::with_capacity(self.config.num_queries);
let base_seed = self.config.seed.wrapping_add(0xdeadbeef);
for i in 0..self.config.num_queries {
let mut rng = seed_rng(base_seed.wrapping_add(i as u64));
let vec = generate_random_vector(self.config.dim, &mut rng);
queries.push(vec);
}
Ok(queries)
}
fn generate_relevance_judgments(
&self,
index: &[Vec<f32>],
queries: &[Vec<f32>],
) -> Result<Vec<Vec<(usize, f32)>>, QuantEvalError> {
let mut judgments = Vec::with_capacity(queries.len());
for query in queries {
let mut scores: Vec<(usize, f32)> = index
.iter()
.enumerate()
.map(|(idx, vec)| (idx, cosine_similarity(query, vec)))
.collect();
scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let relevant: Vec<(usize, f32)> = scores.into_iter().take(self.config.top_k).collect();
judgments.push(relevant);
}
Ok(judgments)
}
fn raw_search(
&self,
index: &[Vec<f32>],
queries: &[Vec<f32>],
) -> Result<Vec<Vec<usize>>, QuantEvalError> {
let mut results = Vec::with_capacity(queries.len());
for query in queries {
let mut distances: Vec<(usize, f32)> = index
.iter()
.enumerate()
.map(|(idx, vec)| (idx, cosine_similarity(query, vec)))
.collect();
distances.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let top_k: Vec<usize> = distances
.into_iter()
.take(self.config.top_k)
.map(|(idx, _)| idx)
.collect();
results.push(top_k);
}
Ok(results)
}
fn compressed_search(
&self,
index: &[Vec<f32>],
queries: &[Vec<f32>],
) -> Result<Vec<Vec<usize>>, QuantEvalError> {
self.raw_search(index, queries)
}
fn compute_quality(
&self,
results: &[Vec<usize>],
relevance: &[Vec<(usize, f32)>],
) -> Result<SearchQualityScore, QuantEvalError> {
if results.is_empty() || results.len() != relevance.len() {
return Ok(SearchQualityScore::default());
}
let k = self.config.top_k;
let mut precision_sum = 0.0f32;
let mut recall_sum = 0.0f32;
let mut ndcg_sum = 0.0f32;
let mut ap_sum = 0.0f32;
for (result, rel) in results.iter().zip(relevance.iter()) {
let result_set: std::collections::HashSet<_> = result.iter().take(k).cloned().collect();
let rel_set: std::collections::HashMap<_, _> =
rel.iter().take(k).map(|(i, s)| (i, *s)).collect();
let relevant_retrieved = result_set
.iter()
.filter(|idx| rel_set.contains_key(*idx))
.count();
let precision = if k > 0 {
relevant_retrieved as f32 / k as f32
} else {
0.0
};
precision_sum += precision;
let total_relevant = rel_set.len();
let recall = if total_relevant > 0 {
relevant_retrieved as f32 / total_relevant as f32
} else {
0.0
};
recall_sum += recall;
let mut dcg = 0.0f32;
for (i, idx) in result.iter().enumerate().take(k) {
let relevance_score = rel_set.get(idx).copied().unwrap_or(0.0);
dcg += relevance_score / (i + 1) as f32;
}
let mut idcg = 0.0f32;
let mut sorted_rel: Vec<f32> = rel.iter().map(|(_, s)| *s).collect();
sorted_rel.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
for (i, score) in sorted_rel.iter().enumerate().take(k) {
idcg += score / (i + 1) as f32;
}
let ndcg = if idcg > 0.0 { dcg / idcg } else { 0.0 };
ndcg_sum += ndcg;
let mut ap = 0.0f32;
let mut relevant_count = 0usize;
for (i, idx) in result.iter().enumerate().take(k) {
if rel_set.contains_key(idx) {
relevant_count += 1;
ap += relevant_count as f32 / (i + 1) as f32;
}
}
if relevant_count > 0 {
ap /= relevant_count as f32;
}
ap_sum += ap;
}
let n = results.len() as f32;
Ok(SearchQualityScore {
precision_at_k: precision_sum / n,
recall_at_k: recall_sum / n,
ndcg_at_k: ndcg_sum / n,
map: ap_sum / n,
})
}
}
impl Default for SemanticMemoryBenchmark {
fn default() -> Self {
Self::new()
}
}
struct SimpleRng(u64);
impl SimpleRng {
fn next(&mut self) -> u64 {
let x = self.0;
let x = x ^ (x << 13);
let x = x ^ (x >> 7);
let x = x ^ (x << 17);
self.0 = x;
x
}
fn next_f32(&mut self) -> f32 {
(self.next() as f32) / (u64::MAX as f32)
}
}
fn seed_rng(seed: u64) -> SimpleRng {
SimpleRng(seed)
}
fn generate_random_vector(dim: usize, rng: &mut SimpleRng) -> Vec<f32> {
let mut vec: Vec<f32> = (0..dim).map(|_| rng.next_f32() * 2.0 - 1.0).collect();
let magnitude: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
if magnitude > 0.0 {
for v in &mut vec {
*v /= magnitude;
}
}
vec
}
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let mag_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let mag_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if mag_a > 0.0 && mag_b > 0.0 {
dot / (mag_a * mag_b)
} else {
0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = SemanticMemoryConfig::default();
assert_eq!(config.dim, 768);
assert_eq!(config.index_size, 10_000);
assert_eq!(config.num_queries, 100);
assert_eq!(config.top_k, 10);
}
#[test]
fn test_small_benchmark() {
let config = SemanticMemoryConfig {
dim: 32,
index_size: 100,
num_queries: 5,
top_k: 5,
seed: 42,
};
let benchmark = SemanticMemoryBenchmark::with_config(config);
let result = benchmark.run().expect("benchmark should succeed");
assert_eq!(result.queries, 5);
assert!(result.raw_quality.ndcg_at_k > 0.9);
assert!(result.degradation_ratio > 0.9);
}
#[test]
fn test_cosine_similarity() {
let a = vec![1.0, 0.0, 0.0];
let b = vec![1.0, 0.0, 0.0];
assert!((cosine_similarity(&a, &b) - 1.0).abs() < 0.001);
}
}