1use std::borrow::Cow;
2use std::env;
3use std::fs::File;
4use std::io::Read;
5use std::path::Path;
6
7use serde_json::{Value, json};
8
9use crate::character_filter::{BoxCharacterFilter, CharacterFilterLoader, OffsetMapping};
10use crate::token_filter::{BoxTokenFilter, TokenFilterLoader};
11use lindera::LinderaResult;
12use lindera::dictionary::Lattice;
13use lindera::error::LinderaErrorKind;
14use lindera::mode::Mode;
15use lindera::segmenter::Segmenter;
16use lindera::token::Token;
17
18pub type TokenizerConfig = Value;
19
20fn yaml_to_config(file_path: &Path) -> LinderaResult<TokenizerConfig> {
21 let mut input_read = File::open(file_path).map_err(|err| {
22 LinderaErrorKind::Io.with_error(err).add_context(format!(
23 "Failed to open tokenizer config file: {}",
24 file_path.display()
25 ))
26 })?;
27
28 let mut buffer = Vec::new();
29 input_read.read_to_end(&mut buffer).map_err(|err| {
30 LinderaErrorKind::Io.with_error(err).add_context(format!(
31 "Failed to read tokenizer config file: {}",
32 file_path.display()
33 ))
34 })?;
35
36 match serde_yaml_ng::from_slice::<serde_yaml_ng::Value>(&buffer) {
37 Ok(value) => {
38 match value {
40 serde_yaml_ng::Value::Mapping(_) => {
41 Ok(serde_json::to_value(value).map_err(|err| {
42 LinderaErrorKind::Deserialize
43 .with_error(err)
44 .add_context(format!(
45 "Failed to convert YAML to JSON for config file: {}",
46 file_path.display()
47 ))
48 })?)
49 }
50 _ => Err(LinderaErrorKind::Deserialize
51 .with_error(anyhow::anyhow!("Invalid YAML"))
52 .add_context(format!(
53 "Config file must contain a YAML mapping: {}",
54 file_path.display()
55 ))),
56 }
57 }
58 Err(err) => Err(LinderaErrorKind::Deserialize
59 .with_error(err)
60 .add_context(format!(
61 "Failed to parse YAML config file: {}",
62 file_path.display()
63 ))),
64 }
65}
66
67fn empty_config() -> Value {
69 json!({
70 "segmenter": {},
71 "character_filters": [],
72 "token_filters": []
73 })
74}
75
76fn ensure_keys(mut config: Value) -> Value {
78 if config.get("segmenter").is_none() {
79 config["segmenter"] = json!({});
80 }
81
82 if config.get("character_filters").is_none() {
83 config["character_filters"] = json!([]);
84 }
85
86 if config.get("token_filters").is_none() {
87 config["token_filters"] = json!([]);
88 }
89
90 config
91}
92
93#[derive(Debug)]
94pub struct TokenizerBuilder {
95 config: TokenizerConfig,
96}
97
98impl TokenizerBuilder {
99 pub fn new() -> LinderaResult<Self> {
100 if let Ok(config_path) = env::var("LINDERA_CONFIG_PATH") {
101 Self::from_file(Path::new(&config_path))
102 } else {
103 Ok(Self {
104 config: empty_config(),
105 })
106 }
107 }
108
109 pub fn from_file(file_path: &Path) -> LinderaResult<Self> {
110 let config = yaml_to_config(file_path)?;
111
112 Ok(TokenizerBuilder {
113 config: ensure_keys(config),
114 })
115 }
116
117 pub fn from_config(config: TokenizerConfig) -> LinderaResult<Self> {
118 Ok(TokenizerBuilder {
119 config: ensure_keys(config),
120 })
121 }
122
123 pub fn set_segmenter_mode(&mut self, mode: &Mode) -> &mut Self {
124 self.config["segmenter"]["mode"] = json!(mode.as_str());
125 self
126 }
127
128 pub fn set_segmenter_dictionary(&mut self, uri: &str) -> &mut Self {
129 self.config["segmenter"]["dictionary"] = json!(uri);
130 self
131 }
132
133 pub fn set_segmenter_user_dictionary(&mut self, uri: &str) -> &mut Self {
134 self.config["segmenter"]["user_dictionary"] = json!(uri);
135 self
136 }
137
138 pub fn set_segmenter_keep_whitespace(&mut self, keep_whitespace: bool) -> &mut Self {
139 self.config["segmenter"]["keep_whitespace"] = json!(keep_whitespace);
140 self
141 }
142
143 pub fn set_segmenter_use_mmap(&mut self, use_mmap: bool) -> &mut Self {
154 self.config["segmenter"]["use_mmap"] = json!(use_mmap);
155 self
156 }
157
158 pub fn append_character_filter(&mut self, kind: &str, args: &Value) -> &mut Self {
159 if let Some(array) = self.config["character_filters"].as_array_mut() {
160 array.push(json!({ "kind": kind, "args": args }));
161 }
162 self
163 }
164
165 pub fn append_token_filter(&mut self, kind: &str, args: &Value) -> &mut Self {
166 if let Some(array) = self.config["token_filters"].as_array_mut() {
167 array.push(json!({ "kind": kind, "args": args }));
168 }
169 self
170 }
171
172 pub fn build(&self) -> LinderaResult<Tokenizer> {
173 Tokenizer::from_config(&self.config).map_err(|err| {
174 LinderaErrorKind::Parse.with_error(anyhow::anyhow!("failed to build tokenizer: {err}"))
175 })
176 }
177}
178
179pub struct Tokenizer {
180 pub segmenter: Segmenter,
185
186 pub character_filters: Vec<BoxCharacterFilter>,
192
193 pub token_filters: Vec<BoxTokenFilter>,
198}
199
200impl Tokenizer {
201 pub fn new(segmenter: Segmenter) -> Self {
217 Self {
218 segmenter,
219 character_filters: Vec::new(),
220 token_filters: Vec::new(),
221 }
222 }
223
224 pub fn from_config(config: &TokenizerConfig) -> LinderaResult<Self> {
225 let segmenter_config = config.get("segmenter").ok_or_else(|| {
226 LinderaErrorKind::Deserialize.with_error(anyhow::anyhow!("missing segmenter config."))
227 })?;
228 let segmenter = Segmenter::from_config(segmenter_config)?;
229
230 let mut tokenizer = Tokenizer::new(segmenter);
232
233 if let Some(character_filter_settings) = config["character_filters"].as_array() {
235 for character_filter_setting in character_filter_settings {
236 let character_filter_name = character_filter_setting["kind"].as_str();
237 if let Some(character_filter_name) = character_filter_name {
238 tokenizer.append_character_filter(CharacterFilterLoader::load_from_value(
240 character_filter_name,
241 &character_filter_setting["args"],
242 )?);
243 }
244 }
245 }
246
247 if let Some(token_filter_settings) = config["token_filters"].as_array() {
249 for token_filter_setting in token_filter_settings {
250 let token_filter_name = token_filter_setting["kind"].as_str();
251 if let Some(token_filter_name) = token_filter_name {
252 tokenizer.append_token_filter(TokenFilterLoader::load_from_value(
254 token_filter_name,
255 &token_filter_setting["args"],
256 )?);
257 }
258 }
259 }
260
261 Ok(tokenizer)
262 }
263
264 pub fn append_character_filter(&mut self, character_filter: BoxCharacterFilter) -> &mut Self {
279 self.character_filters.push(character_filter);
280
281 self
282 }
283
284 pub fn append_token_filter(&mut self, token_filter: BoxTokenFilter) -> &mut Self {
299 self.token_filters.push(token_filter);
300
301 self
302 }
303
304 pub fn tokenize<'a>(&'a self, text: &'a str) -> LinderaResult<Vec<Token<'a>>> {
337 let mut lattice = Lattice::default();
338 self.tokenize_with_lattice(text, &mut lattice)
339 }
340
341 pub fn tokenize_with_lattice<'a>(
375 &'a self,
376 text: &'a str,
377 lattice: &mut Lattice,
378 ) -> LinderaResult<Vec<Token<'a>>> {
379 let mut normalized_text: Cow<'a, str> = Cow::Borrowed(text);
380
381 let mut offset_mappings: Vec<OffsetMapping> =
382 Vec::with_capacity(self.character_filters.len());
383
384 if !self.character_filters.is_empty() {
387 let text_mut = normalized_text.to_mut();
389
390 for character_filter in &self.character_filters {
391 let mapping = character_filter.apply(text_mut)?;
392
393 if !mapping.is_empty() {
394 offset_mappings.push(mapping);
397 }
398 }
399 }
400
401 let final_text_len = normalized_text.len();
403
404 let mut tokens = self
407 .segmenter
408 .segment_with_lattice(normalized_text, lattice)?;
409
410 for token_filter in &self.token_filters {
412 token_filter.apply(&mut tokens)?;
413 }
414
415 if !offset_mappings.is_empty() {
418 for token in tokens.iter_mut() {
419 for mapping in offset_mappings.iter().rev() {
421 token.byte_start = mapping.correct_offset(token.byte_start, final_text_len);
423 token.byte_end = mapping.correct_offset(token.byte_end, final_text_len);
425 }
426 }
427 }
428
429 Ok(tokens)
430 }
431
432 pub fn tokenize_nbest<'a>(
437 &'a self,
438 text: &'a str,
439 n: usize,
440 unique: bool,
441 cost_threshold: Option<i64>,
442 ) -> LinderaResult<Vec<(Vec<Token<'a>>, i64)>> {
443 let mut lattice = Lattice::default();
444 self.tokenize_nbest_with_lattice(text, &mut lattice, n, unique, cost_threshold)
445 }
446
447 pub fn tokenize_nbest_with_lattice<'a>(
453 &'a self,
454 text: &'a str,
455 lattice: &mut Lattice,
456 n: usize,
457 unique: bool,
458 cost_threshold: Option<i64>,
459 ) -> LinderaResult<Vec<(Vec<Token<'a>>, i64)>> {
460 let mut normalized_text: Cow<'a, str> = Cow::Borrowed(text);
461
462 let mut offset_mappings: Vec<OffsetMapping> =
463 Vec::with_capacity(self.character_filters.len());
464
465 if !self.character_filters.is_empty() {
466 let text_mut = normalized_text.to_mut();
467 for character_filter in &self.character_filters {
468 let mapping = character_filter.apply(text_mut)?;
469 if !mapping.is_empty() {
470 offset_mappings.push(mapping);
471 }
472 }
473 }
474
475 let final_text_len = normalized_text.len();
476
477 let mut all_results = self.segmenter.segment_nbest_with_lattice(
478 normalized_text,
479 lattice,
480 n,
481 unique,
482 cost_threshold,
483 )?;
484
485 for (tokens, _cost) in &mut all_results {
487 for token_filter in &self.token_filters {
488 token_filter.apply(tokens)?;
489 }
490
491 if !offset_mappings.is_empty() {
492 for token in tokens.iter_mut() {
493 for mapping in offset_mappings.iter().rev() {
494 token.byte_start = mapping.correct_offset(token.byte_start, final_text_len);
495 token.byte_end = mapping.correct_offset(token.byte_end, final_text_len);
496 }
497 }
498 }
499 }
500
501 Ok(all_results)
502 }
503}
504
505impl Clone for Tokenizer {
506 fn clone(&self) -> Self {
523 let character_filters: Vec<BoxCharacterFilter> = self
524 .character_filters
525 .iter()
526 .map(|filter| filter.box_clone())
527 .collect();
528
529 let token_filters: Vec<BoxTokenFilter> = self
530 .token_filters
531 .iter()
532 .map(|filter| filter.box_clone())
533 .collect();
534
535 Tokenizer {
536 character_filters,
537 segmenter: self.segmenter.clone(),
538 token_filters,
539 }
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::TokenizerBuilder;
546
547 #[test]
548 fn test_set_segmenter_use_mmap_writes_flat_key_under_segmenter() {
549 let mut builder = TokenizerBuilder::new().unwrap();
550 builder.set_segmenter_use_mmap(true);
551
552 assert_eq!(builder.config["segmenter"]["use_mmap"], true);
553 }
554
555 #[cfg(feature = "embed-ipadic")]
556 #[test]
557 fn test_tokenizer_config_from_slice() {
558 use std::path::PathBuf;
559
560 use crate::tokenizer::yaml_to_config;
561
562 let config_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
563 .join("../resources")
564 .join("config")
565 .join("lindera.yml");
566
567 let result = yaml_to_config(&config_file);
568
569 assert!(result.is_ok());
570 }
571
572 #[test]
573 #[cfg(feature = "embed-ipadic")]
574 fn test_tokenizer_config_clone() {
575 use std::path::PathBuf;
576
577 use crate::tokenizer::yaml_to_config;
578
579 let config_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
580 .join("../resources")
581 .join("config")
582 .join("lindera.yml");
583
584 let tokenizer_config = yaml_to_config(&config_file).unwrap();
585
586 let cloned_tokenizer_config = tokenizer_config.clone();
587
588 assert_eq!(tokenizer_config, cloned_tokenizer_config);
589 }
590
591 #[test]
592 #[cfg(feature = "embed-ipadic")]
593 fn test_tokenize_ipadic() {
594 use std::borrow::Cow;
595 use std::path::PathBuf;
596
597 use crate::tokenizer::TokenizerBuilder;
598
599 let config_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
600 .join("../resources")
601 .join("config")
602 .join("lindera.yml");
603
604 let builder = TokenizerBuilder::from_file(&config_file).unwrap();
605
606 let tokenizer = builder.build().unwrap();
607
608 {
609 let text = "リンデラは形態素解析エンジンです。";
610 let mut tokens = tokenizer.tokenize(text).unwrap();
611 let mut tokens_iter = tokens.iter_mut();
612 {
613 let token = tokens_iter.next().unwrap();
614 assert_eq!(token.surface, Cow::Borrowed("Lindera"));
615 assert_eq!(token.byte_start, 0);
616 assert_eq!(token.byte_end, 15);
617 assert_eq!(token.position, 0);
618 assert_eq!(token.position_length, 1);
619 assert!(token.word_id.is_unknown());
620 assert_eq!(
621 token.details,
622 Some(vec![
623 Cow::Borrowed("名詞"),
624 Cow::Borrowed("固有名詞"),
625 Cow::Borrowed("組織"),
626 Cow::Borrowed("*"),
627 Cow::Borrowed("*"),
628 Cow::Borrowed("*"),
629 Cow::Borrowed("*"),
630 Cow::Borrowed("*"),
631 Cow::Borrowed("*"),
632 ])
633 );
634 }
635 {
636 let token = tokens_iter.next().unwrap();
637 assert_eq!(token.surface, Cow::Borrowed("形態素"));
638 assert_eq!(token.byte_start, 18);
639 assert_eq!(token.byte_end, 27);
640 assert_eq!(token.position, 2);
641 assert_eq!(token.position_length, 1);
642 assert_eq!(
643 token.details,
644 Some(vec![
645 Cow::Borrowed("名詞"),
646 Cow::Borrowed("一般"),
647 Cow::Borrowed("*"),
648 Cow::Borrowed("*"),
649 Cow::Borrowed("*"),
650 Cow::Borrowed("*"),
651 Cow::Borrowed("形態素"),
652 Cow::Borrowed("ケイタイソ"),
653 Cow::Borrowed("ケイタイソ"),
654 ])
655 );
656 }
657 {
658 let token = tokens_iter.next().unwrap();
659 assert_eq!(token.surface, Cow::Borrowed("解析"));
660 assert_eq!(token.byte_start, 27);
661 assert_eq!(token.byte_end, 33);
662 assert_eq!(token.position, 3);
663 assert_eq!(token.position_length, 1);
664 assert_eq!(
665 token.details,
666 Some(vec![
667 Cow::Borrowed("名詞"),
668 Cow::Borrowed("サ変接続"),
669 Cow::Borrowed("*"),
670 Cow::Borrowed("*"),
671 Cow::Borrowed("*"),
672 Cow::Borrowed("*"),
673 Cow::Borrowed("解析"),
674 Cow::Borrowed("カイセキ"),
675 Cow::Borrowed("カイセキ"),
676 ])
677 );
678 }
679 {
680 let token = tokens_iter.next().unwrap();
681 assert_eq!(token.surface, Cow::Borrowed("エンジン"));
682 assert_eq!(token.byte_start, 33);
683 assert_eq!(token.byte_end, 48);
684 assert_eq!(token.position, 4);
685 assert_eq!(token.position_length, 1);
686 assert_eq!(
687 token.details,
688 Some(vec![
689 Cow::Borrowed("名詞"),
690 Cow::Borrowed("一般"),
691 Cow::Borrowed("*"),
692 Cow::Borrowed("*"),
693 Cow::Borrowed("*"),
694 Cow::Borrowed("*"),
695 Cow::Borrowed("エンジン"),
696 Cow::Borrowed("エンジン"),
697 Cow::Borrowed("エンジン"),
698 ])
699 );
700 }
701
702 let mut tokens_iter = tokens.iter();
703 {
704 let token = tokens_iter.next().unwrap();
705 let start = token.byte_start;
706 let end = token.byte_end;
707 assert_eq!(token.surface, Cow::Borrowed("Lindera"));
708 assert_eq!(&text[start..end], "リンデラ");
709 }
710 }
711
712 {
713 let text = "10㌎のガソリン";
714 let mut tokens = tokenizer.tokenize(text).unwrap();
715 let mut tokens_iter = tokens.iter_mut();
716 {
717 let token = tokens_iter.next().unwrap();
720 assert_eq!(token.surface, Cow::Owned::<str>("10ガロン".into()));
721 assert_eq!(token.byte_start, 0);
722 assert_eq!(token.byte_end, 9);
723 assert_eq!(token.position, 0);
724 assert_eq!(token.position_length, 2);
725 }
726 {
727 let token = tokens_iter.next().unwrap();
728 assert_eq!(token.surface, Cow::Borrowed("ガソリン"));
729 assert_eq!(token.byte_start, 12);
730 assert_eq!(token.byte_end, 27);
731 assert_eq!(token.position, 3);
732 assert_eq!(token.position_length, 1);
733 assert_eq!(
734 token.details,
735 Some(vec![
736 Cow::Borrowed("名詞"),
737 Cow::Borrowed("一般"),
738 Cow::Borrowed("*"),
739 Cow::Borrowed("*"),
740 Cow::Borrowed("*"),
741 Cow::Borrowed("*"),
742 Cow::Borrowed("ガソリン"),
743 Cow::Borrowed("ガソリン"),
744 Cow::Borrowed("ガソリン"),
745 ])
746 );
747 }
748
749 let mut tokens_iter = tokens.iter();
750 {
751 let token = tokens_iter.next().unwrap();
753 let start = token.byte_start;
754 let end = token.byte_end;
755 assert_eq!(token.surface, Cow::Owned::<str>("10ガロン".into()));
756 assert_eq!(&text[start..end], "10㌎");
757 }
758 {
759 let token = tokens_iter.next().unwrap();
760 let start = token.byte_start;
761 let end = token.byte_end;
762 assert_eq!(token.surface, Cow::Borrowed("ガソリン"));
763 assert_eq!(&text[start..end], "ガソリン");
764 }
765 }
766
767 {
768 let text = "お釣りは百三十四円です。";
769 let mut tokens = tokenizer.tokenize(text).unwrap();
770 let mut tokens_iter = tokens.iter_mut();
771 {
772 let token = tokens_iter.next().unwrap();
773 assert_eq!(token.surface, Cow::Borrowed("お釣り"));
774 assert_eq!(token.byte_start, 0);
775 assert_eq!(token.byte_end, 9);
776 assert_eq!(token.position, 0);
777 assert_eq!(token.position_length, 1);
778 assert_eq!(
779 token.details,
780 Some(vec![
781 Cow::Borrowed("名詞"),
782 Cow::Borrowed("一般"),
783 Cow::Borrowed("*"),
784 Cow::Borrowed("*"),
785 Cow::Borrowed("*"),
786 Cow::Borrowed("*"),
787 Cow::Borrowed("お釣り"),
788 Cow::Borrowed("オツリ"),
789 Cow::Borrowed("オツリ"),
790 ])
791 );
792 }
793 {
794 let token = tokens_iter.next().unwrap();
795 assert_eq!(token.surface, Cow::Borrowed("134円"));
796 assert_eq!(token.byte_start, 12);
797 assert_eq!(token.byte_end, 27);
798 assert_eq!(token.position, 2);
799 assert_eq!(token.position_length, 5);
800 assert_eq!(
801 token.details,
802 Some(vec![
803 Cow::Borrowed("名詞"),
804 Cow::Borrowed("数"),
805 Cow::Borrowed("*"),
806 Cow::Borrowed("*"),
807 Cow::Borrowed("*"),
808 Cow::Borrowed("*"),
809 Cow::Borrowed("*"),
810 Cow::Borrowed("*"),
811 Cow::Borrowed("*"),
812 ])
813 );
814 }
815 }
816
817 {
818 let text = "ここは騒々しい";
819 let mut tokens = tokenizer.tokenize(text).unwrap();
820 let mut tokens_iter = tokens.iter_mut();
821 {
822 let token = tokens_iter.next().unwrap();
823 assert_eq!(token.surface, Cow::Borrowed("ここ"));
824 assert_eq!(token.byte_start, 0);
825 assert_eq!(token.byte_end, 6);
826 assert_eq!(token.position, 0);
827 assert_eq!(token.position_length, 1);
828 assert_eq!(
829 token.details,
830 Some(vec![
831 Cow::Borrowed("名詞"),
832 Cow::Borrowed("代名詞"),
833 Cow::Borrowed("一般"),
834 Cow::Borrowed("*"),
835 Cow::Borrowed("*"),
836 Cow::Borrowed("*"),
837 Cow::Borrowed("ここ"),
838 Cow::Borrowed("ココ"),
839 Cow::Borrowed("ココ"),
840 ])
841 );
842 }
843 {
844 let token = tokens_iter.next().unwrap();
845 assert_eq!(token.surface, Cow::Borrowed("騒騒しい"));
846 assert_eq!(token.byte_start, 9);
847 assert_eq!(token.byte_end, 21);
848 assert_eq!(token.position, 2);
849 assert_eq!(token.position_length, 1);
850 assert_eq!(
851 token.details,
852 Some(vec![
853 Cow::Borrowed("形容詞"),
854 Cow::Borrowed("自立"),
855 Cow::Borrowed("*"),
856 Cow::Borrowed("*"),
857 Cow::Borrowed("形容詞・イ段"),
858 Cow::Borrowed("基本形"),
859 Cow::Borrowed("騒騒しい"),
860 Cow::Borrowed("ソウゾウシイ"),
861 Cow::Borrowed("ソーゾーシイ"),
862 ])
863 );
864 }
865 }
866 }
867
868 #[test]
869 #[cfg(not(windows))]
870 #[should_panic(expected = "No such file or directory")]
871 fn test_create_tokenizer_builder_from_non_existent_file() {
872 use std::path::PathBuf;
873
874 use crate::tokenizer::TokenizerBuilder;
875
876 let config_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
877 .join("../resources")
878 .join("config")
879 .join("non_existent_file.yml");
880
881 TokenizerBuilder::from_file(&config_file).unwrap();
882 }
883
884 #[test]
885 #[cfg(windows)]
886 #[should_panic(expected = "The system cannot find the file specified.")]
887 fn test_create_tokenizer_builder_from_non_existent_file() {
888 use std::path::PathBuf;
889
890 use crate::tokenizer::TokenizerBuilder;
891
892 let config_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
893 .join("../resources")
894 .join("config")
895 .join("non_existent_file.yml");
896
897 TokenizerBuilder::from_file(&config_file).unwrap();
898 }
899
900 #[test]
901 #[should_panic(expected = "Invalid YAML")]
902 fn test_create_tokenizer_builder_from_invalid_file() {
903 use std::path::PathBuf;
904
905 use crate::tokenizer::TokenizerBuilder;
906
907 let config_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
908 .join("../resources")
909 .join("config")
910 .join("invalid.yml");
911
912 TokenizerBuilder::from_file(&config_file).unwrap();
913 }
914
915 #[test]
916 #[cfg(feature = "embed-ipadic")]
917 fn test_tokenize_nbest_1best_matches_tokenize() {
918 use crate::tokenizer::TokenizerBuilder;
919
920 let mut builder = TokenizerBuilder::new().unwrap();
921 builder.set_segmenter_dictionary("embedded://ipadic");
922
923 let tokenizer = builder.build().unwrap();
924
925 let text = "すもももももももものうち";
926 let normal_tokens = tokenizer.tokenize(text).unwrap();
927 let nbest_results = tokenizer.tokenize_nbest(text, 1, false, None).unwrap();
928
929 assert_eq!(nbest_results.len(), 1);
930 let (nbest_tokens, _cost) = &nbest_results[0];
931 assert_eq!(normal_tokens.len(), nbest_tokens.len());
932 for (normal, nbest) in normal_tokens.iter().zip(nbest_tokens.iter()) {
933 assert_eq!(normal.surface.as_ref(), nbest.surface.as_ref());
934 }
935 }
936
937 #[test]
938 #[cfg(feature = "embed-ipadic")]
939 fn test_tokenize_nbest_multiple_results() {
940 use crate::tokenizer::TokenizerBuilder;
941
942 let mut builder = TokenizerBuilder::new().unwrap();
943 builder.set_segmenter_dictionary("embedded://ipadic");
944
945 let tokenizer = builder.build().unwrap();
946
947 let text = "すもももももももものうち";
948 let results = tokenizer.tokenize_nbest(text, 5, false, None).unwrap();
949
950 assert!(results.len() >= 2);
952
953 for (tokens, _cost) in &results {
955 assert!(!tokens.is_empty());
956 }
957 }
958}