use std::collections::{BTreeMap, HashMap};
use super::schema::FeatureValue;
type PitKey = (String, String, String);
pub struct PointInTimeIndex {
index: HashMap<PitKey, BTreeMap<u64, (u64, FeatureValue)>>,
}
impl PointInTimeIndex {
pub fn new() -> Self {
Self {
index: HashMap::new(),
}
}
pub fn insert(
&mut self,
group: &str,
entity_key: &str,
feature: &str,
event_ts: u64,
offset: u64,
value: FeatureValue,
) {
let key = (
group.to_string(),
entity_key.to_string(),
feature.to_string(),
);
self.index
.entry(key)
.or_default()
.insert(event_ts, (offset, value));
}
pub fn lookup(
&self,
group: &str,
entity_key: &str,
feature: &str,
cutoff_ts: u64,
) -> Option<&FeatureValue> {
let key = (
group.to_string(),
entity_key.to_string(),
feature.to_string(),
);
let tree = self.index.get(&key)?;
tree.range(..=cutoff_ts)
.next_back()
.map(|(_, (_, value))| value)
}
pub fn lookup_offset(
&self,
group: &str,
entity_key: &str,
feature: &str,
cutoff_ts: u64,
) -> Option<u64> {
let key = (
group.to_string(),
entity_key.to_string(),
feature.to_string(),
);
let tree = self.index.get(&key)?;
tree.range(..=cutoff_ts)
.next_back()
.map(|(_, (offset, _))| *offset)
}
pub fn get_all(
&self,
group: &str,
entity_key: &str,
feature: &str,
) -> Option<Vec<(u64, &FeatureValue)>> {
let key = (
group.to_string(),
entity_key.to_string(),
feature.to_string(),
);
self.index
.get(&key)
.map(|tree| tree.iter().map(|(ts, (_, val))| (*ts, val)).collect())
}
pub fn len(&self) -> usize {
self.index.values().map(|tree| tree.len()).sum()
}
pub fn is_empty(&self) -> bool {
self.index.is_empty()
}
}
impl Default for PointInTimeIndex {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_insert_and_lookup() {
let mut index = PointInTimeIndex::new();
index.insert(
"users",
"u1",
"score",
1000,
100,
FeatureValue::Float64(1.0),
);
index.insert(
"users",
"u1",
"score",
2000,
200,
FeatureValue::Float64(2.0),
);
index.insert(
"users",
"u1",
"score",
3000,
300,
FeatureValue::Float64(3.0),
);
assert_eq!(
index.lookup("users", "u1", "score", 2000),
Some(&FeatureValue::Float64(2.0))
);
assert_eq!(
index.lookup("users", "u1", "score", 2500),
Some(&FeatureValue::Float64(2.0))
);
assert_eq!(
index.lookup("users", "u1", "score", 5000),
Some(&FeatureValue::Float64(3.0))
);
assert_eq!(index.lookup("users", "u1", "score", 500), None);
}
#[test]
fn test_lookup_nonexistent() {
let index = PointInTimeIndex::new();
assert_eq!(index.lookup("users", "u1", "score", 1000), None);
}
#[test]
fn test_multiple_entities() {
let mut index = PointInTimeIndex::new();
index.insert(
"users",
"u1",
"score",
1000,
100,
FeatureValue::Float64(1.0),
);
index.insert(
"users",
"u2",
"score",
1000,
200,
FeatureValue::Float64(2.0),
);
assert_eq!(
index.lookup("users", "u1", "score", 1000),
Some(&FeatureValue::Float64(1.0))
);
assert_eq!(
index.lookup("users", "u2", "score", 1000),
Some(&FeatureValue::Float64(2.0))
);
}
#[test]
fn test_multiple_features() {
let mut index = PointInTimeIndex::new();
index.insert(
"users",
"u1",
"score",
1000,
100,
FeatureValue::Float64(1.0),
);
index.insert("users", "u1", "count", 1000, 200, FeatureValue::Int64(5));
assert_eq!(
index.lookup("users", "u1", "score", 1000),
Some(&FeatureValue::Float64(1.0))
);
assert_eq!(
index.lookup("users", "u1", "count", 1000),
Some(&FeatureValue::Int64(5))
);
}
#[test]
fn test_get_all() {
let mut index = PointInTimeIndex::new();
index.insert(
"users",
"u1",
"score",
1000,
100,
FeatureValue::Float64(1.0),
);
index.insert(
"users",
"u1",
"score",
2000,
200,
FeatureValue::Float64(2.0),
);
index.insert(
"users",
"u1",
"score",
3000,
300,
FeatureValue::Float64(3.0),
);
let all = index.get_all("users", "u1", "score").unwrap();
assert_eq!(all.len(), 3);
assert_eq!(all[0].0, 1000);
assert_eq!(all[2].0, 3000);
}
#[test]
fn test_pit_invariant_no_future_data() {
let mut index = PointInTimeIndex::new();
index.insert(
"users",
"u1",
"score",
1000,
100,
FeatureValue::Float64(1.0),
);
index.insert(
"users",
"u1",
"score",
2000,
200,
FeatureValue::Float64(2.0),
);
index.insert(
"users",
"u1",
"score",
3000,
300,
FeatureValue::Float64(3.0),
);
let result = index.lookup("users", "u1", "score", 2000);
assert_eq!(result, Some(&FeatureValue::Float64(2.0)));
}
}