1use std::collections::{BTreeMap, HashMap, HashSet};
29use std::time::{SystemTime, UNIX_EPOCH};
30
31use parking_lot::RwLock;
32
33use super::entity::EntityId;
34use super::metadata::{Metadata, MetadataStorage, MetadataValue};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum EdgeDirection {
43 Outgoing,
44 Incoming,
45 Both,
46}
47
48#[derive(Debug, Clone)]
50pub struct AdjacencyEntry {
51 pub edge_id: EntityId,
53 pub neighbor_id: EntityId,
55 pub label: String,
57 pub weight: f32,
59}
60
61pub struct GraphAdjacencyIndex {
68 outgoing: RwLock<HashMap<EntityId, Vec<AdjacencyEntry>>>,
70 incoming: RwLock<HashMap<EntityId, Vec<AdjacencyEntry>>>,
72 by_label: RwLock<HashMap<String, HashSet<EntityId>>>,
74 edge_count: RwLock<usize>,
76 node_count: RwLock<usize>,
78}
79
80impl GraphAdjacencyIndex {
81 pub fn new() -> Self {
83 Self {
84 outgoing: RwLock::new(HashMap::new()),
85 incoming: RwLock::new(HashMap::new()),
86 by_label: RwLock::new(HashMap::new()),
87 edge_count: RwLock::new(0),
88 node_count: RwLock::new(0),
89 }
90 }
91
92 pub fn index_edge(
94 &self,
95 edge_id: EntityId,
96 source_id: EntityId,
97 target_id: EntityId,
98 label: &str,
99 weight: f32,
100 ) {
101 {
103 let mut outgoing = self.outgoing.write();
104 let entry = AdjacencyEntry {
105 edge_id,
106 neighbor_id: target_id,
107 label: label.to_string(),
108 weight,
109 };
110 outgoing.entry(source_id).or_default().push(entry);
111 }
112
113 {
115 let mut incoming = self.incoming.write();
116 let entry = AdjacencyEntry {
117 edge_id,
118 neighbor_id: source_id,
119 label: label.to_string(),
120 weight,
121 };
122 incoming.entry(target_id).or_default().push(entry);
123 }
124
125 {
127 let mut by_label = self.by_label.write();
128 by_label
129 .entry(label.to_string())
130 .or_default()
131 .insert(edge_id);
132 }
133
134 {
136 let mut count = self.edge_count.write();
137 *count += 1;
138 }
139
140 self.update_node_count();
142 }
143
144 pub fn remove_edge(&self, edge_id: EntityId) {
146 {
148 let mut outgoing = self.outgoing.write();
149 for entries in outgoing.values_mut() {
150 entries.retain(|e| e.edge_id != edge_id);
151 }
152 }
153
154 {
156 let mut incoming = self.incoming.write();
157 for entries in incoming.values_mut() {
158 entries.retain(|e| e.edge_id != edge_id);
159 }
160 }
161
162 {
164 let mut by_label = self.by_label.write();
165 for edges in by_label.values_mut() {
166 edges.remove(&edge_id);
167 }
168 }
169
170 {
172 let mut count = self.edge_count.write();
173 *count = count.saturating_sub(1);
174 }
175 }
176
177 pub fn get_neighbors(
179 &self,
180 node_id: EntityId,
181 direction: EdgeDirection,
182 label_filter: Option<&str>,
183 ) -> Vec<AdjacencyEntry> {
184 let mut results = Vec::new();
185
186 if matches!(direction, EdgeDirection::Outgoing | EdgeDirection::Both) {
187 let outgoing = self.outgoing.read();
188 if let Some(entries) = outgoing.get(&node_id) {
189 for entry in entries {
190 if label_filter.is_none_or(|l| entry.label == l) {
191 results.push(entry.clone());
192 }
193 }
194 }
195 }
196
197 if matches!(direction, EdgeDirection::Incoming | EdgeDirection::Both) {
198 let incoming = self.incoming.read();
199 if let Some(entries) = incoming.get(&node_id) {
200 for entry in entries {
201 if label_filter.is_none_or(|l| entry.label == l) {
202 results.push(entry.clone());
203 }
204 }
205 }
206 }
207
208 results
209 }
210
211 pub fn get_edges_by_label(&self, label: &str) -> Vec<EntityId> {
213 let idx = self.by_label.read();
214 idx.get(label)
215 .map(|s| s.iter().copied().collect())
216 .unwrap_or_default()
217 }
218
219 pub fn out_degree(&self, node_id: EntityId) -> usize {
221 self.outgoing
222 .read()
223 .get(&node_id)
224 .map(|v| v.len())
225 .unwrap_or(0)
226 }
227
228 pub fn in_degree(&self, node_id: EntityId) -> usize {
230 self.incoming
231 .read()
232 .get(&node_id)
233 .map(|v| v.len())
234 .unwrap_or(0)
235 }
236
237 pub fn degree(&self, node_id: EntityId) -> usize {
239 self.out_degree(node_id) + self.in_degree(node_id)
240 }
241
242 pub fn edge_count(&self) -> usize {
244 *self.edge_count.read()
245 }
246
247 pub fn node_count(&self) -> usize {
249 *self.node_count.read()
250 }
251
252 pub fn clear(&self) {
254 self.outgoing.write().clear();
255 self.incoming.write().clear();
256 self.by_label.write().clear();
257 *self.edge_count.write() = 0;
258 *self.node_count.write() = 0;
259 }
260
261 fn update_node_count(&self) {
262 let out_nodes: HashSet<_> = self.outgoing.read().keys().copied().collect();
263 let in_nodes: HashSet<_> = self.incoming.read().keys().copied().collect();
264
265 let total: HashSet<_> = out_nodes.union(&in_nodes).collect();
266 *self.node_count.write() = total.len();
267 }
268}
269
270impl Default for GraphAdjacencyIndex {
271 fn default() -> Self {
272 Self::new()
273 }
274}
275
276#[derive(Debug, Clone)]
282pub struct TokenPosition {
283 pub entity_id: EntityId,
284 pub field: String,
285 pub position: u32,
286}
287
288#[derive(Debug, Clone)]
290pub struct PostingEntry {
291 pub entity_id: EntityId,
292 pub collection: String,
293 pub field: String,
294 pub positions: Vec<u32>,
295 pub term_frequency: f32,
296}
297
298pub struct InvertedIndex {
300 index: RwLock<BTreeMap<String, Vec<PostingEntry>>>,
302 doc_count: RwLock<usize>,
304 indexed_fields: RwLock<HashMap<String, HashSet<String>>>,
306}
307
308impl InvertedIndex {
309 pub fn new() -> Self {
311 Self {
312 index: RwLock::new(BTreeMap::new()),
313 doc_count: RwLock::new(0),
314 indexed_fields: RwLock::new(HashMap::new()),
315 }
316 }
317
318 pub fn add_indexed_field(&self, collection: &str, field: &str) {
320 self.indexed_fields
321 .write()
322 .entry(collection.to_string())
323 .or_default()
324 .insert(field.to_string());
325 }
326
327 pub fn index_document(
329 &self,
330 collection: &str,
331 entity_id: EntityId,
332 field: &str,
333 content: &str,
334 ) {
335 let tokens = self.tokenize(content);
336 let term_count = tokens.len() as f32;
337
338 let mut term_freqs: HashMap<String, Vec<u32>> = HashMap::new();
340 for (position, token) in tokens.iter().enumerate() {
341 term_freqs
342 .entry(token.clone())
343 .or_default()
344 .push(position as u32);
345 }
346
347 {
349 let mut index = self.index.write();
350 for (term, positions) in term_freqs {
351 let tf = positions.len() as f32 / term_count.max(1.0);
352
353 let entry = PostingEntry {
354 entity_id,
355 collection: collection.to_string(),
356 field: field.to_string(),
357 positions,
358 term_frequency: tf,
359 };
360
361 index.entry(term).or_default().push(entry);
362 }
363 }
364
365 *self.doc_count.write() += 1;
367 }
368
369 pub fn remove_document(&self, entity_id: EntityId) {
371 let mut index = self.index.write();
372 for postings in index.values_mut() {
373 postings.retain(|p| p.entity_id != entity_id);
374 }
375 }
376
377 pub fn search(&self, query: &str, limit: usize) -> Vec<TextSearchResult> {
379 let terms = self.tokenize(query);
380 if terms.is_empty() {
381 return Vec::new();
382 }
383
384 let index = self.index.read();
385
386 let doc_count = *self.doc_count.read();
387
388 let mut term_postings: Vec<&Vec<PostingEntry>> = Vec::new();
390 for term in &terms {
391 if let Some(postings) = index.get(term) {
392 term_postings.push(postings);
393 } else {
394 return Vec::new();
396 }
397 }
398
399 let mut scores: HashMap<EntityId, f32> = HashMap::new();
401
402 if let Some(first_postings) = term_postings.first() {
404 for posting in *first_postings {
405 let idf = ((doc_count as f32) / (first_postings.len() as f32 + 1.0)).ln();
406 scores.insert(posting.entity_id, posting.term_frequency * idf);
407 }
408 }
409
410 for postings in term_postings.iter().skip(1) {
412 let idf = ((doc_count as f32) / (postings.len() as f32 + 1.0)).ln();
413 let entities_in_term: HashSet<EntityId> =
414 postings.iter().map(|p| p.entity_id).collect();
415
416 scores.retain(|id, _| entities_in_term.contains(id));
418
419 for posting in *postings {
421 if let Some(score) = scores.get_mut(&posting.entity_id) {
422 *score += posting.term_frequency * idf;
423 }
424 }
425 }
426
427 let mut results: Vec<TextSearchResult> = scores
429 .into_iter()
430 .map(|(entity_id, score)| {
431 let collection = term_postings
433 .first()
434 .and_then(|p| p.iter().find(|e| e.entity_id == entity_id))
435 .map(|p| p.collection.clone())
436 .unwrap_or_default();
437
438 TextSearchResult {
439 entity_id,
440 collection,
441 score,
442 matched_terms: terms.clone(),
443 }
444 })
445 .collect();
446
447 results.sort_by(|a, b| {
448 b.score
449 .partial_cmp(&a.score)
450 .unwrap_or(std::cmp::Ordering::Equal)
451 });
452 results.truncate(limit);
453 results
454 }
455
456 pub fn search_prefix(&self, prefix: &str, limit: usize) -> Vec<String> {
458 let prefix_lower = prefix.to_lowercase();
459
460 let index = self.index.read();
461
462 index
463 .range(prefix_lower.clone()..)
464 .take_while(|(term, _)| term.starts_with(&prefix_lower))
465 .take(limit)
466 .map(|(term, _)| term.clone())
467 .collect()
468 }
469
470 fn tokenize(&self, text: &str) -> Vec<String> {
472 text.to_lowercase()
473 .split(|c: char| !c.is_alphanumeric())
474 .filter(|s| s.len() >= 2)
475 .map(|s| s.to_string())
476 .collect()
477 }
478}
479
480impl Default for InvertedIndex {
481 fn default() -> Self {
482 Self::new()
483 }
484}
485
486#[derive(Debug, Clone)]
488pub struct TextSearchResult {
489 pub entity_id: EntityId,
490 pub collection: String,
491 pub score: f32,
492 pub matched_terms: Vec<String>,
493}
494
495#[derive(Debug, Clone)]
501pub struct IntegratedIndexConfig {
502 pub enable_hnsw: bool,
504 pub enable_fulltext: bool,
506 pub enable_metadata: bool,
508 pub enable_graph: bool,
510 pub hnsw_m: usize,
512 pub hnsw_ef_construction: usize,
514 pub hnsw_ef_search: usize,
516}
517
518impl Default for IntegratedIndexConfig {
519 fn default() -> Self {
520 Self {
521 enable_hnsw: true,
522 enable_fulltext: true,
523 enable_metadata: true,
524 enable_graph: true,
525 hnsw_m: 16,
526 hnsw_ef_construction: 100,
527 hnsw_ef_search: 50,
528 }
529 }
530}
531
532#[derive(Debug, Clone, Default)]
534pub struct IndexStats {
535 pub vector_count: usize,
537 pub document_count: usize,
539 pub term_count: usize,
541 pub metadata_entries: usize,
543 pub graph_node_count: usize,
545 pub graph_edge_count: usize,
547 pub created_at: u64,
549 pub updated_at: u64,
551}
552
553#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
555pub enum IndexType {
556 Hnsw,
558 Fulltext,
560 Metadata,
562 Graph,
564}
565
566#[derive(Debug, Clone)]
568pub enum IndexStatus {
569 Ready,
571 Building { progress: f32 },
573 Stale,
575 Disabled,
577 Error(String),
579}
580
581#[derive(Debug, Clone)]
583pub struct IndexEvent {
584 pub index_type: IndexType,
585 pub collection: Option<String>,
586 pub event: IndexEventKind,
587 pub timestamp: u64,
588}
589
590#[derive(Debug, Clone)]
591pub enum IndexEventKind {
592 Created,
593 Dropped,
594 Rebuilt,
595 Updated { entries_affected: usize },
596}
597
598pub struct IntegratedIndexManager {
600 config: IntegratedIndexConfig,
602 text_index: InvertedIndex,
604 metadata_index: RwLock<MetadataStorage>,
606 hnsw_indices: RwLock<HashMap<String, HnswIndexInfo>>,
609 graph_index: GraphAdjacencyIndex,
611 index_status: RwLock<HashMap<(IndexType, Option<String>), IndexStatus>>,
613 event_history: RwLock<Vec<IndexEvent>>,
615 created_at: u64,
617}
618
619struct HnswIndexInfo {
621 dimension: usize,
623 vectors: HashMap<EntityId, Vec<f32>>,
625 entry_point: Option<EntityId>,
628}
629
630mod manager_impl;
631impl Default for IntegratedIndexManager {
632 fn default() -> Self {
633 Self::new()
634 }
635}
636
637#[derive(Debug, Clone)]
639pub struct VectorSearchResult {
640 pub entity_id: EntityId,
641 pub collection: String,
642 pub similarity: f32,
643}
644
645#[derive(Debug, Clone)]
647pub enum MetadataQueryFilter {
648 Equals(MetadataValue),
650 Range {
652 min: Option<MetadataValue>,
653 max: Option<MetadataValue>,
654 },
655 Contains(String),
657 In(Vec<MetadataValue>),
659}
660
661fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
667 if a.len() != b.len() || a.is_empty() {
668 return 0.0;
669 }
670
671 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
672 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
673 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
674
675 if norm_a == 0.0 || norm_b == 0.0 {
676 return 0.0;
677 }
678
679 dot / (norm_a * norm_b)
680}
681
682#[cfg(test)]
687mod tests {
688 use super::*;
689
690 #[test]
691 fn test_inverted_index_basic() {
692 let index = InvertedIndex::new();
693
694 index.index_document(
695 "docs",
696 EntityId(1),
697 "content",
698 "hello world rust programming",
699 );
700 index.index_document("docs", EntityId(2), "content", "rust is fast and safe");
701 index.index_document("docs", EntityId(3), "content", "python is easy to learn");
702
703 let results = index.search("rust", 10);
704 assert_eq!(results.len(), 2);
705 assert!(results.iter().any(|r| r.entity_id == EntityId(1)));
706 assert!(results.iter().any(|r| r.entity_id == EntityId(2)));
707 }
708
709 #[test]
710 fn test_inverted_index_and_query() {
711 let index = InvertedIndex::new();
712
713 index.index_document("docs", EntityId(1), "content", "rust programming language");
714 index.index_document("docs", EntityId(2), "content", "rust systems programming");
715 index.index_document(
716 "docs",
717 EntityId(3),
718 "content",
719 "python programming language",
720 );
721
722 let results = index.search("rust programming", 10);
724 assert_eq!(results.len(), 2);
725
726 let results = index.search("language programming", 10);
729 assert_eq!(results.len(), 2);
730 }
731
732 #[test]
733 fn test_prefix_search() {
734 let index = InvertedIndex::new();
735
736 index.index_document("docs", EntityId(1), "content", "programming rust rustacean");
737
738 let suggestions = index.search_prefix("rust", 10);
739 assert!(suggestions.contains(&"rust".to_string()));
740 assert!(suggestions.contains(&"rustacean".to_string()));
741 }
742
743 #[test]
744 fn test_vector_search() {
745 let manager = IntegratedIndexManager::new();
746
747 manager.index_vector("embeddings", EntityId(1), &[1.0, 0.0, 0.0]);
748 manager.index_vector("embeddings", EntityId(2), &[0.9, 0.1, 0.0]);
749 manager.index_vector("embeddings", EntityId(3), &[0.0, 1.0, 0.0]);
750
751 let results = manager.search_similar("embeddings", &[1.0, 0.0, 0.0], 2);
752 assert_eq!(results.len(), 2);
753 assert_eq!(results[0].entity_id, EntityId(1));
754 assert!(results[0].similarity > 0.99);
755 }
756
757 #[test]
758 fn test_cosine_similarity() {
759 let a = [1.0, 0.0, 0.0];
760 let b = [1.0, 0.0, 0.0];
761 assert!((cosine_similarity(&a, &b) - 1.0).abs() < 0.001);
762
763 let c = [0.0, 1.0, 0.0];
764 assert!(cosine_similarity(&a, &c).abs() < 0.001);
765 }
766
767 #[test]
772 fn test_graph_adjacency_basic() {
773 let index = GraphAdjacencyIndex::new();
774
775 index.index_edge(EntityId(100), EntityId(1), EntityId(2), "KNOWS", 1.0);
777 index.index_edge(EntityId(101), EntityId(2), EntityId(3), "KNOWS", 1.0);
778
779 let neighbors = index.get_neighbors(EntityId(1), EdgeDirection::Outgoing, None);
781 assert_eq!(neighbors.len(), 1);
782 assert_eq!(neighbors[0].neighbor_id, EntityId(2));
783
784 let neighbors = index.get_neighbors(EntityId(2), EdgeDirection::Incoming, None);
786 assert_eq!(neighbors.len(), 1);
787 assert_eq!(neighbors[0].neighbor_id, EntityId(1));
788
789 let neighbors = index.get_neighbors(EntityId(2), EdgeDirection::Both, None);
791 assert_eq!(neighbors.len(), 2);
792 }
793
794 #[test]
795 fn test_graph_adjacency_label_filter() {
796 let index = GraphAdjacencyIndex::new();
797
798 index.index_edge(EntityId(100), EntityId(1), EntityId(2), "KNOWS", 1.0);
800 index.index_edge(EntityId(101), EntityId(1), EntityId(3), "WORKS_WITH", 1.0);
801 index.index_edge(EntityId(102), EntityId(1), EntityId(4), "KNOWS", 1.0);
802
803 let neighbors = index.get_neighbors(EntityId(1), EdgeDirection::Outgoing, Some("KNOWS"));
805 assert_eq!(neighbors.len(), 2);
806
807 let neighbors =
809 index.get_neighbors(EntityId(1), EdgeDirection::Outgoing, Some("WORKS_WITH"));
810 assert_eq!(neighbors.len(), 1);
811 assert_eq!(neighbors[0].neighbor_id, EntityId(3));
812 }
813
814 #[test]
815 fn test_graph_adjacency_degree() {
816 let index = GraphAdjacencyIndex::new();
817
818 index.index_edge(EntityId(100), EntityId(1), EntityId(2), "LINK", 1.0);
820 index.index_edge(EntityId(101), EntityId(1), EntityId(3), "LINK", 1.0);
821 index.index_edge(EntityId(102), EntityId(1), EntityId(4), "LINK", 1.0);
822 index.index_edge(EntityId(103), EntityId(1), EntityId(5), "LINK", 1.0);
823
824 assert_eq!(index.out_degree(EntityId(1)), 4);
825 assert_eq!(index.in_degree(EntityId(1)), 0);
826 assert_eq!(index.degree(EntityId(1)), 4);
827
828 assert_eq!(index.in_degree(EntityId(2)), 1);
830 assert_eq!(index.out_degree(EntityId(2)), 0);
831 }
832
833 #[test]
834 fn test_graph_adjacency_edge_by_label() {
835 let index = GraphAdjacencyIndex::new();
836
837 index.index_edge(EntityId(100), EntityId(1), EntityId(2), "A", 1.0);
838 index.index_edge(EntityId(101), EntityId(2), EntityId(3), "B", 1.0);
839 index.index_edge(EntityId(102), EntityId(3), EntityId(4), "A", 1.0);
840
841 let edges_a = index.get_edges_by_label("A");
842 assert_eq!(edges_a.len(), 2);
843 assert!(edges_a.contains(&EntityId(100)));
844 assert!(edges_a.contains(&EntityId(102)));
845
846 let edges_b = index.get_edges_by_label("B");
847 assert_eq!(edges_b.len(), 1);
848 assert!(edges_b.contains(&EntityId(101)));
849 }
850
851 #[test]
852 fn test_graph_adjacency_remove() {
853 let index = GraphAdjacencyIndex::new();
854
855 index.index_edge(EntityId(100), EntityId(1), EntityId(2), "LINK", 1.0);
856 index.index_edge(EntityId(101), EntityId(1), EntityId(3), "LINK", 1.0);
857
858 assert_eq!(index.edge_count(), 2);
859
860 index.remove_edge(EntityId(100));
862
863 assert_eq!(index.edge_count(), 1);
864 let neighbors = index.get_neighbors(EntityId(1), EdgeDirection::Outgoing, None);
865 assert_eq!(neighbors.len(), 1);
866 assert_eq!(neighbors[0].neighbor_id, EntityId(3));
867 }
868
869 #[test]
874 fn test_index_lifecycle_create_drop() {
875 let manager = IntegratedIndexManager::new();
876
877 let result = manager.create_index(IndexType::Hnsw, Some("my_collection"));
879 assert!(result.is_ok());
880
881 let status = manager.index_status(IndexType::Hnsw, Some("my_collection"));
883 assert!(matches!(status, IndexStatus::Ready));
884
885 let result = manager.drop_index(IndexType::Hnsw, Some("my_collection"));
887 assert!(result.is_ok());
888 }
889
890 #[test]
891 fn test_index_lifecycle_rebuild() {
892 let manager = IntegratedIndexManager::new();
893
894 manager.index_vector("test", EntityId(1), &[1.0, 0.0, 0.0]);
896 manager.index_vector("test", EntityId(2), &[0.0, 1.0, 0.0]);
897
898 let result = manager.rebuild_index(IndexType::Hnsw, Some("test"));
900 assert!(result.is_ok());
901
902 let status = manager.index_status(IndexType::Hnsw, Some("test"));
904 assert!(matches!(status, IndexStatus::Ready));
905 }
906
907 #[test]
908 fn test_index_stats_with_graph() {
909 let manager = IntegratedIndexManager::new();
910
911 manager.index_edge(EntityId(100), EntityId(1), EntityId(2), "LINK", 1.0);
913 manager.index_edge(EntityId(101), EntityId(2), EntityId(3), "LINK", 1.0);
914
915 let stats = manager.stats();
916 assert_eq!(stats.graph_edge_count, 2);
917 assert!(stats.graph_node_count >= 2); }
919
920 #[test]
921 fn test_integrated_manager_graph_operations() {
922 let manager = IntegratedIndexManager::new();
923
924 manager.index_edge(EntityId(100), EntityId(1), EntityId(2), "KNOWS", 1.0);
926 manager.index_edge(EntityId(101), EntityId(2), EntityId(3), "KNOWS", 0.5);
927
928 let neighbors = manager.get_neighbors(EntityId(1), EdgeDirection::Outgoing, None);
930 assert_eq!(neighbors.len(), 1);
931 assert_eq!(neighbors[0].neighbor_id, EntityId(2));
932 assert_eq!(neighbors[0].weight, 1.0);
933
934 assert_eq!(manager.node_degree(EntityId(2), EdgeDirection::Both), 2);
936 }
937}