use super::KnowledgeEntry;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
pub entry: KnowledgeEntry,
pub similarity: f32,
pub relevance_boost: f32,
pub score: f32,
pub distance: f32,
}
impl SearchResult {
pub fn new(entry: KnowledgeEntry, similarity: f32, distance: f32) -> Self {
let relevance_boost = entry.learned_relevance;
let score = similarity * relevance_boost;
Self {
entry,
similarity,
relevance_boost,
score,
distance,
}
}
pub fn id(&self) -> Uuid {
self.entry.id
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchOptions {
pub limit: usize,
pub min_similarity: f32,
pub category: Option<String>,
pub tags: Vec<String>,
pub use_learning: bool,
pub include_related: bool,
pub diversity: f32,
pub hybrid: bool,
pub keyword_weight: f32,
}
impl Default for SearchOptions {
fn default() -> Self {
Self {
limit: 10,
min_similarity: 0.0,
category: None,
tags: Vec::new(),
use_learning: true,
include_related: false,
diversity: 0.0,
hybrid: false,
keyword_weight: 0.3,
}
}
}
impl SearchOptions {
pub fn new(limit: usize) -> Self {
Self {
limit,
..Default::default()
}
}
pub fn with_min_similarity(mut self, threshold: f32) -> Self {
self.min_similarity = threshold.clamp(0.0, 1.0);
self
}
pub fn with_category(mut self, category: impl Into<String>) -> Self {
self.category = Some(category.into());
self
}
pub fn with_tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.tags = tags.into_iter().map(Into::into).collect();
self
}
pub fn without_learning(mut self) -> Self {
self.use_learning = false;
self
}
pub fn with_related(mut self) -> Self {
self.include_related = true;
self
}
pub fn with_diversity(mut self, factor: f32) -> Self {
self.diversity = factor.clamp(0.0, 1.0);
self
}
pub fn hybrid(mut self, keyword_weight: f32) -> Self {
self.hybrid = true;
self.keyword_weight = keyword_weight.clamp(0.0, 1.0);
self
}
}
#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
pub struct Filter {
pub categories: Vec<String>,
pub tags: Vec<String>,
pub min_access_count: Option<u64>,
pub metadata: Vec<(String, String)>,
}
#[allow(dead_code)]
impl Filter {
pub fn new() -> Self {
Self::default()
}
pub fn category(mut self, category: impl Into<String>) -> Self {
self.categories.push(category.into());
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn min_access(mut self, count: u64) -> Self {
self.min_access_count = Some(count);
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.push((key.into(), value.into()));
self
}
pub fn matches(&self, entry: &KnowledgeEntry) -> bool {
if !self.categories.is_empty() {
if let Some(cat) = &entry.category {
if !self.categories.iter().any(|c| c == cat) {
return false;
}
} else {
return false;
}
}
if !self.tags.is_empty()
&& !self
.tags
.iter()
.any(|t| entry.tags.iter().any(|et| et == t))
{
return false;
}
if let Some(min) = self.min_access_count
&& entry.access_count < min
{
return false;
}
for (key, value) in &self.metadata {
if entry.metadata.get(key) != Some(value.as_str()) {
return false;
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_search_options_builder() {
let opts = SearchOptions::new(5)
.with_min_similarity(0.5)
.with_category("Programming")
.with_tags(["rust", "tutorial"])
.with_diversity(0.3)
.hybrid(0.4);
assert_eq!(opts.limit, 5);
assert!((opts.min_similarity - 0.5).abs() < f32::EPSILON);
assert_eq!(opts.category, Some("Programming".to_string()));
assert_eq!(opts.tags, vec!["rust", "tutorial"]);
assert!((opts.diversity - 0.3).abs() < f32::EPSILON);
assert!(opts.hybrid);
assert!((opts.keyword_weight - 0.4).abs() < f32::EPSILON);
}
#[test]
fn test_filter_matching() {
let entry = KnowledgeEntry::new("Test", "Content")
.with_category("Programming")
.with_tags(["rust", "testing"]);
let filter = Filter::new().category("Programming").tag("rust");
assert!(filter.matches(&entry));
let non_matching_filter = Filter::new().category("Other");
assert!(!non_matching_filter.matches(&entry));
}
}