1use std::collections::{BTreeMap, HashMap, HashSet};
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::tile::TileId;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TileMeta {
11 pub id: TileId,
12 pub valence: f64,
13 pub created: DateTime<Utc>,
14 pub accessed: DateTime<Utc>,
15 pub constraints: HashSet<String>,
16}
17
18#[derive(Debug, Default, Serialize, Deserialize)]
20pub struct CrystalIndex {
21 tiles: HashMap<TileId, TileMeta>,
22 by_valence: BTreeMap<OrderedValence, Vec<TileId>>,
23 by_time: BTreeMap<i64, TileId>,
24 constraint_index: HashMap<String, HashSet<TileId>>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29struct OrderedValence(pub f64);
30
31impl PartialEq for OrderedValence {
32 fn eq(&self, other: &Self) -> bool {
33 self.0 == other.0
34 }
35}
36
37impl Eq for OrderedValence {}
38
39impl PartialOrd for OrderedValence {
40 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
41 self.0.partial_cmp(&other.0)
42 }
43}
44
45impl Ord for OrderedValence {
46 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
47 self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
48 }
49}
50
51impl CrystalIndex {
52 pub fn new() -> Self {
54 Self::default()
55 }
56
57 pub fn insert(&mut self, meta: TileMeta) {
59 let id = meta.id.clone();
60 let valence = meta.valence;
61 let created_ts = meta.created.timestamp();
62 let constraints = meta.constraints.clone();
63
64 self.tiles.insert(id.clone(), meta);
66
67 self.by_valence
69 .entry(OrderedValence(valence))
70 .or_default()
71 .push(id.clone());
72
73 self.by_time.insert(created_ts, id.clone());
75
76 for key in &constraints {
78 self.constraint_index
79 .entry(key.clone())
80 .or_default()
81 .insert(id.clone());
82 }
83 }
84
85 pub fn remove(&mut self, id: &TileId) -> Option<TileMeta> {
87 let meta = self.tiles.remove(id)?;
88
89 if let Some(ids) = self.by_valence.get_mut(&OrderedValence(meta.valence)) {
91 ids.retain(|i| i != id);
92 if ids.is_empty() {
93 self.by_valence.remove(&OrderedValence(meta.valence));
94 }
95 }
96
97 let ts = meta.created.timestamp();
99 self.by_time.remove(&ts);
100
101 for key in &meta.constraints {
103 if let Some(set) = self.constraint_index.get_mut(key) {
104 set.remove(id);
105 if set.is_empty() {
106 self.constraint_index.remove(key);
107 }
108 }
109 }
110
111 Some(meta)
112 }
113
114 pub fn query(&self, constraints: &[&str], limit: usize) -> Vec<TileId> {
116 let mut candidates: HashMap<TileId, usize> = HashMap::new();
117
118 for key in constraints {
119 if let Some(ids) = self.constraint_index.get(*key) {
120 for id in ids {
121 *candidates.entry(id.clone()).or_default() += 1;
122 }
123 }
124 }
125
126 let mut ranked: Vec<_> = candidates.into_iter().collect();
128 ranked.sort_by(|a, b| b.1.cmp(&a.1));
129
130 ranked.into_iter().take(limit).map(|(id, _)| id).collect()
131 }
132
133 pub fn top_by_valence(&self, limit: usize) -> Vec<TileId> {
135 self.by_valence
136 .iter()
137 .rev() .flat_map(|(_, ids)| ids.iter().cloned())
139 .take(limit)
140 .collect()
141 }
142
143 pub fn recent(&self, limit: usize) -> Vec<TileId> {
145 self.by_time
146 .iter()
147 .rev() .map(|(_, id)| id.clone())
149 .take(limit)
150 .collect()
151 }
152
153 pub fn get(&self, id: &TileId) -> Option<&TileMeta> {
155 self.tiles.get(id)
156 }
157
158 pub fn len(&self) -> usize {
160 self.tiles.len()
161 }
162
163 pub fn is_empty(&self) -> bool {
165 self.tiles.is_empty()
166 }
167
168 pub fn all_ids(&self) -> Vec<TileId> {
170 self.tiles.keys().cloned().collect()
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn meta(valence: f64, constraints: &[&str]) -> TileMeta {
179 TileMeta {
180 id: TileId::new(),
181 valence,
182 created: Utc::now(),
183 accessed: Utc::now(),
184 constraints: constraints.iter().map(|s| s.to_string()).collect(),
185 }
186 }
187
188 #[test]
189 fn insert_and_query() {
190 let mut index = CrystalIndex::new();
191 let m = meta(0.5, &["rust", "programming"]);
192 let id = m.id.clone();
193 index.insert(m);
194
195 let results = index.query(&["rust"], 10);
196 assert_eq!(results.len(), 1);
197 assert_eq!(results[0], id);
198 }
199
200 #[test]
201 fn top_by_valence() {
202 let mut index = CrystalIndex::new();
203 let m1 = meta(0.3, &[]);
204 let m2 = meta(0.9, &[]);
205 let m3 = meta(0.6, &[]);
206 let high_id = m2.id.clone();
207 index.insert(m1);
208 index.insert(m2);
209 index.insert(m3);
210
211 let top = index.top_by_valence(1);
212 assert_eq!(top.len(), 1);
213 assert_eq!(top[0], high_id);
214 }
215
216 #[test]
217 fn recent() {
218 let mut index = CrystalIndex::new();
219 let mut m1 = meta(0.5, &[]);
220 m1.created = Utc::now() - chrono::Duration::days(1);
221 let m2 = meta(0.5, &[]);
222 let recent_id = m2.id.clone();
223 index.insert(m1);
224 index.insert(m2);
225
226 let recent = index.recent(1);
227 assert_eq!(recent.len(), 1);
228 assert_eq!(recent[0], recent_id);
229 }
230
231 #[test]
232 fn remove_tile() {
233 let mut index = CrystalIndex::new();
234 let m = meta(0.5, &["test"]);
235 let id = m.id.clone();
236 index.insert(m);
237 assert_eq!(index.len(), 1);
238
239 index.remove(&id);
240 assert!(index.is_empty());
241 assert!(index.query(&["test"], 10).is_empty());
242 }
243
244 #[test]
245 fn multi_constraint_query() {
246 let mut index = CrystalIndex::new();
247 let m1 = meta(0.5, &["rust", "systems"]);
248 let m2 = meta(0.5, &["rust", "web"]);
249 index.insert(m1);
250 index.insert(m2);
251
252 let results = index.query(&["rust"], 10);
254 assert_eq!(results.len(), 2);
255
256 let results = index.query(&["systems"], 10);
257 assert_eq!(results.len(), 1);
258 }
259
260 #[test]
261 fn empty_query() {
262 let index = CrystalIndex::new();
263 let results = index.query(&["nonexistent"], 10);
264 assert!(results.is_empty());
265 }
266}