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