converge_knowledge/core/
search.rs1use super::KnowledgeEntry;
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SearchResult {
10 pub entry: KnowledgeEntry,
12
13 pub similarity: f32,
15
16 pub relevance_boost: f32,
18
19 pub score: f32,
21
22 pub distance: f32,
24}
25
26impl SearchResult {
27 pub fn new(entry: KnowledgeEntry, similarity: f32, distance: f32) -> Self {
29 let relevance_boost = entry.learned_relevance;
30 let score = similarity * relevance_boost;
31
32 Self {
33 entry,
34 similarity,
35 relevance_boost,
36 score,
37 distance,
38 }
39 }
40
41 pub fn id(&self) -> Uuid {
43 self.entry.id
44 }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct SearchOptions {
50 pub limit: usize,
52
53 pub min_similarity: f32,
55
56 pub category: Option<String>,
58
59 pub tags: Vec<String>,
61
62 pub use_learning: bool,
64
65 pub include_related: bool,
67
68 pub diversity: f32,
70
71 pub hybrid: bool,
73
74 pub keyword_weight: f32,
76}
77
78impl Default for SearchOptions {
79 fn default() -> Self {
80 Self {
81 limit: 10,
82 min_similarity: 0.0,
83 category: None,
84 tags: Vec::new(),
85 use_learning: true,
86 include_related: false,
87 diversity: 0.0,
88 hybrid: false,
89 keyword_weight: 0.3,
90 }
91 }
92}
93
94impl SearchOptions {
95 pub fn new(limit: usize) -> Self {
97 Self {
98 limit,
99 ..Default::default()
100 }
101 }
102
103 pub fn with_min_similarity(mut self, threshold: f32) -> Self {
105 self.min_similarity = threshold.clamp(0.0, 1.0);
106 self
107 }
108
109 pub fn with_category(mut self, category: impl Into<String>) -> Self {
111 self.category = Some(category.into());
112 self
113 }
114
115 pub fn with_tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
117 self.tags = tags.into_iter().map(Into::into).collect();
118 self
119 }
120
121 pub fn without_learning(mut self) -> Self {
123 self.use_learning = false;
124 self
125 }
126
127 pub fn with_related(mut self) -> Self {
129 self.include_related = true;
130 self
131 }
132
133 pub fn with_diversity(mut self, factor: f32) -> Self {
135 self.diversity = factor.clamp(0.0, 1.0);
136 self
137 }
138
139 pub fn hybrid(mut self, keyword_weight: f32) -> Self {
141 self.hybrid = true;
142 self.keyword_weight = keyword_weight.clamp(0.0, 1.0);
143 self
144 }
145}
146
147#[derive(Debug, Clone, Default)]
149pub struct Filter {
150 pub categories: Vec<String>,
152
153 pub tags: Vec<String>,
155
156 pub min_access_count: Option<u64>,
158
159 pub metadata: Vec<(String, String)>,
161}
162
163impl Filter {
164 pub fn new() -> Self {
166 Self::default()
167 }
168
169 pub fn category(mut self, category: impl Into<String>) -> Self {
171 self.categories.push(category.into());
172 self
173 }
174
175 pub fn tag(mut self, tag: impl Into<String>) -> Self {
177 self.tags.push(tag.into());
178 self
179 }
180
181 pub fn min_access(mut self, count: u64) -> Self {
183 self.min_access_count = Some(count);
184 self
185 }
186
187 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
189 self.metadata.push((key.into(), value.into()));
190 self
191 }
192
193 pub fn matches(&self, entry: &KnowledgeEntry) -> bool {
195 if !self.categories.is_empty() {
197 if let Some(cat) = &entry.category {
198 if !self.categories.iter().any(|c| c == cat) {
199 return false;
200 }
201 } else {
202 return false;
203 }
204 }
205
206 if !self.tags.is_empty()
208 && !self
209 .tags
210 .iter()
211 .any(|t| entry.tags.iter().any(|et| et == t))
212 {
213 return false;
214 }
215
216 if let Some(min) = self.min_access_count {
218 if entry.access_count < min {
219 return false;
220 }
221 }
222
223 for (key, value) in &self.metadata {
225 if entry.metadata.get(key) != Some(value.as_str()) {
226 return false;
227 }
228 }
229
230 true
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn test_search_options_builder() {
240 let opts = SearchOptions::new(5)
241 .with_min_similarity(0.5)
242 .with_category("Programming")
243 .with_tags(["rust", "tutorial"])
244 .with_diversity(0.3)
245 .hybrid(0.4);
246
247 assert_eq!(opts.limit, 5);
248 assert!((opts.min_similarity - 0.5).abs() < f32::EPSILON);
249 assert_eq!(opts.category, Some("Programming".to_string()));
250 assert_eq!(opts.tags, vec!["rust", "tutorial"]);
251 assert!((opts.diversity - 0.3).abs() < f32::EPSILON);
252 assert!(opts.hybrid);
253 assert!((opts.keyword_weight - 0.4).abs() < f32::EPSILON);
254 }
255
256 #[test]
257 fn test_filter_matching() {
258 let entry = KnowledgeEntry::new("Test", "Content")
259 .with_category("Programming")
260 .with_tags(["rust", "testing"]);
261
262 let filter = Filter::new().category("Programming").tag("rust");
263
264 assert!(filter.matches(&entry));
265
266 let non_matching_filter = Filter::new().category("Other");
267 assert!(!non_matching_filter.matches(&entry));
268 }
269}