Skip to main content

grafeo_core/graph/lpg/store/
index.rs

1//! Index management methods for [`LpgStore`].
2
3use super::LpgStore;
4use dashmap::DashMap;
5use grafeo_common::types::{HashableValue, NodeId, PropertyKey, Value};
6use grafeo_common::utils::hash::FxHashSet;
7#[cfg(any(feature = "vector-index", feature = "text-index"))]
8use parking_lot::RwLock;
9#[cfg(any(feature = "vector-index", feature = "text-index"))]
10use std::sync::Arc;
11
12#[cfg(feature = "vector-index")]
13use crate::index::vector::HnswIndex;
14
15impl LpgStore {
16    /// Creates an index on a node property for O(1) lookups by value.
17    ///
18    /// After creating an index, calls to [`Self::find_nodes_by_property`] will be
19    /// O(1) instead of O(n) for this property. The index is automatically
20    /// maintained when properties are set or removed.
21    ///
22    /// # Example
23    ///
24    /// ```
25    /// use grafeo_core::graph::lpg::LpgStore;
26    /// use grafeo_common::types::Value;
27    ///
28    /// let store = LpgStore::new().expect("arena allocation");
29    ///
30    /// // Create nodes with an 'id' property
31    /// let alix = store.create_node(&["Person"]);
32    /// store.set_node_property(alix, "id", Value::from("alice_123"));
33    ///
34    /// // Create an index on the 'id' property
35    /// store.create_property_index("id");
36    ///
37    /// // Now lookups by 'id' are O(1)
38    /// let found = store.find_nodes_by_property("id", &Value::from("alice_123"));
39    /// assert!(found.contains(&alix));
40    /// ```
41    pub fn create_property_index(&self, property: &str) {
42        let key = PropertyKey::new(property);
43
44        let mut indexes = self.property_indexes.write();
45        if indexes.contains_key(&key) {
46            return; // Already indexed
47        }
48
49        // Create the index and populate it with existing data
50        let index: DashMap<HashableValue, FxHashSet<NodeId>> = DashMap::new();
51
52        // Scan all nodes to build the index
53        for node_id in self.node_ids() {
54            if let Some(value) = self.node_properties.get(node_id, &key) {
55                let hv = HashableValue::new(value);
56                index.entry(hv).or_default().insert(node_id);
57            }
58        }
59
60        indexes.insert(key, index);
61    }
62
63    /// Drops an index on a node property.
64    ///
65    /// Returns `true` if the index existed and was removed.
66    pub fn drop_property_index(&self, property: &str) -> bool {
67        let key = PropertyKey::new(property);
68        self.property_indexes.write().remove(&key).is_some()
69    }
70
71    /// Returns `true` if the property has an index.
72    #[must_use]
73    pub fn has_property_index(&self, property: &str) -> bool {
74        let key = PropertyKey::new(property);
75        self.property_indexes.read().contains_key(&key)
76    }
77
78    /// Returns the names of all indexed properties.
79    #[must_use]
80    pub fn property_index_keys(&self) -> Vec<String> {
81        self.property_indexes
82            .read()
83            .keys()
84            .map(|k| k.to_string())
85            .collect()
86    }
87
88    /// Updates property indexes when a property is set.
89    pub(super) fn update_property_index_on_set(
90        &self,
91        node_id: NodeId,
92        key: &PropertyKey,
93        new_value: &Value,
94    ) {
95        let indexes = self.property_indexes.read();
96        if let Some(index) = indexes.get(key) {
97            // Get old value to remove from index
98            if let Some(old_value) = self.node_properties.get(node_id, key) {
99                let old_hv = HashableValue::new(old_value);
100                if let Some(mut nodes) = index.get_mut(&old_hv) {
101                    nodes.remove(&node_id);
102                    if nodes.is_empty() {
103                        drop(nodes);
104                        index.remove(&old_hv);
105                    }
106                }
107            }
108
109            // Add new value to index
110            let new_hv = HashableValue::new(new_value.clone());
111            index
112                .entry(new_hv)
113                .or_insert_with(FxHashSet::default)
114                .insert(node_id);
115        }
116    }
117
118    /// Updates property indexes when a property is removed.
119    pub(super) fn update_property_index_on_remove(&self, node_id: NodeId, key: &PropertyKey) {
120        let indexes = self.property_indexes.read();
121        if let Some(index) = indexes.get(key) {
122            // Get old value to remove from index
123            if let Some(old_value) = self.node_properties.get(node_id, key) {
124                let old_hv = HashableValue::new(old_value);
125                if let Some(mut nodes) = index.get_mut(&old_hv) {
126                    nodes.remove(&node_id);
127                    if nodes.is_empty() {
128                        drop(nodes);
129                        index.remove(&old_hv);
130                    }
131                }
132            }
133        }
134    }
135
136    /// Stores a vector index for a label+property pair.
137    #[cfg(feature = "vector-index")]
138    pub fn add_vector_index(&self, label: &str, property: &str, index: Arc<HnswIndex>) {
139        let key = format!("{label}:{property}");
140        self.vector_indexes.write().insert(key, index);
141    }
142
143    /// Retrieves the vector index for a label+property pair.
144    #[cfg(feature = "vector-index")]
145    #[must_use]
146    pub fn get_vector_index(&self, label: &str, property: &str) -> Option<Arc<HnswIndex>> {
147        let key = format!("{label}:{property}");
148        self.vector_indexes.read().get(&key).cloned()
149    }
150
151    /// Removes a vector index for a label+property pair.
152    ///
153    /// Returns `true` if the index existed and was removed.
154    #[cfg(feature = "vector-index")]
155    pub fn remove_vector_index(&self, label: &str, property: &str) -> bool {
156        let key = format!("{label}:{property}");
157        self.vector_indexes.write().remove(&key).is_some()
158    }
159
160    /// Returns all vector index entries as `(key, index)` pairs.
161    ///
162    /// Keys are in `"label:property"` format.
163    #[cfg(feature = "vector-index")]
164    #[must_use]
165    pub fn vector_index_entries(&self) -> Vec<(String, Arc<HnswIndex>)> {
166        self.vector_indexes
167            .read()
168            .iter()
169            .map(|(k, v)| (k.clone(), v.clone()))
170            .collect()
171    }
172
173    /// Stores a text index for a label+property pair.
174    #[cfg(feature = "text-index")]
175    pub fn add_text_index(
176        &self,
177        label: &str,
178        property: &str,
179        index: Arc<RwLock<crate::index::text::InvertedIndex>>,
180    ) {
181        let key = format!("{label}:{property}");
182        self.text_indexes.write().insert(key, index);
183    }
184
185    /// Retrieves the text index for a label+property pair.
186    #[cfg(feature = "text-index")]
187    #[must_use]
188    pub fn get_text_index(
189        &self,
190        label: &str,
191        property: &str,
192    ) -> Option<Arc<RwLock<crate::index::text::InvertedIndex>>> {
193        let key = format!("{label}:{property}");
194        self.text_indexes.read().get(&key).cloned()
195    }
196
197    /// Removes a text index for a label+property pair.
198    ///
199    /// Returns `true` if the index existed and was removed.
200    #[cfg(feature = "text-index")]
201    pub fn remove_text_index(&self, label: &str, property: &str) -> bool {
202        let key = format!("{label}:{property}");
203        self.text_indexes.write().remove(&key).is_some()
204    }
205
206    /// Returns all text index entries as `(key, index)` pairs.
207    ///
208    /// The key format is `"label:property"`.
209    #[cfg(feature = "text-index")]
210    pub fn text_index_entries(
211        &self,
212    ) -> Vec<(String, Arc<RwLock<crate::index::text::InvertedIndex>>)> {
213        self.text_indexes
214            .read()
215            .iter()
216            .map(|(k, v)| (k.clone(), v.clone()))
217            .collect()
218    }
219
220    /// Updates text indexes when a node property is set.
221    ///
222    /// If the node has a label with a text index on this property key,
223    /// the index is updated with the new value (if it's a string).
224    #[cfg(feature = "text-index")]
225    pub(super) fn update_text_index_on_set(&self, id: NodeId, key: &str, value: &Value) {
226        let text_indexes = self.text_indexes.read();
227        if text_indexes.is_empty() {
228            return;
229        }
230        let id_to_label = self.id_to_label.read();
231        let node_labels = self.node_labels.read();
232        #[cfg(not(feature = "temporal"))]
233        let label_set = node_labels.get(&id);
234        #[cfg(feature = "temporal")]
235        let label_set = node_labels.get(&id).and_then(|log| log.latest());
236        if let Some(label_ids) = label_set {
237            for &label_id in label_ids {
238                if let Some(label_name) = id_to_label.get(label_id as usize) {
239                    let index_key = format!("{label_name}:{key}");
240                    if let Some(index) = text_indexes.get(&index_key) {
241                        let mut idx = index.write();
242                        // Remove old entry first, then insert new if it's a string
243                        idx.remove(id);
244                        if let Value::String(text) = value {
245                            idx.insert(id, text);
246                        }
247                    }
248                }
249            }
250        }
251    }
252
253    /// Updates text indexes when a node property is removed.
254    #[cfg(feature = "text-index")]
255    pub(super) fn update_text_index_on_remove(&self, id: NodeId, key: &str) {
256        let text_indexes = self.text_indexes.read();
257        if text_indexes.is_empty() {
258            return;
259        }
260        let id_to_label = self.id_to_label.read();
261        let node_labels = self.node_labels.read();
262        #[cfg(not(feature = "temporal"))]
263        let label_set = node_labels.get(&id);
264        #[cfg(feature = "temporal")]
265        let label_set = node_labels.get(&id).and_then(|log| log.latest());
266        if let Some(label_ids) = label_set {
267            for &label_id in label_ids {
268                if let Some(label_name) = id_to_label.get(label_id as usize) {
269                    let index_key = format!("{label_name}:{key}");
270                    if let Some(index) = text_indexes.get(&index_key) {
271                        index.write().remove(id);
272                    }
273                }
274            }
275        }
276    }
277
278    /// Removes a node from all text indexes.
279    #[cfg(feature = "text-index")]
280    pub(super) fn remove_from_all_text_indexes(&self, id: NodeId) {
281        let text_indexes = self.text_indexes.read();
282        if text_indexes.is_empty() {
283            return;
284        }
285        for (_, index) in text_indexes.iter() {
286            index.write().remove(id);
287        }
288    }
289}