1use std::io;
18use std::path::Path;
19
20use serde::{Deserialize, Serialize};
21
22#[derive(Debug, Clone)]
26pub struct SearchResult {
27 pub id: String,
29 pub score: f32,
31 pub metadata: serde_json::Value,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37struct Entry {
38 id: String,
39 embedding: Vec<f32>,
40 metadata: serde_json::Value,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ProgressiveSearch {
50 entries: Vec<Entry>,
51}
52
53impl ProgressiveSearch {
54 pub fn new() -> Self {
56 Self {
57 entries: Vec::new(),
58 }
59 }
60
61 pub fn insert(&mut self, id: &str, embedding: Vec<f32>, metadata: serde_json::Value) {
63 self.entries.push(Entry {
64 id: id.to_owned(),
65 embedding,
66 metadata,
67 });
68 }
69
70 pub fn search(&self, query: &[f32], k: usize) -> Vec<SearchResult> {
80 if self.entries.is_empty() || k == 0 {
81 return Vec::new();
82 }
83
84 let mut scored: Vec<SearchResult> = self
85 .entries
86 .iter()
87 .map(|entry| SearchResult {
88 id: entry.id.clone(),
89 score: cosine_similarity(query, &entry.embedding),
90 metadata: entry.metadata.clone(),
91 })
92 .collect();
93
94 scored.sort_by(|a, b| {
95 b.score
96 .partial_cmp(&a.score)
97 .unwrap_or(std::cmp::Ordering::Equal)
98 });
99
100 scored.truncate(k);
101 scored
102 }
103
104 pub fn len(&self) -> usize {
106 self.entries.len()
107 }
108
109 pub fn is_empty(&self) -> bool {
111 self.entries.is_empty()
112 }
113
114 pub fn save(&self, path: &Path) -> io::Result<()> {
116 let json = serde_json::to_string(self).map_err(|e| {
117 io::Error::new(io::ErrorKind::InvalidData, format!("serialize failed: {e}"))
118 })?;
119 std::fs::write(path, json)
120 }
121
122 pub fn load(path: &Path) -> io::Result<Self> {
124 let json = std::fs::read_to_string(path)?;
125 serde_json::from_str(&json).map_err(|e| {
126 io::Error::new(
127 io::ErrorKind::InvalidData,
128 format!("deserialize failed: {e}"),
129 )
130 })
131 }
132}
133
134impl Default for ProgressiveSearch {
135 fn default() -> Self {
136 Self::new()
137 }
138}
139
140fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
146 if a.len() != b.len() {
147 return 0.0;
148 }
149
150 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
151 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
152 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
153
154 if norm_a == 0.0 || norm_b == 0.0 {
155 return 0.0;
156 }
157
158 dot / (norm_a * norm_b)
159}
160
161#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn insert_and_search_returns_correct_top_k() {
169 let mut idx = ProgressiveSearch::new();
170 idx.insert("a", vec![1.0, 0.0, 0.0], serde_json::json!({"tag": "a"}));
171 idx.insert("b", vec![0.0, 1.0, 0.0], serde_json::json!({"tag": "b"}));
172 idx.insert("c", vec![0.7, 0.7, 0.0], serde_json::json!({"tag": "c"}));
173
174 let results = idx.search(&[1.0, 0.0, 0.0], 2);
175 assert_eq!(results.len(), 2);
176 assert_eq!(results[0].id, "a");
177 assert!((results[0].score - 1.0).abs() < 0.01);
178 assert_eq!(results[1].id, "c");
180 }
181
182 #[test]
183 fn cosine_similarity_identical_vectors() {
184 let score = cosine_similarity(&[0.6, 0.8], &[0.6, 0.8]);
185 assert!(
186 (score - 1.0).abs() < 0.001,
187 "identical vectors should score ~1.0, got {score}"
188 );
189 }
190
191 #[test]
192 fn cosine_similarity_orthogonal_vectors() {
193 let score = cosine_similarity(&[1.0, 0.0], &[0.0, 1.0]);
194 assert!(
195 score.abs() < 0.001,
196 "orthogonal vectors should score ~0.0, got {score}"
197 );
198 }
199
200 #[test]
201 fn cosine_similarity_opposite_vectors() {
202 let score = cosine_similarity(&[1.0, 0.0], &[-1.0, 0.0]);
203 assert!(
204 (score - (-1.0)).abs() < 0.001,
205 "opposite vectors should score ~-1.0, got {score}"
206 );
207 }
208
209 #[test]
210 fn cosine_similarity_different_lengths() {
211 let score = cosine_similarity(&[1.0, 0.0], &[1.0]);
212 assert!(
213 score.abs() < f32::EPSILON,
214 "mismatched lengths should return 0.0"
215 );
216 }
217
218 #[test]
219 fn cosine_similarity_zero_vector() {
220 let score = cosine_similarity(&[0.0, 0.0], &[1.0, 0.0]);
221 assert!(score.abs() < f32::EPSILON, "zero norm should return 0.0");
222 }
223
224 #[test]
225 fn save_and_load_round_trip() {
226 let mut idx = ProgressiveSearch::new();
227 idx.insert("x", vec![1.0, 2.0, 3.0], serde_json::json!({"v": 42}));
228 idx.insert("y", vec![4.0, 5.0, 6.0], serde_json::json!(null));
229
230 let dir = std::env::temp_dir().join("clawft_progressive_test");
231 std::fs::create_dir_all(&dir).unwrap();
232 let path = dir.join("test_index.json");
233
234 idx.save(&path).unwrap();
235 let loaded = ProgressiveSearch::load(&path).unwrap();
236
237 assert_eq!(loaded.len(), 2);
238
239 let orig_results = idx.search(&[1.0, 2.0, 3.0], 1);
241 let loaded_results = loaded.search(&[1.0, 2.0, 3.0], 1);
242
243 assert_eq!(orig_results[0].id, loaded_results[0].id);
244 assert!((orig_results[0].score - loaded_results[0].score).abs() < f32::EPSILON);
245
246 let _ = std::fs::remove_dir_all(&dir);
248 }
249
250 #[test]
251 fn empty_search_returns_empty() {
252 let idx = ProgressiveSearch::new();
253 assert!(idx.search(&[1.0, 0.0], 5).is_empty());
254 }
255
256 #[test]
257 fn search_with_k_zero_returns_empty() {
258 let mut idx = ProgressiveSearch::new();
259 idx.insert("a", vec![1.0], serde_json::json!(null));
260 assert!(idx.search(&[1.0], 0).is_empty());
261 }
262
263 #[test]
264 fn len_and_is_empty() {
265 let mut idx = ProgressiveSearch::new();
266 assert!(idx.is_empty());
267 assert_eq!(idx.len(), 0);
268
269 idx.insert("a", vec![1.0], serde_json::json!(null));
270 assert!(!idx.is_empty());
271 assert_eq!(idx.len(), 1);
272 }
273
274 #[test]
275 fn default_creates_empty() {
276 let idx = ProgressiveSearch::default();
277 assert!(idx.is_empty());
278 }
279
280 #[test]
281 fn search_preserves_metadata() {
282 let mut idx = ProgressiveSearch::new();
283 let meta = serde_json::json!({"source": "test", "priority": 5});
284 idx.insert("m", vec![1.0, 0.0], meta.clone());
285
286 let results = idx.search(&[1.0, 0.0], 1);
287 assert_eq!(results[0].metadata, meta);
288 }
289
290 #[test]
291 fn search_top_k_larger_than_store() {
292 let mut idx = ProgressiveSearch::new();
293 idx.insert("only", vec![1.0], serde_json::json!(null));
294 let results = idx.search(&[1.0], 100);
295 assert_eq!(results.len(), 1);
296 }
297
298 #[test]
299 fn load_nonexistent_file_errors() {
300 let result = ProgressiveSearch::load(Path::new("/nonexistent/path/index.json"));
301 assert!(result.is_err());
302 }
303}