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    /// Updates property indexes when a property is set.
79    pub(super) fn update_property_index_on_set(
80        &self,
81        node_id: NodeId,
82        key: &PropertyKey,
83        new_value: &Value,
84    ) {
85        let indexes = self.property_indexes.read();
86        if let Some(index) = indexes.get(key) {
87            // Get old value to remove from index
88            if let Some(old_value) = self.node_properties.get(node_id, key) {
89                let old_hv = HashableValue::new(old_value);
90                if let Some(mut nodes) = index.get_mut(&old_hv) {
91                    nodes.remove(&node_id);
92                    if nodes.is_empty() {
93                        drop(nodes);
94                        index.remove(&old_hv);
95                    }
96                }
97            }
98
99            // Add new value to index
100            let new_hv = HashableValue::new(new_value.clone());
101            index
102                .entry(new_hv)
103                .or_insert_with(FxHashSet::default)
104                .insert(node_id);
105        }
106    }
107
108    /// Updates property indexes when a property is removed.
109    pub(super) fn update_property_index_on_remove(&self, node_id: NodeId, key: &PropertyKey) {
110        let indexes = self.property_indexes.read();
111        if let Some(index) = indexes.get(key) {
112            // Get old value to remove from index
113            if let Some(old_value) = self.node_properties.get(node_id, key) {
114                let old_hv = HashableValue::new(old_value);
115                if let Some(mut nodes) = index.get_mut(&old_hv) {
116                    nodes.remove(&node_id);
117                    if nodes.is_empty() {
118                        drop(nodes);
119                        index.remove(&old_hv);
120                    }
121                }
122            }
123        }
124    }
125
126    /// Stores a vector index for a label+property pair.
127    #[cfg(feature = "vector-index")]
128    pub fn add_vector_index(&self, label: &str, property: &str, index: Arc<HnswIndex>) {
129        let key = format!("{label}:{property}");
130        self.vector_indexes.write().insert(key, index);
131    }
132
133    /// Retrieves the vector index for a label+property pair.
134    #[cfg(feature = "vector-index")]
135    #[must_use]
136    pub fn get_vector_index(&self, label: &str, property: &str) -> Option<Arc<HnswIndex>> {
137        let key = format!("{label}:{property}");
138        self.vector_indexes.read().get(&key).cloned()
139    }
140
141    /// Removes a vector index for a label+property pair.
142    ///
143    /// Returns `true` if the index existed and was removed.
144    #[cfg(feature = "vector-index")]
145    pub fn remove_vector_index(&self, label: &str, property: &str) -> bool {
146        let key = format!("{label}:{property}");
147        self.vector_indexes.write().remove(&key).is_some()
148    }
149
150    /// Returns all vector index entries as `(key, index)` pairs.
151    ///
152    /// Keys are in `"label:property"` format.
153    #[cfg(feature = "vector-index")]
154    #[must_use]
155    pub fn vector_index_entries(&self) -> Vec<(String, Arc<HnswIndex>)> {
156        self.vector_indexes
157            .read()
158            .iter()
159            .map(|(k, v)| (k.clone(), v.clone()))
160            .collect()
161    }
162
163    /// Stores a text index for a label+property pair.
164    #[cfg(feature = "text-index")]
165    pub fn add_text_index(
166        &self,
167        label: &str,
168        property: &str,
169        index: Arc<RwLock<crate::index::text::InvertedIndex>>,
170    ) {
171        let key = format!("{label}:{property}");
172        self.text_indexes.write().insert(key, index);
173    }
174
175    /// Retrieves the text index for a label+property pair.
176    #[cfg(feature = "text-index")]
177    #[must_use]
178    pub fn get_text_index(
179        &self,
180        label: &str,
181        property: &str,
182    ) -> Option<Arc<RwLock<crate::index::text::InvertedIndex>>> {
183        let key = format!("{label}:{property}");
184        self.text_indexes.read().get(&key).cloned()
185    }
186
187    /// Removes a text index for a label+property pair.
188    ///
189    /// Returns `true` if the index existed and was removed.
190    #[cfg(feature = "text-index")]
191    pub fn remove_text_index(&self, label: &str, property: &str) -> bool {
192        let key = format!("{label}:{property}");
193        self.text_indexes.write().remove(&key).is_some()
194    }
195
196    /// Returns all text index entries as `(key, index)` pairs.
197    ///
198    /// The key format is `"label:property"`.
199    #[cfg(feature = "text-index")]
200    pub fn text_index_entries(
201        &self,
202    ) -> Vec<(String, Arc<RwLock<crate::index::text::InvertedIndex>>)> {
203        self.text_indexes
204            .read()
205            .iter()
206            .map(|(k, v)| (k.clone(), v.clone()))
207            .collect()
208    }
209
210    /// Updates text indexes when a node property is set.
211    ///
212    /// If the node has a label with a text index on this property key,
213    /// the index is updated with the new value (if it's a string).
214    #[cfg(feature = "text-index")]
215    pub(super) fn update_text_index_on_set(&self, id: NodeId, key: &str, value: &Value) {
216        let text_indexes = self.text_indexes.read();
217        if text_indexes.is_empty() {
218            return;
219        }
220        let id_to_label = self.id_to_label.read();
221        let node_labels = self.node_labels.read();
222        if let Some(label_ids) = node_labels.get(&id) {
223            for &label_id in label_ids {
224                if let Some(label_name) = id_to_label.get(label_id as usize) {
225                    let index_key = format!("{label_name}:{key}");
226                    if let Some(index) = text_indexes.get(&index_key) {
227                        let mut idx = index.write();
228                        // Remove old entry first, then insert new if it's a string
229                        idx.remove(id);
230                        if let Value::String(text) = value {
231                            idx.insert(id, text);
232                        }
233                    }
234                }
235            }
236        }
237    }
238
239    /// Updates text indexes when a node property is removed.
240    #[cfg(feature = "text-index")]
241    pub(super) fn update_text_index_on_remove(&self, id: NodeId, key: &str) {
242        let text_indexes = self.text_indexes.read();
243        if text_indexes.is_empty() {
244            return;
245        }
246        let id_to_label = self.id_to_label.read();
247        let node_labels = self.node_labels.read();
248        if let Some(label_ids) = node_labels.get(&id) {
249            for &label_id in label_ids {
250                if let Some(label_name) = id_to_label.get(label_id as usize) {
251                    let index_key = format!("{label_name}:{key}");
252                    if let Some(index) = text_indexes.get(&index_key) {
253                        index.write().remove(id);
254                    }
255                }
256            }
257        }
258    }
259
260    /// Removes a node from all text indexes.
261    #[cfg(feature = "text-index")]
262    pub(super) fn remove_from_all_text_indexes(&self, id: NodeId) {
263        let text_indexes = self.text_indexes.read();
264        if text_indexes.is_empty() {
265            return;
266        }
267        for (_, index) in text_indexes.iter() {
268            index.write().remove(id);
269        }
270    }
271}