Skip to main content

ai_model_vault/rag/
knowledge.rs

1//! Knowledge base for RAG systems.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use super::documents::{ChunkInfo, Document, DocumentStore};
7
8/// Knowledge base for RAG systems
9pub struct KnowledgeBase {
10    /// Document store
11    pub store: DocumentStore,
12
13    /// Knowledge base name
14    pub name: String,
15
16    /// Configuration
17    pub config: KnowledgeBaseConfig,
18}
19
20/// Knowledge base configuration
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct KnowledgeBaseConfig {
23    /// Embedding dimension
24    pub embedding_dim: usize,
25
26    /// Chunk size (for document splitting)
27    pub chunk_size: usize,
28
29    /// Chunk overlap
30    pub chunk_overlap: usize,
31
32    /// Maximum results to return
33    pub max_results: usize,
34
35    /// Minimum similarity threshold
36    pub similarity_threshold: f32,
37}
38
39impl Default for KnowledgeBaseConfig {
40    fn default() -> Self {
41        Self {
42            embedding_dim: 384, // Common for all-MiniLM-L6-v2
43            chunk_size: 512,
44            chunk_overlap: 50,
45            max_results: 5,
46            similarity_threshold: 0.5,
47        }
48    }
49}
50
51impl KnowledgeBase {
52    /// Create a new knowledge base
53    pub fn new(name: String, config: KnowledgeBaseConfig) -> Self {
54        Self {
55            store: DocumentStore::new(),
56            name,
57            config,
58        }
59    }
60
61    /// Add a document
62    pub fn add(&mut self, doc: Document) -> crate::error::Result<()> {
63        self.store.add_document(doc)
64    }
65
66    /// Retrieve relevant documents
67    pub fn retrieve(&self, query_embedding: &[f32], top_k: Option<usize>) -> Vec<Document> {
68        let k = top_k.unwrap_or(self.config.max_results);
69        let results = self.store.search_similar(query_embedding, k);
70
71        results
72            .into_iter()
73            .filter(|(_, score)| *score >= self.config.similarity_threshold)
74            .filter_map(|(id, _)| self.store.get_document(&id).cloned())
75            .collect()
76    }
77
78    /// Split text into chunks
79    pub fn chunk_text(&self, text: &str, doc_id: &str) -> Vec<Document> {
80        let chars: Vec<char> = text.chars().collect();
81        let mut chunks = Vec::new();
82        let mut start = 0;
83        let chunk_size = self.config.chunk_size;
84        let overlap = self.config.chunk_overlap;
85
86        let mut chunk_index = 0;
87        while start < chars.len() {
88            let end = (start + chunk_size).min(chars.len());
89            let chunk_text: String = chars[start..end].iter().collect();
90
91            let chunk = Document {
92                id: format!("{}_chunk_{}", doc_id, chunk_index),
93                content: chunk_text,
94                metadata: {
95                    let mut meta = HashMap::new();
96                    meta.insert("parent_id".to_string(), doc_id.to_string());
97                    meta.insert("chunk_index".to_string(), chunk_index.to_string());
98                    meta
99                },
100                embedding: None,
101                chunk_info: Some(ChunkInfo {
102                    parent_id: Some(doc_id.to_string()),
103                    chunk_index,
104                    total_chunks: 0, // Will be updated later
105                    overlap,
106                }),
107            };
108
109            chunks.push(chunk);
110            chunk_index += 1;
111
112            if end >= chars.len() {
113                break;
114            }
115
116            start = end - overlap.min(end);
117        }
118
119        // Update total_chunks
120        let total = chunks.len();
121        for chunk in &mut chunks {
122            if let Some(info) = &mut chunk.chunk_info {
123                info.total_chunks = total;
124            }
125        }
126
127        chunks
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_knowledge_base_config_default() {
137        let cfg = KnowledgeBaseConfig::default();
138        assert_eq!(cfg.embedding_dim, 384);
139        assert_eq!(cfg.chunk_size, 512);
140        assert_eq!(cfg.chunk_overlap, 50);
141        assert_eq!(cfg.max_results, 5);
142        assert!((cfg.similarity_threshold - 0.5).abs() < 1e-5);
143    }
144
145    #[test]
146    fn test_knowledge_base_add_and_retrieve() {
147        let kb = KnowledgeBase::new("test".to_string(), KnowledgeBaseConfig::default());
148        // Empty retrieve
149        let results = kb.retrieve(&[1.0, 0.0], None);
150        assert!(results.is_empty());
151    }
152
153    #[test]
154    fn test_knowledge_base_add_doc() {
155        let mut kb = KnowledgeBase::new("test".to_string(), KnowledgeBaseConfig::default());
156        let doc = Document {
157            id: "d1".to_string(),
158            content: "hello".to_string(),
159            metadata: HashMap::new(),
160            embedding: Some(vec![1.0, 0.0]),
161            chunk_info: None,
162        };
163        kb.add(doc).unwrap();
164        assert_eq!(kb.store.count(), 1);
165    }
166
167    #[test]
168    fn test_knowledge_base_retrieve_with_threshold() {
169        let cfg = KnowledgeBaseConfig {
170            similarity_threshold: 0.9,
171            ..KnowledgeBaseConfig::default()
172        };
173        let mut kb = KnowledgeBase::new("test".to_string(), cfg);
174
175        let doc = Document {
176            id: "d1".to_string(),
177            content: "hello".to_string(),
178            metadata: HashMap::new(),
179            embedding: Some(vec![1.0, 0.0]),
180            chunk_info: None,
181        };
182        kb.add(doc).unwrap();
183
184        // Query orthogonal — should be filtered by threshold
185        let results = kb.retrieve(&[0.0, 1.0], Some(10));
186        assert!(results.is_empty());
187
188        // Query same direction — should pass threshold
189        let results = kb.retrieve(&[1.0, 0.0], Some(10));
190        assert_eq!(results.len(), 1);
191    }
192
193    #[test]
194    fn test_chunk_text() {
195        let cfg = KnowledgeBaseConfig {
196            chunk_size: 10,
197            chunk_overlap: 3,
198            ..KnowledgeBaseConfig::default()
199        };
200        let kb = KnowledgeBase::new("kb".to_string(), cfg);
201
202        let text = "abcdefghijklmnopqrstuvwxyz"; // 26 chars
203        let chunks = kb.chunk_text(text, "doc1");
204        assert!(chunks.len() >= 3);
205
206        // First chunk
207        assert_eq!(chunks[0].content.len(), 10);
208        assert_eq!(chunks[0].id, "doc1_chunk_0");
209
210        // Verify chunk_info
211        for chunk in &chunks {
212            let info = chunk.chunk_info.as_ref().unwrap();
213            assert_eq!(info.total_chunks, chunks.len());
214            assert_eq!(info.parent_id, Some("doc1".to_string()));
215        }
216    }
217}