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 source_map: Option<Box<[Option<SourceSpan>]>>,
42}
43
44#[derive(Clone, Debug, Default, Eq, PartialEq)]
46pub struct MappedText {
47 pub text: String,
49 pub source_map: Vec<Option<SourceSpan>>,
51}
52
53#[derive(Clone, Debug, Default)]
55pub struct MappedTextBuilder {
56 mapped: MappedText,
57}
58
59impl MappedTextBuilder {
60 pub fn new() -> Self {
62 Self::default()
63 }
64
65 pub fn push_str(&mut self, text: &str, source: Option<SourceSpan>) {
67 self.mapped.text.push_str(text);
68 self.mapped.source_map.extend(text.chars().map(|_| source));
69 }
70
71 pub fn push_char(&mut self, ch: char, source: Option<SourceSpan>) {
73 self.mapped.text.push(ch);
74 self.mapped.source_map.push(source);
75 }
76
77 pub fn push_unmapped_char(&mut self, ch: char) {
79 self.mapped.text.push(ch);
80 self.mapped.source_map.push(None);
81 }
82
83 pub fn finish(self) -> MappedText {
85 self.mapped
86 }
87}
88
89impl SearchKey {
90 pub fn new(kind: KeyKind, text: impl Into<String>) -> Self {
92 Self {
93 text: text.into(),
94 kind,
95 weight: Self::default_weight(kind),
96 source_map: None,
97 }
98 }
99
100 pub fn default_weight(kind: KeyKind) -> i32 {
102 match kind {
103 KeyKind::Original => 3000,
104 KeyKind::Normalized => 2800,
105 KeyKind::KanaReading => 1700,
106 KeyKind::RomajiReading => 1800,
107 KeyKind::PinyinFull => 1750,
108 KeyKind::PinyinJoined => 1800,
109 KeyKind::PinyinInitials => 1850,
110 KeyKind::KoreanRomanized => 1800,
111 KeyKind::KoreanInitials => 1850,
112 KeyKind::KoreanKeyboard => 1750,
113 KeyKind::LearnedAlias => 2500,
114 }
115 }
116
117 pub fn with_source_map(mut self, source_map: Vec<Option<SourceSpan>>) -> Self {
119 self.source_map = Some(source_map.into_boxed_slice());
120 self
121 }
122
123 pub fn original(text: impl Into<String>) -> Self {
125 Self::new(KeyKind::Original, text)
126 }
127
128 pub fn normalized(text: impl Into<String>) -> Self {
130 Self::new(KeyKind::Normalized, text)
131 }
132
133 pub fn kana_reading(text: impl Into<String>) -> Self {
135 Self::new(KeyKind::KanaReading, text)
136 }
137
138 pub fn romaji_reading(text: impl Into<String>) -> Self {
140 Self::new(KeyKind::RomajiReading, text)
141 }
142
143 pub fn pinyin_full(text: impl Into<String>) -> Self {
145 Self::new(KeyKind::PinyinFull, text)
146 }
147
148 pub fn pinyin_joined(text: impl Into<String>) -> Self {
150 Self::new(KeyKind::PinyinJoined, text)
151 }
152
153 pub fn pinyin_initials(text: impl Into<String>) -> Self {
155 Self::new(KeyKind::PinyinInitials, text)
156 }
157
158 pub fn korean_romanized(text: impl Into<String>) -> Self {
160 Self::new(KeyKind::KoreanRomanized, text)
161 }
162
163 pub fn korean_initials(text: impl Into<String>) -> Self {
165 Self::new(KeyKind::KoreanInitials, text)
166 }
167
168 pub fn korean_keyboard(text: impl Into<String>) -> Self {
170 Self::new(KeyKind::KoreanKeyboard, text)
171 }
172
173 pub fn learned_alias(text: impl Into<String>) -> Self {
175 Self::new(KeyKind::LearnedAlias, text)
176 }
177}
178
179pub fn build_candidate(
181 id: usize,
182 display: impl Into<String>,
183 backend: &dyn LanguageBackend,
184 config: &SearchConfig,
185) -> Candidate {
186 let display = display.into();
187 let mut keys = vec![SearchKey::original(display.clone())];
188 if config.normalize {
189 keys.push(SearchKey::normalized(backend.normalize_candidate(&display)));
190 }
191 keys.extend(backend.build_candidate_keys(&display, config.key_budget()));
192 let keys = dedup_and_limit_keys(keys, config);
193
194 Candidate { id, display, keys }
195}
196
197pub fn build_index<I, S>(
199 items: I,
200 backend: &dyn LanguageBackend,
201 config: &SearchConfig,
202) -> Vec<Candidate>
203where
204 I: IntoIterator<Item = S>,
205 S: Into<String>,
206{
207 let items: Vec<_> = items.into_iter().map(Into::into).collect();
208 if should_build_index_parallel(items.len()) {
209 return items
210 .into_par_iter()
211 .enumerate()
212 .map(|(id, item)| build_candidate(id, item, backend, config))
213 .collect();
214 }
215
216 items
217 .into_iter()
218 .enumerate()
219 .map(|(id, item)| build_candidate(id, item, backend, config))
220 .collect()
221}
222
223fn should_build_index_parallel(len: usize) -> bool {
224 len >= PARALLEL_INDEX_THRESHOLD && rayon::current_num_threads() > 1
225}
226
227pub fn dedup_and_limit_keys(keys: Vec<SearchKey>, config: &SearchConfig) -> Vec<SearchKey> {
229 let mut seen = HashSet::new();
230 let mut out = Vec::new();
231 let mut total_bytes = 0usize;
232
233 for key in keys {
234 if !seen.insert((key.kind, key.text.clone())) {
235 continue;
236 }
237
238 let required_base_key = matches!(key.kind, KeyKind::Original | KeyKind::Normalized);
239 let would_exceed_count = out.len() >= config.max_search_keys_per_candidate;
240 let would_exceed_bytes =
241 total_bytes + key.text.len() > config.max_total_key_bytes_per_candidate;
242
243 if !required_base_key && (would_exceed_count || would_exceed_bytes) {
244 continue;
245 }
246
247 total_bytes += key.text.len();
248 out.push(key);
249 }
250
251 out
252}
253
254#[cfg(test)]
255mod tests;