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)]
149#[allow(dead_code)]
150pub struct Filter {
151 pub categories: Vec<String>,
153
154 pub tags: Vec<String>,
156
157 pub min_access_count: Option<u64>,
159
160 pub metadata: Vec<(String, String)>,
162}
163
164#[allow(dead_code)]
165impl Filter {
166 pub fn new() -> Self {
168 Self::default()
169 }
170
171 pub fn category(mut self, category: impl Into<String>) -> Self {
173 self.categories.push(category.into());
174 self
175 }
176
177 pub fn tag(mut self, tag: impl Into<String>) -> Self {
179 self.tags.push(tag.into());
180 self
181 }
182
183 pub fn min_access(mut self, count: u64) -> Self {
185 self.min_access_count = Some(count);
186 self
187 }
188
189 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
191 self.metadata.push((key.into(), value.into()));
192 self
193 }
194
195 pub fn matches(&self, entry: &KnowledgeEntry) -> bool {
197 if !self.categories.is_empty() {
199 if let Some(cat) = &entry.category {
200 if !self.categories.iter().any(|c| c == cat) {
201 return false;
202 }
203 } else {
204 return false;
205 }
206 }
207
208 if !self.tags.is_empty()
210 && !self
211 .tags
212 .iter()
213 .any(|t| entry.tags.iter().any(|et| et == t))
214 {
215 return false;
216 }
217
218 if let Some(min) = self.min_access_count {
220 if entry.access_count < min {
221 return false;
222 }
223 }
224
225 for (key, value) in &self.metadata {
227 if entry.metadata.get(key) != Some(value.as_str()) {
228 return false;
229 }
230 }
231
232 true
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_search_options_builder() {
242 let opts = SearchOptions::new(5)
243 .with_min_similarity(0.5)
244 .with_category("Programming")
245 .with_tags(["rust", "tutorial"])
246 .with_diversity(0.3)
247 .hybrid(0.4);
248
249 assert_eq!(opts.limit, 5);
250 assert!((opts.min_similarity - 0.5).abs() < f32::EPSILON);
251 assert_eq!(opts.category, Some("Programming".to_string()));
252 assert_eq!(opts.tags, vec!["rust", "tutorial"]);
253 assert!((opts.diversity - 0.3).abs() < f32::EPSILON);
254 assert!(opts.hybrid);
255 assert!((opts.keyword_weight - 0.4).abs() < f32::EPSILON);
256 }
257
258 #[test]
259 fn test_filter_matching() {
260 let entry = KnowledgeEntry::new("Test", "Content")
261 .with_category("Programming")
262 .with_tags(["rust", "testing"]);
263
264 let filter = Filter::new().category("Programming").tag("rust");
265
266 assert!(filter.matches(&entry));
267
268 let non_matching_filter = Filter::new().category("Other");
269 assert!(!non_matching_filter.matches(&entry));
270 }
271}