Skip to main content

yuru_core/
query.rs

1use std::collections::HashMap;
2
3use crate::{
4    KeyBudget, KeyKind, LangMode, LanguageBackend, QueryBudget, QueryVariantKind, SearchConfig,
5};
6
7/// Search text variant produced from the user's query.
8#[derive(Clone, Debug, Eq, PartialEq)]
9pub struct QueryVariant {
10    /// Text that will be matched against compatible search keys.
11    pub text: String,
12    /// Variant type used to filter compatible key kinds.
13    pub kind: QueryVariantKind,
14    /// Score adjustment for this variant type.
15    pub weight: i32,
16}
17
18impl QueryVariant {
19    /// Creates an original query variant.
20    pub fn original(text: impl Into<String>) -> Self {
21        Self {
22            text: text.into(),
23            kind: QueryVariantKind::Original,
24            weight: 500,
25        }
26    }
27
28    /// Creates a normalized query variant.
29    pub fn normalized(text: impl Into<String>) -> Self {
30        Self {
31            text: text.into(),
32            kind: QueryVariantKind::Normalized,
33            weight: 450,
34        }
35    }
36
37    /// Creates a kana query variant.
38    pub fn kana(text: impl Into<String>) -> Self {
39        Self {
40            text: text.into(),
41            kind: QueryVariantKind::Kana,
42            weight: 350,
43        }
44    }
45
46    /// Creates a romaji-to-kana query variant.
47    pub fn romaji_to_kana(text: impl Into<String>) -> Self {
48        Self {
49            text: text.into(),
50            kind: QueryVariantKind::RomajiToKana,
51            weight: 200,
52        }
53    }
54
55    /// Creates a pinyin query variant.
56    pub fn pinyin(text: impl Into<String>) -> Self {
57        Self {
58            text: text.into(),
59            kind: QueryVariantKind::Pinyin,
60            weight: 250,
61        }
62    }
63
64    /// Creates an initials query variant.
65    pub fn initials(text: impl Into<String>) -> Self {
66        Self {
67            text: text.into(),
68            kind: QueryVariantKind::Initials,
69            weight: 250,
70        }
71    }
72}
73
74#[derive(Clone, Debug, Default)]
75/// Backend that only uses original and normalized text.
76pub struct PlainBackend;
77
78impl LanguageBackend for PlainBackend {
79    fn mode(&self) -> LangMode {
80        LangMode::Plain
81    }
82
83    fn build_candidate_keys(&self, _text: &str, _budget: KeyBudget) -> Vec<crate::SearchKey> {
84        Vec::new()
85    }
86
87    fn expand_query(&self, query: &str, _budget: QueryBudget) -> Vec<QueryVariant> {
88        base_query_variants(query)
89    }
90}
91
92/// Builds the language-neutral original and normalized query variants.
93pub fn base_query_variants(query: &str) -> Vec<QueryVariant> {
94    let mut variants = vec![QueryVariant::original(query)];
95    let normalized = crate::normalize::normalize(query);
96    if normalized != query {
97        variants.push(QueryVariant::normalized(normalized));
98    }
99    variants
100}
101
102/// Deduplicates variants by text and key coverage, then applies the query cap.
103pub fn dedup_and_limit_variants(
104    variants: Vec<QueryVariant>,
105    max_query_variants: usize,
106) -> Vec<QueryVariant> {
107    let mut seen_coverage_by_text = HashMap::new();
108    let mut out = Vec::new();
109
110    for variant in variants {
111        let coverage = key_kind_coverage(variant.kind);
112        let seen_coverage = seen_coverage_by_text
113            .entry(variant.text.clone())
114            .or_insert(0u16);
115        if coverage & !*seen_coverage != 0 {
116            *seen_coverage |= coverage;
117            out.push(variant);
118        }
119        if out.len() >= max_query_variants {
120            break;
121        }
122    }
123
124    out
125}
126
127/// Expands and caps query variants for one search run.
128pub(crate) fn prepare_query_variants(
129    query: &str,
130    backend: &dyn LanguageBackend,
131    config: &SearchConfig,
132) -> Vec<QueryVariant> {
133    dedup_and_limit_variants(
134        backend.expand_query(query, config.query_budget()),
135        config.max_query_variants,
136    )
137}
138
139/// Returns whether a search key is disabled by case/normalization settings.
140///
141/// The normalized key is wrong for case-sensitive search and absent when normalization is
142/// off. Beyond that, a normalized key that only case-folds the display text
143/// ([`crate::SearchKey::case_fold_only`]) is redundant *when whatever will score it folds
144/// case the same way*: the original key then carries the same match with a higher weight,
145/// so scoring both keys can only repeat work.
146///
147/// `scorer_folds_case` carries that condition, and must be true only when the scorer folds
148/// with [`crate::matcher::fold_case_char`], the mapping the flag was computed with. Yuru's
149/// own scoring paths pass `!config.case_sensitive`; paths that score through a
150/// caller-supplied [`crate::MatcherBackend`] pass
151/// [`crate::MatcherBackend::folds_case`], whose default `false` keeps the folded key on
152/// offer for a matcher that never claimed to fold case.
153pub(crate) fn key_blocked_by_config(
154    key: &crate::SearchKey,
155    config: &SearchConfig,
156    scorer_folds_case: bool,
157) -> bool {
158    key.kind == KeyKind::Normalized
159        && (config.case_sensitive || !config.normalize || (key.case_fold_only && scorer_folds_case))
160}
161
162/// Returns whether a query variant is disabled by case/normalization settings.
163pub(crate) fn variant_blocked_by_config(kind: QueryVariantKind, config: &SearchConfig) -> bool {
164    kind == QueryVariantKind::Normalized && (config.case_sensitive || !config.normalize)
165}
166
167fn key_kind_coverage(kind: QueryVariantKind) -> u16 {
168    compatible_key_kinds(kind)
169        .iter()
170        .fold(0, |coverage, kind| coverage | key_kind_bit(*kind))
171}
172
173/// Returns whether a query variant may be scored against a key kind.
174pub fn key_kind_allowed(variant: &QueryVariant, kind: KeyKind) -> bool {
175    compatible_key_kinds(variant.kind).contains(&kind)
176}
177
178const ORIGINAL_QUERY_KEYS: &[KeyKind] = &[
179    KeyKind::Original,
180    KeyKind::Normalized,
181    KeyKind::RomajiReading,
182    KeyKind::PinyinFull,
183    KeyKind::PinyinJoined,
184    KeyKind::KoreanRomanized,
185    KeyKind::KoreanInitials,
186    KeyKind::KoreanKeyboard,
187    KeyKind::LearnedAlias,
188];
189const KANA_QUERY_KEYS: &[KeyKind] = &[KeyKind::KanaReading];
190const PINYIN_QUERY_KEYS: &[KeyKind] = &[KeyKind::PinyinFull, KeyKind::PinyinJoined];
191const INITIAL_QUERY_KEYS: &[KeyKind] = &[
192    KeyKind::PinyinInitials,
193    KeyKind::KoreanInitials,
194    KeyKind::LearnedAlias,
195];
196
197fn compatible_key_kinds(kind: QueryVariantKind) -> &'static [KeyKind] {
198    match kind {
199        QueryVariantKind::Original | QueryVariantKind::Normalized => ORIGINAL_QUERY_KEYS,
200        QueryVariantKind::Kana | QueryVariantKind::RomajiToKana => KANA_QUERY_KEYS,
201        QueryVariantKind::Pinyin => PINYIN_QUERY_KEYS,
202        QueryVariantKind::Initials => INITIAL_QUERY_KEYS,
203    }
204}
205
206fn key_kind_bit(kind: KeyKind) -> u16 {
207    1 << (kind as u16)
208}
209
210#[cfg(test)]
211mod tests;