Skip to main content

ipfrs_semantic/
metadata.rs

1//! Metadata storage and filtering for hybrid search
2//!
3//! This module provides metadata management for vectors, enabling
4//! hybrid search that combines vector similarity with attribute filtering.
5
6use 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
12// Type aliases for complex index structures
13type StringIndexMap = HashMap<String, HashMap<String, HashSet<Cid>>>;
14type NumericIndexMap = HashMap<String, BTreeMap<i64, HashSet<Cid>>>;
15
16/// Metadata value types
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub enum MetadataValue {
19    /// String value
20    String(String),
21    /// Integer value
22    Integer(i64),
23    /// Float value
24    Float(f64),
25    /// Boolean value
26    Boolean(bool),
27    /// Timestamp (Unix epoch seconds)
28    Timestamp(u64),
29    /// Array of strings
30    StringArray(Vec<String>),
31    /// Null value
32    Null,
33}
34
35impl MetadataValue {
36    /// Create a timestamp for now
37    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    /// Get as string if possible
46    pub fn as_string(&self) -> Option<&str> {
47        match self {
48            MetadataValue::String(s) => Some(s),
49            _ => None,
50        }
51    }
52
53    /// Get as integer if possible
54    pub fn as_integer(&self) -> Option<i64> {
55        match self {
56            MetadataValue::Integer(i) => Some(*i),
57            _ => None,
58        }
59    }
60
61    /// Get as float if possible
62    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    /// Get as timestamp if possible
71    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    /// Get as boolean if possible
80    pub fn as_boolean(&self) -> Option<bool> {
81        match self {
82            MetadataValue::Boolean(b) => Some(*b),
83            _ => None,
84        }
85    }
86}
87
88/// Metadata record for a vector
89#[derive(Debug, Clone, Default, Serialize, Deserialize)]
90pub struct Metadata {
91    /// Key-value pairs
92    pub fields: HashMap<String, MetadataValue>,
93    /// Creation timestamp
94    pub created_at: u64,
95    /// Last updated timestamp
96    pub updated_at: u64,
97}
98
99impl Metadata {
100    /// Create new metadata with current timestamp
101    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    /// Set a field value
115    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    /// Get a field value
125    pub fn get(&self, key: &str) -> Option<&MetadataValue> {
126        self.fields.get(key)
127    }
128
129    /// Check if a field exists
130    pub fn has(&self, key: &str) -> bool {
131        self.fields.contains_key(key)
132    }
133
134    /// Remove a field
135    pub fn remove(&mut self, key: &str) -> Option<MetadataValue> {
136        self.fields.remove(key)
137    }
138
139    /// Builder method for string field
140    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    /// Builder method for integer field
146    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    /// Builder method for timestamp field
152    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    /// Builder method for boolean field
158    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/// Metadata filter expression
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub enum MetadataFilter {
167    /// Equality check: field == value
168    Equals(String, MetadataValue),
169    /// Not equal: field != value
170    NotEquals(String, MetadataValue),
171    /// Greater than: field > value
172    GreaterThan(String, MetadataValue),
173    /// Greater than or equal: field >= value
174    GreaterThanOrEqual(String, MetadataValue),
175    /// Less than: field < value
176    LessThan(String, MetadataValue),
177    /// Less than or equal: field <= value
178    LessThanOrEqual(String, MetadataValue),
179    /// String contains: field contains substring
180    Contains(String, String),
181    /// String starts with: field starts with prefix
182    StartsWith(String, String),
183    /// String ends with: field ends with suffix
184    EndsWith(String, String),
185    /// Value in set: field in \[values\]
186    In(String, Vec<MetadataValue>),
187    /// Value not in set: field not in \[values\]
188    NotIn(String, Vec<MetadataValue>),
189    /// Field exists
190    Exists(String),
191    /// Field does not exist
192    NotExists(String),
193    /// Timestamp range: created_at or updated_at within range
194    TimeRange {
195        field: String,
196        start: Option<u64>,
197        end: Option<u64>,
198    },
199    /// Logical AND of multiple filters
200    And(Vec<MetadataFilter>),
201    /// Logical OR of multiple filters
202    Or(Vec<MetadataFilter>),
203    /// Logical NOT
204    Not(Box<MetadataFilter>),
205}
206
207impl MetadataFilter {
208    /// Create an equals filter
209    pub fn eq(field: impl Into<String>, value: MetadataValue) -> Self {
210        MetadataFilter::Equals(field.into(), value)
211    }
212
213    /// Create a not equals filter
214    pub fn ne(field: impl Into<String>, value: MetadataValue) -> Self {
215        MetadataFilter::NotEquals(field.into(), value)
216    }
217
218    /// Create a greater than filter
219    pub fn gt(field: impl Into<String>, value: MetadataValue) -> Self {
220        MetadataFilter::GreaterThan(field.into(), value)
221    }
222
223    /// Create a greater than or equal filter
224    pub fn gte(field: impl Into<String>, value: MetadataValue) -> Self {
225        MetadataFilter::GreaterThanOrEqual(field.into(), value)
226    }
227
228    /// Create a less than filter
229    pub fn lt(field: impl Into<String>, value: MetadataValue) -> Self {
230        MetadataFilter::LessThan(field.into(), value)
231    }
232
233    /// Create a less than or equal filter
234    pub fn lte(field: impl Into<String>, value: MetadataValue) -> Self {
235        MetadataFilter::LessThanOrEqual(field.into(), value)
236    }
237
238    /// Create a time range filter
239    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    /// Create an AND filter
248    pub fn and(filters: Vec<MetadataFilter>) -> Self {
249        MetadataFilter::And(filters)
250    }
251
252    /// Create an OR filter
253    pub fn or(filters: Vec<MetadataFilter>) -> Self {
254        MetadataFilter::Or(filters)
255    }
256
257    /// Create a NOT filter
258    pub fn negate(filter: MetadataFilter) -> Self {
259        MetadataFilter::Not(Box::new(filter))
260    }
261
262    /// Evaluate the filter against metadata
263    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
367/// Metadata store for CID-indexed metadata
368pub struct MetadataStore {
369    /// CID to metadata mapping
370    data: Arc<RwLock<HashMap<Cid, Metadata>>>,
371    /// Inverted index for string fields (field -> value -> CIDs)
372    string_index: Arc<RwLock<StringIndexMap>>,
373    /// Sorted index for numeric fields (field -> sorted (value, CID) pairs)
374    numeric_index: Arc<RwLock<NumericIndexMap>>,
375    /// Timestamp index for temporal queries
376    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    /// Create a new metadata store
387    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    /// Insert or update metadata for a CID
397    pub fn insert(&self, cid: Cid, metadata: Metadata) -> Result<()> {
398        // Remove old indexes if updating
399        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        // Update indexes
409        self.add_to_indexes(&cid, &metadata)?;
410
411        // Store metadata
412        self.data
413            .write()
414            .unwrap_or_else(|e| e.into_inner())
415            .insert(cid, metadata);
416
417        Ok(())
418    }
419
420    /// Get metadata for a CID
421    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    /// Remove metadata for a CID
430    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    /// Check if metadata exists for a CID
440    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    /// Get all CIDs with metadata
448    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    /// Get number of stored metadata records
458    pub fn len(&self) -> usize {
459        self.data.read().unwrap_or_else(|e| e.into_inner()).len()
460    }
461
462    /// Check if store is empty
463    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    /// Filter CIDs by metadata filter
471    pub fn filter(&self, filter: &MetadataFilter) -> Vec<Cid> {
472        // Try to use indexes for efficient filtering
473        if let Some(cids) = self.filter_with_index(filter) {
474            return cids;
475        }
476
477        // Fall back to linear scan
478        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    /// Filter using indexes if possible
488    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                // Intersect results from indexed filters
514                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    /// Add metadata to indexes
533    fn add_to_indexes(&self, cid: &Cid, metadata: &Metadata) -> Result<()> {
534        // Index string fields
535        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        // Index creation timestamp
560        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    /// Remove metadata from indexes
571    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            // Remove from string index
575            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            // Remove from timestamp index
604            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    /// Get CIDs created within a time range
618    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    /// Get unique values for a field
633    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    /// Get facet counts for a string field
645    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    /// Clear all metadata
659    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/// Temporal query options
677#[derive(Debug, Clone, Serialize, Deserialize)]
678pub struct TemporalOptions {
679    /// Time range start (Unix timestamp)
680    pub start: Option<u64>,
681    /// Time range end (Unix timestamp)
682    pub end: Option<u64>,
683    /// Apply recency boost to scores
684    pub recency_boost: bool,
685    /// Recency decay factor (higher = faster decay)
686    pub decay_factor: f32,
687    /// Reference time for recency calculation (default: now)
688    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    /// Create options for a specific time range
705    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    /// Create options with recency boosting
714    pub fn with_recency(decay_factor: f32) -> Self {
715        Self {
716            recency_boost: true,
717            decay_factor,
718            ..Default::default()
719        }
720    }
721
722    /// Calculate recency boost multiplier for a timestamp
723    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        // Exponential decay: e^(-decay_factor * age_days)
743        (-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        // Equals filter
822        assert!(
823            MetadataFilter::eq("category", MetadataValue::String("tech".to_string()))
824                .matches(&metadata)
825        );
826
827        // Greater than filter
828        assert!(MetadataFilter::gt("views", MetadataValue::Integer(50)).matches(&metadata));
829        assert!(!MetadataFilter::gt("views", MetadataValue::Integer(200)).matches(&metadata));
830
831        // Time range filter
832        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        // Filter by type
863        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        // AND filter
877        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        // OR filter
884        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        // NOT filter
891        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        // Recent timestamp should have high multiplier
913        let recent_mult = options.recency_multiplier(now - 86400); // 1 day ago
914        assert!(recent_mult > 0.9);
915
916        // Old timestamp should have low multiplier
917        let old_mult = options.recency_multiplier(now - 86400 * 90); // 90 days ago
918        assert!(old_mult < 0.5);
919    }
920
921    #[test]
922    fn test_facet_counts() {
923        let store = MetadataStore::new();
924
925        // Use known valid CIDs
926        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}