Skip to main content

pulsedb/search/
mod.rs

1//! Search operations for PulseDB.
2//!
3//! This module provides search filtering and query building for experience
4//! retrieval operations (recent, similarity, context candidates).
5
6mod context;
7mod filter;
8pub(crate) mod rerank;
9
10pub use context::{ContextCandidates, ContextRequest};
11pub use filter::SearchFilter;
12
13use crate::config::RecallWeights;
14use crate::experience::Experience;
15
16/// Options for recall search.
17///
18/// `SearchOptions` is the call-time configuration for [`PulseDB::search()`].
19/// Recall-weight precedence is: explicit `weights` here > the collective's
20/// configured `DecayConfig.default_recall_weights` (per-collective stored config,
21/// else the global `Config.decay`) > legacy pure-similarity ranking. So
22/// `weights: None` preserves legacy ranking only when no default is configured.
23///
24/// [`PulseDB::search()`]: crate::PulseDB::search
25#[derive(Clone, Debug)]
26pub struct SearchOptions {
27    /// Maximum number of results to return.
28    pub k: usize,
29
30    /// Filter criteria applied after vector retrieval.
31    pub filter: SearchFilter,
32
33    /// Optional recall weights for similarity and temporal energy. `None` falls
34    /// back to the configured `default_recall_weights`, then to legacy ranking.
35    pub weights: Option<RecallWeights>,
36}
37
38impl Default for SearchOptions {
39    fn default() -> Self {
40        Self {
41            k: 10,
42            filter: SearchFilter::default(),
43            weights: None,
44        }
45    }
46}
47
48/// A search result pairing an experience with its similarity score.
49///
50/// Returned by [`PulseDB::search_similar()`](crate::PulseDB::search_similar) and
51/// [`PulseDB::search_similar_filtered()`](crate::PulseDB::search_similar_filtered).
52/// Results are sorted by `similarity` descending (most similar first).
53///
54/// # Similarity Score
55///
56/// The `similarity` field is computed as `1.0 - cosine_distance`, where
57/// cosine distance ranges from 0.0 (identical) to 2.0 (opposite). This
58/// gives a similarity range of [-1.0, 1.0], where:
59/// - `1.0` = identical vectors
60/// - `0.0` = orthogonal vectors
61/// - `-1.0` = opposite vectors
62///
63/// In practice, experience embeddings from transformer models (e.g.,
64/// all-MiniLM-L6-v2) produce non-negative values, so similarity is
65/// typically in [0.0, 1.0].
66///
67/// # Example
68///
69/// ```rust
70/// # fn main() -> pulsedb::Result<()> {
71/// # let dir = tempfile::tempdir().unwrap();
72/// # let db = pulsedb::PulseDB::open(dir.path().join("test.db"), pulsedb::Config::default())?;
73/// # let collective_id = db.create_collective("example")?;
74/// # let query_embedding = vec![0.1f32; 384];
75/// let results = db.search_similar(collective_id, &query_embedding, 10)?;
76/// for result in &results {
77///     println!(
78///         "similarity={:.3}: {}",
79///         result.similarity, result.experience.content
80///     );
81/// }
82/// # Ok(())
83/// # }
84/// ```
85#[derive(Clone, Debug)]
86pub struct SearchResult {
87    /// The full experience record.
88    pub experience: Experience,
89
90    /// Similarity score (1.0 - cosine_distance).
91    ///
92    /// Higher is more similar. Typically in [0.0, 1.0] for transformer
93    /// embeddings. Theoretical range is [-1.0, 1.0].
94    pub similarity: f32,
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::experience::ExperienceType;
101    use crate::types::{AgentId, CollectiveId, ExperienceId, Timestamp};
102
103    /// Helper to create a SearchResult with a given similarity.
104    fn make_result(similarity: f32) -> SearchResult {
105        let timestamp = Timestamp::now();
106        SearchResult {
107            experience: Experience {
108                id: ExperienceId::new(),
109                collective_id: CollectiveId::new(),
110                content: format!("test sim={}", similarity),
111                embedding: vec![0.1; 384],
112                experience_type: ExperienceType::default(),
113                importance: 0.5,
114                confidence: 0.8,
115                applications: std::collections::BTreeMap::new(),
116                domain: vec!["test".to_string()],
117                related_files: vec![],
118                source_agent: AgentId::new("agent-1"),
119                source_task: None,
120                timestamp,
121                last_reinforced: timestamp,
122                archived: false,
123            },
124            similarity,
125        }
126    }
127
128    #[test]
129    fn test_search_result_clone_and_debug() {
130        let result = make_result(0.95);
131        let cloned = result.clone();
132        assert_eq!(cloned.similarity, 0.95);
133        // Debug should not panic
134        let debug = format!("{:?}", result);
135        assert!(!debug.is_empty());
136    }
137
138    #[test]
139    fn test_search_result_similarity_identity() {
140        // 1.0 - 0.0 distance = 1.0 similarity (identical vectors)
141        let result = make_result(1.0);
142        assert_eq!(result.similarity, 1.0);
143    }
144
145    #[test]
146    fn test_search_result_similarity_can_be_negative() {
147        // 1.0 - 2.0 distance = -1.0 similarity (opposite vectors)
148        let result = make_result(-1.0);
149        assert!(result.similarity < 0.0);
150    }
151}