Skip to main content

mecab_ko_core/
tokenizer.rs

1//! # 토크나이저 모듈
2//!
3//! 형태소 분석의 메인 인터페이스입니다.
4//!
5//! ## 개요
6//!
7//! Tokenizer는 다음 컴포넌트들을 통합하여 형태소 분석을 수행합니다:
8//! - **Trie**: 사전 검색 (mecab-ko-dict)
9//! - **Matrix**: 연접 비용 계산
10//! - **Lattice**: 후보 그래프 구축
11//! - **Viterbi**: 최적 경로 탐색
12//! - `UnknownHandler`: 미등록어 처리
13//!
14//! ## 분석 과정
15//!
16//! 1. **입력 텍스트 전처리**: 공백 제거 및 위치 정보 생성
17//! 2. **Lattice 구축**: 각 위치에서 사전 검색 및 노드 추가
18//! 3. **미등록어 처리**: 사전에 없는 부분에 대해 미등록어 노드 추가
19//! 4. **Viterbi 탐색**: 최소 비용 경로 계산
20//! 5. **Token 변환**: 최적 경로의 노드를 Token으로 변환
21//!
22//! ## Example
23//!
24//! ```rust,no_run
25//! use mecab_ko_core::tokenizer::Tokenizer;
26//!
27//! // 기본 사전으로 초기화
28//! let mut tokenizer = Tokenizer::new().unwrap();
29//!
30//! // 형태소 분석
31//! let tokens = tokenizer.tokenize("아버지가방에들어가신다");
32//! for token in tokens {
33//!     println!("{}: {} ({}~{})", token.surface, token.pos, token.start_pos, token.end_pos);
34//! }
35//! ```
36
37use std::borrow::Cow;
38use std::path::Path;
39
40use mecab_ko_dict::{SystemDictionary, UserDictionary};
41
42use crate::error::Result;
43use crate::lattice::{Lattice, Node, NodeBuilder, NodeType};
44use crate::normalizer::{NormalizationConfig, Normalizer};
45use crate::pool::{PoolManager, PoolStats};
46use crate::pos_tag::PosTag;
47use crate::unknown::UnknownHandler;
48use crate::viterbi::{SpacePenalty, ViterbiSearcher};
49
50/// 토큰
51///
52/// 형태소 분석 결과의 개별 토큰을 표현합니다.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct Token {
55    /// 표면형 (원본 텍스트의 형태)
56    pub surface: String,
57
58    /// 품사 태그
59    pub pos: String,
60
61    /// 시작 위치 (문자 단위, 0-based)
62    pub start_pos: usize,
63
64    /// 끝 위치 (문자 단위, exclusive)
65    pub end_pos: usize,
66
67    /// 시작 위치 (바이트 단위)
68    pub start_byte: usize,
69
70    /// 끝 위치 (바이트 단위)
71    pub end_byte: usize,
72
73    /// 읽기 (발음)
74    pub reading: Option<String>,
75
76    /// 원형 (기본형)
77    pub lemma: Option<String>,
78
79    /// 비용
80    pub cost: i32,
81
82    /// 전체 품사 정보 (CSV feature string)
83    pub features: String,
84
85    /// 정규화된 형태 (외래어 정규화 활성화 시)
86    pub normalized: Option<String>,
87}
88
89impl Token {
90    /// 새 토큰 생성
91    #[must_use]
92    pub const fn new(
93        surface: String,
94        pos: String,
95        start_pos: usize,
96        end_pos: usize,
97        start_byte: usize,
98        end_byte: usize,
99    ) -> Self {
100        Self {
101            surface,
102            pos,
103            start_pos,
104            end_pos,
105            start_byte,
106            end_byte,
107            reading: None,
108            lemma: None,
109            cost: 0,
110            features: String::new(),
111            normalized: None,
112        }
113    }
114
115    /// Lattice 노드에서 토큰 생성
116    ///
117    /// # Arguments
118    ///
119    /// * `node` - Lattice 노드
120    #[must_use]
121    pub fn from_node(node: &Node) -> Self {
122        let features = node.feature.to_string();
123        let (pos, reading, lemma) = parse_features(&features);
124
125        Self {
126            surface: node.surface.to_string(),
127            pos: pos.to_string(),
128            start_pos: node.start_pos,
129            end_pos: node.end_pos,
130            start_byte: node.start_byte,
131            end_byte: node.end_byte,
132            reading,
133            lemma,
134            cost: node.total_cost,
135            features,
136            normalized: None,
137        }
138    }
139
140    /// 토큰 길이 (문자 단위)
141    #[inline]
142    #[must_use]
143    pub const fn char_len(&self) -> usize {
144        self.end_pos - self.start_pos
145    }
146
147    /// 토큰 길이 (바이트 단위)
148    #[inline]
149    #[must_use]
150    pub const fn byte_len(&self) -> usize {
151        self.end_byte - self.start_byte
152    }
153
154    /// 품사 태그를 `PosTag` 타입으로 파싱
155    #[must_use]
156    pub fn pos_tag(&self) -> Option<PosTag> {
157        self.pos.parse().ok()
158    }
159}
160
161/// Feature 문자열 파싱
162///
163/// `MeCab` feature 포맷: `품사,의미분류,종성유무,읽기,타입,첫번째품사,마지막품사,표현`
164///
165/// # Returns
166///
167/// (품사, 읽기, 원형)
168fn parse_features(features: &str) -> (Cow<'_, str>, Option<String>, Option<String>) {
169    // Avoid allocating a Vec – iterate the splits directly.
170    let mut split = features.splitn(5, ',');
171
172    let pos = split.next().unwrap_or("*");
173
174    // indices: 0=pos, 1=semantic, 2=jongseong, 3=reading
175    let reading = split
176        .nth(2) // skip indices 1 and 2, land on index 3
177        .filter(|s| !s.is_empty() && *s != "*")
178        .map(std::string::ToString::to_string);
179
180    let lemma = reading.clone();
181
182    (Cow::Borrowed(pos), reading, lemma)
183}
184
185/// 토크나이저
186///
187/// 형태소 분석의 메인 인터페이스입니다.
188/// 시스템 사전, 사용자 사전, 미등록어 처리기를 통합하여 형태소 분석을 수행합니다.
189///
190/// # 메모리 최적화
191///
192/// - `lattice` 재사용으로 매 분석마다 재할당 방지
193/// - `pool_manager`로 Token, Node 객체 재사용
194/// - String interning으로 중복 문자열 제거
195pub struct Tokenizer {
196    /// 시스템 사전
197    dictionary: SystemDictionary,
198
199    /// 미등록어 처리기
200    unknown_handler: UnknownHandler,
201
202    /// Viterbi 탐색기
203    viterbi_searcher: ViterbiSearcher,
204
205    /// 재사용 가능한 Lattice (성능 최적화)
206    lattice: Lattice,
207
208    /// 외래어 정규화기 (옵션)
209    normalizer: Option<Normalizer>,
210
211    /// 정규화 활성화 여부
212    enable_normalization: bool,
213
214    /// 메모리 풀 관리자
215    pool_manager: PoolManager,
216}
217
218impl Tokenizer {
219    /// 기본 사전으로 토크나이저 생성
220    ///
221    /// 환경변수 `MECAB_DICDIR`이나 기본 경로에서 시스템 사전을 로드합니다.
222    ///
223    /// # Errors
224    ///
225    /// - 사전을 찾을 수 없는 경우
226    /// - 사전 파일 포맷이 잘못된 경우
227    ///
228    /// # Example
229    ///
230    /// ```rust,no_run
231    /// use mecab_ko_core::tokenizer::Tokenizer;
232    ///
233    /// let mut tokenizer = Tokenizer::new().unwrap();
234    /// let tokens = tokenizer.tokenize("안녕하세요");
235    /// ```
236    pub fn new() -> Result<Self> {
237        let dictionary = SystemDictionary::load_default()?;
238        let unknown_handler = UnknownHandler::korean_default();
239        let viterbi_searcher = ViterbiSearcher::new();
240
241        // 초기 Lattice 생성 (빈 텍스트)
242        let lattice = Lattice::new("");
243
244        Ok(Self {
245            dictionary,
246            unknown_handler,
247            viterbi_searcher,
248            lattice,
249            normalizer: None,
250            enable_normalization: false,
251            pool_manager: PoolManager::new(),
252        })
253    }
254
255    /// 사전 경로를 지정하여 토크나이저 생성
256    ///
257    /// # Arguments
258    ///
259    /// * `dict_path` - 사전 디렉토리 경로
260    ///
261    /// # Errors
262    ///
263    /// - 사전을 찾을 수 없는 경우
264    /// - 사전 파일 포맷이 잘못된 경우
265    pub fn with_dict<P: AsRef<Path>>(dict_path: P) -> Result<Self> {
266        let dictionary = SystemDictionary::load(dict_path)?;
267        let unknown_handler = UnknownHandler::korean_default();
268        let viterbi_searcher = ViterbiSearcher::new();
269
270        let lattice = Lattice::new("");
271
272        Ok(Self {
273            dictionary,
274            unknown_handler,
275            viterbi_searcher,
276            lattice,
277            normalizer: None,
278            enable_normalization: false,
279            pool_manager: PoolManager::new(),
280        })
281    }
282
283    /// 사용자 사전 추가
284    ///
285    /// # Arguments
286    ///
287    /// * `user_dict` - 사용자 사전
288    ///
289    /// # Example
290    ///
291    /// ```rust,no_run
292    /// use mecab_ko_core::tokenizer::Tokenizer;
293    /// use mecab_ko_dict::UserDictionary;
294    ///
295    /// let mut user_dict = UserDictionary::new();
296    /// user_dict.add_entry("딥러닝", "NNG", Some(-1000), None);
297    ///
298    /// let tokenizer = Tokenizer::new().unwrap()
299    ///     .with_user_dict(user_dict);
300    /// ```
301    #[must_use]
302    pub fn with_user_dict(mut self, user_dict: UserDictionary) -> Self {
303        self.dictionary.set_user_dictionary(user_dict);
304        self
305    }
306
307    /// 사용자 사전 설정 (in-place)
308    ///
309    /// 이미 생성된 토크나이저에 사용자 사전을 설정합니다.
310    /// 빌더 패턴이 필요 없는 경우 사용합니다.
311    ///
312    /// # Arguments
313    ///
314    /// * `user_dict` - 사용자 사전
315    ///
316    /// # Example
317    ///
318    /// ```rust,no_run
319    /// use mecab_ko_core::Tokenizer;
320    /// use mecab_ko_dict::UserDictionary;
321    ///
322    /// let mut tokenizer = Tokenizer::new().unwrap();
323    ///
324    /// let mut user_dict = UserDictionary::new();
325    /// user_dict.add_entry("챗GPT", "NNP", Some(-2000), None);
326    /// tokenizer.set_user_dict(user_dict);
327    /// ```
328    pub fn set_user_dict(&mut self, user_dict: UserDictionary) {
329        self.dictionary.set_user_dictionary(user_dict);
330    }
331
332    /// Hot-reload v2 사전 설정 (in-place)
333    ///
334    /// 이미 생성된 토크나이저에 `HotReloadDictV2` 인스턴스를 설정합니다.
335    /// 도메인 사전의 동적 리로드를 활성화합니다.
336    ///
337    /// # Arguments
338    ///
339    /// * `hr` - `HotReloadDictV2` 인스턴스
340    #[cfg(feature = "hot-reload-v2")]
341    pub fn set_hot_reload(
342        &mut self,
343        hr: std::sync::Arc<mecab_ko_dict::hot_reload_v2::HotReloadDictV2>,
344    ) {
345        self.dictionary.set_hot_reload(hr);
346    }
347
348    /// 띄어쓰기 패널티 설정
349    ///
350    /// # Arguments
351    ///
352    /// * `penalty` - 띄어쓰기 패널티 설정
353    #[must_use]
354    pub fn with_space_penalty(mut self, penalty: SpacePenalty) -> Self {
355        self.viterbi_searcher = ViterbiSearcher::new().with_space_penalty(penalty);
356        self
357    }
358
359    /// 형태소 분석
360    ///
361    /// 입력 텍스트를 형태소 단위로 분석하여 Token 목록을 반환합니다.
362    ///
363    /// # Arguments
364    ///
365    /// * `text` - 분석할 텍스트
366    ///
367    /// # Returns
368    ///
369    /// 토큰 목록
370    ///
371    /// # Example
372    ///
373    /// ```rust,no_run
374    /// # use mecab_ko_core::tokenizer::Tokenizer;
375    /// # let mut tokenizer = Tokenizer::new().unwrap();
376    /// let tokens = tokenizer.tokenize("아버지가방에들어가신다");
377    /// for token in tokens {
378    ///     println!("{}: {}", token.surface, token.pos);
379    /// }
380    /// ```
381    pub fn tokenize(&mut self, text: &str) -> Vec<Token> {
382        if text.is_empty() {
383            return Vec::new();
384        }
385
386        // Lattice 재설정
387        self.lattice.reset(text);
388
389        // Lattice 구축
390        self.build_lattice();
391
392        // Viterbi 탐색
393        let path = self
394            .viterbi_searcher
395            .search(&mut self.lattice, self.dictionary.matrix());
396
397        // Token 변환 (byte positions mapped to original text)
398        path.iter()
399            .filter_map(|&node_id| self.lattice.node(node_id))
400            .map(|node| {
401                let mut token = Token::from_node(node);
402                let orig_start = self.lattice.original_byte_pos(node.start_pos);
403                token.start_byte = orig_start;
404                token.end_byte = orig_start + node.surface.len();
405                token
406            })
407            .collect()
408    }
409
410    /// Lattice 구축
411    ///
412    /// 입력 텍스트의 각 위치에서 사전 검색 및 미등록어 처리를 수행하여
413    /// Lattice에 노드를 추가합니다.
414    fn build_lattice(&mut self) {
415        let char_len = self.lattice.char_len();
416
417        // 각 문자 위치에서 사전 검색 및 미등록어 처리
418        for pos in 0..char_len {
419            // 사전 검색
420            let has_dict_entry = self.add_dict_nodes(pos);
421
422            // 미등록어 처리
423            self.unknown_handler
424                .add_unknown_nodes(&mut self.lattice, pos, has_dict_entry);
425        }
426    }
427
428    /// 사전 노드 추가
429    ///
430    /// 특정 위치에서 시작하는 모든 사전 엔트리를 Lattice에 추가합니다.
431    ///
432    /// # Arguments
433    ///
434    /// * `start_pos` - 시작 위치 (문자 단위)
435    ///
436    /// # Returns
437    ///
438    /// 사전 엔트리가 하나라도 있으면 true
439    fn add_dict_nodes(&mut self, start_pos: usize) -> bool {
440        // Get the byte range for the suffix starting at `start_pos` without
441        // allocating a new String.  We collect only the trie-match indices
442        // (small integers) before any lattice mutation, so the immutable borrow
443        // of `self.lattice` is released before we call `add_node`.
444        let char_len = self.lattice.char_len();
445        let search_text: &str = self.lattice.substring(start_pos, char_len);
446
447        if search_text.is_empty() {
448            return false;
449        }
450
451        // Use dictionary.common_prefix_search which returns all entries for
452        // the same surface (not just the first one). This is essential for
453        // the Viterbi algorithm to consider all possible POS tags and select
454        // the best path based on connection costs.
455        let dict_entries: Vec<_> = self
456            .dictionary
457            .common_prefix_search(search_text)
458            .unwrap_or_default();
459
460        // Collect user-dict entries as owned data before mutating lattice.
461        // user_dict.common_prefix_search returns owned UserEntry values so
462        // this is already allocation-minimal; we just need to separate the
463        // immutable borrow from the mutable one.
464        let user_entries: Vec<_> = self
465            .dictionary
466            .user_dictionary()
467            .map(|ud| ud.common_prefix_search(search_text))
468            .unwrap_or_default();
469
470        // Immutable borrows on self.lattice are now finished; we can mutate.
471        let mut found = false;
472
473        for (entry, byte_len) in dict_entries {
474            // Use the trie-provided byte_len to compute end_pos via
475            // binary search on char_positions, avoiding chars().count().
476            let end_pos = self
477                .lattice
478                .char_pos_from_start_and_byte_len(start_pos, byte_len);
479
480            self.lattice.add_node(
481                NodeBuilder::new(&entry.surface, start_pos, end_pos)
482                    .left_id(entry.left_id)
483                    .right_id(entry.right_id)
484                    .word_cost(i32::from(entry.cost))
485                    .node_type(NodeType::Known)
486                    .feature(&entry.feature),
487            );
488
489            found = true;
490        }
491
492        for user_entry in user_entries {
493            let surface_char_len = user_entry.surface.chars().count();
494            let end_pos = start_pos + surface_char_len;
495
496            self.lattice.add_node(
497                NodeBuilder::new(&user_entry.surface, start_pos, end_pos)
498                    .left_id(user_entry.left_id)
499                    .right_id(user_entry.right_id)
500                    .word_cost(i32::from(user_entry.cost))
501                    .node_type(NodeType::User)
502                    .feature(&user_entry.feature),
503            );
504
505            found = true;
506        }
507
508        found
509    }
510
511    /// Lattice를 반환하여 검사
512    ///
513    /// Viterbi 탐색 전의 Lattice 상태를 반환합니다. (디버깅/테스트용)
514    ///
515    /// # Arguments
516    ///
517    /// * `text` - 분석할 텍스트
518    ///
519    /// # Returns
520    ///
521    /// 구축된 Lattice
522    pub fn tokenize_to_lattice(&mut self, text: &str) -> &Lattice {
523        if !text.is_empty() {
524            self.lattice.reset(text);
525            self.build_lattice();
526        }
527        &self.lattice
528    }
529
530    /// 표면형만 추출 (wakati)
531    ///
532    /// # Arguments
533    ///
534    /// * `text` - 분석할 텍스트
535    ///
536    /// # Returns
537    ///
538    /// 분리된 표면형 목록 (wakati gaki)
539    ///
540    /// 일본어 형태소 분석기의 wakati gaki 모드와 동일합니다.
541    /// 형태소로 분리된 표면형만 반환합니다.
542    ///
543    /// # Arguments
544    ///
545    /// * `text` - 분석할 텍스트
546    ///
547    /// # Returns
548    ///
549    /// 분리된 표면형 목록
550    ///
551    /// # Example
552    ///
553    /// ```rust,no_run
554    /// use mecab_ko_core::Tokenizer;
555    ///
556    /// let mut tokenizer = Tokenizer::new().unwrap();
557    /// let surfaces = tokenizer.wakati("아버지가방에들어가신다");
558    /// // ["아버지", "가", "방", "에", "들어가", "신다"]
559    /// ```
560    pub fn wakati(&mut self, text: &str) -> Vec<String> {
561        self.tokenize(text).into_iter().map(|t| t.surface).collect()
562    }
563
564    /// 명사만 추출
565    ///
566    /// # Arguments
567    ///
568    /// * `text` - 분석할 텍스트
569    ///
570    /// # Returns
571    ///
572    /// 명사 목록
573    pub fn nouns(&mut self, text: &str) -> Vec<String> {
574        self.tokenize(text)
575            .into_iter()
576            .filter(|t| t.pos.starts_with("NN"))
577            .map(|t| t.surface)
578            .collect()
579    }
580
581    /// 형태소 목록 추출
582    ///
583    /// [`wakati`](Self::wakati)와 동일한 기능입니다.
584    /// Python의 `KoNLPy` 인터페이스와 호환됩니다.
585    ///
586    /// # Arguments
587    ///
588    /// * `text` - 분석할 텍스트
589    ///
590    /// # Returns
591    ///
592    /// 형태소 목록
593    pub fn morphs(&mut self, text: &str) -> Vec<String> {
594        self.wakati(text)
595    }
596
597    /// 품사 태깅
598    ///
599    /// 형태소와 품사 태그 쌍을 반환합니다.
600    /// Python의 `KoNLPy` 인터페이스와 호환됩니다.
601    ///
602    /// # Arguments
603    ///
604    /// * `text` - 분석할 텍스트
605    ///
606    /// # Returns
607    ///
608    /// `(표면형, 품사)` 쌍의 벡터
609    ///
610    /// # Example
611    ///
612    /// ```rust,no_run
613    /// use mecab_ko_core::Tokenizer;
614    ///
615    /// let mut tokenizer = Tokenizer::new().unwrap();
616    /// let tagged = tokenizer.pos("아버지가방에들어가신다");
617    /// // [("아버지", "NNG"), ("가", "JKS"), ("방", "NNG"), ...]
618    /// ```
619    pub fn pos(&mut self, text: &str) -> Vec<(String, String)> {
620        self.tokenize(text)
621            .into_iter()
622            .map(|t| (t.surface, t.pos))
623            .collect()
624    }
625
626    /// 시스템 사전 참조 반환
627    ///
628    /// 내부 시스템 사전에 대한 읽기 전용 참조를 반환합니다.
629    /// 사전 정보 조회나 디버깅에 유용합니다.
630    #[must_use]
631    pub const fn dictionary(&self) -> &SystemDictionary {
632        &self.dictionary
633    }
634
635    /// Lattice 통계 정보
636    ///
637    /// 마지막 분석에서 생성된 Lattice의 통계 정보를 반환합니다.
638    /// 노드 수, 엣지 수 등 디버깅 및 프로파일링에 유용합니다.
639    #[must_use]
640    pub fn lattice_stats(&self) -> crate::lattice::LatticeStats {
641        self.lattice.stats()
642    }
643
644    /// 메모리 풀 통계 정보
645    ///
646    /// 메모리 풀의 사용 현황을 반환합니다.
647    #[must_use]
648    pub fn pool_stats(&self) -> PoolStats {
649        self.pool_manager.stats()
650    }
651
652    /// 메모리 사용량 통계
653    ///
654    /// 토크나이저의 메모리 사용 현황을 반환합니다.
655    #[must_use]
656    pub fn memory_stats(&self) -> crate::memory::MemoryStats {
657        crate::memory::MemoryStats {
658            dictionary_bytes: 0, // 사전 크기는 별도 측정 필요
659            lattice_bytes: self.lattice.memory_usage(),
660            pool_bytes: self.pool_manager.total_memory_usage(),
661            cache_bytes: 0,
662            interner_bytes: 0,
663            token_bytes: 0,
664        }
665    }
666
667    /// 메모리 풀 초기화
668    ///
669    /// 모든 풀을 비워 메모리를 해제합니다.
670    /// 장기 실행 프로세스에서 주기적으로 호출하여 메모리 누수 방지.
671    pub fn clear_pools(&self) {
672        self.pool_manager.clear_all();
673    }
674
675    /// 외래어 정규화 활성화
676    ///
677    /// # Arguments
678    ///
679    /// * `enable` - 정규화 활성화 여부
680    /// * `config` - 정규화 설정 (None이면 기본 설정 사용)
681    ///
682    /// # Errors
683    ///
684    /// 정규화기 초기화 실패 시 에러 반환
685    pub fn set_normalization(
686        &mut self,
687        enable: bool,
688        config: Option<NormalizationConfig>,
689    ) -> Result<()> {
690        self.enable_normalization = enable;
691
692        if enable {
693            let normalizer_config = config.unwrap_or_default();
694            self.normalizer = Some(Normalizer::new(normalizer_config)?);
695        } else {
696            self.normalizer = None;
697        }
698
699        Ok(())
700    }
701
702    /// 외래어 정규화기 참조 반환
703    #[must_use]
704    pub const fn normalizer(&self) -> Option<&Normalizer> {
705        self.normalizer.as_ref()
706    }
707
708    /// 정규화가 활성화되어 있는지 확인
709    #[must_use]
710    pub const fn is_normalization_enabled(&self) -> bool {
711        self.enable_normalization
712    }
713
714    /// 정규화 적용 형태소 분석
715    ///
716    /// 토큰의 표면형에 대해 정규화를 적용하고, 정규화된 형태도 함께 반환합니다.
717    ///
718    /// # Arguments
719    ///
720    /// * `text` - 분석할 텍스트
721    ///
722    /// # Returns
723    ///
724    /// 정규화 정보가 포함된 토큰 목록
725    pub fn tokenize_with_normalization(&mut self, text: &str) -> Vec<Token> {
726        let mut tokens = self.tokenize(text);
727
728        // 정규화 적용
729        if let Some(normalizer) = &self.normalizer {
730            for token in &mut tokens {
731                token.normalized = Some(normalizer.normalize(&token.surface));
732            }
733        }
734
735        tokens
736    }
737
738    /// 변이형 확장 검색
739    ///
740    /// 입력 단어의 변이형들을 모두 고려하여 사전 검색을 수행합니다.
741    ///
742    /// # Arguments
743    ///
744    /// * `word` - 검색할 단어
745    ///
746    /// # Returns
747    ///
748    /// `(표준형, [변이형들])` 튜플
749    #[must_use]
750    pub fn get_word_variants(&self, word: &str) -> (String, Vec<String>) {
751        self.normalizer.as_ref().map_or_else(
752            || (word.to_string(), Vec::new()),
753            |normalizer| {
754                let standard = normalizer.normalize(word);
755                let variants = normalizer.get_variants(&standard);
756                (standard, variants)
757            },
758        )
759    }
760}
761
762// Note: Default implementation is not provided for Tokenizer because initialization
763// can fail (dictionary loading, etc.). Use Tokenizer::new() explicitly instead.
764
765#[cfg(test)]
766#[allow(clippy::expect_used, clippy::vec_init_then_push)]
767mod tests {
768    use super::*;
769    use mecab_ko_dict::{matrix::DenseMatrix, trie::TrieBuilder, DictEntry};
770
771    /// 테스트용 토크나이저 생성
772    fn create_test_tokenizer() -> Tokenizer {
773        // 테스트용 Trie 생성
774        let mut trie_entries = vec![
775            ("아버지", 0u32),
776            ("가", 1),
777            ("방", 2),
778            ("에", 3),
779            ("들어가", 4),
780            ("신다", 5),
781        ];
782        let trie_bytes = TrieBuilder::build_unsorted(&mut trie_entries).expect("should build trie");
783        let trie =
784            mecab_ko_dict::TrieBackend::Owned(mecab_ko_dict::Trie::from_vec(trie_bytes));
785
786        // 테스트용 Matrix 생성
787        let matrix = DenseMatrix::new(10, 10, 100);
788        let matrix = mecab_ko_dict::matrix::ConnectionMatrix::Dense(matrix);
789
790        // 테스트용 엔트리 생성
791        let mut entries = Vec::new();
792        entries.push(DictEntry::new(
793            "아버지",
794            1,
795            1,
796            1000,
797            "NNG,*,T,아버지,*,*,*,*",
798        ));
799        entries.push(DictEntry::new("가", 5, 5, 500, "JKS,*,F,가,*,*,*,*"));
800        entries.push(DictEntry::new("방", 2, 2, 2000, "NNG,*,T,방,*,*,*,*"));
801        entries.push(DictEntry::new("에", 6, 6, 400, "JKB,*,F,에,*,*,*,*"));
802        entries.push(DictEntry::new(
803            "들어가",
804            3,
805            3,
806            1500,
807            "VV,*,F,들어가다,*,*,*,*",
808        ));
809        entries.push(DictEntry::new("신다", 4, 4, 1800, "VV+EP,*,F,신다,*,*,*,*"));
810
811        let dictionary = SystemDictionary::new_test(
812            std::path::PathBuf::from("./test_dic"),
813            trie,
814            matrix,
815            entries,
816        );
817
818        let unknown_handler = UnknownHandler::korean_default();
819        let viterbi_searcher = ViterbiSearcher::new();
820        let lattice = Lattice::new("");
821
822        Tokenizer {
823            dictionary,
824            unknown_handler,
825            viterbi_searcher,
826            lattice,
827            normalizer: None,
828            enable_normalization: false,
829            pool_manager: PoolManager::new(),
830        }
831    }
832
833    #[test]
834    fn test_token_creation() {
835        let token = Token::new("안녕".to_string(), "NNG".to_string(), 0, 2, 0, 6);
836
837        assert_eq!(token.surface, "안녕");
838        assert_eq!(token.pos, "NNG");
839        assert_eq!(token.start_pos, 0);
840        assert_eq!(token.end_pos, 2);
841        assert_eq!(token.char_len(), 2);
842        assert_eq!(token.byte_len(), 6);
843    }
844
845    #[test]
846    fn test_parse_features() {
847        let features = "NNG,*,T,안녕,*,*,*,*";
848        let (pos, reading, lemma) = parse_features(features);
849
850        assert_eq!(pos, "NNG");
851        assert_eq!(reading, Some("안녕".to_string()));
852        assert_eq!(lemma, Some("안녕".to_string()));
853    }
854
855    #[test]
856    fn test_parse_features_no_reading() {
857        let features = "JKS,*,F,*,*,*,*,*";
858        let (pos, reading, _lemma) = parse_features(features);
859
860        assert_eq!(pos, "JKS");
861        assert_eq!(reading, None);
862    }
863
864    #[test]
865    fn test_tokenize_simple() {
866        let mut tokenizer = create_test_tokenizer();
867        let tokens = tokenizer.tokenize("아버지");
868
869        assert!(!tokens.is_empty());
870        assert_eq!(tokens[0].surface, "아버지");
871        assert_eq!(tokens[0].pos, "NNG");
872    }
873
874    #[test]
875    fn test_tokenize_with_particle() {
876        let mut tokenizer = create_test_tokenizer();
877        let tokens = tokenizer.tokenize("아버지가");
878
879        assert_eq!(tokens.len(), 2);
880        assert_eq!(tokens[0].surface, "아버지");
881        assert_eq!(tokens[0].pos, "NNG");
882        assert_eq!(tokens[1].surface, "가");
883        assert_eq!(tokens[1].pos, "JKS");
884    }
885
886    #[test]
887    fn test_tokenize_complex() {
888        let mut tokenizer = create_test_tokenizer();
889        let tokens = tokenizer.tokenize("아버지가방에들어가신다");
890
891        // 최소한 "아버지", "가", "방", "에", ... 등이 분석되어야 함
892        assert!(!tokens.is_empty());
893
894        // 첫 토큰은 "아버지"
895        assert_eq!(tokens[0].surface, "아버지");
896    }
897
898    #[test]
899    fn test_tokenize_empty() {
900        let mut tokenizer = create_test_tokenizer();
901        let tokens = tokenizer.tokenize("");
902
903        assert!(tokens.is_empty());
904    }
905
906    #[test]
907    fn test_tokenize_with_spaces() {
908        let mut tokenizer = create_test_tokenizer();
909        let tokens = tokenizer.tokenize("아버지 가방");
910
911        // 공백은 제거되고 "아버지가방"으로 분석됨
912        assert!(!tokens.is_empty());
913    }
914
915    #[test]
916    fn test_wakati() {
917        let mut tokenizer = create_test_tokenizer();
918        let surfaces = tokenizer.wakati("아버지가");
919
920        assert_eq!(surfaces.len(), 2);
921        assert_eq!(surfaces[0], "아버지");
922        assert_eq!(surfaces[1], "가");
923    }
924
925    #[test]
926    fn test_nouns() {
927        let mut tokenizer = create_test_tokenizer();
928        let nouns = tokenizer.nouns("아버지가방에");
929
930        // "아버지"와 "방"이 명사 (NNG)
931        assert!(nouns.contains(&"아버지".to_string()));
932        assert!(nouns.contains(&"방".to_string()));
933        assert!(!nouns.contains(&"가".to_string())); // 조사는 제외
934    }
935
936    #[test]
937    fn test_pos() {
938        let mut tokenizer = create_test_tokenizer();
939        let pos_tags = tokenizer.pos("아버지가");
940
941        assert_eq!(pos_tags.len(), 2);
942        assert_eq!(pos_tags[0], ("아버지".to_string(), "NNG".to_string()));
943        assert_eq!(pos_tags[1], ("가".to_string(), "JKS".to_string()));
944    }
945
946    #[test]
947    fn test_tokenize_to_lattice() {
948        let mut tokenizer = create_test_tokenizer();
949        let lattice = tokenizer.tokenize_to_lattice("아버지가");
950
951        // Lattice에 노드가 추가되었는지 확인
952        assert!(lattice.node_count() > 2); // BOS, EOS 외에 최소 1개 이상
953
954        // 통계 확인
955        let stats = lattice.stats();
956        assert!(stats.total_nodes > 2);
957    }
958
959    #[test]
960    fn test_lattice_stats() {
961        let mut tokenizer = create_test_tokenizer();
962        tokenizer.tokenize("아버지가");
963
964        let stats = tokenizer.lattice_stats();
965        assert!(stats.total_nodes > 0);
966        assert!(stats.char_length > 0);
967    }
968
969    #[test]
970    fn test_token_positions() {
971        let mut tokenizer = create_test_tokenizer();
972        let tokens = tokenizer.tokenize("아버지가");
973
974        // 첫 번째 토큰: "아버지"
975        assert_eq!(tokens[0].start_pos, 0);
976        assert_eq!(tokens[0].end_pos, 3);
977
978        // 두 번째 토큰: "가"
979        assert_eq!(tokens[1].start_pos, 3);
980        assert_eq!(tokens[1].end_pos, 4);
981    }
982
983    #[test]
984    fn test_multiple_tokenize_calls() {
985        let mut tokenizer = create_test_tokenizer();
986
987        // 첫 번째 분석
988        let tokens1 = tokenizer.tokenize("아버지");
989        assert!(!tokens1.is_empty());
990
991        // 두 번째 분석 (Lattice 재사용)
992        let tokens2 = tokenizer.tokenize("가방");
993        assert!(!tokens2.is_empty());
994
995        // 각 분석이 독립적으로 동작해야 함
996        assert_ne!(tokens1[0].surface, tokens2[0].surface);
997    }
998
999    #[test]
1000    fn test_token_from_node() {
1001        use crate::lattice::Node;
1002        use std::borrow::Cow;
1003
1004        let node = Node {
1005            id: 1,
1006            surface: Cow::Borrowed("테스트"),
1007            start_pos: 0,
1008            end_pos: 3,
1009            start_byte: 0,
1010            end_byte: 9,
1011            left_id: 1,
1012            right_id: 1,
1013            word_cost: 1000,
1014            total_cost: 1500,
1015            prev_node_id: 0,
1016            node_type: NodeType::Known,
1017            feature: Cow::Borrowed("NNG,*,T,테스트,*,*,*,*"),
1018            has_space_before: false,
1019        };
1020
1021        let token = Token::from_node(&node);
1022
1023        assert_eq!(token.surface, "테스트");
1024        assert_eq!(token.pos, "NNG");
1025        assert_eq!(token.start_pos, 0);
1026        assert_eq!(token.end_pos, 3);
1027        assert_eq!(token.reading, Some("테스트".to_string()));
1028        assert_eq!(token.cost, 1500);
1029    }
1030
1031    #[test]
1032    fn test_with_user_dict() {
1033        let mut tokenizer = create_test_tokenizer();
1034
1035        let mut user_dict = UserDictionary::new();
1036        user_dict.add_entry("딥러닝", "NNG", Some(-1000), None);
1037
1038        tokenizer.set_user_dict(user_dict);
1039
1040        // 사용자 사전이 설정되었는지 확인
1041        assert!(tokenizer.dictionary().user_dictionary().is_some());
1042    }
1043}