lindera_dictionary/builder/
user_dictionary.rs1use std::collections::BTreeMap;
2use std::fs;
3use std::fs::File;
4use std::io;
5use std::io::Write;
6use std::path::Path;
7
8use byteorder::{LittleEndian, WriteBytesExt};
9use csv::StringRecord;
10use daachorse::DoubleArrayAhoCorasickBuilder;
11use log::debug;
12
13use crate::LinderaResult;
14use crate::dictionary::UserDictionary;
15use crate::dictionary::prefix_dictionary::{DaTrust, PrefixDictionary};
16use crate::error::LinderaErrorKind;
17use crate::viterbi::WordEntry;
18
19type StringRecordProcessor = Option<Box<dyn Fn(&StringRecord) -> LinderaResult<Vec<String>>>>;
20
21pub struct UserDictionaryBuilder {
22 user_dictionary_fields_num: usize,
23 dictionary_fields_num: usize,
24 default_word_cost: i16,
25 default_left_context_id: u16,
26 default_right_context_id: u16,
27 flexible_csv: bool,
28 user_dictionary_handler: StringRecordProcessor,
29}
30
31#[derive(Default)]
35pub struct UserDictionaryBuilderOptions {
36 user_dictionary_fields_num: Option<usize>,
37 dictionary_fields_num: Option<usize>,
38 default_word_cost: Option<i16>,
39 default_left_context_id: Option<u16>,
40 default_right_context_id: Option<u16>,
41 flexible_csv: Option<bool>,
42 user_dictionary_handler: StringRecordProcessor,
43}
44
45impl UserDictionaryBuilderOptions {
46 pub fn user_dictionary_fields_num(mut self, value: usize) -> Self {
47 self.user_dictionary_fields_num = Some(value);
48 self
49 }
50
51 pub fn dictionary_fields_num(mut self, value: usize) -> Self {
52 self.dictionary_fields_num = Some(value);
53 self
54 }
55
56 pub fn default_word_cost(mut self, value: i16) -> Self {
57 self.default_word_cost = Some(value);
58 self
59 }
60
61 pub fn default_left_context_id(mut self, value: u16) -> Self {
62 self.default_left_context_id = Some(value);
63 self
64 }
65
66 pub fn default_right_context_id(mut self, value: u16) -> Self {
67 self.default_right_context_id = Some(value);
68 self
69 }
70
71 pub fn flexible_csv(mut self, value: bool) -> Self {
72 self.flexible_csv = Some(value);
73 self
74 }
75
76 pub fn user_dictionary_handler(mut self, value: StringRecordProcessor) -> Self {
77 self.user_dictionary_handler = value;
78 self
79 }
80
81 pub fn builder(self) -> UserDictionaryBuilder {
82 UserDictionaryBuilder {
83 user_dictionary_fields_num: self.user_dictionary_fields_num.unwrap_or(3),
84 dictionary_fields_num: self.dictionary_fields_num.unwrap_or(12),
85 default_word_cost: self.default_word_cost.unwrap_or(-10000),
86 default_left_context_id: self.default_left_context_id.unwrap_or(0),
87 default_right_context_id: self.default_right_context_id.unwrap_or(0),
88 flexible_csv: self.flexible_csv.unwrap_or(true),
89 user_dictionary_handler: self.user_dictionary_handler,
90 }
91 }
92}
93
94impl UserDictionaryBuilder {
95 pub fn build(&self, input_file: &Path) -> LinderaResult<UserDictionary> {
96 debug!("reading {input_file:?}");
97
98 let mut rdr = csv::ReaderBuilder::new()
99 .has_headers(false)
100 .flexible(self.flexible_csv)
101 .from_path(input_file)
102 .map_err(|err| {
103 LinderaErrorKind::Io
104 .with_error(anyhow::anyhow!(err))
105 .add_context(format!(
106 "Failed to open user dictionary CSV file: {input_file:?}"
107 ))
108 })?;
109
110 let mut rows: Vec<StringRecord> = vec![];
111 for (line_num, result) in rdr.records().enumerate() {
112 let record = result.map_err(|err| {
113 LinderaErrorKind::Content
114 .with_error(anyhow::anyhow!(err))
115 .add_context(format!(
116 "Failed to parse CSV record at line {} in file: {:?}",
117 line_num + 1,
118 input_file
119 ))
120 })?;
121 rows.push(record);
122 }
123 rows.sort_by_cached_key(|row| row[0].to_string());
126
127 let mut word_entry_map: BTreeMap<String, Vec<WordEntry>> = BTreeMap::new();
128
129 for (row_id, row) in rows.iter().enumerate() {
130 let surface = row[0].to_string();
131 let word_cost = if row.len() == self.user_dictionary_fields_num {
132 self.default_word_cost
133 } else {
134 row[3].parse::<i16>().map_err(|_err| {
135 LinderaErrorKind::Parse
136 .with_error(anyhow::anyhow!("failed to parse word cost"))
137 .add_context(format!(
138 "Invalid word cost '{}' at row {} (surface: '{}')",
139 &row[3],
140 row_id + 1,
141 &row[0]
142 ))
143 })?
144 };
145 let (left_id, right_id) = if row.len() == self.user_dictionary_fields_num {
146 (self.default_left_context_id, self.default_right_context_id)
147 } else {
148 (
149 row[1].parse::<u16>().map_err(|_err| {
150 LinderaErrorKind::Parse
151 .with_error(anyhow::anyhow!("failed to parse left context id"))
152 .add_context(format!(
153 "Invalid left context ID '{}' at row {} (surface: '{}')",
154 &row[1],
155 row_id + 1,
156 &row[0]
157 ))
158 })?,
159 row[2].parse::<u16>().map_err(|_err| {
160 LinderaErrorKind::Parse
161 .with_error(anyhow::anyhow!("failed to parse right context id"))
162 .add_context(format!(
163 "Invalid right context ID '{}' at row {} (surface: '{}')",
164 &row[2],
165 row_id + 1,
166 &row[0]
167 ))
168 })?,
169 )
170 };
171
172 word_entry_map
173 .entry(surface)
174 .or_default()
175 .push(WordEntry::new(
176 crate::viterbi::WordId::new(crate::viterbi::LexType::User, row_id as u32),
177 word_cost,
178 left_id,
179 right_id,
180 ));
181 }
182
183 let mut words_data = Vec::<u8>::new();
184 let mut words_idx_data = Vec::<u8>::new();
185 for row in rows.iter() {
186 let word_detail = if row.len() == self.user_dictionary_fields_num {
187 if let Some(handler) = &self.user_dictionary_handler {
188 handler(row)?
189 } else {
190 row.iter()
191 .skip(1)
192 .map(|s| s.to_string())
193 .collect::<Vec<String>>()
194 }
195 } else if row.len() >= self.dictionary_fields_num {
196 let mut tmp_word_detail = Vec::new();
197 for item in row.iter().skip(4) {
198 tmp_word_detail.push(item.to_string());
199 }
200 tmp_word_detail
201 } else {
202 return Err(LinderaErrorKind::Content
203 .with_error(anyhow::anyhow!(
204 "user dictionary should be a CSV with {} or {}+ fields",
205 self.user_dictionary_fields_num,
206 self.dictionary_fields_num
207 ))
208 .add_context(format!(
209 "Row {} has {} fields (surface: '{}')",
210 rows.iter().position(|r| std::ptr::eq(r, row)).unwrap_or(0) + 1,
211 row.len(),
212 row.get(0).unwrap_or("<empty>")
213 )));
214 };
215
216 let offset = words_data.len();
217 words_idx_data
218 .write_u32::<LittleEndian>(offset as u32)
219 .map_err(|err| {
220 LinderaErrorKind::Io
221 .with_error(anyhow::anyhow!(err))
222 .add_context("Failed to write word offset to user dictionary words index")
223 })?;
224
225 let joined_details = word_detail.join("\0");
227 let joined_details_len = u32::try_from(joined_details.len()).map_err(|err| {
228 LinderaErrorKind::Serialize
229 .with_error(anyhow::anyhow!(err))
230 .add_context(format!(
231 "Word details length too large: {} bytes for word '{}'",
232 joined_details.len(),
233 row.get(0).unwrap_or("<unknown>")
234 ))
235 })?;
236
237 words_data
238 .write_u32::<LittleEndian>(joined_details_len)
239 .map_err(|err| {
240 LinderaErrorKind::Serialize
241 .with_error(anyhow::anyhow!(err))
242 .add_context(
243 "Failed to write word details length to user dictionary words data",
244 )
245 })?;
246 words_data
247 .write_all(joined_details.as_bytes())
248 .map_err(|err| {
249 LinderaErrorKind::Serialize
250 .with_error(anyhow::anyhow!(err))
251 .add_context("Failed to write word details to user dictionary words data")
252 })?;
253 }
254
255 let mut id = 0u32;
256
257 let mut keyset: Vec<(&[u8], u32)> = vec![];
259 for (key, word_entries) in &word_entry_map {
260 let len = word_entries.len() as u32;
261 let val = (id << 8) | len;
266 keyset.push((key.as_bytes(), val));
267 id += len;
268 }
269 let da_bytes = DoubleArrayAhoCorasickBuilder::new()
270 .build_with_values(keyset)
271 .map_err(|err| {
272 LinderaErrorKind::Build
273 .with_error(anyhow::anyhow!(err))
274 .add_context("Failed to build DoubleArray for user dictionary")
275 })?
276 .serialize();
277
278 let mut vals_data = Vec::<u8>::new();
280 for word_entries in word_entry_map.values() {
281 for word_entry in word_entries {
282 word_entry.serialize(&mut vals_data).map_err(|err| {
283 LinderaErrorKind::Serialize
284 .with_error(anyhow::anyhow!(err))
285 .add_context(format!(
286 "Failed to serialize user dictionary word entry (id: {})",
287 word_entry.word_id().id()
288 ))
289 })?;
290 }
291 }
292
293 let dict = PrefixDictionary::load(
294 da_bytes,
295 vals_data,
296 words_idx_data,
297 words_data,
298 false,
299 DaTrust::Untrusted,
300 )?;
301
302 Ok(UserDictionary { dict })
303 }
304}
305
306pub fn build_user_dictionary(user_dict: UserDictionary, output_file: &Path) -> LinderaResult<()> {
307 let parent_dir = match output_file.parent() {
308 Some(parent_dir) => parent_dir,
309 None => {
310 return Err(LinderaErrorKind::Io
311 .with_error(anyhow::anyhow!(
312 "failed to get parent directory of output file"
313 ))
314 .add_context(format!("Invalid output file path: {output_file:?}")));
315 }
316 };
317 fs::create_dir_all(parent_dir).map_err(|err| {
318 LinderaErrorKind::Io
319 .with_error(anyhow::anyhow!(err))
320 .add_context(format!("Failed to create parent directory: {parent_dir:?}"))
321 })?;
322
323 let mut wtr = io::BufWriter::new(File::create(output_file).map_err(|err| {
324 LinderaErrorKind::Io
325 .with_error(anyhow::anyhow!(err))
326 .add_context(format!(
327 "Failed to create user dictionary output file: {output_file:?}"
328 ))
329 })?);
330 let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&user_dict).map_err(|err| {
331 LinderaErrorKind::Serialize
332 .with_error(anyhow::anyhow!(err))
333 .add_context(format!(
334 "Failed to serialize user dictionary to file: {output_file:?}"
335 ))
336 })?;
337 wtr.write_all(&bytes).map_err(|err| {
338 LinderaErrorKind::Io
339 .with_error(anyhow::anyhow!(err))
340 .add_context(format!(
341 "Failed to write user dictionary to file: {output_file:?}"
342 ))
343 })?;
344 wtr.flush().map_err(|err| {
345 LinderaErrorKind::Io
346 .with_error(anyhow::anyhow!(err))
347 .add_context(format!(
348 "Failed to flush user dictionary output file: {output_file:?}"
349 ))
350 })?;
351
352 Ok(())
353}