Skip to main content

summa_core/query/
wildcard.rs

1//! Whole-term Unicode wildcard filters over the existing term dictionary.
2use 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/// Constant-score union of indexed terms matching a whole-term wildcard.
11///
12/// `*` matches any sequence of Unicode scalar values, `?` exactly one, and `\`
13/// escapes the next character. Patterns are not tokenized or stemmed. Expansion
14/// uses the same 1,024-term / 5,000,000-posting per-segment limits as prefixes;
15/// scans additionally stop with an error after 1,000,000 candidate terms.
16#[derive(Debug, Clone)]
17pub struct WildcardQuery(TermPatternQuery);
18
19impl WildcardQuery {
20    /// Compile a case-sensitive pattern against indexed UTF-8 terms.
21    pub fn new(field: Field, pattern: impl AsRef<str>) -> Result<Self> {
22        let source = pattern.as_ref();
23        check_length(source, "wildcard")?;
24        let mut expression = String::new();
25        let mut prefix = String::new();
26        let mut suffix = String::new();
27        let mut stars = 0usize;
28        let mut questions = false;
29        let mut literal_prefix = true;
30        let mut characters = source.chars();
31        while let Some(character) = characters.next() {
32            match character {
33                '*' => {
34                    stars += 1;
35                    expression.push_str(".*");
36                    literal_prefix = false;
37                }
38                '?' => {
39                    questions = true;
40                    expression.push('.');
41                    literal_prefix = false;
42                }
43                _ => {
44                    let literal = if character == '\\' {
45                        characters.next().ok_or_else(|| {
46                            Error::Query("wildcard pattern ends with an escape".into())
47                        })?
48                    } else {
49                        character
50                    };
51                    expression.push_str(&regex::escape(literal.encode_utf8(&mut [0; 4])));
52                    if literal_prefix {
53                        prefix.push(literal);
54                    } else {
55                        suffix.push(literal);
56                    }
57                }
58            }
59        }
60        if stars == 1 && !questions {
61            return Ok(Self(TermPatternQuery::single_star(
62                field,
63                source,
64                prefix.into_bytes(),
65                suffix.into_bytes(),
66            )));
67        }
68        Ok(Self(TermPatternQuery::compile(
69            field,
70            source,
71            &expression,
72            vec![prefix.into_bytes()],
73            "wildcard",
74        )?))
75    }
76
77    /// Lowercase a pattern to match a lowercase term vocabulary.
78    pub fn text(field: Field, pattern: &str) -> Result<Self> {
79        check_length(pattern, "wildcard")?;
80        Self::new(field, pattern.to_lowercase())
81    }
82}
83
84impl std::fmt::Display for WildcardQuery {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        self.0.fmt(f)
87    }
88}
89impl Query for WildcardQuery {
90    fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
91        self.0.scorer(reader, limit)
92    }
93    #[cfg(feature = "sync")]
94    fn scorer_sync<'a>(
95        &self,
96        reader: &'a SegmentReader,
97        limit: usize,
98    ) -> Result<Box<dyn Scorer + 'a>> {
99        self.0.scorer_sync(reader, limit)
100    }
101    fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
102        self.0.count_estimate(reader)
103    }
104    fn is_filter(&self) -> bool {
105        true
106    }
107}