1use super::coref::{CorefChain, Mention, MentionType};
103use anno::{Entity, EntityType};
104use serde::{Deserialize, Serialize};
105use std::collections::{HashMap, VecDeque};
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
113pub enum MemoryPolicy {
114 #[default]
116 Unbounded,
117 LeastRecentlyUsed {
119 max_clusters: usize,
121 },
122 LeastFrequentlyUsed {
124 max_clusters: usize,
126 },
127 DualCache {
131 l_cache_size: usize,
133 g_cache_size: usize,
135 },
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct IncrementalConfig {
141 pub window_size: usize,
143 pub window_overlap: usize,
145 pub memory_policy: MemoryPolicy,
147 pub similarity_threshold: f64,
149 pub token_based: bool,
151 pub max_pronoun_search_windows: usize,
153 pub use_exact_match: bool,
155 pub use_substring_match: bool,
157 pub grouped_window_size: usize,
160}
161
162impl Default for IncrementalConfig {
163 fn default() -> Self {
164 Self {
165 window_size: 1500,
166 window_overlap: 200,
167 memory_policy: MemoryPolicy::Unbounded,
168 similarity_threshold: 0.7,
169 token_based: true,
170 max_pronoun_search_windows: 3,
171 use_exact_match: true,
172 use_substring_match: true,
173 grouped_window_size: 10,
174 }
175 }
176}
177
178#[derive(Debug, Clone)]
184struct ClusterMetadata {
185 id: u64,
187 representative: String,
189 mentions: Vec<MentionRecord>,
191 last_accessed_window: usize,
193 access_count: usize,
195 entity_type: Option<EntityType>,
197 #[allow(dead_code)]
199 first_mention_offset: usize,
200}
201
202#[derive(Debug, Clone)]
204struct MentionRecord {
205 text: String,
206 start: usize,
207 end: usize,
208 #[allow(dead_code)]
209 window_index: usize,
210 mention_type: MentionType,
211}
212
213#[derive(Debug)]
217pub struct EntityMemory {
218 clusters: HashMap<u64, ClusterMetadata>,
220 next_cluster_id: u64,
222 policy: MemoryPolicy,
224 current_window: usize,
226 l_cache: VecDeque<u64>,
228 g_cache: Vec<u64>,
230}
231
232impl EntityMemory {
233 pub fn new(policy: MemoryPolicy) -> Self {
235 Self {
236 clusters: HashMap::new(),
237 next_cluster_id: 0,
238 policy,
239 current_window: 0,
240 l_cache: VecDeque::new(),
241 g_cache: Vec::new(),
242 }
243 }
244
245 fn create_cluster(&mut self, mention: &MentionRecord, entity_type: Option<EntityType>) -> u64 {
247 let id = self.next_cluster_id;
248 self.next_cluster_id += 1;
249
250 let cluster = ClusterMetadata {
251 id,
252 representative: mention.text.clone(),
253 mentions: vec![mention.clone()],
254 last_accessed_window: self.current_window,
255 access_count: 1,
256 entity_type,
257 first_mention_offset: mention.start,
258 };
259
260 self.clusters.insert(id, cluster);
261 self.update_cache_on_access(id);
262 self.maybe_evict();
263
264 id
265 }
266
267 fn add_to_cluster(&mut self, cluster_id: u64, mention: &MentionRecord) {
269 if let Some(cluster) = self.clusters.get_mut(&cluster_id) {
270 cluster.mentions.push(mention.clone());
271 cluster.last_accessed_window = self.current_window;
272 cluster.access_count += 1;
273
274 if mention.text.len() > cluster.representative.len()
276 && mention.mention_type != MentionType::Pronominal
277 {
278 cluster.representative = mention.text.clone();
279 }
280 }
281 self.update_cache_on_access(cluster_id);
282 }
283
284 pub fn find_best_match(
286 &self,
287 mention_text: &str,
288 mention_type: MentionType,
289 similarity_threshold: f64,
290 use_exact_match: bool,
291 use_substring_match: bool,
292 ) -> Option<u64> {
293 let mention_key = name_key(mention_text);
294 let mut best_match: Option<(u64, f64)> = None;
295
296 for (id, cluster) in &self.clusters {
297 if !self.types_compatible(mention_type, cluster) {
299 continue;
300 }
301
302 let score = self.compute_match_score(
303 &mention_key,
304 mention_type,
305 cluster,
306 use_exact_match,
307 use_substring_match,
308 );
309
310 if score >= similarity_threshold {
311 let should_update = match best_match {
312 None => true,
313 Some((_, best_score)) => score > best_score,
314 };
315 if should_update {
316 best_match = Some((*id, score));
317 }
318 }
319 }
320
321 best_match.map(|(id, _)| id)
322 }
323
324 fn compute_match_score(
326 &self,
327 mention_key: &str,
328 mention_type: MentionType,
329 cluster: &ClusterMetadata,
330 use_exact_match: bool,
331 use_substring_match: bool,
332 ) -> f64 {
333 let rep_key = name_key(&cluster.representative);
334
335 if use_exact_match && mention_key == rep_key {
337 return 1.0;
338 }
339
340 for m in &cluster.mentions {
342 let m_key = name_key(&m.text);
343 if mention_key == m_key {
344 return 0.95;
345 }
346 }
347
348 if use_substring_match && mention_type != MentionType::Pronominal {
350 if rep_key.contains(mention_key) || mention_key.contains(&rep_key) {
352 return 0.85;
353 }
354
355 let mention_parts: Vec<&str> = mention_key.split_whitespace().collect();
357 let rep_parts: Vec<&str> = rep_key.split_whitespace().collect();
358
359 if !mention_parts.is_empty() && !rep_parts.is_empty() {
360 if mention_parts.last() == rep_parts.last() {
362 return 0.8;
363 }
364 if mention_parts.first() == rep_parts.first() {
366 return 0.75;
367 }
368 }
369 }
370
371 if mention_type == MentionType::Pronominal {
373 return 0.6; }
375
376 let sim = trigram_similarity(mention_key, &rep_key);
378 sim * 0.7 }
380
381 fn types_compatible(&self, mention_type: MentionType, cluster: &ClusterMetadata) -> bool {
383 match cluster.entity_type {
384 Some(EntityType::Person) => {
385 true
387 }
388 Some(EntityType::Organization) | Some(EntityType::Location) => {
389 mention_type != MentionType::Pronominal
391 }
392 _ => true,
393 }
394 }
395
396 fn update_cache_on_access(&mut self, cluster_id: u64) {
398 match self.policy {
399 MemoryPolicy::DualCache { l_cache_size, .. } => {
400 self.l_cache.retain(|&id| id != cluster_id);
402 self.l_cache.push_front(cluster_id);
403
404 if let Some(cluster) = self.clusters.get(&cluster_id) {
406 let access_count = cluster.access_count;
407 self.g_cache.retain(|&id| id != cluster_id);
408
409 let pos = self.g_cache.iter().position(|&id| {
411 self.clusters
412 .get(&id)
413 .map(|c| c.access_count < access_count)
414 .unwrap_or(true)
415 });
416 match pos {
417 Some(p) => self.g_cache.insert(p, cluster_id),
418 None => self.g_cache.push(cluster_id),
419 }
420 }
421
422 while self.l_cache.len() > l_cache_size {
424 self.l_cache.pop_back();
425 }
426 }
427 MemoryPolicy::LeastRecentlyUsed { .. } => {
428 }
430 MemoryPolicy::LeastFrequentlyUsed { .. } => {
431 }
433 MemoryPolicy::Unbounded => {}
434 }
435 }
436
437 fn maybe_evict(&mut self) {
439 match self.policy {
440 MemoryPolicy::LeastRecentlyUsed { max_clusters } => {
441 while self.clusters.len() > max_clusters {
442 let lru_id = self
444 .clusters
445 .iter()
446 .min_by_key(|(_, c)| c.last_accessed_window)
447 .map(|(&id, _)| id);
448
449 if let Some(id) = lru_id {
450 self.clusters.remove(&id);
451 } else {
452 break;
453 }
454 }
455 }
456 MemoryPolicy::LeastFrequentlyUsed { max_clusters } => {
457 while self.clusters.len() > max_clusters {
458 let lfu_id = self
460 .clusters
461 .iter()
462 .min_by_key(|(_, c)| c.access_count)
463 .map(|(&id, _)| id);
464
465 if let Some(id) = lfu_id {
466 self.clusters.remove(&id);
467 } else {
468 break;
469 }
470 }
471 }
472 MemoryPolicy::DualCache { g_cache_size, .. } => {
473 while self.g_cache.len() > g_cache_size {
475 if let Some(id) = self.g_cache.pop() {
476 if !self.l_cache.contains(&id) {
478 self.clusters.remove(&id);
479 }
480 }
481 }
482 }
483 MemoryPolicy::Unbounded => {}
484 }
485 }
486
487 pub fn advance_window(&mut self) {
489 self.current_window += 1;
490 }
491
492 pub fn to_chains(&self) -> Vec<CorefChain> {
494 self.clusters
495 .values()
496 .map(|cluster| {
497 let mentions: Vec<Mention> = cluster
498 .mentions
499 .iter()
500 .map(|m| {
501 let mut mention = Mention::new(&m.text, m.start, m.end);
502 mention.mention_type = Some(m.mention_type);
503 mention
504 })
505 .collect();
506
507 let mut chain = CorefChain::new(mentions);
508 chain.cluster_id = Some(cluster.id.into());
509 chain.entity_type = cluster.entity_type.as_ref().map(|t| format!("{:?}", t));
510 chain
511 })
512 .collect()
513 }
514
515 pub fn cluster_count(&self) -> usize {
517 self.clusters.len()
518 }
519
520 pub fn mention_count(&self) -> usize {
522 self.clusters.values().map(|c| c.mentions.len()).sum()
523 }
524}
525
526#[derive(Debug)]
534pub struct IncrementalCorefResolver {
535 config: IncrementalConfig,
536}
537
538impl Default for IncrementalCorefResolver {
539 fn default() -> Self {
540 Self::new(IncrementalConfig::default())
541 }
542}
543
544impl IncrementalCorefResolver {
545 pub fn new(config: IncrementalConfig) -> Self {
547 Self { config }
548 }
549
550 pub fn resolve_document(&self, text: &str) -> Vec<CorefChain> {
554 let mut memory = EntityMemory::new(self.config.memory_policy);
555 let windows = self.split_into_windows(text);
556
557 for (window_idx, (window_text, window_offset)) in windows.iter().enumerate() {
558 let mentions = self.extract_mentions(window_text, *window_offset);
560
561 for mention in mentions {
563 self.process_mention(&mut memory, &mention);
564 }
565
566 memory.advance_window();
567
568 if self.config.grouped_window_size > 0
570 && (window_idx + 1) % self.config.grouped_window_size == 0
571 {
572 self.expand_grouped_window(&mut memory, window_idx);
573 }
574 }
575
576 memory.to_chains()
577 }
578
579 pub fn resolve_entities(&self, entities: &[Entity]) -> Vec<Entity> {
583 let mut memory = EntityMemory::new(self.config.memory_policy);
584 let mut resolved = entities.to_vec();
585
586 let mut current_window_start = 0usize;
588 let mut window_idx = 0;
589
590 for (i, entity) in entities.iter().enumerate() {
591 if entity.start() >= current_window_start + self.config.window_size {
593 window_idx += 1;
594 current_window_start = entity.start().saturating_sub(self.config.window_overlap);
595 memory.advance_window();
596 }
597
598 let mention = MentionRecord {
599 text: entity.text.clone(),
600 start: entity.start(),
601 end: entity.end(),
602 window_index: window_idx,
603 mention_type: self.classify_mention_type(&entity.text),
604 };
605
606 let cluster_id = self.process_mention(&mut memory, &mention);
607 resolved[i].canonical_id = Some(cluster_id.into());
608 }
609
610 resolved
611 }
612
613 fn split_into_windows(&self, text: &str) -> Vec<(String, usize)> {
615 let mut windows = Vec::new();
616 let mut offset = 0;
617
618 if self.config.token_based {
619 #[derive(Debug, Clone, Copy)]
622 struct TokenSpan {
623 start_char: usize,
624 end_char: usize,
625 }
626
627 fn tokenize_with_char_offsets(text: &str) -> Vec<TokenSpan> {
628 let mut tokens = Vec::new();
629
630 let mut in_word = false;
631 let mut word_start_char = 0;
632 let mut char_pos = 0;
633
634 for c in text.chars() {
635 if c.is_whitespace() {
636 if in_word {
637 tokens.push(TokenSpan {
638 start_char: word_start_char,
639 end_char: char_pos,
640 });
641 in_word = false;
642 }
643 } else if !in_word {
644 in_word = true;
645 word_start_char = char_pos;
646 }
647 char_pos += 1;
648 }
649
650 if in_word {
651 tokens.push(TokenSpan {
652 start_char: word_start_char,
653 end_char: char_pos,
654 });
655 }
656
657 tokens
658 }
659
660 let tokens = tokenize_with_char_offsets(text);
661 let step = self
662 .config
663 .window_size
664 .saturating_sub(self.config.window_overlap);
665
666 while offset < tokens.len() {
667 let end = (offset + self.config.window_size).min(tokens.len());
668 if end == 0 || offset >= end {
669 break;
670 }
671
672 let char_start = tokens[offset].start_char;
673 let char_end = tokens[end - 1].end_char;
674 let window_text = anno::offset::TextSpan::from_chars(text, char_start, char_end)
675 .extract(text)
676 .to_string();
677
678 windows.push((window_text, char_start));
679
680 if end >= tokens.len() {
681 break;
682 }
683 offset += step.max(1);
684 }
685 } else {
686 let text_char_len = text.chars().count();
688 let step = self
689 .config
690 .window_size
691 .saturating_sub(self.config.window_overlap);
692
693 while offset < text_char_len {
694 let end = (offset + self.config.window_size).min(text_char_len);
695
696 let adjusted_end = if end < text_char_len {
699 let end_byte = anno::offset::TextSpan::from_chars(text, end, end).byte_start;
700 let mut adjusted_end_byte = end_byte;
701
702 if let Some(ws_byte_pos) = text[..end_byte].rfind(char::is_whitespace) {
703 let ws_len = text[ws_byte_pos..]
705 .chars()
706 .next()
707 .map(|c| c.len_utf8())
708 .unwrap_or(1);
709 adjusted_end_byte = ws_byte_pos + ws_len;
710
711 while adjusted_end_byte < end_byte {
713 match text[adjusted_end_byte..].chars().next() {
714 Some(c) if c.is_whitespace() => {
715 adjusted_end_byte += c.len_utf8();
716 }
717 _ => break,
718 }
719 }
720 }
721
722 let (adjusted_end_char, _) =
723 anno::offset::bytes_to_chars(text, adjusted_end_byte, adjusted_end_byte);
724 if adjusted_end_char > offset {
725 adjusted_end_char.min(text_char_len)
726 } else {
727 end
728 }
729 } else {
730 end
731 };
732
733 let window_text = anno::offset::TextSpan::from_chars(text, offset, adjusted_end)
734 .extract(text)
735 .to_string();
736 windows.push((window_text, offset));
737
738 if adjusted_end >= text_char_len {
739 break;
740 }
741 offset += step.max(1);
742 }
743 }
744
745 windows
746 }
747
748 fn extract_mentions(&self, text: &str, offset: usize) -> Vec<MentionRecord> {
753 let mut mentions = Vec::new();
754
755 let mut in_word = false;
757 let mut word_start_byte = 0;
758 let mut word_start_char = 0;
759 let mut char_pos = 0;
760
761 let mut maybe_push_mention = |word: &str, local_start: usize, local_end: usize| {
762 let mention_type = self.classify_mention_type(word);
763
764 let should_track = match mention_type {
766 MentionType::Pronominal => true,
767 MentionType::Proper => true,
768 _ => word
769 .chars()
770 .next()
771 .map(|c| c.is_uppercase())
772 .unwrap_or(false),
773 };
774
775 if should_track {
776 mentions.push(MentionRecord {
777 text: word.to_string(),
778 start: offset + local_start,
779 end: offset + local_end,
780 window_index: 0, mention_type,
782 });
783 }
784 };
785
786 for (byte_idx, c) in text.char_indices() {
787 if c.is_whitespace() {
788 if in_word {
789 let word = &text[word_start_byte..byte_idx];
790 maybe_push_mention(word, word_start_char, char_pos);
791 in_word = false;
792 }
793 } else if !in_word {
794 in_word = true;
795 word_start_byte = byte_idx;
796 word_start_char = char_pos;
797 }
798 char_pos += 1;
799 }
800
801 if in_word {
802 let word = &text[word_start_byte..];
803 maybe_push_mention(word, word_start_char, char_pos);
804 }
805
806 mentions
807 }
808
809 fn classify_mention_type(&self, text: &str) -> MentionType {
811 let lower = text.to_lowercase();
812
813 let pronouns = [
815 "he",
816 "him",
817 "his",
818 "himself",
819 "she",
820 "her",
821 "hers",
822 "herself",
823 "they",
824 "them",
825 "their",
826 "theirs",
827 "themself",
828 "themselves",
829 "it",
830 "its",
831 "itself",
832 "i",
833 "me",
834 "my",
835 "mine",
836 "myself",
837 "we",
838 "us",
839 "our",
840 "ours",
841 "ourselves",
842 "you",
843 "your",
844 "yours",
845 "yourself",
846 "yourselves",
847 "xe",
849 "xem",
850 "xyr",
851 "xyrs",
852 "ze",
853 "zir",
854 "zirs",
855 "ey",
856 "em",
857 "eir",
858 "eirs",
859 ];
860
861 if pronouns.contains(&lower.as_str()) {
862 return MentionType::Pronominal;
863 }
864
865 if text
867 .chars()
868 .next()
869 .map(|c| c.is_uppercase())
870 .unwrap_or(false)
871 {
872 return MentionType::Proper;
873 }
874
875 if lower.starts_with("the ") {
877 return MentionType::Nominal;
878 }
879
880 MentionType::Unknown
881 }
882
883 fn process_mention(&self, memory: &mut EntityMemory, mention: &MentionRecord) -> u64 {
885 if let Some(cluster_id) = memory.find_best_match(
887 &mention.text,
888 mention.mention_type,
889 self.config.similarity_threshold,
890 self.config.use_exact_match,
891 self.config.use_substring_match,
892 ) {
893 memory.add_to_cluster(cluster_id, mention);
894 cluster_id
895 } else {
896 let entity_type = if mention.mention_type == MentionType::Proper {
898 Some(EntityType::Person) } else {
900 None
901 };
902 memory.create_cluster(mention, entity_type)
903 }
904 }
905
906 fn expand_grouped_window(&self, memory: &mut EntityMemory, _window_idx: usize) {
911 let cluster_ids: Vec<u64> = memory.clusters.keys().copied().collect();
918
919 for i in 0..cluster_ids.len() {
920 for j in (i + 1)..cluster_ids.len() {
921 let id_i = cluster_ids[i];
922 let id_j = cluster_ids[j];
923
924 if self.should_merge_clusters(memory, id_i, id_j) {
926 if let Some(cluster_j) = memory.clusters.remove(&id_j) {
928 if let Some(cluster_i) = memory.clusters.get_mut(&id_i) {
929 cluster_i.mentions.extend(cluster_j.mentions);
930 cluster_i.access_count += cluster_j.access_count;
931 }
932 }
933 }
934 }
935 }
936 }
937
938 fn should_merge_clusters(&self, memory: &EntityMemory, id_a: u64, id_b: u64) -> bool {
940 let (cluster_a, cluster_b) = match (memory.clusters.get(&id_a), memory.clusters.get(&id_b))
941 {
942 (Some(a), Some(b)) => (a, b),
943 _ => return false,
944 };
945
946 if cluster_a.entity_type.is_some()
948 && cluster_b.entity_type.is_some()
949 && cluster_a.entity_type != cluster_b.entity_type
950 {
951 return false;
952 }
953
954 let rep_a = name_key(&cluster_a.representative);
956 let rep_b = name_key(&cluster_b.representative);
957
958 if rep_a == rep_b {
960 return true;
961 }
962
963 if rep_a.contains(&rep_b) || rep_b.contains(&rep_a) {
965 return true;
966 }
967
968 if trigram_similarity(&rep_a, &rep_b) > 0.8 {
970 return true;
971 }
972
973 false
974 }
975}
976
977fn name_key(s: &str) -> String {
986 use std::borrow::Cow;
987
988 fn strip_bom_only(s: &str) -> Cow<'_, str> {
991 if !s.chars().any(|c| c == '\u{FEFF}') {
992 return Cow::Borrowed(s);
993 }
994 Cow::Owned(s.chars().filter(|&c| c != '\u{FEFF}').collect())
995 }
996
997 let s = strip_bom_only(s);
998 let cfg = textprep::ScrubConfig {
999 normalize_newlines: true,
1000 remove_zero_width: false,
1001 remove_bidi_controls: true,
1002 collapse_whitespace: true,
1003 normalization: textprep::ScrubNormalization::Nfc,
1004 case: textprep::ScrubCase::Lower,
1005 strip_diacritics: false,
1006 };
1007 textprep::scrub_with(s.as_ref(), &cfg)
1008}
1009
1010fn trigram_similarity(a: &str, b: &str) -> f64 {
1014 if a.is_empty() || b.is_empty() {
1015 return 0.0;
1016 }
1017 if a.chars().count() < 3 || b.chars().count() < 3 {
1019 return 0.0;
1020 }
1021 textprep::similarity::trigram_jaccard(a, b)
1022}
1023
1024#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1030pub struct IncrementalStats {
1031 pub windows_processed: usize,
1033 pub mentions_processed: usize,
1035 pub final_clusters: usize,
1037 pub clusters_evicted: usize,
1039 pub avg_mentions_per_cluster: f64,
1041 pub max_cluster_size: usize,
1043}
1044
1045impl IncrementalStats {
1046 pub fn from_memory(memory: &EntityMemory, windows: usize) -> Self {
1048 let cluster_sizes: Vec<usize> =
1049 memory.clusters.values().map(|c| c.mentions.len()).collect();
1050
1051 Self {
1052 windows_processed: windows,
1053 mentions_processed: memory.mention_count(),
1054 final_clusters: memory.cluster_count(),
1055 clusters_evicted: 0, avg_mentions_per_cluster: if memory.cluster_count() > 0 {
1057 memory.mention_count() as f64 / memory.cluster_count() as f64
1058 } else {
1059 0.0
1060 },
1061 max_cluster_size: cluster_sizes.into_iter().max().unwrap_or(0),
1062 }
1063 }
1064}
1065
1066#[cfg(test)]
1071mod tests {
1072 use super::*;
1073
1074 #[test]
1075 fn test_windowing_and_mentions_use_character_offsets_on_unicode() {
1076 use anno::offset::TextSpan;
1077
1078 let config = IncrementalConfig {
1079 token_based: false,
1080 window_size: 12,
1081 window_overlap: 3,
1082 ..Default::default()
1083 };
1084 let resolver = IncrementalCorefResolver::new(config);
1085
1086 let text = "🎉 Dr. John went to 東京. He waved.";
1088
1089 let windows = resolver.split_into_windows(text);
1090 assert!(!windows.is_empty());
1091
1092 for (window_text, window_offset) in &windows {
1095 let span = TextSpan::from_chars(
1096 text,
1097 *window_offset,
1098 *window_offset + window_text.chars().count(),
1099 );
1100 assert_eq!(span.extract(text), window_text);
1101 }
1102
1103 let mentions = resolver.extract_mentions(text, 0);
1105 let john = mentions
1106 .iter()
1107 .find(|m| m.text == "John")
1108 .expect("expected to detect 'John'");
1109 let extracted = TextSpan::from_chars(text, john.start, john.end).extract(text);
1110 assert_eq!(extracted, "John");
1111 assert_eq!(
1112 john.start, 6,
1113 "🎉(1) + space(1) + Dr.(2) + .(1) + space(1) = 6"
1114 );
1115 assert_eq!(john.end, 10);
1116 }
1117
1118 #[test]
1119 fn test_trigram_similarity() {
1120 assert!((trigram_similarity("hello", "hello") - 1.0).abs() < 0.001);
1121 assert!(trigram_similarity("hello", "world") < 0.3);
1122 assert!(trigram_similarity("john", "johnson") > 0.3);
1123 assert!(trigram_similarity("", "hello") == 0.0);
1124 }
1125
1126 #[test]
1127 fn test_entity_memory_basic() {
1128 let mut memory = EntityMemory::new(MemoryPolicy::Unbounded);
1129
1130 let mention1 = MentionRecord {
1131 text: "John Smith".to_string(),
1132 start: 0,
1133 end: 10,
1134 window_index: 0,
1135 mention_type: MentionType::Proper,
1136 };
1137
1138 let cluster_id = memory.create_cluster(&mention1, Some(EntityType::Person));
1139 assert_eq!(memory.cluster_count(), 1);
1140
1141 let mention2 = MentionRecord {
1142 text: "Smith".to_string(),
1143 start: 50,
1144 end: 55,
1145 window_index: 0,
1146 mention_type: MentionType::Proper,
1147 };
1148
1149 memory.add_to_cluster(cluster_id, &mention2);
1150 assert_eq!(memory.mention_count(), 2);
1151 }
1152
1153 #[test]
1154 fn test_entity_memory_lru_eviction() {
1155 let mut memory = EntityMemory::new(MemoryPolicy::LeastRecentlyUsed { max_clusters: 2 });
1156
1157 let mention1 = MentionRecord {
1158 text: "John".to_string(),
1159 start: 0,
1160 end: 4,
1161 window_index: 0,
1162 mention_type: MentionType::Proper,
1163 };
1164 memory.create_cluster(&mention1, None);
1165
1166 memory.advance_window();
1167
1168 let mention2 = MentionRecord {
1169 text: "Mary".to_string(),
1170 start: 10,
1171 end: 14,
1172 window_index: 1,
1173 mention_type: MentionType::Proper,
1174 };
1175 memory.create_cluster(&mention2, None);
1176
1177 memory.advance_window();
1178
1179 let mention3 = MentionRecord {
1180 text: "Bob".to_string(),
1181 start: 20,
1182 end: 23,
1183 window_index: 2,
1184 mention_type: MentionType::Proper,
1185 };
1186 memory.create_cluster(&mention3, None);
1187
1188 assert_eq!(memory.cluster_count(), 2);
1190 }
1191
1192 #[test]
1193 fn test_find_best_match() {
1194 let mut memory = EntityMemory::new(MemoryPolicy::Unbounded);
1195
1196 let mention1 = MentionRecord {
1197 text: "John Smith".to_string(),
1198 start: 0,
1199 end: 10,
1200 window_index: 0,
1201 mention_type: MentionType::Proper,
1202 };
1203 memory.create_cluster(&mention1, Some(EntityType::Person));
1204
1205 let match_result = memory.find_best_match("Smith", MentionType::Proper, 0.7, true, true);
1207 assert!(match_result.is_some());
1208 }
1209
1210 #[test]
1211 fn test_incremental_resolver_basic() {
1212 let config = IncrementalConfig {
1213 window_size: 100,
1214 window_overlap: 20,
1215 token_based: false,
1216 ..Default::default()
1217 };
1218
1219 let resolver = IncrementalCorefResolver::new(config);
1220
1221 let text = "John went to the store. He bought milk. John came home.";
1222 let chains = resolver.resolve_document(text);
1223
1224 assert!(!chains.is_empty());
1226 }
1227
1228 #[test]
1229 fn test_resolve_entities() {
1230 let resolver = IncrementalCorefResolver::default();
1231
1232 let entities = vec![
1233 Entity::new("John Smith", EntityType::Person, 0, 10, 0.9),
1234 Entity::new("Smith", EntityType::Person, 50, 55, 0.85),
1235 Entity::new("he", EntityType::Person, 100, 102, 0.7),
1236 ];
1237
1238 let resolved = resolver.resolve_entities(&entities);
1239
1240 assert_eq!(resolved[0].canonical_id, resolved[1].canonical_id);
1242 }
1243
1244 #[test]
1245 fn test_mention_type_classification() {
1246 let resolver = IncrementalCorefResolver::default();
1247
1248 assert_eq!(
1249 resolver.classify_mention_type("he"),
1250 MentionType::Pronominal
1251 );
1252 assert_eq!(
1253 resolver.classify_mention_type("she"),
1254 MentionType::Pronominal
1255 );
1256 assert_eq!(
1257 resolver.classify_mention_type("they"),
1258 MentionType::Pronominal
1259 );
1260 assert_eq!(
1261 resolver.classify_mention_type("xe"),
1262 MentionType::Pronominal
1263 );
1264 assert_eq!(resolver.classify_mention_type("John"), MentionType::Proper);
1265 }
1266
1267 #[test]
1268 fn test_dual_cache_policy() {
1269 let memory = EntityMemory::new(MemoryPolicy::DualCache {
1270 l_cache_size: 5,
1271 g_cache_size: 10,
1272 });
1273 assert_eq!(memory.cluster_count(), 0);
1274 }
1275}