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;
8
9pub use context::{ContextCandidates, ContextRequest};
10pub use filter::SearchFilter;
11
12use crate::experience::Experience;
13
14/// A search result pairing an experience with its similarity score.
15///
16/// Returned by [`PulseDB::search_similar()`](crate::PulseDB::search_similar) and
17/// [`PulseDB::search_similar_filtered()`](crate::PulseDB::search_similar_filtered).
18/// Results are sorted by `similarity` descending (most similar first).
19///
20/// # Similarity Score
21///
22/// The `similarity` field is computed as `1.0 - cosine_distance`, where
23/// cosine distance ranges from 0.0 (identical) to 2.0 (opposite). This
24/// gives a similarity range of [-1.0, 1.0], where:
25/// - `1.0` = identical vectors
26/// - `0.0` = orthogonal vectors
27/// - `-1.0` = opposite vectors
28///
29/// In practice, experience embeddings from transformer models (e.g.,
30/// all-MiniLM-L6-v2) produce non-negative values, so similarity is
31/// typically in [0.0, 1.0].
32///
33/// # Example
34///
35/// ```rust
36/// # fn main() -> pulsedb::Result<()> {
37/// # let dir = tempfile::tempdir().unwrap();
38/// # let db = pulsedb::PulseDB::open(dir.path().join("test.db"), pulsedb::Config::default())?;
39/// # let collective_id = db.create_collective("example")?;
40/// # let query_embedding = vec![0.1f32; 384];
41/// let results = db.search_similar(collective_id, &query_embedding, 10)?;
42/// for result in &results {
43///     println!(
44///         "similarity={:.3}: {}",
45///         result.similarity, result.experience.content
46///     );
47/// }
48/// # Ok(())
49/// # }
50/// ```
51#[derive(Clone, Debug)]
52pub struct SearchResult {
53    /// The full experience record.
54    pub experience: Experience,
55
56    /// Similarity score (1.0 - cosine_distance).
57    ///
58    /// Higher is more similar. Typically in [0.0, 1.0] for transformer
59    /// embeddings. Theoretical range is [-1.0, 1.0].
60    pub similarity: f32,
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::experience::ExperienceType;
67    use crate::types::{AgentId, CollectiveId, ExperienceId, Timestamp};
68
69    /// Helper to create a SearchResult with a given similarity.
70    fn make_result(similarity: f32) -> SearchResult {
71        SearchResult {
72            experience: Experience {
73                id: ExperienceId::new(),
74                collective_id: CollectiveId::new(),
75                content: format!("test sim={}", similarity),
76                embedding: vec![0.1; 384],
77                experience_type: ExperienceType::default(),
78                importance: 0.5,
79                confidence: 0.8,
80                applications: 0,
81                domain: vec!["test".to_string()],
82                related_files: vec![],
83                source_agent: AgentId::new("agent-1"),
84                source_task: None,
85                timestamp: Timestamp::now(),
86                archived: false,
87            },
88            similarity,
89        }
90    }
91
92    #[test]
93    fn test_search_result_clone_and_debug() {
94        let result = make_result(0.95);
95        let cloned = result.clone();
96        assert_eq!(cloned.similarity, 0.95);
97        // Debug should not panic
98        let debug = format!("{:?}", result);
99        assert!(!debug.is_empty());
100    }
101
102    #[test]
103    fn test_search_result_similarity_identity() {
104        // 1.0 - 0.0 distance = 1.0 similarity (identical vectors)
105        let result = make_result(1.0);
106        assert_eq!(result.similarity, 1.0);
107    }
108
109    #[test]
110    fn test_search_result_similarity_can_be_negative() {
111        // 1.0 - 2.0 distance = -1.0 similarity (opposite vectors)
112        let result = make_result(-1.0);
113        assert!(result.similarity < 0.0);
114    }
115}