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}