use hashbrown::HashTable;
use log::{error, trace};
use crate::people::HashValueType;
use crate::{Context, ContextPeopleExt, HashSet, PersonId, PersonProperty};
pub type BxIndex = Box<dyn TypeErasedIndex>;
#[derive(Default)]
pub struct Index<T: PersonProperty> {
#[allow(dead_code)]
pub(super) name: &'static str,
lookup: HashTable<(T::CanonicalValue, HashSet<PersonId>)>,
pub(super) max_indexed: usize,
pub(super) is_indexed: bool,
}
impl<T: PersonProperty> Index<T> {
#[must_use]
pub fn new() -> Box<Self> {
Box::new(Self {
name: T::name(),
lookup: HashTable::default(),
max_indexed: 0,
is_indexed: false,
})
}
pub fn add_person(&mut self, key: &T::CanonicalValue, entity_id: PersonId) -> bool {
trace!("adding person {} to index {}", entity_id, T::name());
let hash = T::hash_property_value(key);
#[allow(clippy::cast_possible_truncation)]
let hasher = |(stored_value, _stored_set): &_| T::hash_property_value(stored_value) as u64;
let hash128_equality = |(stored_value, _): &_| T::hash_property_value(stored_value) == hash;
#[allow(clippy::cast_possible_truncation)]
self.lookup
.entry(hash as u64, hash128_equality, hasher)
.or_insert_with(|| (*key, HashSet::default()))
.get_mut()
.1
.insert(entity_id)
}
pub fn remove_person(&mut self, key: &T::CanonicalValue, entity_id: PersonId) {
let hash = T::hash_property_value(key);
self.remove_person_with_hash(hash, entity_id);
}
}
pub trait TypeErasedIndex {
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
#[allow(dead_code)]
fn add_person_with_hash(&mut self, hash: HashValueType, entity_id: PersonId);
fn remove_person_with_hash(&mut self, hash: HashValueType, entity_id: PersonId);
fn get_with_hash(&self, hash: HashValueType) -> Option<&HashSet<PersonId>>;
fn is_indexed(&self) -> bool;
fn set_indexed(&mut self, is_indexed: bool);
fn index_unindexed_people(&mut self, context: &Context);
fn iter_serialized_values_people(
&self,
) -> Box<dyn Iterator<Item = (String, &HashSet<PersonId>)> + '_>;
}
impl<T: PersonProperty> TypeErasedIndex for Index<T> {
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn add_person_with_hash(&mut self, hash: HashValueType, entity_id: PersonId) {
let hash128_equality = |(stored_value, _): &_| T::hash_property_value(stored_value) == hash;
#[allow(clippy::cast_possible_truncation)]
if let Ok(mut entry) = self.lookup.find_entry(hash as u64, hash128_equality) {
let (_, set) = entry.get_mut();
set.insert(entity_id);
} else {
error!(
"could not find entry for hash {} when adding person {} to index",
hash, entity_id
);
}
}
fn remove_person_with_hash(&mut self, hash: HashValueType, entity_id: PersonId) {
let hash128_equality = |(stored_value, _): &_| T::hash_property_value(stored_value) == hash;
#[allow(clippy::cast_possible_truncation)]
if let Ok(mut entry) = self.lookup.find_entry(hash as u64, hash128_equality) {
let (_, set) = entry.get_mut();
set.remove(&entity_id);
if set.is_empty() {
entry.remove();
}
} else {
error!(
"could not find entry for hash {} when removing person {} from index",
hash, entity_id
);
}
}
fn get_with_hash(&self, hash: HashValueType) -> Option<&HashSet<PersonId>> {
let hash128_equality = |(stored_value, _): &_| T::hash_property_value(stored_value) == hash;
#[allow(clippy::cast_possible_truncation)]
self.lookup
.find(hash as u64, hash128_equality)
.map(|(_, set)| set)
}
fn is_indexed(&self) -> bool {
self.is_indexed
}
fn set_indexed(&mut self, is_indexed: bool) {
self.is_indexed = is_indexed;
}
fn index_unindexed_people(&mut self, context: &Context) {
if !self.is_indexed {
return;
}
let current_pop = context.get_current_population();
trace!(
"{}: indexing unindexed people {}..<{}",
T::name(),
self.max_indexed,
current_pop
);
for id in self.max_indexed..current_pop {
let person_id = PersonId(id);
let value = context.get_person_property(person_id, T::get_instance());
self.add_person(&T::make_canonical(value), person_id);
}
self.max_indexed = current_pop;
}
fn iter_serialized_values_people(
&self,
) -> Box<dyn Iterator<Item = (String, &HashSet<PersonId>)> + '_> {
Box::new(self.lookup.iter().map(|(k, v)| (T::get_display(k), v)))
}
}
pub fn process_indices(
context: &Context,
remaining_indices: &[&BxIndex],
property_names: &mut Vec<String>,
current_matches: &HashSet<PersonId>,
print_fn: &dyn Fn(&Context, &[String], usize),
) {
if remaining_indices.is_empty() {
print_fn(context, property_names, current_matches.len());
return;
}
let (&next_index, rest_indices) = remaining_indices.split_first().unwrap();
if !next_index.is_indexed() {
return;
}
for (display, people) in next_index.iter_serialized_values_people() {
let intersect = !property_names.is_empty();
property_names.push(display);
let matches = if intersect {
¤t_matches.intersection(people).copied().collect()
} else {
people
};
process_indices(context, rest_indices, property_names, matches, print_fn);
property_names.pop();
}
}
#[cfg(test)]
mod test {
use log::LevelFilter;
use crate::hashing::{hash_serialized_128, one_shot_128};
use crate::people::index::Index;
use crate::prelude::*;
use crate::{define_multi_property, set_log_level, set_module_filter, PersonProperty};
define_person_property!(Age, u8);
define_person_property!(Weight, u8);
define_person_property!(Height, u8);
define_multi_property!(AWH, (Age, Weight, Height));
define_multi_property!(WHA, (Weight, Height, Age));
#[test]
fn test_multi_property_index_typed_api() {
let mut context = Context::new();
set_log_level(LevelFilter::Trace);
set_module_filter("ixa", LevelFilter::Trace);
context.index_property(WHA);
context.index_property(AWH);
context
.add_person(((Age, 1u8), (Weight, 2u8), (Height, 3u8)))
.unwrap();
let mut results_a = Default::default();
context.with_query_results((AWH, (1u8, 2u8, 3u8)), &mut |results| {
results_a = results.clone()
});
assert_eq!(results_a.len(), 1);
let mut results_b = Default::default();
context.with_query_results((WHA, (2u8, 3u8, 1u8)), &mut |results| {
results_b = results.clone()
});
assert_eq!(results_b.len(), 1);
assert_eq!(results_a, results_b);
println!("Results: {:?}", results_a);
context
.add_person(((Weight, 1u8), (Height, 2u8), (Age, 3u8)))
.unwrap();
let mut results_a = Default::default();
context.with_query_results((WHA, (1u8, 2u8, 3u8)), &mut |results| {
results_a = results.clone()
});
assert_eq!(results_a.len(), 1);
let mut results_b = Default::default();
context.with_query_results((AWH, (3u8, 1u8, 2u8)), &mut |results| {
results_b = results.clone()
});
assert_eq!(results_b.len(), 1);
assert_eq!(results_a, results_b);
println!("Results: {:?}", results_a);
set_module_filter("ixa", LevelFilter::Info);
set_log_level(LevelFilter::Off);
}
#[test]
fn index_name() {
let index = Index::<Age>::new();
assert!(index.name.contains("Age"));
}
#[test]
fn test_index_value_compute_same_values() {
let value = hash_serialized_128("test value");
let value2 = hash_serialized_128("test value");
assert_eq!(one_shot_128(&value), one_shot_128(&value2));
}
#[test]
fn test_index_value_compute_different_values() {
let value1 = 42;
let value2 = 43;
assert_ne!(
<Age as PersonProperty>::hash_property_value(&value1),
<Age as PersonProperty>::hash_property_value(&value2)
);
}
}