Skip to main content

rushdb_types/
index.rs

1//! Declared secondary indexes: the client chooses what is fast.
2//!
3//! An index belongs to one record label and one property (by normalized
4//! family name, so it covers every typed definition of that property). Its
5//! entries are derived rows maintained by the apply-time deriver; the state
6//! decides which records they cover, so entries always equal a from-scratch
7//! derivation of the base rows, including while a build or drop is running.
8
9use serde::{Deserialize, Serialize};
10
11/// One declared index.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct IndexDef {
14    /// Tenant-local index id (allocated at definition, never reused).
15    pub id: u32,
16    /// Tenant-unique name.
17    pub name: String,
18    /// Record label the index covers.
19    pub label: String,
20    /// Normalized property (family) name.
21    pub property: String,
22    /// Index kind.
23    pub kind: IndexKind,
24    /// Build/drop state.
25    pub state: IndexState,
26}
27
28/// What an index answers.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub enum IndexKind {
32    /// Ordered entries `(value, owner)`: equality, `$in`, ranges, prefix.
33    Ordered,
34    /// Full-text (BM25) over the property's string values — every string
35    /// property of the record when the property is [`TEXT_ALL_PROPERTIES`].
36    /// Maintained by the text applier from pending rows.
37    Text {
38        /// When a search sees a write.
39        consistency: TextConsistency,
40    },
41    /// Structural embeddings: one vector per record of the label, derived
42    /// from the graph around it (`FastRP` over every relationship, ignoring
43    /// direction), bound into the vector space named after the index.
44    /// Maintained by the structural applier from pending rows: a
45    /// relationship or record change marks the records within two hops.
46    /// Its property is [`TEXT_ALL_PROPERTIES`]: it reads topology, not a
47    /// property.
48    Structural {
49        /// Vector dimensions.
50        dimensions: u32,
51    },
52}
53
54impl IndexKind {
55    /// Whether an applier outside the store maintains the index from pending
56    /// rows (text and structural), rather than apply-time entries.
57    #[must_use]
58    pub fn applied(self) -> bool {
59        matches!(self, Self::Text { .. } | Self::Structural { .. })
60    }
61}
62
63/// Property name of a text index covering every string property (and of a
64/// structural index, which covers no single property).
65pub const TEXT_ALL_PROPERTIES: &str = "*";
66
67/// When a text search sees committed writes.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub enum TextConsistency {
71    /// A search first applies every pending change up to its snapshot
72    /// (read-your-writes).
73    Immediate,
74    /// A search reads what the background applier has applied and reports
75    /// the backlog.
76    Eventual,
77}
78
79/// Lifecycle of an index. Records covered by the entries:
80/// `Building{cursor}` covers ordinals `< cursor`, `Ready` covers all, and
81/// `Dropping{cursor}` covers ordinals `≥ cursor`.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase", tag = "state")]
84pub enum IndexState {
85    /// Backfill has covered records below `cursor`.
86    Building {
87        /// First record ordinal not yet backfilled.
88        cursor: u64,
89    },
90    /// Every record is covered; the planner may use the index.
91    Ready,
92    /// Entries of records below `cursor` have been removed.
93    Dropping {
94        /// First record ordinal whose entries remain.
95        cursor: u64,
96    },
97}
98
99impl IndexState {
100    /// Half-open ordinal interval `[start, end)` of covered records.
101    #[must_use]
102    pub fn covered(self) -> (u64, u64) {
103        match self {
104            Self::Building { cursor } => (0, cursor),
105            Self::Ready => (0, u64::MAX),
106            Self::Dropping { cursor } => (cursor, u64::MAX),
107        }
108    }
109
110    /// Whether record ordinal `ord` is covered.
111    #[must_use]
112    pub fn covers(self, ord: u64) -> bool {
113        let (start, end) = self.covered();
114        start <= ord && ord < end
115    }
116
117    /// Wire label (`building`, `ready`, `dropping`).
118    #[must_use]
119    pub fn label(self) -> &'static str {
120        match self {
121            Self::Building { .. } => "building",
122            Self::Ready => "ready",
123            Self::Dropping { .. } => "dropping",
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn coverage_follows_the_cursor() {
134        assert!(!IndexState::Building { cursor: 0 }.covers(0));
135        let building = IndexState::Building { cursor: 5 };
136        assert!(building.covers(0) && building.covers(4) && !building.covers(5));
137        assert!(IndexState::Ready.covers(u64::MAX - 1));
138        let dropping = IndexState::Dropping { cursor: 5 };
139        assert!(!dropping.covers(4) && dropping.covers(5));
140    }
141}