1use ipfrs_core::{Cid, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::{BTreeMap, HashMap, HashSet};
9use std::sync::{Arc, RwLock};
10use std::time::{SystemTime, UNIX_EPOCH};
11
12type StringIndexMap = HashMap<String, HashMap<String, HashSet<Cid>>>;
14type NumericIndexMap = HashMap<String, BTreeMap<i64, HashSet<Cid>>>;
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub enum MetadataValue {
19 String(String),
21 Integer(i64),
23 Float(f64),
25 Boolean(bool),
27 Timestamp(u64),
29 StringArray(Vec<String>),
31 Null,
33}
34
35impl MetadataValue {
36 pub fn now() -> Self {
38 let timestamp = SystemTime::now()
39 .duration_since(UNIX_EPOCH)
40 .unwrap_or_default()
41 .as_secs();
42 MetadataValue::Timestamp(timestamp)
43 }
44
45 pub fn as_string(&self) -> Option<&str> {
47 match self {
48 MetadataValue::String(s) => Some(s),
49 _ => None,
50 }
51 }
52
53 pub fn as_integer(&self) -> Option<i64> {
55 match self {
56 MetadataValue::Integer(i) => Some(*i),
57 _ => None,
58 }
59 }
60
61 pub fn as_float(&self) -> Option<f64> {
63 match self {
64 MetadataValue::Float(f) => Some(*f),
65 MetadataValue::Integer(i) => Some(*i as f64),
66 _ => None,
67 }
68 }
69
70 pub fn as_timestamp(&self) -> Option<u64> {
72 match self {
73 MetadataValue::Timestamp(t) => Some(*t),
74 MetadataValue::Integer(i) if *i >= 0 => Some(*i as u64),
75 _ => None,
76 }
77 }
78
79 pub fn as_boolean(&self) -> Option<bool> {
81 match self {
82 MetadataValue::Boolean(b) => Some(*b),
83 _ => None,
84 }
85 }
86}
87
88#[derive(Debug, Clone, Default, Serialize, Deserialize)]
90pub struct Metadata {
91 pub fields: HashMap<String, MetadataValue>,
93 pub created_at: u64,
95 pub updated_at: u64,
97}
98
99impl Metadata {
100 pub fn new() -> Self {
102 let now = SystemTime::now()
103 .duration_since(UNIX_EPOCH)
104 .unwrap_or_default()
105 .as_secs();
106
107 Self {
108 fields: HashMap::new(),
109 created_at: now,
110 updated_at: now,
111 }
112 }
113
114 pub fn set(&mut self, key: impl Into<String>, value: MetadataValue) -> &mut Self {
116 self.fields.insert(key.into(), value);
117 self.updated_at = SystemTime::now()
118 .duration_since(UNIX_EPOCH)
119 .unwrap_or_default()
120 .as_secs();
121 self
122 }
123
124 pub fn get(&self, key: &str) -> Option<&MetadataValue> {
126 self.fields.get(key)
127 }
128
129 pub fn has(&self, key: &str) -> bool {
131 self.fields.contains_key(key)
132 }
133
134 pub fn remove(&mut self, key: &str) -> Option<MetadataValue> {
136 self.fields.remove(key)
137 }
138
139 pub fn with_string(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
141 self.set(key, MetadataValue::String(value.into()));
142 self
143 }
144
145 pub fn with_integer(mut self, key: impl Into<String>, value: i64) -> Self {
147 self.set(key, MetadataValue::Integer(value));
148 self
149 }
150
151 pub fn with_timestamp(mut self, key: impl Into<String>, value: u64) -> Self {
153 self.set(key, MetadataValue::Timestamp(value));
154 self
155 }
156
157 pub fn with_boolean(mut self, key: impl Into<String>, value: bool) -> Self {
159 self.set(key, MetadataValue::Boolean(value));
160 self
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub enum MetadataFilter {
167 Equals(String, MetadataValue),
169 NotEquals(String, MetadataValue),
171 GreaterThan(String, MetadataValue),
173 GreaterThanOrEqual(String, MetadataValue),
175 LessThan(String, MetadataValue),
177 LessThanOrEqual(String, MetadataValue),
179 Contains(String, String),
181 StartsWith(String, String),
183 EndsWith(String, String),
185 In(String, Vec<MetadataValue>),
187 NotIn(String, Vec<MetadataValue>),
189 Exists(String),
191 NotExists(String),
193 TimeRange {
195 field: String,
196 start: Option<u64>,
197 end: Option<u64>,
198 },
199 And(Vec<MetadataFilter>),
201 Or(Vec<MetadataFilter>),
203 Not(Box<MetadataFilter>),
205}
206
207impl MetadataFilter {
208 pub fn eq(field: impl Into<String>, value: MetadataValue) -> Self {
210 MetadataFilter::Equals(field.into(), value)
211 }
212
213 pub fn ne(field: impl Into<String>, value: MetadataValue) -> Self {
215 MetadataFilter::NotEquals(field.into(), value)
216 }
217
218 pub fn gt(field: impl Into<String>, value: MetadataValue) -> Self {
220 MetadataFilter::GreaterThan(field.into(), value)
221 }
222
223 pub fn gte(field: impl Into<String>, value: MetadataValue) -> Self {
225 MetadataFilter::GreaterThanOrEqual(field.into(), value)
226 }
227
228 pub fn lt(field: impl Into<String>, value: MetadataValue) -> Self {
230 MetadataFilter::LessThan(field.into(), value)
231 }
232
233 pub fn lte(field: impl Into<String>, value: MetadataValue) -> Self {
235 MetadataFilter::LessThanOrEqual(field.into(), value)
236 }
237
238 pub fn time_range(field: impl Into<String>, start: Option<u64>, end: Option<u64>) -> Self {
240 MetadataFilter::TimeRange {
241 field: field.into(),
242 start,
243 end,
244 }
245 }
246
247 pub fn and(filters: Vec<MetadataFilter>) -> Self {
249 MetadataFilter::And(filters)
250 }
251
252 pub fn or(filters: Vec<MetadataFilter>) -> Self {
254 MetadataFilter::Or(filters)
255 }
256
257 pub fn negate(filter: MetadataFilter) -> Self {
259 MetadataFilter::Not(Box::new(filter))
260 }
261
262 pub fn matches(&self, metadata: &Metadata) -> bool {
264 match self {
265 MetadataFilter::Equals(field, value) => metadata.get(field) == Some(value),
266 MetadataFilter::NotEquals(field, value) => metadata.get(field) != Some(value),
267 MetadataFilter::GreaterThan(field, value) => {
268 Self::compare_gt(metadata.get(field), value)
269 }
270 MetadataFilter::GreaterThanOrEqual(field, value) => {
271 Self::compare_gte(metadata.get(field), value)
272 }
273 MetadataFilter::LessThan(field, value) => Self::compare_lt(metadata.get(field), value),
274 MetadataFilter::LessThanOrEqual(field, value) => {
275 Self::compare_lte(metadata.get(field), value)
276 }
277 MetadataFilter::Contains(field, substring) => metadata
278 .get(field)
279 .and_then(|v| v.as_string())
280 .is_some_and(|s| s.contains(substring)),
281 MetadataFilter::StartsWith(field, prefix) => metadata
282 .get(field)
283 .and_then(|v| v.as_string())
284 .is_some_and(|s| s.starts_with(prefix)),
285 MetadataFilter::EndsWith(field, suffix) => metadata
286 .get(field)
287 .and_then(|v| v.as_string())
288 .is_some_and(|s| s.ends_with(suffix)),
289 MetadataFilter::In(field, values) => {
290 metadata.get(field).is_some_and(|v| values.contains(v))
291 }
292 MetadataFilter::NotIn(field, values) => {
293 metadata.get(field).is_none_or(|v| !values.contains(v))
294 }
295 MetadataFilter::Exists(field) => metadata.has(field),
296 MetadataFilter::NotExists(field) => !metadata.has(field),
297 MetadataFilter::TimeRange { field, start, end } => {
298 let timestamp = if field == "created_at" {
299 Some(metadata.created_at)
300 } else if field == "updated_at" {
301 Some(metadata.updated_at)
302 } else {
303 metadata.get(field).and_then(|v| v.as_timestamp())
304 };
305
306 timestamp.is_some_and(|t| {
307 let after_start = start.is_none_or(|s| t >= s);
308 let before_end = end.is_none_or(|e| t <= e);
309 after_start && before_end
310 })
311 }
312 MetadataFilter::And(filters) => filters.iter().all(|f| f.matches(metadata)),
313 MetadataFilter::Or(filters) => filters.iter().any(|f| f.matches(metadata)),
314 MetadataFilter::Not(filter) => !filter.matches(metadata),
315 }
316 }
317
318 fn compare_gt(field_value: Option<&MetadataValue>, compare_value: &MetadataValue) -> bool {
319 match (field_value, compare_value) {
320 (Some(MetadataValue::Integer(a)), MetadataValue::Integer(b)) => a > b,
321 (Some(MetadataValue::Float(a)), MetadataValue::Float(b)) => a > b,
322 (Some(MetadataValue::Integer(a)), MetadataValue::Float(b)) => (*a as f64) > *b,
323 (Some(MetadataValue::Float(a)), MetadataValue::Integer(b)) => *a > (*b as f64),
324 (Some(MetadataValue::Timestamp(a)), MetadataValue::Timestamp(b)) => a > b,
325 (Some(MetadataValue::String(a)), MetadataValue::String(b)) => a > b,
326 _ => false,
327 }
328 }
329
330 fn compare_gte(field_value: Option<&MetadataValue>, compare_value: &MetadataValue) -> bool {
331 match (field_value, compare_value) {
332 (Some(MetadataValue::Integer(a)), MetadataValue::Integer(b)) => a >= b,
333 (Some(MetadataValue::Float(a)), MetadataValue::Float(b)) => a >= b,
334 (Some(MetadataValue::Integer(a)), MetadataValue::Float(b)) => (*a as f64) >= *b,
335 (Some(MetadataValue::Float(a)), MetadataValue::Integer(b)) => *a >= (*b as f64),
336 (Some(MetadataValue::Timestamp(a)), MetadataValue::Timestamp(b)) => a >= b,
337 (Some(MetadataValue::String(a)), MetadataValue::String(b)) => a >= b,
338 _ => false,
339 }
340 }
341
342 fn compare_lt(field_value: Option<&MetadataValue>, compare_value: &MetadataValue) -> bool {
343 match (field_value, compare_value) {
344 (Some(MetadataValue::Integer(a)), MetadataValue::Integer(b)) => a < b,
345 (Some(MetadataValue::Float(a)), MetadataValue::Float(b)) => a < b,
346 (Some(MetadataValue::Integer(a)), MetadataValue::Float(b)) => (*a as f64) < *b,
347 (Some(MetadataValue::Float(a)), MetadataValue::Integer(b)) => *a < (*b as f64),
348 (Some(MetadataValue::Timestamp(a)), MetadataValue::Timestamp(b)) => a < b,
349 (Some(MetadataValue::String(a)), MetadataValue::String(b)) => a < b,
350 _ => false,
351 }
352 }
353
354 fn compare_lte(field_value: Option<&MetadataValue>, compare_value: &MetadataValue) -> bool {
355 match (field_value, compare_value) {
356 (Some(MetadataValue::Integer(a)), MetadataValue::Integer(b)) => a <= b,
357 (Some(MetadataValue::Float(a)), MetadataValue::Float(b)) => a <= b,
358 (Some(MetadataValue::Integer(a)), MetadataValue::Float(b)) => (*a as f64) <= *b,
359 (Some(MetadataValue::Float(a)), MetadataValue::Integer(b)) => *a <= (*b as f64),
360 (Some(MetadataValue::Timestamp(a)), MetadataValue::Timestamp(b)) => a <= b,
361 (Some(MetadataValue::String(a)), MetadataValue::String(b)) => a <= b,
362 _ => false,
363 }
364 }
365}
366
367pub struct MetadataStore {
369 data: Arc<RwLock<HashMap<Cid, Metadata>>>,
371 string_index: Arc<RwLock<StringIndexMap>>,
373 numeric_index: Arc<RwLock<NumericIndexMap>>,
375 timestamp_index: Arc<RwLock<BTreeMap<u64, HashSet<Cid>>>>,
377}
378
379impl Default for MetadataStore {
380 fn default() -> Self {
381 Self::new()
382 }
383}
384
385impl MetadataStore {
386 pub fn new() -> Self {
388 Self {
389 data: Arc::new(RwLock::new(HashMap::new())),
390 string_index: Arc::new(RwLock::new(HashMap::new())),
391 numeric_index: Arc::new(RwLock::new(HashMap::new())),
392 timestamp_index: Arc::new(RwLock::new(BTreeMap::new())),
393 }
394 }
395
396 pub fn insert(&self, cid: Cid, metadata: Metadata) -> Result<()> {
398 if self
400 .data
401 .read()
402 .unwrap_or_else(|e| e.into_inner())
403 .contains_key(&cid)
404 {
405 self.remove_from_indexes(&cid)?;
406 }
407
408 self.add_to_indexes(&cid, &metadata)?;
410
411 self.data
413 .write()
414 .unwrap_or_else(|e| e.into_inner())
415 .insert(cid, metadata);
416
417 Ok(())
418 }
419
420 pub fn get(&self, cid: &Cid) -> Option<Metadata> {
422 self.data
423 .read()
424 .unwrap_or_else(|e| e.into_inner())
425 .get(cid)
426 .cloned()
427 }
428
429 pub fn remove(&self, cid: &Cid) -> Result<Option<Metadata>> {
431 self.remove_from_indexes(cid)?;
432 Ok(self
433 .data
434 .write()
435 .unwrap_or_else(|e| e.into_inner())
436 .remove(cid))
437 }
438
439 pub fn contains(&self, cid: &Cid) -> bool {
441 self.data
442 .read()
443 .unwrap_or_else(|e| e.into_inner())
444 .contains_key(cid)
445 }
446
447 pub fn cids(&self) -> Vec<Cid> {
449 self.data
450 .read()
451 .unwrap_or_else(|e| e.into_inner())
452 .keys()
453 .copied()
454 .collect()
455 }
456
457 pub fn len(&self) -> usize {
459 self.data.read().unwrap_or_else(|e| e.into_inner()).len()
460 }
461
462 pub fn is_empty(&self) -> bool {
464 self.data
465 .read()
466 .unwrap_or_else(|e| e.into_inner())
467 .is_empty()
468 }
469
470 pub fn filter(&self, filter: &MetadataFilter) -> Vec<Cid> {
472 if let Some(cids) = self.filter_with_index(filter) {
474 return cids;
475 }
476
477 self.data
479 .read()
480 .unwrap_or_else(|e| e.into_inner())
481 .iter()
482 .filter(|(_, m)| filter.matches(m))
483 .map(|(cid, _)| *cid)
484 .collect()
485 }
486
487 fn filter_with_index(&self, filter: &MetadataFilter) -> Option<Vec<Cid>> {
489 match filter {
490 MetadataFilter::Equals(field, MetadataValue::String(value)) => {
491 let index = self.string_index.read().unwrap_or_else(|e| e.into_inner());
492 index
493 .get(field)
494 .and_then(|field_index| field_index.get(value))
495 .map(|cids| cids.iter().copied().collect())
496 }
497 MetadataFilter::TimeRange { field, start, end } if field == "created_at" => {
498 let index = self
499 .timestamp_index
500 .read()
501 .unwrap_or_else(|e| e.into_inner());
502 let range_start = start.unwrap_or(0);
503 let range_end = end.unwrap_or(u64::MAX);
504
505 let cids: HashSet<Cid> = index
506 .range(range_start..=range_end)
507 .flat_map(|(_, cids)| cids.iter().copied())
508 .collect();
509
510 Some(cids.into_iter().collect())
511 }
512 MetadataFilter::And(filters) => {
513 let mut result: Option<HashSet<Cid>> = None;
515
516 for f in filters {
517 if let Some(cids) = self.filter_with_index(f) {
518 let cid_set: HashSet<Cid> = cids.into_iter().collect();
519 result = Some(match result {
520 Some(existing) => existing.intersection(&cid_set).copied().collect(),
521 None => cid_set,
522 });
523 }
524 }
525
526 result.map(|s| s.into_iter().collect())
527 }
528 _ => None,
529 }
530 }
531
532 fn add_to_indexes(&self, cid: &Cid, metadata: &Metadata) -> Result<()> {
534 for (key, value) in &metadata.fields {
536 if let MetadataValue::String(s) = value {
537 self.string_index
538 .write()
539 .unwrap_or_else(|e| e.into_inner())
540 .entry(key.clone())
541 .or_default()
542 .entry(s.clone())
543 .or_default()
544 .insert(*cid);
545 }
546
547 if let Some(i) = value.as_integer() {
548 self.numeric_index
549 .write()
550 .unwrap_or_else(|e| e.into_inner())
551 .entry(key.clone())
552 .or_default()
553 .entry(i)
554 .or_default()
555 .insert(*cid);
556 }
557 }
558
559 self.timestamp_index
561 .write()
562 .unwrap_or_else(|e| e.into_inner())
563 .entry(metadata.created_at)
564 .or_default()
565 .insert(*cid);
566
567 Ok(())
568 }
569
570 fn remove_from_indexes(&self, cid: &Cid) -> Result<()> {
572 let data = self.data.read().unwrap_or_else(|e| e.into_inner());
573 if let Some(metadata) = data.get(cid) {
574 for (key, value) in &metadata.fields {
576 if let MetadataValue::String(s) = value {
577 if let Some(field_index) = self
578 .string_index
579 .write()
580 .unwrap_or_else(|e| e.into_inner())
581 .get_mut(key)
582 {
583 if let Some(cids) = field_index.get_mut(s) {
584 cids.remove(cid);
585 }
586 }
587 }
588
589 if let Some(i) = value.as_integer() {
590 if let Some(field_index) = self
591 .numeric_index
592 .write()
593 .unwrap_or_else(|e| e.into_inner())
594 .get_mut(key)
595 {
596 if let Some(cids) = field_index.get_mut(&i) {
597 cids.remove(cid);
598 }
599 }
600 }
601 }
602
603 if let Some(cids) = self
605 .timestamp_index
606 .write()
607 .unwrap_or_else(|e| e.into_inner())
608 .get_mut(&metadata.created_at)
609 {
610 cids.remove(cid);
611 }
612 }
613
614 Ok(())
615 }
616
617 pub fn get_by_time_range(&self, start: Option<u64>, end: Option<u64>) -> Vec<Cid> {
619 let index = self
620 .timestamp_index
621 .read()
622 .unwrap_or_else(|e| e.into_inner());
623 let range_start = start.unwrap_or(0);
624 let range_end = end.unwrap_or(u64::MAX);
625
626 index
627 .range(range_start..=range_end)
628 .flat_map(|(_, cids)| cids.iter().copied())
629 .collect()
630 }
631
632 pub fn get_field_values(&self, field: &str) -> Vec<MetadataValue> {
634 self.data
635 .read()
636 .unwrap_or_else(|e| e.into_inner())
637 .values()
638 .filter_map(|m| m.get(field).cloned())
639 .collect::<HashSet<_>>()
640 .into_iter()
641 .collect()
642 }
643
644 pub fn get_facet_counts(&self, field: &str) -> HashMap<String, usize> {
646 let index = self.string_index.read().unwrap_or_else(|e| e.into_inner());
647 index
648 .get(field)
649 .map(|field_index| {
650 field_index
651 .iter()
652 .map(|(value, cids)| (value.clone(), cids.len()))
653 .collect()
654 })
655 .unwrap_or_default()
656 }
657
658 pub fn clear(&self) {
660 self.data.write().unwrap_or_else(|e| e.into_inner()).clear();
661 self.string_index
662 .write()
663 .unwrap_or_else(|e| e.into_inner())
664 .clear();
665 self.numeric_index
666 .write()
667 .unwrap_or_else(|e| e.into_inner())
668 .clear();
669 self.timestamp_index
670 .write()
671 .unwrap_or_else(|e| e.into_inner())
672 .clear();
673 }
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize)]
678pub struct TemporalOptions {
679 pub start: Option<u64>,
681 pub end: Option<u64>,
683 pub recency_boost: bool,
685 pub decay_factor: f32,
687 pub reference_time: Option<u64>,
689}
690
691impl Default for TemporalOptions {
692 fn default() -> Self {
693 Self {
694 start: None,
695 end: None,
696 recency_boost: false,
697 decay_factor: 1.0,
698 reference_time: None,
699 }
700 }
701}
702
703impl TemporalOptions {
704 pub fn range(start: u64, end: u64) -> Self {
706 Self {
707 start: Some(start),
708 end: Some(end),
709 ..Default::default()
710 }
711 }
712
713 pub fn with_recency(decay_factor: f32) -> Self {
715 Self {
716 recency_boost: true,
717 decay_factor,
718 ..Default::default()
719 }
720 }
721
722 pub fn recency_multiplier(&self, timestamp: u64) -> f32 {
724 if !self.recency_boost {
725 return 1.0;
726 }
727
728 let reference = self.reference_time.unwrap_or_else(|| {
729 SystemTime::now()
730 .duration_since(UNIX_EPOCH)
731 .unwrap_or_default()
732 .as_secs()
733 });
734
735 if timestamp >= reference {
736 return 1.0;
737 }
738
739 let age_seconds = reference - timestamp;
740 let age_days = age_seconds as f32 / 86400.0;
741
742 (-self.decay_factor * age_days / 30.0).exp()
744 }
745}
746
747impl std::hash::Hash for MetadataValue {
748 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
749 match self {
750 MetadataValue::String(s) => {
751 0u8.hash(state);
752 s.hash(state);
753 }
754 MetadataValue::Integer(i) => {
755 1u8.hash(state);
756 i.hash(state);
757 }
758 MetadataValue::Float(f) => {
759 2u8.hash(state);
760 f.to_bits().hash(state);
761 }
762 MetadataValue::Boolean(b) => {
763 3u8.hash(state);
764 b.hash(state);
765 }
766 MetadataValue::Timestamp(t) => {
767 4u8.hash(state);
768 t.hash(state);
769 }
770 MetadataValue::StringArray(arr) => {
771 5u8.hash(state);
772 arr.hash(state);
773 }
774 MetadataValue::Null => {
775 6u8.hash(state);
776 }
777 }
778 }
779}
780
781impl Eq for MetadataValue {}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786
787 fn test_cid() -> Cid {
788 "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
789 .parse()
790 .expect("test: known-valid CID string should parse")
791 }
792
793 fn test_cid2() -> Cid {
794 "bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354"
795 .parse()
796 .expect("test: known-valid CID string should parse")
797 }
798
799 #[test]
800 fn test_metadata_creation() {
801 let metadata = Metadata::new()
802 .with_string("type", "document")
803 .with_integer("size", 1024)
804 .with_boolean("indexed", true);
805
806 assert_eq!(
807 metadata.get("type"),
808 Some(&MetadataValue::String("document".to_string()))
809 );
810 assert_eq!(metadata.get("size"), Some(&MetadataValue::Integer(1024)));
811 assert_eq!(metadata.get("indexed"), Some(&MetadataValue::Boolean(true)));
812 }
813
814 #[test]
815 fn test_metadata_filter() {
816 let metadata = Metadata::new()
817 .with_string("category", "tech")
818 .with_integer("views", 100)
819 .with_timestamp("published", 1700000000);
820
821 assert!(
823 MetadataFilter::eq("category", MetadataValue::String("tech".to_string()))
824 .matches(&metadata)
825 );
826
827 assert!(MetadataFilter::gt("views", MetadataValue::Integer(50)).matches(&metadata));
829 assert!(!MetadataFilter::gt("views", MetadataValue::Integer(200)).matches(&metadata));
830
831 assert!(
833 MetadataFilter::time_range("published", Some(1699999999), Some(1700000001))
834 .matches(&metadata)
835 );
836 }
837
838 #[test]
839 fn test_metadata_store() {
840 let store = MetadataStore::new();
841
842 let cid1 = test_cid();
843 let cid2 = test_cid2();
844
845 let meta1 = Metadata::new()
846 .with_string("type", "image")
847 .with_integer("size", 1024);
848
849 let meta2 = Metadata::new()
850 .with_string("type", "document")
851 .with_integer("size", 2048);
852
853 store
854 .insert(cid1, meta1)
855 .expect("test: insert cid1 into store should succeed");
856 store
857 .insert(cid2, meta2)
858 .expect("test: insert cid2 into store should succeed");
859
860 assert_eq!(store.len(), 2);
861
862 let filter = MetadataFilter::eq("type", MetadataValue::String("image".to_string()));
864 let results = store.filter(&filter);
865 assert_eq!(results.len(), 1);
866 assert_eq!(results[0], cid1);
867 }
868
869 #[test]
870 fn test_compound_filters() {
871 let metadata = Metadata::new()
872 .with_string("category", "tech")
873 .with_integer("views", 100)
874 .with_boolean("published", true);
875
876 let and_filter = MetadataFilter::and(vec![
878 MetadataFilter::eq("category", MetadataValue::String("tech".to_string())),
879 MetadataFilter::gt("views", MetadataValue::Integer(50)),
880 ]);
881 assert!(and_filter.matches(&metadata));
882
883 let or_filter = MetadataFilter::or(vec![
885 MetadataFilter::eq("category", MetadataValue::String("science".to_string())),
886 MetadataFilter::gt("views", MetadataValue::Integer(50)),
887 ]);
888 assert!(or_filter.matches(&metadata));
889
890 let not_filter = MetadataFilter::negate(MetadataFilter::eq(
892 "published",
893 MetadataValue::Boolean(false),
894 ));
895 assert!(not_filter.matches(&metadata));
896 }
897
898 #[test]
899 fn test_temporal_options() {
900 let now = SystemTime::now()
901 .duration_since(UNIX_EPOCH)
902 .expect("test: system time should be after UNIX_EPOCH")
903 .as_secs();
904
905 let options = TemporalOptions {
906 recency_boost: true,
907 decay_factor: 1.0,
908 reference_time: Some(now),
909 ..Default::default()
910 };
911
912 let recent_mult = options.recency_multiplier(now - 86400); assert!(recent_mult > 0.9);
915
916 let old_mult = options.recency_multiplier(now - 86400 * 90); assert!(old_mult < 0.5);
919 }
920
921 #[test]
922 fn test_facet_counts() {
923 let store = MetadataStore::new();
924
925 let valid_cids = [
927 "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
928 "bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354",
929 "bafybeibvfkifsqbapirjrj7zbfwddz5qz5awvbftjgktpcqcxjkzstszlm",
930 ];
931
932 for (i, cid_str) in valid_cids.iter().enumerate() {
933 let cid: Cid = cid_str
934 .parse()
935 .expect("test: known-valid CID string should parse");
936 let meta = Metadata::new().with_string("type", if i < 2 { "image" } else { "doc" });
937 store
938 .insert(cid, meta)
939 .expect("test: insert CID metadata should succeed");
940 }
941
942 let counts = store.get_facet_counts("type");
943 assert_eq!(counts.get("image").copied().unwrap_or(0), 2);
944 assert_eq!(counts.get("doc").copied().unwrap_or(0), 1);
945 }
946}