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}