summa_core/query/
regex.rs1use super::term_pattern::{TermPatternQuery, check_length};
3#[cfg(feature = "sync")]
4use super::traits::Scorer;
5use super::traits::{CountFuture, Query, ScorerFuture};
6use crate::dsl::Field;
7use crate::segment::SegmentReader;
8use crate::{Error, Result};
9
10#[derive(Debug, Clone)]
17pub struct RegexQuery(TermPatternQuery);
18
19impl RegexQuery {
20 pub fn new(field: Field, pattern: impl AsRef<str>) -> Result<Self> {
21 let source = pattern.as_ref();
22 check_length(source, "regex")?;
23 validate_syntax(source)?;
24 Ok(Self(TermPatternQuery::compile(
25 field,
26 source,
27 source,
28 literal_prefixes(source)?,
29 "regex",
30 )?))
31 }
32}
33
34fn literal_prefixes(source: &str) -> Result<Vec<Vec<u8>>> {
37 let hir = regex_syntax::ParserBuilder::new()
38 .dot_matches_new_line(true)
39 .build()
40 .parse(source)
41 .map_err(|error| Error::Query(format!("invalid regex pattern: {error}")))?;
42 let sequence = regex_syntax::hir::literal::Extractor::new()
43 .limit_total(64)
44 .limit_literal_len(64)
45 .limit_repeat(8)
46 .extract(&hir);
47 let Some(literals) = sequence.literals() else {
48 return Ok(vec![Vec::new()]);
49 };
50 let mut prefixes: Vec<_> = literals
51 .iter()
52 .map(|literal| literal.as_bytes().to_vec())
53 .collect();
54 prefixes.sort_unstable();
55 let mut disjoint: Vec<Vec<u8>> = Vec::with_capacity(prefixes.len());
56 for prefix in prefixes {
57 if disjoint
58 .last()
59 .is_none_or(|previous| !prefix.starts_with(previous))
60 {
61 disjoint.push(prefix);
62 }
63 }
64 Ok(disjoint)
65}
66
67fn validate_syntax(source: &str) -> Result<()> {
68 let unsupported = || {
69 Error::Query(
70 "unsupported regex syntax; use literals, classes, groups, alternation and repetition"
71 .into(),
72 )
73 };
74 let mut chars = source.chars().peekable();
75 let mut class = false;
76 while let Some(ch) = chars.next() {
77 match ch {
78 '\\' => {
79 let escaped = chars
80 .next()
81 .ok_or_else(|| Error::Query("regex pattern ends with an escape".into()))?;
82 if escaped.is_alphanumeric() {
83 return Err(unsupported());
84 }
85 }
86 '[' if class => return Err(unsupported()),
87 '[' => class = true,
88 ']' => class = false,
89 '&' | '~' | '#' | '@' | '<' | '>' | '"' if !class => return Err(unsupported()),
90 '^' | '$' if !class => return Err(unsupported()),
91 '(' if !class && chars.peek() == Some(&'?') => return Err(unsupported()),
92 '&' | '~' | '|' | '-' if class && chars.peek() == Some(&ch) => {
93 return Err(unsupported());
94 }
95 _ => {}
96 }
97 }
98 Ok(())
99}
100
101impl std::fmt::Display for RegexQuery {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 self.0.fmt(f)
104 }
105}
106impl Query for RegexQuery {
107 fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
108 self.0.scorer(reader, limit)
109 }
110 #[cfg(feature = "sync")]
111 fn scorer_sync<'a>(
112 &self,
113 reader: &'a SegmentReader,
114 limit: usize,
115 ) -> Result<Box<dyn Scorer + 'a>> {
116 self.0.scorer_sync(reader, limit)
117 }
118 fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
119 self.0.count_estimate(reader)
120 }
121 fn is_filter(&self) -> bool {
122 true
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 #[test]
129 fn extracted_ranges_keep_alternation_optional_prefixes_and_unicode() {
130 let ranges = |source| super::literal_prefixes(source).unwrap();
133 assert_eq!(ranges("colou?r"), [b"color".to_vec(), b"colour".to_vec()]);
134 assert_eq!(
135 ranges("(www|http|https)"),
136 [b"http".to_vec(), b"www".to_vec()]
137 );
138 assert_eq!(ranges("(alpha|alphabet).*"), [b"alpha".to_vec()]);
139 assert_eq!(ranges("a?b"), [b"ab".to_vec(), b"b".to_vec()]);
140 assert_eq!(ranges(".*tion"), [Vec::<u8>::new()]);
141 assert_eq!(
142 ranges("(é|🦀)x"),
143 ["éx".as_bytes().to_vec(), "🦀x".as_bytes().to_vec()]
144 );
145 let regex = regex::Regex::new("^(?:[ab]{50})$").unwrap();
146 let prefixes = ranges("[ab]{50}");
147 assert!(prefixes.len() <= 64);
148 for term in ["a".repeat(50), "b".repeat(50), "ab".repeat(25)] {
149 assert!(regex.is_match(&term));
150 assert!(
151 prefixes
152 .iter()
153 .any(|prefix| term.as_bytes().starts_with(prefix))
154 );
155 }
156 }
157}