1use std::collections::HashSet;
2
3use crate::{KeyKind, LanguageBackend, SearchConfig};
4use rayon::prelude::*;
5
6#[cfg(not(test))]
7const PARALLEL_INDEX_THRESHOLD: usize = 50_000;
8#[cfg(test)]
9const PARALLEL_INDEX_THRESHOLD: usize = 4;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub struct SourceSpan {
14 pub start_char: usize,
16 pub end_char: usize,
18}
19
20#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct Candidate {
23 pub id: usize,
25 pub display: String,
27 pub keys: Vec<SearchKey>,
29}
30
31#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct SearchKey {
34 pub text: String,
36 pub kind: KeyKind,
38 pub weight: i32,
40 pub case_fold_only: bool,
45 pub source_map: Option<Box<[Option<SourceSpan>]>>,
47}
48
49#[derive(Clone, Debug, Default, Eq, PartialEq)]
51pub struct MappedText {
52 pub text: String,
54 pub source_map: Vec<Option<SourceSpan>>,
56}
57
58#[derive(Clone, Debug, Default)]
60pub struct MappedTextBuilder {
61 mapped: MappedText,
62}
63
64impl MappedTextBuilder {
65 pub fn new() -> Self {
67 Self::default()
68 }
69
70 pub fn push_str(&mut self, text: &str, source: Option<SourceSpan>) {
72 self.mapped.text.push_str(text);
73 self.mapped.source_map.extend(text.chars().map(|_| source));
74 }
75
76 pub fn push_char(&mut self, ch: char, source: Option<SourceSpan>) {
78 self.mapped.text.push(ch);
79 self.mapped.source_map.push(source);
80 }
81
82 pub fn push_unmapped_char(&mut self, ch: char) {
84 self.mapped.text.push(ch);
85 self.mapped.source_map.push(None);
86 }
87
88 pub fn finish(self) -> MappedText {
90 self.mapped
91 }
92}
93
94impl SearchKey {
95 pub fn new(kind: KeyKind, text: impl Into<String>) -> Self {
97 Self {
98 text: text.into(),
99 kind,
100 weight: Self::default_weight(kind),
101 case_fold_only: false,
102 source_map: None,
103 }
104 }
105
106 pub fn default_weight(kind: KeyKind) -> i32 {
108 match kind {
109 KeyKind::Original => 3000,
110 KeyKind::Normalized => 2800,
111 KeyKind::KanaReading => 1700,
112 KeyKind::RomajiReading => 1800,
113 KeyKind::PinyinFull => 1750,
114 KeyKind::PinyinJoined => 1800,
115 KeyKind::PinyinInitials => 1850,
116 KeyKind::KoreanRomanized => 1800,
117 KeyKind::KoreanInitials => 1850,
118 KeyKind::KoreanKeyboard => 1750,
119 KeyKind::LearnedAlias => 2500,
120 }
121 }
122
123 pub fn with_case_fold_only(mut self, case_fold_only: bool) -> Self {
125 self.case_fold_only = case_fold_only;
126 self
127 }
128
129 pub fn with_source_map(mut self, source_map: Vec<Option<SourceSpan>>) -> Self {
131 self.source_map = Some(source_map.into_boxed_slice());
132 self
133 }
134
135 pub fn original(text: impl Into<String>) -> Self {
137 Self::new(KeyKind::Original, text)
138 }
139
140 pub fn normalized(text: impl Into<String>) -> Self {
142 Self::new(KeyKind::Normalized, text)
143 }
144
145 pub fn kana_reading(text: impl Into<String>) -> Self {
147 Self::new(KeyKind::KanaReading, text)
148 }
149
150 pub fn romaji_reading(text: impl Into<String>) -> Self {
152 Self::new(KeyKind::RomajiReading, text)
153 }
154
155 pub fn pinyin_full(text: impl Into<String>) -> Self {
157 Self::new(KeyKind::PinyinFull, text)
158 }
159
160 pub fn pinyin_joined(text: impl Into<String>) -> Self {
162 Self::new(KeyKind::PinyinJoined, text)
163 }
164
165 pub fn pinyin_initials(text: impl Into<String>) -> Self {
167 Self::new(KeyKind::PinyinInitials, text)
168 }
169
170 pub fn korean_romanized(text: impl Into<String>) -> Self {
172 Self::new(KeyKind::KoreanRomanized, text)
173 }
174
175 pub fn korean_initials(text: impl Into<String>) -> Self {
177 Self::new(KeyKind::KoreanInitials, text)
178 }
179
180 pub fn korean_keyboard(text: impl Into<String>) -> Self {
182 Self::new(KeyKind::KoreanKeyboard, text)
183 }
184
185 pub fn learned_alias(text: impl Into<String>) -> Self {
187 Self::new(KeyKind::LearnedAlias, text)
188 }
189}
190
191pub fn build_candidate(
193 id: usize,
194 display: impl Into<String>,
195 backend: &dyn LanguageBackend,
196 config: &SearchConfig,
197) -> Candidate {
198 let display = display.into();
199 let mut keys = vec![SearchKey::original(display.clone())];
200 if config.normalize {
201 keys.push(normalized_base_key(&display, backend));
202 }
203 keys.extend(backend.build_candidate_keys(&display, config.key_budget()));
204 let keys = dedup_and_limit_keys(keys, config);
205
206 Candidate { id, display, keys }
207}
208
209fn normalized_base_key(display: &str, backend: &dyn LanguageBackend) -> SearchKey {
217 let normalized = backend.normalize_candidate(display);
218 let case_fold_only = normalized
222 .as_bytes()
223 .eq_ignore_ascii_case(display.as_bytes())
224 || normalized
225 .chars()
226 .eq(display.chars().map(crate::matcher::fold_case_char));
227
228 SearchKey::normalized(normalized).with_case_fold_only(case_fold_only)
229}
230
231pub fn build_index<I, S>(
233 items: I,
234 backend: &dyn LanguageBackend,
235 config: &SearchConfig,
236) -> Vec<Candidate>
237where
238 I: IntoIterator<Item = S>,
239 S: Into<String>,
240{
241 let items: Vec<_> = items.into_iter().map(Into::into).collect();
242 if should_build_index_parallel(items.len()) {
243 return items
244 .into_par_iter()
245 .enumerate()
246 .map(|(id, item)| build_candidate(id, item, backend, config))
247 .collect();
248 }
249
250 items
251 .into_iter()
252 .enumerate()
253 .map(|(id, item)| build_candidate(id, item, backend, config))
254 .collect()
255}
256
257fn should_build_index_parallel(len: usize) -> bool {
258 len >= PARALLEL_INDEX_THRESHOLD && rayon::current_num_threads() > 1
259}
260
261pub fn dedup_and_limit_keys(keys: Vec<SearchKey>, config: &SearchConfig) -> Vec<SearchKey> {
263 let mut seen = HashSet::new();
264 let mut out = Vec::new();
265 let mut total_bytes = 0usize;
266
267 for key in keys {
268 if !seen.insert((key.kind, key.text.clone())) {
269 continue;
270 }
271
272 let required_base_key = matches!(key.kind, KeyKind::Original | KeyKind::Normalized);
273 let would_exceed_count = out.len() >= config.max_search_keys_per_candidate;
274 let would_exceed_bytes =
275 total_bytes + key.text.len() > config.max_total_key_bytes_per_candidate;
276
277 if !required_base_key && (would_exceed_count || would_exceed_bytes) {
278 continue;
279 }
280
281 total_bytes += key.text.len();
282 out.push(key);
283 }
284
285 out
286}
287
288#[cfg(test)]
289mod tests;