sqlite_graphrag/
extraction.rs1use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct ExtractedUrl {
22 pub url: String,
24 pub start: usize,
26 pub end: usize,
28}
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct ExtractedEntity {
34 pub name: String,
36 pub entity_type: String,
38 pub start: usize,
40 pub end: usize,
42}
43
44#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
49pub struct ExtractionResult {
50 pub entities: Vec<ExtractedEntity>,
52 pub urls: Vec<ExtractedUrl>,
54 pub elapsed_ms: u64,
56}
57
58pub trait Extractor: Send + Sync {
61 fn name(&self) -> &'static str;
63 fn extract(&self, body: &str) -> Result<ExtractionResult, crate::errors::AppError>;
65}
66
67pub struct RegexExtractor;
70
71impl Extractor for RegexExtractor {
72 fn name(&self) -> &'static str {
73 "regex"
74 }
75 fn extract(&self, body: &str) -> Result<ExtractionResult, crate::errors::AppError> {
76 Ok(ExtractionResult {
77 entities: Vec::new(),
78 urls: extract_urls(body),
79 elapsed_ms: 0,
80 })
81 }
82}
83
84pub fn extract_urls(body: &str) -> Vec<ExtractedUrl> {
87 let mut out = Vec::new();
88 let mut cursor = 0usize;
89 while cursor < body.len() {
90 let hay = &body[cursor..];
91 let http_at = hay.find("http://");
93 let https_at = hay.find("https://");
94 let (rel_start, scheme_len) = match (http_at, https_at) {
95 (Some(a), Some(b)) => {
96 if a <= b {
97 (a, 7)
98 } else {
99 (b, 8)
100 }
101 }
102 (Some(a), None) => (a, 7),
103 (None, Some(b)) => (b, 8),
104 (None, None) => break,
105 };
106 let abs_start = cursor + rel_start;
107 let after_scheme = abs_start + scheme_len;
108 let mut end = after_scheme;
109 for (i, c) in body[after_scheme..].char_indices() {
110 if c.is_whitespace() || matches!(c, ')' | ']' | '}' | '"' | '\'' | '<') {
111 end = after_scheme + i;
112 break;
113 }
114 end = after_scheme + i + c.len_utf8();
115 }
116 out.push(ExtractedUrl {
117 url: body[abs_start..end].to_string(),
118 start: abs_start,
119 end,
120 });
121 cursor = end;
122 }
123 out
124}
125
126pub fn extract_graph_auto(
130 body: &str,
131 _paths: &crate::paths::AppPaths,
132) -> Result<ExtractionResult, crate::errors::AppError> {
133 let start = std::time::Instant::now();
134 let urls = extract_urls(body);
135 Ok(ExtractionResult {
136 entities: Vec::new(),
137 urls,
138 elapsed_ms: start.elapsed().as_millis() as u64,
139 })
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn extract_urls_finds_http_and_https() {
148 let body = "see https://example.com/foo and http://bar.baz/qux end";
149 let urls = extract_urls(body);
150 assert_eq!(urls.len(), 2, "got {urls:?} for body {body:?}");
151 assert_eq!(urls[0].url, "https://example.com/foo");
152 assert_eq!(urls[1].url, "http://bar.baz/qux");
153 }
154
155 #[test]
156 fn extract_urls_handles_trailing_punctuation() {
157 let body = "see https://example.com/foo).";
158 let urls = extract_urls(body);
159 assert_eq!(urls.len(), 1);
160 assert_eq!(urls[0].url, "https://example.com/foo");
161 }
162
163 #[test]
164 fn extract_urls_empty_body() {
165 assert!(extract_urls("").is_empty());
166 }
167
168 #[test]
169 fn regex_extractor_returns_only_urls() {
170 let result = RegexExtractor.extract("see https://example.com").unwrap();
171 assert_eq!(result.entities.len(), 0);
172 assert_eq!(result.urls.len(), 1);
173 }
174}