use super::*;
use crate::memory::graph::types::EntityOccurrenceIndex;
use std::collections::{BTreeMap, BTreeSet};
use anyhow::Result;
#[derive(Default)]
struct InMemoryIndex {
by_entity: BTreeMap<String, BTreeSet<String>>,
by_node: BTreeMap<String, BTreeSet<String>>,
}
impl InMemoryIndex {
fn new() -> Self {
Self::default()
}
fn index_entity(&mut self, entity_id: &str, node_id: &str) {
self.by_entity
.entry(entity_id.to_string())
.or_default()
.insert(node_id.to_string());
self.by_node
.entry(node_id.to_string())
.or_default()
.insert(entity_id.to_string());
}
}
impl EntityOccurrenceIndex for InMemoryIndex {
fn nodes_for_entity(&self, entity_id: &str) -> Result<Vec<String>> {
Ok(self
.by_entity
.get(entity_id)
.map(|s| s.iter().cloned().collect())
.unwrap_or_default())
}
fn entities_on_node(&self, node_id: &str) -> Result<Vec<String>> {
Ok(self
.by_node
.get(node_id)
.map(|s| s.iter().cloned().collect())
.unwrap_or_default())
}
}
#[test]
fn empty_when_no_co_occurrence() {
let mut index = InMemoryIndex::new();
index.index_entity("email:alice@example.com", "leaf-1");
let edges = co_occurring_entities(&index, "email:alice@example.com", None).unwrap();
assert!(edges.is_empty());
}
#[test]
fn single_co_occurrence_weight_one() {
let mut index = InMemoryIndex::new();
index.index_entity("email:alice@example.com", "leaf-1");
index.index_entity("email:bob@example.com", "leaf-1");
let edges = co_occurring_entities(&index, "email:alice@example.com", None).unwrap();
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].object, "email:bob@example.com");
assert_eq!(edges[0].weight, 1);
}
#[test]
fn weight_counts_distinct_nodes_not_rows() {
let mut index = InMemoryIndex::new();
for leaf in &["leaf-1", "leaf-2", "leaf-3"] {
index.index_entity("email:alice@example.com", leaf);
index.index_entity("email:bob@example.com", leaf);
}
index.index_entity("email:alice@example.com", "leaf-1");
index.index_entity("email:bob@example.com", "leaf-1");
let edges = co_occurring_entities(&index, "email:alice@example.com", None).unwrap();
assert_eq!(edges[0].weight, 3);
}
#[test]
fn excludes_self_edges() {
let mut index = InMemoryIndex::new();
index.index_entity("email:alice@example.com", "leaf-1");
index.index_entity("email:alice@example.com", "leaf-2");
let edges = co_occurring_entities(&index, "email:alice@example.com", None).unwrap();
assert!(edges.is_empty());
}
#[test]
fn neighbors_returns_ids_in_weight_order() {
let mut index = InMemoryIndex::new();
index.index_entity("email:alice@example.com", "leaf-1");
index.index_entity("email:bob@example.com", "leaf-1");
index.index_entity("email:alice@example.com", "leaf-2");
index.index_entity("email:bob@example.com", "leaf-2");
index.index_entity("email:alice@example.com", "leaf-3");
index.index_entity("email:carol@example.com", "leaf-3");
let ids = neighbors(&index, "email:alice@example.com", None).unwrap();
assert_eq!(
ids,
vec![
"email:bob@example.com".to_string(),
"email:carol@example.com".to_string(),
]
);
}
#[test]
fn ties_break_on_object_id_ascending() {
let mut index = InMemoryIndex::new();
index.index_entity("email:alice@example.com", "leaf-1");
index.index_entity("email:carol@example.com", "leaf-1");
index.index_entity("email:alice@example.com", "leaf-2");
index.index_entity("email:bob@example.com", "leaf-2");
let ids = neighbors(&index, "email:alice@example.com", None).unwrap();
assert_eq!(
ids,
vec![
"email:bob@example.com".to_string(),
"email:carol@example.com".to_string(),
]
);
}
#[test]
fn limit_caps_result_set() {
let mut index = InMemoryIndex::new();
for i in 0..5 {
let node = format!("leaf-{i}");
index.index_entity("email:alice@example.com", &node);
index.index_entity(&format!("email:n{i}@example.com"), &node);
}
let edges = co_occurring_entities(&index, "email:alice@example.com", Some(2)).unwrap();
assert_eq!(edges.len(), 2);
}
#[test]
fn group_by_weight_buckets_neighbors() {
let mut index = InMemoryIndex::new();
index.index_entity("email:alice@example.com", "leaf-1");
index.index_entity("email:bob@example.com", "leaf-1");
index.index_entity("email:alice@example.com", "leaf-2");
index.index_entity("email:bob@example.com", "leaf-2");
index.index_entity("email:alice@example.com", "leaf-3");
index.index_entity("email:carol@example.com", "leaf-3");
let edges = co_occurring_entities(&index, "email:alice@example.com", None).unwrap();
let grouped = group_by_weight(edges);
assert_eq!(
grouped.get(&2).unwrap(),
&vec!["email:bob@example.com".to_string()]
);
assert_eq!(
grouped.get(&1).unwrap(),
&vec!["email:carol@example.com".to_string()]
);
}