Skip to main content

relay_knowledge/domain/core/
index.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use super::GraphVersion;
6
7/// Derived index families maintained from the graph mutation log.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum IndexKind {
11    Bm25,
12    Semantic,
13    Vector,
14}
15
16impl IndexKind {
17    /// All v1 index families required by the hybrid retrieval contract.
18    pub const ALL: [Self; 3] = [Self::Bm25, Self::Semantic, Self::Vector];
19
20    /// Stable storage and API representation.
21    pub const fn as_str(self) -> &'static str {
22        match self {
23            Self::Bm25 => "bm25",
24            Self::Semantic => "semantic",
25            Self::Vector => "vector",
26        }
27    }
28}
29
30impl fmt::Display for IndexKind {
31    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32        formatter.write_str(self.as_str())
33    }
34}
35
36/// Source modality covered by a derived index cursor.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum IndexModality {
40    Text,
41    Image,
42    Layout,
43    Table,
44}
45
46impl IndexModality {
47    /// The v1 evidence modality refreshed by BM25, semantic, and vector indexes.
48    pub const TEXT: Self = Self::Text;
49
50    /// Stable storage and API representation.
51    pub const fn as_str(self) -> &'static str {
52        match self {
53            Self::Text => "text",
54            Self::Image => "image",
55            Self::Layout => "layout",
56            Self::Table => "table",
57        }
58    }
59}
60
61impl fmt::Display for IndexModality {
62    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63        formatter.write_str(self.as_str())
64    }
65}
66
67/// Operational state of a derived index.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub enum IndexState {
71    Fresh,
72    Stale,
73    Failed,
74    Paused,
75}
76
77impl IndexState {
78    /// Stable storage and API representation.
79    pub const fn as_str(self) -> &'static str {
80        match self {
81            Self::Fresh => "fresh",
82            Self::Stale => "stale",
83            Self::Failed => "failed",
84            Self::Paused => "paused",
85        }
86    }
87}
88
89/// Versioned status for a derived index.
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct IndexStatus {
92    pub kind: IndexKind,
93    pub index_version: u64,
94    pub indexed_graph_version: GraphVersion,
95    pub state: IndexState,
96    pub last_error: Option<String>,
97}
98
99/// Scoped cursor for a derived index read model.
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub struct IndexCursor {
102    pub kind: IndexKind,
103    pub source_scope: String,
104    pub modality: IndexModality,
105    pub index_version: u64,
106    pub indexed_graph_version: GraphVersion,
107    pub state: IndexState,
108    pub last_error: Option<String>,
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub source_hash: Option<String>,
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub backend_cursor: Option<String>,
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub model_name: Option<String>,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub model_dimension: Option<u32>,
117}
118
119/// Per-kind lag included in diagnostics snapshots.
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121pub struct IndexLag {
122    pub kind: IndexKind,
123    pub lag_versions: u64,
124}
125
126/// Structured reason explaining why an index family or scoped cursor is stale.
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub struct IndexStalenessReason {
129    pub kind: IndexKind,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub source_scope: Option<String>,
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub modality: Option<IndexModality>,
134    pub reason: String,
135    pub lag_versions: u64,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub last_error: Option<String>,
138}
139
140/// Queue, dead-letter, and stale-index diagnostics shared by APIs and storage.
141#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
142pub struct IndexRefreshDiagnostics {
143    pub queue_depth: usize,
144    pub running_count: usize,
145    pub retrying_count: usize,
146    pub dead_letter_count: usize,
147    pub oldest_unfinished_age_ms: Option<u64>,
148    pub index_lag_by_kind: Vec<IndexLag>,
149    pub max_index_lag_versions: u64,
150    pub stale_index_count: usize,
151    pub stale_reasons: Vec<IndexStalenessReason>,
152}
153
154impl IndexStatus {
155    /// Creates the initial stale status for an empty derived index.
156    pub const fn empty(kind: IndexKind) -> Self {
157        Self {
158            kind,
159            index_version: 0,
160            indexed_graph_version: GraphVersion::ZERO,
161            state: IndexState::Stale,
162            last_error: None,
163        }
164    }
165
166    /// Returns whether this index is behind the supplied graph version.
167    pub fn is_stale_for(&self, graph_version: GraphVersion) -> bool {
168        self.state != IndexState::Fresh || self.indexed_graph_version < graph_version
169    }
170}
171
172#[cfg(test)]
173#[path = "index_tests.rs"]
174mod tests;