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(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::VectorIndexKind;
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<VectorIndexKind>) {
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<VectorIndexKind>> {
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<VectorIndexKind>)> {
166        self.vector_indexes
167            .read()
168            .iter()
169            .map(|(k, v)| (k.clone(), v.clone()))
170            .collect()
171    }
172
173    /// Looks up a vector index by its `"label:property"` key.
174    #[cfg(feature = "vector-index")]
175    #[must_use]
176    pub fn get_vector_index_by_key(&self, key: &str) -> Option<Arc<VectorIndexKind>> {
177        self.vector_indexes.read().get(key).cloned()
178    }
179
180    /// Stores a text index for a label+property pair.
181    #[cfg(feature = "text-index")]
182    pub fn add_text_index(
183        &self,
184        label: &str,
185        property: &str,
186        index: Arc<RwLock<crate::index::text::InvertedIndex>>,
187    ) {
188        let key = format!("{label}:{property}");
189        self.text_indexes.write().insert(key, index);
190    }
191
192    /// Retrieves the text index for a label+property pair.
193    #[cfg(feature = "text-index")]
194    #[must_use]
195    pub fn get_text_index(
196        &self,
197        label: &str,
198        property: &str,
199    ) -> Option<Arc<RwLock<crate::index::text::InvertedIndex>>> {
200        let key = format!("{label}:{property}");
201        self.text_indexes.read().get(&key).cloned()
202    }
203
204    /// Removes a text index for a label+property pair.
205    ///
206    /// Returns `true` if the index existed and was removed.
207    #[cfg(feature = "text-index")]
208    pub fn remove_text_index(&self, label: &str, property: &str) -> bool {
209        let key = format!("{label}:{property}");
210        self.text_indexes.write().remove(&key).is_some()
211    }
212
213    /// Returns all text index entries as `(key, index)` pairs.
214    ///
215    /// The key format is `"label:property"`.
216    #[cfg(feature = "text-index")]
217    pub fn text_index_entries(
218        &self,
219    ) -> Vec<(String, Arc<RwLock<crate::index::text::InvertedIndex>>)> {
220        self.text_indexes
221            .read()
222            .iter()
223            .map(|(k, v)| (k.clone(), v.clone()))
224            .collect()
225    }
226
227    /// Updates text indexes when a node property is set.
228    ///
229    /// If the node has a label with a text index on this property key,
230    /// the index is updated with the new value (if it's a string).
231    #[cfg(feature = "text-index")]
232    pub(super) fn update_text_index_on_set(&self, id: NodeId, key: &str, value: &Value) {
233        let text_indexes = self.text_indexes.read();
234        if text_indexes.is_empty() {
235            return;
236        }
237        let registry = self.label_registry.read();
238        let node_labels = self.node_labels.read();
239        #[cfg(not(feature = "temporal"))]
240        let label_set = node_labels.get(&id);
241        #[cfg(feature = "temporal")]
242        let label_set = node_labels.get(&id).and_then(|log| log.latest());
243        if let Some(label_ids) = label_set {
244            for &label_id in label_ids {
245                if let Some(label_name) = registry.get_name(label_id) {
246                    let index_key = format!("{label_name}:{key}");
247                    if let Some(index) = text_indexes.get(&index_key) {
248                        let mut idx = index.write();
249                        // Remove old entry first, then insert new if it's a string
250                        idx.remove(id);
251                        if let Value::String(text) = value {
252                            idx.insert(id, text);
253                        }
254                    }
255                }
256            }
257        }
258    }
259
260    /// Updates text indexes when a node property is removed.
261    #[cfg(feature = "text-index")]
262    pub(super) fn update_text_index_on_remove(&self, id: NodeId, key: &str) {
263        let text_indexes = self.text_indexes.read();
264        if text_indexes.is_empty() {
265            return;
266        }
267        let registry = self.label_registry.read();
268        let node_labels = self.node_labels.read();
269        #[cfg(not(feature = "temporal"))]
270        let label_set = node_labels.get(&id);
271        #[cfg(feature = "temporal")]
272        let label_set = node_labels.get(&id).and_then(|log| log.latest());
273        if let Some(label_ids) = label_set {
274            for &label_id in label_ids {
275                if let Some(label_name) = registry.get_name(label_id) {
276                    let index_key = format!("{label_name}:{key}");
277                    if let Some(index) = text_indexes.get(&index_key) {
278                        index.write().remove(id);
279                    }
280                }
281            }
282        }
283    }
284
285    /// Removes a node from all text indexes.
286    #[cfg(feature = "text-index")]
287    pub(super) fn remove_from_all_text_indexes(&self, id: NodeId) {
288        let text_indexes = self.text_indexes.read();
289        if text_indexes.is_empty() {
290            return;
291        }
292        for (_, index) in text_indexes.iter() {
293            index.write().remove(id);
294        }
295    }
296}