use ipfrs_core::{Cid, Error, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RMIConfig {
pub num_models: usize,
pub model_type: ModelType,
pub training_iterations: usize,
pub learning_rate: f32,
pub error_threshold: f32,
}
impl Default for RMIConfig {
fn default() -> Self {
Self {
num_models: 10,
model_type: ModelType::Linear,
training_iterations: 100,
learning_rate: 0.01,
error_threshold: 0.05,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ModelType {
Linear,
NeuralNetwork,
Polynomial,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Model {
model_type: ModelType,
weights: Vec<f32>,
bias: f32,
input_dim: usize,
}
impl Model {
fn new(model_type: ModelType, input_dim: usize) -> Self {
let weight_count = match model_type {
ModelType::Linear => input_dim,
ModelType::Polynomial => input_dim * 2, ModelType::NeuralNetwork => input_dim * 8 + 8, };
Self {
model_type,
weights: vec![0.01; weight_count],
bias: 0.0,
input_dim,
}
}
fn predict(&self, input: &[f32]) -> f32 {
match self.model_type {
ModelType::Linear => self.predict_linear(input),
ModelType::Polynomial => self.predict_polynomial(input),
ModelType::NeuralNetwork => self.predict_neural(input),
}
}
fn predict_linear(&self, input: &[f32]) -> f32 {
let mut sum = self.bias;
for (i, &val) in input.iter().enumerate() {
if i < self.weights.len() {
sum += self.weights[i] * val;
}
}
sum.clamp(0.0, 1.0)
}
fn predict_polynomial(&self, input: &[f32]) -> f32 {
let mut sum = self.bias;
let half = self.weights.len() / 2;
for (i, &val) in input.iter().enumerate() {
if i < half {
sum += self.weights[i] * val;
}
}
for (i, &val) in input.iter().enumerate() {
if half + i < self.weights.len() {
sum += self.weights[half + i] * val * val;
}
}
sum.clamp(0.0, 1.0)
}
fn predict_neural(&self, input: &[f32]) -> f32 {
let hidden_size = 8;
let input_weights = &self.weights[0..self.input_dim * hidden_size];
let output_weights = &self.weights[self.input_dim * hidden_size..];
let mut hidden = vec![0.0; hidden_size];
for h in 0..hidden_size {
let mut sum = 0.0;
for (i, &val) in input.iter().enumerate() {
if h * self.input_dim + i < input_weights.len() {
sum += input_weights[h * self.input_dim + i] * val;
}
}
hidden[h] = sum.max(0.0); }
let mut output = self.bias;
for (h, &val) in hidden.iter().enumerate() {
if h < output_weights.len() {
output += output_weights[h] * val;
}
}
1.0 / (1.0 + (-output).exp())
}
#[allow(dead_code)]
fn train(&mut self, data: &[(Vec<f32>, f32)], learning_rate: f32, iterations: usize) {
for _ in 0..iterations {
for (input, target) in data {
let prediction = self.predict(input);
let error = target - prediction;
match self.model_type {
ModelType::Linear => {
for (i, &val) in input.iter().enumerate() {
if i < self.weights.len() {
self.weights[i] += learning_rate * error * val;
}
}
self.bias += learning_rate * error;
}
ModelType::Polynomial => {
let half = self.weights.len() / 2;
for (i, &val) in input.iter().enumerate() {
if i < half {
self.weights[i] += learning_rate * error * val;
}
if half + i < self.weights.len() {
self.weights[half + i] += learning_rate * error * val * val;
}
}
self.bias += learning_rate * error;
}
ModelType::NeuralNetwork => {
for i in 0..self.weights.len() {
self.weights[i] += learning_rate * error * 0.01;
}
self.bias += learning_rate * error;
}
}
}
}
}
}
pub struct LearnedIndex {
config: RMIConfig,
root_model: Option<Model>,
stage1_models: Vec<Model>,
data: Vec<(Cid, Vec<f32>)>,
dimension: Option<usize>,
stats: IndexStats,
}
#[derive(Debug, Default)]
struct IndexStats {
searches: usize,
total_error: f32,
data_points: usize,
}
impl LearnedIndex {
pub fn new(config: RMIConfig) -> Self {
Self {
config,
root_model: None,
stage1_models: Vec::new(),
data: Vec::new(),
dimension: None,
stats: IndexStats::default(),
}
}
pub fn add(&mut self, cid: Cid, embedding: Vec<f32>) -> Result<()> {
if let Some(dim) = self.dimension {
if embedding.len() != dim {
return Err(Error::InvalidInput(format!(
"Dimension mismatch: expected {}, got {}",
dim,
embedding.len()
)));
}
} else {
self.dimension = Some(embedding.len());
}
self.data.push((cid, embedding));
self.stats.data_points += 1;
if self.data.len().is_multiple_of(100) {
self.rebuild()?;
}
Ok(())
}
pub fn rebuild(&mut self) -> Result<()> {
if self.data.is_empty() {
return Ok(());
}
let dim = self
.dimension
.ok_or_else(|| Error::InvalidInput("No dimension set".to_string()))?;
self.data.sort_by(|a, b| {
a.1[0]
.partial_cmp(&b.1[0])
.unwrap_or(std::cmp::Ordering::Equal)
});
self.root_model = Some(Model::new(self.config.model_type, dim));
self.stage1_models = (0..self.config.num_models)
.map(|_| Model::new(self.config.model_type, dim))
.collect();
self.train_models()?;
Ok(())
}
fn train_models(&mut self) -> Result<()> {
if self.data.is_empty() {
return Ok(());
}
let n = self.data.len();
let mut root_training_data = Vec::new();
for (i, (_cid, embedding)) in self.data.iter().enumerate() {
let normalized_pos = i as f32 / n as f32;
let normalized_embedding = self.normalize_embedding(embedding);
root_training_data.push((normalized_embedding, normalized_pos));
}
if let Some(ref mut root) = self.root_model {
root.train(
&root_training_data,
self.config.learning_rate,
self.config.training_iterations,
);
}
let chunk_size = n / self.config.num_models;
let mut all_model_training_data = Vec::new();
for model_idx in 0..self.config.num_models {
let start = model_idx * chunk_size;
let end = if model_idx == self.config.num_models - 1 {
n
} else {
(model_idx + 1) * chunk_size
};
let mut model_training_data = Vec::new();
for i in start..end {
if let Some((_cid, embedding)) = self.data.get(i) {
let local_pos = (i - start) as f32 / (end - start) as f32;
let normalized_embedding = self.normalize_embedding(embedding);
model_training_data.push((normalized_embedding, local_pos));
}
}
all_model_training_data.push(model_training_data);
}
for (model, training_data) in self
.stage1_models
.iter_mut()
.zip(all_model_training_data.iter())
{
if !training_data.is_empty() {
model.train(
training_data,
self.config.learning_rate,
self.config.training_iterations,
);
}
}
Ok(())
}
fn normalize_embedding(&self, embedding: &[f32]) -> Vec<f32> {
let min = embedding.iter().cloned().fold(f32::INFINITY, f32::min);
let max = embedding.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let range = max - min;
if range > 1e-6 {
embedding.iter().map(|&x| (x - min) / range).collect()
} else {
vec![0.5; embedding.len()]
}
}
pub fn search(&mut self, query: &[f32], k: usize) -> Result<Vec<(Cid, f32)>> {
if self.data.is_empty() {
return Ok(Vec::new());
}
let dim = self
.dimension
.ok_or_else(|| Error::InvalidInput("No dimension set".to_string()))?;
if query.len() != dim {
return Err(Error::InvalidInput(format!(
"Dimension mismatch: expected {}, got {}",
dim,
query.len()
)));
}
if self.root_model.is_none() {
self.rebuild()?;
}
self.stats.searches += 1;
let predicted_pos = self.predict_position(query)?;
let n = self.data.len();
let start_idx = (predicted_pos * n as f32) as usize;
let window_size = (n as f32 * self.config.error_threshold).max(k as f32 * 2.0) as usize;
let search_start = start_idx.saturating_sub(window_size / 2);
let search_end = (start_idx + window_size / 2).min(n);
let mut candidates = Vec::new();
for i in search_start..search_end {
if let Some((cid, embedding)) = self.data.get(i) {
let distance = self.compute_distance(query, embedding);
candidates.push((*cid, distance));
}
}
candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
Ok(candidates.into_iter().take(k).collect())
}
fn predict_position(&mut self, query: &[f32]) -> Result<f32> {
let normalized_query = self.normalize_embedding(query);
let root_prediction = if let Some(ref root) = self.root_model {
root.predict(&normalized_query)
} else {
return Err(Error::InvalidInput("No root model".to_string()));
};
let model_idx = ((root_prediction * self.config.num_models as f32) as usize)
.min(self.config.num_models - 1);
let local_prediction = if let Some(model) = self.stage1_models.get(model_idx) {
model.predict(&normalized_query)
} else {
0.5
};
let chunk_size = 1.0 / self.config.num_models as f32;
let final_prediction = model_idx as f32 * chunk_size + local_prediction * chunk_size;
Ok(final_prediction.clamp(0.0, 1.0))
}
fn compute_distance(&self, a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y) * (x - y))
.sum::<f32>()
.sqrt()
}
pub fn stats(&self) -> LearnedIndexStats {
LearnedIndexStats {
data_points: self.stats.data_points,
searches: self.stats.searches,
num_models: self.stage1_models.len() + 1,
avg_error: if self.stats.searches > 0 {
self.stats.total_error / self.stats.searches as f32
} else {
0.0
},
}
}
pub fn size(&self) -> usize {
self.data.len()
}
pub fn clear(&mut self) {
self.data.clear();
self.root_model = None;
self.stage1_models.clear();
self.stats = IndexStats::default();
}
}
#[derive(Debug, Clone)]
pub struct LearnedIndexStats {
pub data_points: usize,
pub searches: usize,
pub num_models: usize,
pub avg_error: f32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_learned_index_creation() {
let index = LearnedIndex::new(RMIConfig::default());
assert_eq!(index.size(), 0);
}
#[test]
fn test_add_and_search() {
let mut index = LearnedIndex::new(RMIConfig::default());
for i in 0..100 {
let cid = Cid::default();
let embedding = vec![i as f32 / 100.0, 0.5, 0.5, 0.5];
index
.add(cid, embedding)
.expect("test: add embedding to learned index");
}
assert_eq!(index.size(), 100);
let query = vec![0.5, 0.5, 0.5, 0.5];
let results = index.search(&query, 5).expect("test: search learned index");
assert_eq!(results.len(), 5);
}
#[test]
fn test_model_prediction() {
let model = Model::new(ModelType::Linear, 4);
let input = vec![0.1, 0.2, 0.3, 0.4];
let prediction = model.predict(&input);
assert!((0.0..=1.0).contains(&prediction));
}
#[test]
fn test_polynomial_model() {
let model = Model::new(ModelType::Polynomial, 4);
let input = vec![0.5, 0.5, 0.5, 0.5];
let prediction = model.predict(&input);
assert!((0.0..=1.0).contains(&prediction));
}
#[test]
fn test_neural_model() {
let model = Model::new(ModelType::NeuralNetwork, 4);
let input = vec![0.3, 0.4, 0.5, 0.6];
let prediction = model.predict(&input);
assert!((0.0..=1.0).contains(&prediction));
}
#[test]
fn test_dimension_mismatch() {
let mut index = LearnedIndex::new(RMIConfig::default());
let cid1 = Cid::default();
index
.add(cid1, vec![1.0, 2.0, 3.0])
.expect("test: add first embedding");
let cid2 = Cid::default();
let result = index.add(cid2, vec![1.0, 2.0]);
assert!(result.is_err());
}
#[test]
fn test_rebuild_index() {
let mut index = LearnedIndex::new(RMIConfig::default());
for i in 0..50 {
let cid = Cid::default();
let embedding = vec![i as f32, 0.0, 0.0];
index
.add(cid, embedding)
.expect("test: add embedding for rebuild");
}
index.rebuild().expect("test: rebuild index");
let query = vec![25.0, 0.0, 0.0];
let results = index.search(&query, 3).expect("test: search after rebuild");
assert_eq!(results.len(), 3);
}
#[test]
fn test_stats() {
let mut index = LearnedIndex::new(RMIConfig::default());
for i in 0..10 {
let cid = Cid::default();
index
.add(cid, vec![i as f32, 0.0])
.expect("test: add embedding for stats");
}
let query = vec![5.0, 0.0];
let _ = index.search(&query, 3).expect("test: search for stats");
let stats = index.stats();
assert_eq!(stats.data_points, 10);
assert_eq!(stats.searches, 1);
}
#[test]
fn test_clear() {
let mut index = LearnedIndex::new(RMIConfig::default());
let cid = Cid::default();
index
.add(cid, vec![1.0, 2.0, 3.0])
.expect("test: add embedding for clear");
assert_eq!(index.size(), 1);
index.clear();
assert_eq!(index.size(), 0);
}
#[test]
fn test_config_variants() {
let configs = vec![
RMIConfig {
model_type: ModelType::Linear,
..Default::default()
},
RMIConfig {
model_type: ModelType::Polynomial,
..Default::default()
},
RMIConfig {
model_type: ModelType::NeuralNetwork,
..Default::default()
},
];
for config in configs {
let mut index = LearnedIndex::new(config);
for i in 0..20 {
let cid = Cid::default();
index
.add(cid, vec![i as f32, 0.0, 0.0])
.expect("test: add embedding for config variant");
}
let query = vec![10.0, 0.0, 0.0];
let results = index
.search(&query, 5)
.expect("test: search for config variant");
assert!(!results.is_empty());
}
}
}