1use crate::{AppendEntry, MicroscopeReader, LAYER_NAMES};
16
17#[derive(Debug, Clone)]
18pub struct Query {
19 pub keywords: Vec<String>,
20 pub op: BoolOp,
21 pub layer_filter: Option<u8>,
22 pub depth_filter: Option<(u8, u8)>, pub spatial_filter: Option<(f32, f32, f32, f32)>, pub limit: usize,
25}
26
27#[derive(Debug, Clone, PartialEq)]
28pub enum BoolOp {
29 And,
30 Or,
31}
32
33impl Default for Query {
34 fn default() -> Self {
35 Self {
36 keywords: Vec::new(),
37 op: BoolOp::And,
38 layer_filter: None,
39 depth_filter: None,
40 spatial_filter: None,
41 limit: 10,
42 }
43 }
44}
45
46pub fn parse(input: &str) -> Query {
48 let mut q = Query::default();
49 let mut remaining = input.trim();
50
51 while !remaining.is_empty() {
52 remaining = remaining.trim_start();
53 if remaining.is_empty() {
54 break;
55 }
56
57 if let Some(rest) = remaining.strip_prefix("layer:") {
59 let (val, rest2) = take_word(rest);
60 q.layer_filter = Some(crate::layer_to_id(&val));
61 remaining = rest2;
62 continue;
63 }
64
65 if let Some(rest) = remaining.strip_prefix("depth:") {
67 let (val, rest2) = take_word(rest);
68 if let Some((a, b)) = val.split_once("..") {
69 let lo = a.parse::<u8>().unwrap_or(0);
70 let hi = b.parse::<u8>().unwrap_or(8);
71 q.depth_filter = Some((lo, hi));
72 } else if let Ok(d) = val.parse::<u8>() {
73 q.depth_filter = Some((d, d));
74 }
75 remaining = rest2;
76 continue;
77 }
78
79 if let Some(rest) = remaining.strip_prefix("near:") {
81 let (val, rest2) = take_word(rest);
82 let parts: Vec<f32> = val.split(',').filter_map(|s| s.parse().ok()).collect();
83 if parts.len() >= 3 {
84 let r = if parts.len() >= 4 { parts[3] } else { 0.1 };
85 q.spatial_filter = Some((parts[0], parts[1], parts[2], r));
86 }
87 remaining = rest2;
88 continue;
89 }
90
91 if let Some(rest) = remaining.strip_prefix("limit:") {
93 let (val, rest2) = take_word(rest);
94 if let Ok(n) = val.parse::<usize>() {
95 q.limit = n;
96 }
97 remaining = rest2;
98 continue;
99 }
100
101 if let Some(rest) = remaining.strip_prefix("AND") {
103 if rest.is_empty() || rest.starts_with(' ') {
104 q.op = BoolOp::And;
105 remaining = rest;
106 continue;
107 }
108 }
109 if let Some(rest) = remaining.strip_prefix("OR") {
110 if rest.is_empty() || rest.starts_with(' ') {
111 q.op = BoolOp::Or;
112 remaining = rest;
113 continue;
114 }
115 }
116
117 if remaining.starts_with('"') {
119 if let Some(end) = remaining[1..].find('"') {
120 q.keywords.push(remaining[1..1 + end].to_lowercase());
121 remaining = &remaining[2 + end..];
122 continue;
123 }
124 }
125
126 let (word, rest2) = take_word(remaining);
128 if !word.is_empty() {
129 q.keywords.push(word.to_lowercase());
130 }
131 remaining = rest2;
132 }
133
134 q
135}
136
137fn take_word(s: &str) -> (String, &str) {
138 let s = s.trim_start();
139 let end = s.find(char::is_whitespace).unwrap_or(s.len());
140 (s[..end].to_string(), &s[end..])
141}
142
143#[derive(Debug)]
145pub struct QueryResult {
146 pub score: f32,
147 pub block_idx: usize,
148 pub is_main: bool, }
150
151pub fn execute(q: &Query, reader: &MicroscopeReader, appended: &[AppendEntry]) -> Vec<QueryResult> {
153 let mut results = Vec::new();
154
155 for i in 0..reader.block_count {
157 let h = reader.header(i);
158
159 if let Some(lid) = q.layer_filter {
161 if h.layer_id != lid {
162 continue;
163 }
164 }
165
166 if let Some((lo, hi)) = q.depth_filter {
168 if h.depth < lo || h.depth > hi {
169 continue;
170 }
171 }
172
173 if let Some((sx, sy, sz, sr)) = q.spatial_filter {
175 let dx = h.x - sx;
176 let dy = h.y - sy;
177 let dz = h.z - sz;
178 if dx * dx + dy * dy + dz * dz > sr * sr {
179 continue;
180 }
181 }
182
183 let text = reader.text(i).to_lowercase();
185 let score = keyword_score(&text, &q.keywords, &q.op);
186 if score > 0.0 {
187 results.push(QueryResult {
188 score,
189 block_idx: i,
190 is_main: true,
191 });
192 }
193 }
194
195 for (ai, entry) in appended.iter().enumerate() {
197 if let Some(lid) = q.layer_filter {
198 if entry.layer_id != lid {
199 continue;
200 }
201 }
202 if let Some((lo, hi)) = q.depth_filter {
203 if entry.depth < lo || entry.depth > hi {
204 continue;
205 }
206 }
207 if let Some((sx, sy, sz, sr)) = q.spatial_filter {
208 let dx = entry.x - sx;
209 let dy = entry.y - sy;
210 let dz = entry.z - sz;
211 if dx * dx + dy * dy + dz * dz > sr * sr {
212 continue;
213 }
214 }
215
216 let text = entry.text.to_lowercase();
217 let score = keyword_score(&text, &q.keywords, &q.op);
218 if score > 0.0 {
219 results.push(QueryResult {
220 score,
221 block_idx: ai + 1_000_000,
222 is_main: false,
223 });
224 }
225 }
226
227 results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
229 results.truncate(q.limit);
230 results
231}
232
233fn keyword_score(text: &str, keywords: &[String], op: &BoolOp) -> f32 {
234 if keywords.is_empty() {
235 return 1.0;
236 }
237
238 let hits: Vec<bool> = keywords
239 .iter()
240 .map(|kw| text.contains(kw.as_str()))
241 .collect();
242 let hit_count = hits.iter().filter(|&&h| h).count();
243
244 match op {
245 BoolOp::And => {
246 if hit_count == keywords.len() {
247 hit_count as f32
248 } else {
249 0.0
250 }
251 }
252 BoolOp::Or => hit_count as f32,
253 }
254}
255
256#[allow(dead_code)]
258pub fn layer_name(id: u8) -> &'static str {
259 LAYER_NAMES.get(id as usize).unwrap_or(&"?")
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn test_parse_simple() {
268 let q = parse("hello");
269 assert_eq!(q.keywords, vec!["hello"]);
270 assert_eq!(q.op, BoolOp::And);
271 }
272
273 #[test]
274 fn test_parse_quoted() {
275 let q = parse("\"hello world\"");
276 assert_eq!(q.keywords, vec!["hello world"]);
277 }
278
279 #[test]
280 fn test_parse_filters() {
281 let q = parse("layer:long_term depth:2..5 \"memory\"");
282 assert_eq!(q.layer_filter, Some(1)); assert_eq!(q.depth_filter, Some((2, 5)));
284 assert_eq!(q.keywords, vec!["memory"]);
285 }
286
287 #[test]
288 fn test_parse_bool_op() {
289 let q = parse("\"foo\" OR \"bar\"");
290 assert_eq!(q.op, BoolOp::Or);
291 assert_eq!(q.keywords, vec!["foo", "bar"]);
292 }
293
294 #[test]
295 fn test_parse_limit() {
296 let q = parse("limit:20 hello");
297 assert_eq!(q.limit, 20);
298 }
299
300 #[test]
301 fn test_parse_spatial() {
302 let q = parse("near:0.2,0.3,0.1,0.05 test");
303 let (x, y, z, r) = q.spatial_filter.unwrap();
304 assert!((x - 0.2).abs() < 0.001);
305 assert!((y - 0.3).abs() < 0.001);
306 assert!((z - 0.1).abs() < 0.001);
307 assert!((r - 0.05).abs() < 0.001);
308 }
309
310 #[test]
311 fn test_keyword_score_and() {
312 assert_eq!(
313 keyword_score(
314 "hello world",
315 &["hello".into(), "world".into()],
316 &BoolOp::And
317 ),
318 2.0
319 );
320 assert_eq!(
321 keyword_score("hello", &["hello".into(), "world".into()], &BoolOp::And),
322 0.0
323 );
324 }
325
326 #[test]
327 fn test_keyword_score_or() {
328 assert_eq!(
329 keyword_score("hello", &["hello".into(), "world".into()], &BoolOp::Or),
330 1.0
331 );
332 assert_eq!(
333 keyword_score("nope", &["hello".into(), "world".into()], &BoolOp::Or),
334 0.0
335 );
336 }
337}