1use super::registry::{LabelDefinition, SemanticRegistry};
6use super::RelationTriple;
7use crate::{Entity, EntityType};
8use anno_core::Relation;
9
10#[derive(Debug, Clone)]
12pub struct RelationExtractionConfig {
13 pub max_span_distance: usize,
15 pub threshold: f32,
17 pub extract_triggers: bool,
19}
20
21impl Default for RelationExtractionConfig {
22 fn default() -> Self {
23 Self {
24 max_span_distance: 50,
25 threshold: 0.5,
26 extract_triggers: true,
27 }
28 }
29}
30
31pub fn extract_relations(
45 entities: &[Entity],
46 text: &str,
47 registry: &SemanticRegistry,
48 config: &RelationExtractionConfig,
49) -> Vec<Relation> {
50 let mut relations = Vec::new();
51 let span_converter = crate::offset::SpanConverter::new(text);
54
55 let relation_labels: Vec<_> = registry.relation_labels().collect();
57 if relation_labels.is_empty() {
58 return relations;
59 }
60
61 for (i, head) in entities.iter().enumerate() {
63 for (j, tail) in entities.iter().enumerate() {
64 if i == j {
65 continue;
66 }
67
68 let distance = if head.end <= tail.start {
70 tail.start - head.end
71 } else {
72 head.start.saturating_sub(tail.end)
73 };
74
75 if distance > config.max_span_distance {
76 continue;
77 }
78
79 let (span_start, span_end) = if head.end <= tail.start {
81 (head.end, tail.start)
82 } else {
83 (tail.end, head.start)
84 };
85
86 let between_span = span_converter.from_chars(span_start, span_end);
87 let between_text = text
88 .get(between_span.byte_start..between_span.byte_end)
89 .unwrap_or("");
90
91 let relation_type = detect_relation_type(head, tail, between_text, &relation_labels);
93
94 if let Some((rel_type, mut confidence, trigger)) = relation_type {
95 let distance_penalty = if distance < config.max_span_distance {
98 let penalty_factor =
99 1.0 - (distance as f64 / config.max_span_distance as f64) * 0.5;
100 penalty_factor.max(0.5) } else {
102 0.5 };
104 confidence *= distance_penalty;
105
106 if confidence < config.threshold as f64 {
107 continue;
108 }
109
110 let trigger_span = if config.extract_triggers {
112 trigger.map(|(s, e)| {
113 let trigger_start_byte = between_span.byte_start.saturating_add(s);
114 let trigger_end_byte = between_span.byte_start.saturating_add(e);
115 (
116 span_converter.byte_to_char(trigger_start_byte),
117 span_converter.byte_to_char(trigger_end_byte),
118 )
119 })
120 } else {
121 None
122 };
123
124 relations.push(Relation {
125 head: head.clone(),
126 tail: tail.clone(),
127 relation_type: rel_type.to_string(),
128 trigger_span,
129 confidence: confidence.clamp(0.0, 1.0), });
131 }
132 }
133 }
134
135 relations
136}
137
138#[must_use]
147pub fn extract_relation_triples(
148 entities: &[Entity],
149 text: &str,
150 registry: &SemanticRegistry,
151 config: &RelationExtractionConfig,
152) -> Vec<RelationTriple> {
153 let mut triples = Vec::new();
154 if entities.len() < 2 {
155 return triples;
156 }
157
158 let span_converter = crate::offset::SpanConverter::new(text);
160
161 let relation_labels: Vec<_> = registry.relation_labels().collect();
162 if relation_labels.is_empty() {
163 return triples;
164 }
165
166 for (i, head) in entities.iter().enumerate() {
167 for (j, tail) in entities.iter().enumerate() {
168 if i == j {
169 continue;
170 }
171
172 if head.start < tail.end && tail.start < head.end {
174 continue;
175 }
176
177 let distance = if head.end <= tail.start {
179 tail.start - head.end
180 } else {
181 head.start.saturating_sub(tail.end)
182 };
183 if distance > config.max_span_distance {
184 continue;
185 }
186
187 let (span_start, span_end) = if head.end <= tail.start {
188 (head.end, tail.start)
189 } else {
190 (tail.end, head.start)
191 };
192
193 let between_span = span_converter.from_chars(span_start, span_end);
194 let between_text = text
195 .get(between_span.byte_start..between_span.byte_end)
196 .unwrap_or("");
197
198 if let Some((rel_type, mut confidence, _trigger)) =
199 detect_relation_type(head, tail, between_text, &relation_labels)
200 {
201 let distance_penalty = if distance < config.max_span_distance {
203 let penalty_factor =
204 1.0 - (distance as f64 / config.max_span_distance as f64) * 0.5;
205 penalty_factor.max(0.5)
206 } else {
207 0.5
208 };
209 confidence *= distance_penalty;
210
211 if confidence < config.threshold as f64 {
212 continue;
213 }
214
215 triples.push(RelationTriple {
216 head_idx: i,
217 tail_idx: j,
218 relation_type: rel_type.to_string(),
219 confidence: confidence as f32,
220 });
221 }
222 }
223 }
224
225 triples
226}
227
228type RelationMatch<'a> = (&'a str, f64, Option<(usize, usize)>);
230
231fn detect_relation_type<'a>(
233 head: &Entity,
234 tail: &Entity,
235 between_text: &str,
236 relation_labels: &[&'a LabelDefinition],
237) -> Option<RelationMatch<'a>> {
238 let between_lower = between_text.to_lowercase();
241
242 fn norm_rel_slug(s: &str) -> String {
246 let mut out = String::with_capacity(s.len());
248 let mut prev_underscore = false;
249 for ch in s.chars() {
250 if ch.is_alphanumeric() {
251 if ch.is_ascii_alphabetic() {
253 out.push(ch.to_ascii_uppercase());
254 } else {
255 out.push(ch);
256 }
257 prev_underscore = false;
258 } else if !prev_underscore {
259 out.push('_');
260 prev_underscore = true;
261 }
262 }
263 while out.starts_with('_') {
264 out.remove(0);
265 }
266 while out.ends_with('_') {
267 out.pop();
268 }
269 out
270 }
271
272 struct RelPattern {
274 slug: &'static str,
275 triggers: &'static [&'static str],
276 confidence: f64,
277 }
278
279 let patterns: &[RelPattern] = &[
280 RelPattern {
282 slug: "CEO_OF",
283 triggers: &[
284 "ceo of",
285 "chief executive",
286 "chief executive officer",
287 "leads",
288 "founded",
289 "founder of",
290 ],
291 confidence: 0.8,
292 },
293 RelPattern {
294 slug: "WORKS_FOR",
295 triggers: &[
296 "works for",
297 "works at",
298 "employed by",
299 "employee of",
300 "works with",
301 "staff at",
302 "member of",
303 ],
304 confidence: 0.7,
305 },
306 RelPattern {
307 slug: "FOUNDED",
308 triggers: &[
309 "founded",
310 "co-founded",
311 "cofounder",
312 "started",
313 "established",
314 "created",
315 "launched",
316 ],
317 confidence: 0.8,
318 },
319 RelPattern {
320 slug: "MANAGES",
321 triggers: &[
322 "manages",
323 "managing",
324 "oversees",
325 "directs",
326 "supervises",
327 "runs",
328 ],
329 confidence: 0.75,
330 },
331 RelPattern {
332 slug: "REPORTS_TO",
333 triggers: &["reports to", "reported to", "under", "reports directly to"],
334 confidence: 0.7,
335 },
336 RelPattern {
338 slug: "LOCATED_IN",
339 triggers: &[
340 "in",
341 "at",
342 "based in",
343 "located in",
344 "headquartered in",
345 "situated in",
346 "found in",
347 ],
348 confidence: 0.6,
349 },
350 RelPattern {
351 slug: "BORN_IN",
352 triggers: &[
353 "born in",
354 "native of",
355 "from",
356 "hails from",
357 "originated in",
358 ],
359 confidence: 0.7,
360 },
361 RelPattern {
362 slug: "LIVES_IN",
363 triggers: &["lives in", "resides in", "living in", "based in"],
364 confidence: 0.65,
365 },
366 RelPattern {
367 slug: "DIED_IN",
368 triggers: &["died in", "passed away in", "deceased in"],
369 confidence: 0.8,
370 },
371 RelPattern {
373 slug: "OCCURRED_ON",
374 triggers: &["on", "occurred on", "happened on", "took place on", "dated"],
375 confidence: 0.6,
376 },
377 RelPattern {
378 slug: "STARTED_ON",
379 triggers: &["started on", "began on", "commenced on", "initiated on"],
380 confidence: 0.7,
381 },
382 RelPattern {
383 slug: "ENDED_ON",
384 triggers: &["ended on", "concluded on", "finished on", "completed on"],
385 confidence: 0.7,
386 },
387 RelPattern {
389 slug: "PART_OF",
390 triggers: &[
391 "part of",
392 "member of",
393 "belongs to",
394 "subsidiary of",
395 "division of",
396 "branch of",
397 ],
398 confidence: 0.7,
399 },
400 RelPattern {
401 slug: "ACQUIRED",
402 triggers: &[
403 "acquired",
404 "bought",
405 "purchased",
406 "took over",
407 "merged with",
408 ],
409 confidence: 0.75,
410 },
411 RelPattern {
412 slug: "MERGED_WITH",
413 triggers: &["merged with", "merged into", "combined with", "joined with"],
414 confidence: 0.8,
415 },
416 RelPattern {
417 slug: "PARENT_OF",
418 triggers: &["parent of", "parent company of", "owns", "owner of"],
419 confidence: 0.75,
420 },
421 RelPattern {
423 slug: "MARRIED_TO",
424 triggers: &["married to", "wed to", "spouse of", "husband of", "wife of"],
425 confidence: 0.85,
426 },
427 RelPattern {
428 slug: "CHILD_OF",
429 triggers: &["son of", "daughter of", "child of", "offspring of"],
430 confidence: 0.8,
431 },
432 RelPattern {
433 slug: "SIBLING_OF",
434 triggers: &["brother of", "sister of", "sibling of"],
435 confidence: 0.8,
436 },
437 RelPattern {
439 slug: "STUDIED_AT",
440 triggers: &[
441 "studied at",
442 "attended",
443 "graduated from",
444 "alumni of",
445 "educated at",
446 ],
447 confidence: 0.75,
448 },
449 RelPattern {
450 slug: "TEACHES_AT",
451 triggers: &["teaches at", "professor at", "instructor at", "faculty at"],
452 confidence: 0.8,
453 },
454 RelPattern {
456 slug: "DEVELOPS",
457 triggers: &[
458 "develops",
459 "created",
460 "built",
461 "designed",
462 "produces",
463 "manufactures",
464 ],
465 confidence: 0.7,
466 },
467 RelPattern {
468 slug: "USES",
469 triggers: &["uses", "utilizes", "employs", "adopts", "implements"],
470 confidence: 0.6,
471 },
472 RelPattern {
478 slug: "NAMED",
479 triggers: &[
480 "called",
481 "known as",
482 "also known as",
483 "named",
484 "referred to as",
485 "nickname",
486 ],
487 confidence: 0.6,
488 },
489 RelPattern {
490 slug: "TYPE_OF",
491 triggers: &[
492 "type of",
493 "kind of",
494 "form of",
495 "a type of",
496 "is a",
497 "are a",
498 ],
499 confidence: 0.6,
500 },
501 RelPattern {
502 slug: "RELATED_TO",
503 triggers: &["related to", "associated with", "connected to", "linked to"],
504 confidence: 0.55,
505 },
506 RelPattern {
507 slug: "ORIGIN",
508 triggers: &[
509 "from",
510 "born",
511 "originated",
512 "created by",
513 "invented by",
514 "derived from",
515 "spinoff",
516 "spin-off",
517 ],
518 confidence: 0.55,
519 },
520 RelPattern {
521 slug: "ROLE",
522 triggers: &[
523 "president",
524 "ceo",
525 "chair",
526 "director",
527 "editor",
528 "producer",
529 "actor",
530 "professor",
531 "fellow",
532 "member",
533 ],
534 confidence: 0.55,
535 },
536 RelPattern {
537 slug: "TEMPORAL",
538 triggers: &[
539 "in 19", "in 20", "during", "before", "after", "between", "until", "since",
540 ],
541 confidence: 0.5,
542 },
543 RelPattern {
544 slug: "PHYSICAL",
545 triggers: &["located in", "based in", "headquartered in", "at "],
546 confidence: 0.55,
547 },
548 RelPattern {
549 slug: "TOPIC",
550 triggers: &["topic", "about", "on", "regarding", "focused on"],
551 confidence: 0.5,
552 },
553 RelPattern {
554 slug: "OPPOSITE",
555 triggers: &["opposite", "contrasts with", "as opposed to"],
556 confidence: 0.6,
557 },
558 RelPattern {
559 slug: "WIN_DEFEAT",
560 triggers: &["defeated", "beat", "won", "win", "lose", "lost to"],
561 confidence: 0.6,
562 },
563 RelPattern {
564 slug: "CAUSE_EFFECT",
565 triggers: &["caused", "causes", "leads to", "results in", "because"],
566 confidence: 0.55,
567 },
568 RelPattern {
569 slug: "USAGE",
570 triggers: &["use", "uses", "used", "using", "utilize", "employ", "adopt"],
571 confidence: 0.55,
572 },
573 RelPattern {
574 slug: "ARTIFACT",
575 triggers: &[
576 "tool",
577 "library",
578 "framework",
579 "system",
580 "artifact",
581 "implementation",
582 ],
583 confidence: 0.55,
584 },
585 RelPattern {
586 slug: "COMPARE",
587 triggers: &[
588 "compare",
589 "compared to",
590 "versus",
591 "vs",
592 "better than",
593 "worse than",
594 ],
595 confidence: 0.55,
596 },
597 RelPattern {
598 slug: "GENERAL_AFFILIATION",
599 triggers: &[
600 "affiliation",
601 "affiliated with",
602 "member of",
603 "part of",
604 "associated with",
605 ],
606 confidence: 0.55,
607 },
608 RelPattern {
610 slug: "父母",
611 triggers: &["父", "母", "父母"],
612 confidence: 0.7,
613 },
614 RelPattern {
615 slug: "兄弟",
616 triggers: &["兄", "弟", "兄弟"],
617 confidence: 0.7,
618 },
619 RelPattern {
620 slug: "別名",
621 triggers: &["別名", "别名"],
622 confidence: 0.75,
623 },
624 RelPattern {
625 slug: "到達",
626 triggers: &["到", "至", "達", "到達", "到达"],
627 confidence: 0.6,
628 },
629 RelPattern {
630 slug: "出生於某地",
631 triggers: &["生於", "生于", "出生於", "出生于"],
632 confidence: 0.65,
633 },
634 RelPattern {
635 slug: "任職",
636 triggers: &["任", "拜", "任職", "任职"],
637 confidence: 0.6,
638 },
639 RelPattern {
640 slug: "管理",
641 triggers: &["管", "治", "守", "管理"],
642 confidence: 0.55,
643 },
644 RelPattern {
645 slug: "駐守",
646 triggers: &["駐", "驻", "守", "駐守", "驻守"],
647 confidence: 0.55,
648 },
649 RelPattern {
650 slug: "敵對攻伐",
651 triggers: &["敵", "敌", "攻", "伐", "戰", "战"],
652 confidence: 0.55,
653 },
654 RelPattern {
655 slug: "同僚",
656 triggers: &["同僚"],
657 confidence: 0.55,
658 },
659 RelPattern {
660 slug: "政治奧援",
661 triggers: &["奧援", "奥援"],
662 confidence: 0.55,
663 },
664 RelPattern {
666 slug: "MET_WITH",
667 triggers: &["met with", "met", "met up with", "encountered", "saw"],
668 confidence: 0.65,
669 },
670 RelPattern {
671 slug: "SPOKE_WITH",
672 triggers: &[
673 "spoke with",
674 "talked with",
675 "discussed with",
676 "conversed with",
677 ],
678 confidence: 0.7,
679 },
680 RelPattern {
682 slug: "OWNS",
683 triggers: &["owns", "owner of", "possesses", "holds"],
684 confidence: 0.75,
685 },
686 RelPattern {
691 slug: "WORKS_FOR",
692 triggers: &["trabaja en", "trabaja para", "empleado de", "trabaja con"],
693 confidence: 0.7,
694 },
695 RelPattern {
696 slug: "FOUNDED",
697 triggers: &["fundó", "fundada", "creó", "creada", "estableció", "inició"],
698 confidence: 0.8,
699 },
700 RelPattern {
701 slug: "LOCATED_IN",
702 triggers: &[
703 "en",
704 "ubicado en",
705 "situado en",
706 "basado en",
707 "localizado en",
708 ],
709 confidence: 0.6,
710 },
711 RelPattern {
712 slug: "BORN_IN",
713 triggers: &["nació en", "nacido en", "originario de", "de"],
714 confidence: 0.7,
715 },
716 RelPattern {
717 slug: "LIVES_IN",
718 triggers: &["cerno en", "reside en", "viviendo en"],
719 confidence: 0.65,
720 },
721 RelPattern {
722 slug: "MARRIED_TO",
723 triggers: &["casado con", "casada con", "esposo de", "esposa de"],
724 confidence: 0.85,
725 },
726 RelPattern {
728 slug: "WORKS_FOR",
729 triggers: &[
730 "travaille pour",
731 "travaille à",
732 "employé de",
733 "travaille avec",
734 ],
735 confidence: 0.7,
736 },
737 RelPattern {
738 slug: "FOUNDED",
739 triggers: &["fondé", "fondée", "créé", "créée", "établi", "établie"],
740 confidence: 0.8,
741 },
742 RelPattern {
743 slug: "LOCATED_IN",
744 triggers: &["dans", "à", "situé en", "basé en", "localisé en"],
745 confidence: 0.6,
746 },
747 RelPattern {
748 slug: "BORN_IN",
749 triggers: &["né en", "née en", "originaire de", "de"],
750 confidence: 0.7,
751 },
752 RelPattern {
753 slug: "LIVES_IN",
754 triggers: &["vit en", "réside en", "vivant en"],
755 confidence: 0.65,
756 },
757 RelPattern {
758 slug: "MARRIED_TO",
759 triggers: &["marié avec", "mariée avec", "époux de", "épouse de"],
760 confidence: 0.85,
761 },
762 RelPattern {
764 slug: "WORKS_FOR",
765 triggers: &[
766 "arbeitet für",
767 "arbeitet bei",
768 "angestellt bei",
769 "arbeitet mit",
770 ],
771 confidence: 0.7,
772 },
773 RelPattern {
774 slug: "FOUNDED",
775 triggers: &[
776 "gegründet",
777 "gründete",
778 "erstellt",
779 "errichtet",
780 "etabliert",
781 ],
782 confidence: 0.8,
783 },
784 RelPattern {
785 slug: "LOCATED_IN",
786 triggers: &["in", "bei", "situiert in", "basiert in", "befindet sich in"],
787 confidence: 0.6,
788 },
789 RelPattern {
790 slug: "BORN_IN",
791 triggers: &["geboren in", "geboren am", "stammt aus", "aus"],
792 confidence: 0.7,
793 },
794 RelPattern {
795 slug: "LIVES_IN",
796 triggers: &["lebt in", "wohnt in", "lebend in"],
797 confidence: 0.65,
798 },
799 RelPattern {
800 slug: "MARRIED_TO",
801 triggers: &["verheiratet mit", "ehemann von", "ehefrau von"],
802 confidence: 0.85,
803 },
804 RelPattern {
806 slug: "WORKS_FOR",
807 triggers: &["为", "在", "工作于", "就职于", "任职于"],
808 confidence: 0.7,
809 },
810 RelPattern {
811 slug: "FOUNDED",
812 triggers: &["创立", "创建", "建立", "成立", "创办"],
813 confidence: 0.8,
814 },
815 RelPattern {
816 slug: "LOCATED_IN",
817 triggers: &["在", "位于", "坐落于", "地处"],
818 confidence: 0.6,
819 },
820 RelPattern {
821 slug: "BORN_IN",
822 triggers: &["出生于", "生于", "来自", "出生于"],
823 confidence: 0.7,
824 },
825 RelPattern {
826 slug: "LIVES_IN",
827 triggers: &["居住于", "住在", "生活在"],
828 confidence: 0.65,
829 },
830 RelPattern {
831 slug: "MARRIED_TO",
832 triggers: &["与...结婚", "嫁给", "娶了"],
833 confidence: 0.85,
834 },
835 RelPattern {
837 slug: "WORKS_FOR",
838 triggers: &["で働く", "に勤務", "に所属", "で就職"],
839 confidence: 0.7,
840 },
841 RelPattern {
842 slug: "FOUNDED",
843 triggers: &["設立", "創立", "設立した", "創設"],
844 confidence: 0.8,
845 },
846 RelPattern {
847 slug: "LOCATED_IN",
848 triggers: &["に", "で", "に位置", "に所在"],
849 confidence: 0.6,
850 },
851 RelPattern {
852 slug: "BORN_IN",
853 triggers: &["に生まれた", "の出身", "で生まれた"],
854 confidence: 0.7,
855 },
856 RelPattern {
857 slug: "LIVES_IN",
858 triggers: &["に住む", "に居住", "に在住"],
859 confidence: 0.65,
860 },
861 RelPattern {
862 slug: "MARRIED_TO",
863 triggers: &["と結婚", "と結婚した", "の配偶者"],
864 confidence: 0.85,
865 },
866 RelPattern {
868 slug: "WORKS_FOR",
869 triggers: &["يعمل في", "يعمل لصالح", "موظف في", "يعمل مع"],
870 confidence: 0.7,
871 },
872 RelPattern {
873 slug: "FOUNDED",
874 triggers: &["أسس", "أنشأ", "تأسست", "أنشأت"],
875 confidence: 0.8,
876 },
877 RelPattern {
878 slug: "LOCATED_IN",
879 triggers: &["في", "ب", "يقع في", "موجود في"],
880 confidence: 0.6,
881 },
882 RelPattern {
883 slug: "BORN_IN",
884 triggers: &["ولد في", "من مواليد", "من"],
885 confidence: 0.7,
886 },
887 RelPattern {
888 slug: "LIVES_IN",
889 triggers: &["يعيش في", "يسكن في", "مقيم في"],
890 confidence: 0.65,
891 },
892 RelPattern {
893 slug: "MARRIED_TO",
894 triggers: &["متزوج من", "زوج", "زوجة"],
895 confidence: 0.85,
896 },
897 RelPattern {
899 slug: "WORKS_FOR",
900 triggers: &["работает в", "работает на", "работает для", "сотрудник"],
901 confidence: 0.7,
902 },
903 RelPattern {
904 slug: "FOUNDED",
905 triggers: &["основал", "основала", "создал", "создала", "учредил"],
906 confidence: 0.8,
907 },
908 RelPattern {
909 slug: "LOCATED_IN",
910 triggers: &["в", "на", "расположен в", "находится в"],
911 confidence: 0.6,
912 },
913 RelPattern {
914 slug: "BORN_IN",
915 triggers: &["родился в", "родилась в", "родом из", "из"],
916 confidence: 0.7,
917 },
918 RelPattern {
919 slug: "LIVES_IN",
920 triggers: &["живет в", "проживает в", "живущий в"],
921 confidence: 0.65,
922 },
923 RelPattern {
924 slug: "MARRIED_TO",
925 triggers: &["женат на", "замужем за", "супруг", "супруга"],
926 confidence: 0.85,
927 },
928 ];
929
930 for pattern in patterns {
931 let label = match relation_labels.iter().find(|l| {
934 norm_rel_slug(&l.slug) == pattern.slug || l.slug.eq_ignore_ascii_case(pattern.slug)
938 }) {
939 Some(l) => *l,
940 None => continue,
941 };
942
943 for trigger in pattern.triggers {
944 if let Some(pos) = between_lower.find(trigger) {
945 let valid = match pattern.slug {
947 "CEO_OF" | "WORKS_FOR" | "FOUNDED" | "MANAGES" | "REPORTS_TO" => {
949 matches!(
952 head.entity_type,
953 EntityType::Other(_) | EntityType::Custom { .. }
954 ) || matches!(
955 tail.entity_type,
956 EntityType::Other(_) | EntityType::Custom { .. }
957 ) || (matches!(head.entity_type, EntityType::Person)
958 && matches!(tail.entity_type, EntityType::Organization))
959 }
960 "LOCATED_IN" | "BORN_IN" | "LIVES_IN" | "DIED_IN" => {
962 matches!(
963 tail.entity_type,
964 EntityType::Other(_) | EntityType::Custom { .. }
965 ) || matches!(tail.entity_type, EntityType::Location)
966 }
967 "OCCURRED_ON" | "STARTED_ON" | "ENDED_ON" => {
969 matches!(
970 tail.entity_type,
971 EntityType::Other(_) | EntityType::Custom { .. }
972 ) || matches!(tail.entity_type, EntityType::Date | EntityType::Time)
973 }
974 "PART_OF" | "ACQUIRED" | "MERGED_WITH" | "PARENT_OF" => {
976 matches!(
977 head.entity_type,
978 EntityType::Other(_) | EntityType::Custom { .. }
979 ) || matches!(
980 tail.entity_type,
981 EntityType::Other(_) | EntityType::Custom { .. }
982 ) || (matches!(head.entity_type, EntityType::Organization)
983 && matches!(tail.entity_type, EntityType::Organization))
984 }
985 "MARRIED_TO" | "CHILD_OF" | "SIBLING_OF" => {
987 matches!(
988 head.entity_type,
989 EntityType::Other(_) | EntityType::Custom { .. }
990 ) || matches!(
991 tail.entity_type,
992 EntityType::Other(_) | EntityType::Custom { .. }
993 ) || (matches!(head.entity_type, EntityType::Person)
994 && matches!(tail.entity_type, EntityType::Person))
995 }
996 "STUDIED_AT" | "TEACHES_AT" => {
998 matches!(
999 head.entity_type,
1000 EntityType::Other(_) | EntityType::Custom { .. }
1001 ) || matches!(
1002 tail.entity_type,
1003 EntityType::Other(_) | EntityType::Custom { .. }
1004 ) || (matches!(head.entity_type, EntityType::Person)
1005 && matches!(
1006 tail.entity_type,
1007 EntityType::Organization | EntityType::Location
1008 ))
1009 }
1010 "DEVELOPS" | "USES" => {
1012 matches!(
1013 head.entity_type,
1014 EntityType::Other(_) | EntityType::Custom { .. }
1015 ) || matches!(
1016 head.entity_type,
1017 EntityType::Organization | EntityType::Person
1018 )
1019 }
1020 "MET_WITH" | "SPOKE_WITH" => {
1022 matches!(
1023 head.entity_type,
1024 EntityType::Other(_) | EntityType::Custom { .. }
1025 ) || matches!(
1026 tail.entity_type,
1027 EntityType::Other(_) | EntityType::Custom { .. }
1028 ) || (matches!(head.entity_type, EntityType::Person)
1029 && matches!(
1030 tail.entity_type,
1031 EntityType::Person | EntityType::Organization
1032 ))
1033 }
1034 "OWNS" => {
1036 matches!(
1037 head.entity_type,
1038 EntityType::Other(_) | EntityType::Custom { .. }
1039 ) || matches!(
1040 head.entity_type,
1041 EntityType::Person | EntityType::Organization
1042 )
1043 }
1044 _ => true, };
1046
1047 if valid {
1048 return Some((
1049 label.slug.as_str(),
1050 pattern.confidence,
1051 Some((pos, pos + trigger.len())),
1052 ));
1053 }
1054 }
1055 }
1056 }
1057
1058 None
1059}