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}