Skip to main content

grafeo_core/index/
hash.rs

1//! Hash index for O(1) point lookups.
2//!
3//! Use this when you need to find entities by exact key - like looking up
4//! a user by their unique username or finding a node by a primary key.
5//!
6//! Uses DashMap for lock-free concurrent reads (sharded hash map).
7//! This provides 4-6x improvement over RwLock-based approaches under contention.
8
9use dashmap::DashMap;
10use grafeo_common::types::NodeId;
11use std::hash::Hash;
12
13/// A thread-safe hash index for O(1) key lookups.
14///
15/// Backed by DashMap (sharded concurrent hash map) for lock-free reads.
16/// Each read operation only locks one of ~16 internal shards, enabling
17/// high concurrent throughput without global lock contention.
18///
19/// Best for exact-match queries on unique keys.
20///
21/// # Performance
22///
23/// - Concurrent reads: ~6x faster than RwLock under contention
24/// - Point lookups: O(1) average case
25/// - No lock acquisition for reads in common case
26///
27/// # Example
28///
29/// ```
30/// use grafeo_core::index::HashIndex;
31/// use grafeo_common::types::NodeId;
32///
33/// let index: HashIndex<String, NodeId> = HashIndex::new();
34/// index.insert("alix".to_string(), NodeId::new(1));
35/// index.insert("gus".to_string(), NodeId::new(2));
36///
37/// assert_eq!(index.get(&"alix".to_string()), Some(NodeId::new(1)));
38/// ```
39pub struct HashIndex<K: Hash + Eq, V: Copy> {
40    /// The underlying sharded hash map for lock-free reads.
41    map: DashMap<K, V>,
42}
43
44impl<K: Hash + Eq, V: Copy> HashIndex<K, V> {
45    /// Creates a new empty hash index.
46    #[must_use]
47    pub fn new() -> Self {
48        Self {
49            map: DashMap::new(),
50        }
51    }
52
53    /// Creates a new hash index with the given capacity.
54    #[must_use]
55    pub fn with_capacity(capacity: usize) -> Self {
56        Self {
57            map: DashMap::with_capacity(capacity),
58        }
59    }
60
61    /// Inserts a key-value pair into the index.
62    ///
63    /// Returns the previous value if the key was already present.
64    pub fn insert(&self, key: K, value: V) -> Option<V> {
65        self.map.insert(key, value)
66    }
67
68    /// Gets the value for a key.
69    ///
70    /// This is a lock-free read operation that only briefly locks
71    /// one of the internal shards.
72    pub fn get(&self, key: &K) -> Option<V> {
73        self.map.get(key).map(|v| *v)
74    }
75
76    /// Removes a key from the index.
77    ///
78    /// Returns the value if the key was present.
79    pub fn remove(&self, key: &K) -> Option<V> {
80        self.map.remove(key).map(|(_, v)| v)
81    }
82
83    /// Checks if a key exists in the index.
84    pub fn contains(&self, key: &K) -> bool {
85        self.map.contains_key(key)
86    }
87
88    /// Returns the number of entries in the index.
89    pub fn len(&self) -> usize {
90        self.map.len()
91    }
92
93    /// Returns true if the index is empty.
94    pub fn is_empty(&self) -> bool {
95        self.map.is_empty()
96    }
97
98    /// Clears all entries from the index.
99    pub fn clear(&self) {
100        self.map.clear();
101    }
102}
103
104impl<K: Hash + Eq, V: Copy> Default for HashIndex<K, V> {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110/// A hash index from string keys to NodeIds.
111pub type StringKeyIndex = HashIndex<String, NodeId>;
112
113/// A hash index from NodeIds to NodeIds.
114pub type NodeIdIndex = HashIndex<NodeId, NodeId>;
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_hash_index_basic() {
122        let index: HashIndex<u64, NodeId> = HashIndex::new();
123
124        index.insert(1, NodeId::new(100));
125        index.insert(2, NodeId::new(200));
126
127        assert_eq!(index.get(&1), Some(NodeId::new(100)));
128        assert_eq!(index.get(&2), Some(NodeId::new(200)));
129        assert_eq!(index.get(&3), None);
130    }
131
132    #[test]
133    fn test_hash_index_update() {
134        let index: HashIndex<u64, NodeId> = HashIndex::new();
135
136        index.insert(1, NodeId::new(100));
137        let old = index.insert(1, NodeId::new(200));
138
139        assert_eq!(old, Some(NodeId::new(100)));
140        assert_eq!(index.get(&1), Some(NodeId::new(200)));
141    }
142
143    #[test]
144    fn test_hash_index_remove() {
145        let index: HashIndex<u64, NodeId> = HashIndex::new();
146
147        index.insert(1, NodeId::new(100));
148        assert!(index.contains(&1));
149
150        let removed = index.remove(&1);
151        assert_eq!(removed, Some(NodeId::new(100)));
152        assert!(!index.contains(&1));
153    }
154
155    #[test]
156    fn test_hash_index_len() {
157        let index: HashIndex<u64, NodeId> = HashIndex::new();
158
159        assert!(index.is_empty());
160        assert_eq!(index.len(), 0);
161
162        index.insert(1, NodeId::new(100));
163        index.insert(2, NodeId::new(200));
164
165        assert!(!index.is_empty());
166        assert_eq!(index.len(), 2);
167    }
168
169    #[test]
170    fn test_hash_index_clear() {
171        let index: HashIndex<u64, NodeId> = HashIndex::new();
172
173        index.insert(1, NodeId::new(100));
174        index.insert(2, NodeId::new(200));
175
176        index.clear();
177
178        assert!(index.is_empty());
179        assert_eq!(index.get(&1), None);
180    }
181
182    #[test]
183    fn test_string_key_index() {
184        let index: StringKeyIndex = HashIndex::new();
185
186        index.insert("alix".to_string(), NodeId::new(1));
187        index.insert("gus".to_string(), NodeId::new(2));
188
189        assert_eq!(index.get(&"alix".to_string()), Some(NodeId::new(1)));
190        assert_eq!(index.get(&"gus".to_string()), Some(NodeId::new(2)));
191    }
192}