Skip to main content

velesdb_core/collection/graph/
label_index.rs

1//! Label index for fast graph node lookups by label.
2//!
3//! Provides O(1) label-to-node-id lookups using `RoaringBitmap`, enabling
4//! `find_start_nodes()` to skip the O(N) full scan when a MATCH pattern
5//! specifies node labels.
6
7use super::helpers::safe_bitmap_id;
8use roaring::RoaringBitmap;
9use std::collections::HashMap;
10
11/// Index mapping label names to the set of node IDs carrying that label.
12///
13/// Each label maps to a `RoaringBitmap` of node IDs (u32). When a MATCH
14/// query specifies `(n:Person)`, the index returns the bitmap of all
15/// `Person`-labeled nodes in O(1), avoiding a full payload scan.
16///
17/// # Limitations
18///
19/// `RoaringBitmap` only supports u32 IDs. Node IDs exceeding `u32::MAX`
20/// are silently skipped (logged at warn level on insert).
21///
22/// # Example
23///
24/// ```rust,ignore
25/// let mut index = LabelIndex::new();
26/// index.insert("Person", 1);
27/// index.insert("Person", 2);
28/// index.insert("Company", 3);
29///
30/// let persons = index.lookup("Person");
31/// assert!(persons.map_or(false, |b| b.contains(1)));
32/// assert!(persons.map_or(false, |b| b.contains(2)));
33/// ```
34#[derive(Debug, Default)]
35pub struct LabelIndex {
36    /// label_name -> set of node IDs with that label.
37    labels: HashMap<String, RoaringBitmap>,
38    /// Set to `true` when any `index_from_payload` call encounters a node ID
39    /// exceeding `u32::MAX`. Callers should fall back to a full scan when this
40    /// flag is set and the bitmap lookup returns no results.
41    has_large_ids: bool,
42}
43
44impl LabelIndex {
45    /// Creates a new empty label index.
46    #[must_use]
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Indexes a node under one or more labels.
52    ///
53    /// Extracts `_labels` from the JSON payload (expected to be an array of
54    /// strings) and inserts the node ID into the bitmap for each label.
55    ///
56    /// Returns the number of labels successfully indexed (0 if payload has
57    /// no `_labels` array or node ID exceeds `u32::MAX`).
58    pub fn index_from_payload(&mut self, node_id: u64, payload: &serde_json::Value) -> usize {
59        let Some(safe_id) = safe_bitmap_id(node_id) else {
60            tracing::warn!(
61                node_id,
62                "LabelIndex: node_id exceeds u32::MAX, cannot index"
63            );
64            // Track that we have unindexable IDs so callers can fall back
65            // to a full scan instead of returning silently incomplete results.
66            if payload.get("_labels").and_then(|v| v.as_array()).is_some() {
67                self.has_large_ids = true;
68            }
69            return 0;
70        };
71
72        let Some(labels_arr) = payload.get("_labels").and_then(|v| v.as_array()) else {
73            return 0;
74        };
75
76        let mut count = 0usize;
77        for label_val in labels_arr {
78            if let Some(label_str) = label_val.as_str() {
79                self.labels
80                    .entry(label_str.to_string())
81                    .or_default()
82                    .insert(safe_id);
83                count += 1;
84            }
85        }
86        count
87    }
88
89    /// Inserts a single `(label, node_id)` pair into the index.
90    ///
91    /// Returns `true` if the node was added (new entry). Returns `false` if
92    /// the node ID exceeds `u32::MAX` or was already present.
93    pub fn insert(&mut self, label: &str, node_id: u64) -> bool {
94        let Some(safe_id) = safe_bitmap_id(node_id) else {
95            return false;
96        };
97        self.labels
98            .entry(label.to_string())
99            .or_default()
100            .insert(safe_id)
101    }
102
103    /// Removes a node from all label bitmaps.
104    ///
105    /// Call this before removing a node to keep the index consistent.
106    pub fn remove_from_payload(&mut self, node_id: u64, payload: &serde_json::Value) {
107        let Some(safe_id) = safe_bitmap_id(node_id) else {
108            return;
109        };
110
111        let Some(labels_arr) = payload.get("_labels").and_then(|v| v.as_array()) else {
112            return;
113        };
114
115        for label_val in labels_arr {
116            if let Some(label_str) = label_val.as_str() {
117                if let Some(bitmap) = self.labels.get_mut(label_str) {
118                    bitmap.remove(safe_id);
119                    if bitmap.is_empty() {
120                        self.labels.remove(label_str);
121                    }
122                }
123            }
124        }
125    }
126
127    /// Returns `true` if any node with `_labels` had an ID exceeding `u32::MAX`
128    /// and could not be indexed. Callers should fall back to a full scan when
129    /// this is `true` and the bitmap lookup returns empty results.
130    #[must_use]
131    pub fn has_large_ids(&self) -> bool {
132        self.has_large_ids
133    }
134
135    /// Returns the bitmap of node IDs carrying the given label.
136    ///
137    /// Returns `None` if no nodes have been indexed with this label.
138    #[must_use]
139    pub fn lookup(&self, label: &str) -> Option<&RoaringBitmap> {
140        self.labels.get(label)
141    }
142
143    /// Returns the intersection of bitmaps for all required labels.
144    ///
145    /// When a MATCH pattern requires multiple labels (e.g., `(n:Person:Employee)`),
146    /// only nodes carrying ALL labels should match. Returns `None` if any
147    /// required label has no indexed nodes (empty intersection).
148    #[must_use]
149    pub fn lookup_intersection(&self, labels: &[String]) -> Option<RoaringBitmap> {
150        let mut iter = labels.iter();
151        let first = iter.next()?;
152        let mut result = self.labels.get(first.as_str())?.clone();
153
154        for label in iter {
155            match self.labels.get(label.as_str()) {
156                Some(bitmap) => result &= bitmap,
157                None => return None, // Label has no nodes → empty intersection
158            }
159            if result.is_empty() {
160                return None;
161            }
162        }
163
164        if result.is_empty() {
165            None
166        } else {
167            Some(result)
168        }
169    }
170
171    /// Returns the number of distinct labels in the index.
172    #[must_use]
173    pub fn label_count(&self) -> usize {
174        self.labels.len()
175    }
176
177    /// Returns `true` if the index contains no entries.
178    #[must_use]
179    pub fn is_empty(&self) -> bool {
180        self.labels.is_empty()
181    }
182
183    /// Clears all entries from the index.
184    pub fn clear(&mut self) {
185        self.labels.clear();
186    }
187
188    /// Returns an estimated memory usage in bytes.
189    #[must_use]
190    pub fn memory_usage(&self) -> usize {
191        let mut total = std::mem::size_of::<Self>();
192        for (label, bitmap) in &self.labels {
193            total += label.len() + std::mem::size_of::<String>();
194            total += bitmap.serialized_size();
195        }
196        total
197    }
198}