1use std::path::Path;
11use std::str::FromStr;
12use std::sync::{Mutex, MutexGuard};
13
14use serde_json::Value;
15
16use lindera::dictionary::{Dictionary, UserDictionary};
17use lindera::mode::Mode;
18use lindera::segmenter::Segmenter;
19use lindera::space_penalty::SpacePenaltyConfig;
20use lindera_analysis::tokenizer::{Tokenizer, TokenizerBuilder};
21use lindera_analysis::worker::AnalysisWorker;
22
23use crate::error::{CoreError, CoreResult};
24use crate::token::{SurfaceView, TokenView};
25
26#[derive(Debug, Clone, PartialEq)]
29enum SpacePenaltySetting {
30 Default,
32 Off,
34 FromDictionary,
36 Rules(SpacePenaltyConfig),
38}
39
40impl SpacePenaltySetting {
41 fn from_value(value: &Value) -> CoreResult<Self> {
52 match value {
53 Value::Null => Ok(Self::Default),
54 Value::Bool(false) => Ok(Self::Off),
55 Value::Bool(true) => Ok(Self::FromDictionary),
56 Value::Object(_) => serde_json::from_value(value.clone())
57 .map(Self::Rules)
58 .map_err(|err| {
59 CoreError::invalid_argument(format!("invalid space_penalty rules: {err}"))
60 }),
61 other => Err(CoreError::invalid_argument(format!(
62 "space_penalty must be null, a boolean or an object with \"rules\", got {other}"
63 ))),
64 }
65 }
66
67 fn apply(&self, segmenter: &mut Segmenter) -> CoreResult<()> {
79 match self {
80 Self::Default => {}
81 Self::Off => segmenter.set_space_penalty(None)?,
82 Self::FromDictionary => segmenter.set_space_penalty_from_dictionary()?,
83 Self::Rules(config) => segmenter.set_space_penalty(Some(config.clone()))?,
84 }
85 Ok(())
86 }
87}
88
89pub struct CoreTokenizerBuilder {
95 inner: TokenizerBuilder,
97}
98
99impl CoreTokenizerBuilder {
100 pub fn new() -> CoreResult<Self> {
102 Ok(Self {
103 inner: TokenizerBuilder::new()?,
104 })
105 }
106
107 pub fn from_file(file_path: &Path) -> CoreResult<Self> {
109 Ok(Self {
110 inner: TokenizerBuilder::from_file(file_path)?,
111 })
112 }
113
114 pub fn set_mode(&mut self, mode: &str) -> CoreResult<&mut Self> {
116 let mode = Mode::from_str(mode)?;
117 self.inner.set_segmenter_mode(&mode);
118 Ok(self)
119 }
120
121 pub fn set_dictionary(&mut self, uri: &str) -> &mut Self {
123 self.inner.set_segmenter_dictionary(uri);
124 self
125 }
126
127 pub fn set_user_dictionary(&mut self, uri: &str) -> &mut Self {
129 self.inner.set_segmenter_user_dictionary(uri);
130 self
131 }
132
133 pub fn set_keep_whitespace(&mut self, keep_whitespace: bool) -> &mut Self {
135 self.inner.set_segmenter_keep_whitespace(keep_whitespace);
136 self
137 }
138
139 pub fn set_space_penalty(&mut self, value: &Value) -> CoreResult<&mut Self> {
160 match SpacePenaltySetting::from_value(value)? {
161 SpacePenaltySetting::Default => {
162 self.inner.reset_segmenter_space_penalty();
163 }
164 SpacePenaltySetting::Off => {
165 self.inner.set_segmenter_space_penalty(None);
166 }
167 SpacePenaltySetting::FromDictionary => {
168 self.inner.set_segmenter_space_penalty_from_dictionary(true);
169 }
170 SpacePenaltySetting::Rules(config) => {
171 self.inner.set_segmenter_space_penalty(Some(&config));
172 }
173 }
174 Ok(self)
175 }
176
177 pub fn append_character_filter(&mut self, kind: &str, args: &Value) -> &mut Self {
179 self.inner.append_character_filter(kind, args);
180 self
181 }
182
183 pub fn append_token_filter(&mut self, kind: &str, args: &Value) -> &mut Self {
185 self.inner.append_token_filter(kind, args);
186 self
187 }
188
189 pub fn build(&self) -> CoreResult<CoreTokenizer> {
191 Ok(CoreTokenizer::from_tokenizer(self.inner.build()?))
192 }
193}
194
195pub struct CoreTokenizer {
209 worker: Mutex<AnalysisWorker>,
212}
213
214fn lock_worker(mutex: &Mutex<AnalysisWorker>) -> MutexGuard<'_, AnalysisWorker> {
229 match mutex.lock() {
230 Ok(guard) => guard,
231 Err(poisoned) => {
232 let mut guard = poisoned.into_inner();
233 guard.reset();
234 guard
235 }
236 }
237}
238
239impl CoreTokenizer {
240 pub fn from_segmenter(
242 mode: &str,
243 dictionary: Dictionary,
244 user_dictionary: Option<UserDictionary>,
245 ) -> CoreResult<Self> {
246 let mode = Mode::from_str(mode)?;
247 let segmenter = Segmenter::new(mode, dictionary, user_dictionary);
248 Ok(Self::from_tokenizer(Tokenizer::new(segmenter)))
249 }
250
251 pub fn from_segmenter_with_space_penalty(
272 mode: &str,
273 dictionary: Dictionary,
274 user_dictionary: Option<UserDictionary>,
275 space_penalty: &Value,
276 ) -> CoreResult<Self> {
277 let setting = SpacePenaltySetting::from_value(space_penalty)?;
278 let mode = Mode::from_str(mode)?;
279 let mut segmenter = Segmenter::new(mode, dictionary, user_dictionary);
280 setting.apply(&mut segmenter)?;
281 Ok(Self::from_tokenizer(Tokenizer::new(segmenter)))
282 }
283
284 pub fn from_tokenizer(tokenizer: Tokenizer) -> Self {
287 Self {
288 worker: Mutex::new(tokenizer.into_worker()),
289 }
290 }
291
292 pub fn tokenize(&self, text: &str) -> CoreResult<Vec<TokenView>> {
297 let mut worker = lock_worker(&self.worker);
298 let tokens = worker.tokenize(text)?;
299 Ok(tokens.into_iter().map(TokenView::from_token).collect())
300 }
301
302 pub fn tokenize_surfaces(&self, text: &str) -> CoreResult<Vec<SurfaceView>> {
313 let mut worker = lock_worker(&self.worker);
314 let tokens = worker.tokenize(text)?;
315 Ok(tokens.into_iter().map(SurfaceView::from_token).collect())
316 }
317
318 pub fn tokenize_nbest(
323 &self,
324 text: &str,
325 n: usize,
326 unique: bool,
327 cost_threshold: Option<i64>,
328 ) -> CoreResult<Vec<(Vec<TokenView>, i64)>> {
329 let mut worker = lock_worker(&self.worker);
330 let results = worker.tokenize_nbest(text, n, unique, cost_threshold)?;
331 Ok(results
332 .into_iter()
333 .map(|(tokens, cost)| {
334 (
335 tokens.into_iter().map(TokenView::from_token).collect(),
336 cost,
337 )
338 })
339 .collect())
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn builder_new_succeeds() {
349 assert!(CoreTokenizerBuilder::new().is_ok());
350 }
351
352 #[test]
353 fn set_mode_accepts_known_modes() {
354 let mut builder = CoreTokenizerBuilder::new().expect("builder");
355 assert!(builder.set_mode("normal").is_ok());
356 assert!(builder.set_mode("decompose").is_ok());
357 }
358
359 #[test]
360 fn set_mode_rejects_unknown_mode() {
361 let mut builder = CoreTokenizerBuilder::new().expect("builder");
362 assert!(builder.set_mode("definitely-not-a-mode").is_err());
363 }
364
365 #[test]
366 fn infallible_setters_chain() {
367 let mut builder = CoreTokenizerBuilder::new().expect("builder");
368 builder
369 .set_dictionary("embedded://ipadic")
370 .set_keep_whitespace(true)
371 .append_token_filter("japanese_compound_word", &Value::Object(Default::default()));
372 }
374
375 #[test]
376 fn set_space_penalty_accepts_the_config_forms() {
377 let mut builder = CoreTokenizerBuilder::new().expect("builder");
378 for value in [
379 Value::Null,
380 Value::Bool(true),
381 Value::Bool(false),
382 serde_json::json!({ "rules": [{ "pos": ["JKS"], "cost": 6000 }] }),
383 serde_json::json!({ "rules": [] }),
384 ] {
385 assert!(
386 builder.set_space_penalty(&value).is_ok(),
387 "rejected {value}"
388 );
389 }
390 }
391
392 #[test]
393 fn set_space_penalty_rejects_other_values() {
394 let mut builder = CoreTokenizerBuilder::new().expect("builder");
395 for value in [
396 serde_json::json!(1),
397 serde_json::json!("false"),
398 serde_json::json!(["JKS"]),
399 serde_json::json!({ "rules": "JKS" }),
400 serde_json::json!({ "rules": [{ "pos": ["JKS"] }] }),
401 ] {
402 let err = match builder.set_space_penalty(&value) {
403 Ok(_) => panic!("accepted {value}"),
404 Err(err) => err,
405 };
406 assert_eq!(err.kind(), crate::error::ErrorKind::InvalidArgument);
407 }
408 }
409
410 #[test]
416 fn core_tokenizer_is_send_and_sync() {
417 fn assert_send_sync<T: Send + Sync>() {}
418 assert_send_sync::<CoreTokenizer>();
419 assert_send_sync::<CoreTokenizerBuilder>();
420 }
421
422 #[cfg(feature = "embed-ipadic")]
423 mod with_ipadic {
424 use super::super::*;
425
426 fn ipadic_tokenizer() -> CoreTokenizer {
427 let dictionary = match lindera::dictionary::load_dictionary("embedded://ipadic") {
428 Ok(dictionary) => dictionary,
429 Err(err) => panic!("failed to load embedded IPADIC: {err}"),
430 };
431 match CoreTokenizer::from_segmenter("normal", dictionary, None) {
432 Ok(tokenizer) => tokenizer,
433 Err(err) => panic!("failed to build CoreTokenizer: {err}"),
434 }
435 }
436
437 #[test]
440 fn tokenize_repeated_calls_are_stable() {
441 let tokenizer = ipadic_tokenizer();
442 let first = match tokenizer.tokenize("すもももももももものうち") {
443 Ok(tokens) => tokens,
444 Err(err) => panic!("tokenize failed: {err}"),
445 };
446 assert!(!first.is_empty());
447 for _ in 0..100 {
448 let again = match tokenizer.tokenize("すもももももももものうち") {
449 Ok(tokens) => tokens,
450 Err(err) => panic!("tokenize failed: {err}"),
451 };
452 assert_eq!(first.len(), again.len());
453 for (a, b) in first.iter().zip(again.iter()) {
454 assert_eq!(a.surface, b.surface);
455 assert_eq!(a.byte_start, b.byte_start);
456 assert_eq!(a.byte_end, b.byte_end);
457 assert_eq!(a.details, b.details);
458 }
459 }
460 }
461
462 #[test]
464 fn tokenize_nbest_repeated_calls_are_stable() {
465 let tokenizer = ipadic_tokenizer();
466 let first = match tokenizer.tokenize_nbest("すもももももももものうち", 3, false, None)
467 {
468 Ok(results) => results,
469 Err(err) => panic!("tokenize_nbest failed: {err}"),
470 };
471 assert!(!first.is_empty());
472 for _ in 0..10 {
473 let again =
474 match tokenizer.tokenize_nbest("すもももももももものうち", 3, false, None)
475 {
476 Ok(results) => results,
477 Err(err) => panic!("tokenize_nbest failed: {err}"),
478 };
479 assert_eq!(first.len(), again.len());
480 for ((a_tokens, a_cost), (b_tokens, b_cost)) in first.iter().zip(again.iter()) {
481 assert_eq!(a_cost, b_cost);
482 assert_eq!(a_tokens.len(), b_tokens.len());
483 }
484 }
485 }
486
487 #[test]
490 fn tokenize_surfaces_matches_tokenize() {
491 let tokenizer = ipadic_tokenizer();
492 let texts = [
493 "すもももももももものうち",
494 "関西国際空港限定トートバッグ",
495 "",
496 ];
497 for _ in 0..3 {
498 for text in texts {
499 let full = match tokenizer.tokenize(text) {
500 Ok(tokens) => tokens,
501 Err(err) => panic!("tokenize failed: {err}"),
502 };
503 let surfaces = match tokenizer.tokenize_surfaces(text) {
504 Ok(views) => views,
505 Err(err) => panic!("tokenize_surfaces failed: {err}"),
506 };
507 assert_eq!(full.len(), surfaces.len(), "count for {text:?}");
508 for (a, b) in full.iter().zip(surfaces.iter()) {
509 assert_eq!(a.surface, b.surface);
510 assert_eq!(a.byte_start, b.byte_start);
511 assert_eq!(a.byte_end, b.byte_end);
512 }
513 }
514 }
515 }
516
517 #[test]
521 fn tokenize_recovers_from_poisoned_lock() {
522 use std::sync::Arc;
523
524 let tokenizer = Arc::new(ipadic_tokenizer());
525 let expected = match tokenizer.tokenize("すもももももももものうち") {
526 Ok(tokens) => tokens,
527 Err(err) => panic!("tokenize failed: {err}"),
528 };
529
530 let poisoner = Arc::clone(&tokenizer);
532 let result = std::thread::spawn(move || {
533 let _guard = lock_worker(&poisoner.worker);
534 panic!("intentional panic to poison the worker lock");
535 })
536 .join();
537 assert!(result.is_err(), "poisoning thread must have panicked");
538 assert!(tokenizer.worker.is_poisoned(), "lock must be poisoned");
539
540 let after = match tokenizer.tokenize("すもももももももものうち") {
542 Ok(tokens) => tokens,
543 Err(err) => panic!("tokenize after poison failed: {err}"),
544 };
545 assert_eq!(expected.len(), after.len());
546 for (a, b) in expected.iter().zip(after.iter()) {
547 assert_eq!(a.surface, b.surface);
548 }
549 }
550
551 #[test]
554 fn space_penalty_true_fails_on_a_dictionary_without_rules() {
555 for (value, should_build) in [
556 (Value::Null, true),
557 (Value::Bool(false), true),
558 (Value::Bool(true), false),
559 ] {
560 let mut builder = CoreTokenizerBuilder::new().expect("builder");
561 builder
562 .set_dictionary("embedded://ipadic")
563 .set_space_penalty(&value)
564 .expect("valid value");
565 assert_eq!(builder.build().is_ok(), should_build, "value {value}");
566 }
567 }
568
569 #[test]
571 fn from_segmenter_with_space_penalty_on_ipadic() {
572 for (value, should_build) in [
573 (Value::Null, true),
574 (Value::Bool(false), true),
575 (Value::Bool(true), false),
576 (
577 serde_json::json!({ "rules": [{ "pos": ["助詞"], "cost": 1000 }] }),
578 true,
579 ),
580 ] {
581 let dictionary = match lindera::dictionary::load_dictionary("embedded://ipadic") {
582 Ok(dictionary) => dictionary,
583 Err(err) => panic!("failed to load embedded IPADIC: {err}"),
584 };
585 let result = CoreTokenizer::from_segmenter_with_space_penalty(
586 "normal", dictionary, None, &value,
587 );
588 assert_eq!(result.is_ok(), should_build, "value {value}");
589 }
590 }
591 }
592
593 #[cfg(feature = "embed-ko-dic")]
594 mod with_ko_dic {
595 use super::super::*;
596
597 fn tags(space_penalty: &Value, text: &str) -> Vec<(String, String)> {
600 let mut builder = CoreTokenizerBuilder::new().expect("builder");
601 builder
602 .set_dictionary("embedded://ko-dic")
603 .set_space_penalty(space_penalty)
604 .expect("valid value");
605 let tokenizer = match builder.build() {
606 Ok(tokenizer) => tokenizer,
607 Err(err) => panic!("build failed: {err}"),
608 };
609 match tokenizer.tokenize(text) {
610 Ok(tokens) => tokens
611 .into_iter()
612 .map(|t| (t.surface, t.details.first().cloned().unwrap_or_default()))
613 .collect(),
614 Err(err) => panic!("tokenize failed: {err}"),
615 }
616 }
617
618 #[test]
622 fn space_penalty_setting_changes_ko_dic_output() {
623 let text = "서울 시 에서 출발";
624 let default = tags(&Value::Null, text);
625 let off = tags(&Value::Bool(false), text);
626 let required = tags(&Value::Bool(true), text);
627 let shipped = serde_json::json!({ "rules": [
628 { "pos": ["EC", "EF", "EP", "ETM", "ETN", "VCP", "XSA", "XSN", "XSV"], "cost": 3000 },
629 { "pos": ["JC", "JKB", "JKC", "JKG", "JKO", "JKQ", "JKS", "JKV", "JX"], "cost": 6000 }
630 ] });
631 let explicit = tags(&shipped, text);
632
633 assert_ne!(
634 default, off,
635 "turning the penalty off must change the output"
636 );
637 assert_eq!(default, required);
638 assert_eq!(default, explicit);
639 assert!(
642 off.iter().any(|(s, tag)| s == "시" && tag == "EP"),
643 "{off:?}"
644 );
645 assert!(
646 !default.iter().any(|(s, tag)| s == "시" && tag == "EP"),
647 "{default:?}"
648 );
649 }
650
651 #[test]
654 fn from_segmenter_with_space_penalty_matches_the_builder() {
655 let text = "서울 시 에서 출발";
656 for value in [Value::Null, Value::Bool(false), Value::Bool(true)] {
657 let dictionary = match lindera::dictionary::load_dictionary("embedded://ko-dic") {
658 Ok(dictionary) => dictionary,
659 Err(err) => panic!("failed to load embedded ko-dic: {err}"),
660 };
661 let tokenizer = match CoreTokenizer::from_segmenter_with_space_penalty(
662 "normal", dictionary, None, &value,
663 ) {
664 Ok(tokenizer) => tokenizer,
665 Err(err) => panic!("build failed for {value}: {err}"),
666 };
667 let from_instance: Vec<(String, String)> = match tokenizer.tokenize(text) {
668 Ok(tokens) => tokens
669 .into_iter()
670 .map(|t| (t.surface, t.details.first().cloned().unwrap_or_default()))
671 .collect(),
672 Err(err) => panic!("tokenize failed: {err}"),
673 };
674 assert_eq!(from_instance, tags(&value, text), "value {value}");
675 }
676 }
677 }
678}