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 변환
398        path.iter()
399            .filter_map(|&node_id| self.lattice.node(node_id))
400            .map(Token::from_node)
401            .collect()
402    }
403
404    /// Lattice 구축
405    ///
406    /// 입력 텍스트의 각 위치에서 사전 검색 및 미등록어 처리를 수행하여
407    /// Lattice에 노드를 추가합니다.
408    fn build_lattice(&mut self) {
409        let char_len = self.lattice.char_len();
410
411        // 각 문자 위치에서 사전 검색 및 미등록어 처리
412        for pos in 0..char_len {
413            // 사전 검색
414            let has_dict_entry = self.add_dict_nodes(pos);
415
416            // 미등록어 처리
417            self.unknown_handler
418                .add_unknown_nodes(&mut self.lattice, pos, has_dict_entry);
419        }
420    }
421
422    /// 사전 노드 추가
423    ///
424    /// 특정 위치에서 시작하는 모든 사전 엔트리를 Lattice에 추가합니다.
425    ///
426    /// # Arguments
427    ///
428    /// * `start_pos` - 시작 위치 (문자 단위)
429    ///
430    /// # Returns
431    ///
432    /// 사전 엔트리가 하나라도 있으면 true
433    fn add_dict_nodes(&mut self, start_pos: usize) -> bool {
434        // Get the byte range for the suffix starting at `start_pos` without
435        // allocating a new String.  We collect only the trie-match indices
436        // (small integers) before any lattice mutation, so the immutable borrow
437        // of `self.lattice` is released before we call `add_node`.
438        let char_len = self.lattice.char_len();
439        let search_text: &str = self.lattice.substring(start_pos, char_len);
440
441        if search_text.is_empty() {
442            return false;
443        }
444
445        // Use dictionary.common_prefix_search which returns all entries for
446        // the same surface (not just the first one). This is essential for
447        // the Viterbi algorithm to consider all possible POS tags and select
448        // the best path based on connection costs.
449        let dict_entries: Vec<_> = self
450            .dictionary
451            .common_prefix_search(search_text)
452            .unwrap_or_default();
453
454        // Collect user-dict entries as owned data before mutating lattice.
455        // user_dict.common_prefix_search returns owned UserEntry values so
456        // this is already allocation-minimal; we just need to separate the
457        // immutable borrow from the mutable one.
458        let user_entries: Vec<_> = self
459            .dictionary
460            .user_dictionary()
461            .map(|ud| ud.common_prefix_search(search_text))
462            .unwrap_or_default();
463
464        // Immutable borrows on self.lattice are now finished; we can mutate.
465        let mut found = false;
466
467        for (entry, byte_len) in dict_entries {
468            // Use the trie-provided byte_len to compute end_pos via
469            // binary search on char_positions, avoiding chars().count().
470            let end_pos = self
471                .lattice
472                .char_pos_from_start_and_byte_len(start_pos, byte_len);
473
474            self.lattice.add_node(
475                NodeBuilder::new(&entry.surface, start_pos, end_pos)
476                    .left_id(entry.left_id)
477                    .right_id(entry.right_id)
478                    .word_cost(i32::from(entry.cost))
479                    .node_type(NodeType::Known)
480                    .feature(&entry.feature),
481            );
482
483            found = true;
484        }
485
486        for user_entry in user_entries {
487            let surface_char_len = user_entry.surface.chars().count();
488            let end_pos = start_pos + surface_char_len;
489
490            self.lattice.add_node(
491                NodeBuilder::new(&user_entry.surface, start_pos, end_pos)
492                    .left_id(user_entry.left_id)
493                    .right_id(user_entry.right_id)
494                    .word_cost(i32::from(user_entry.cost))
495                    .node_type(NodeType::User)
496                    .feature(&user_entry.feature),
497            );
498
499            found = true;
500        }
501
502        found
503    }
504
505    /// Lattice를 반환하여 검사
506    ///
507    /// Viterbi 탐색 전의 Lattice 상태를 반환합니다. (디버깅/테스트용)
508    ///
509    /// # Arguments
510    ///
511    /// * `text` - 분석할 텍스트
512    ///
513    /// # Returns
514    ///
515    /// 구축된 Lattice
516    pub fn tokenize_to_lattice(&mut self, text: &str) -> &Lattice {
517        if !text.is_empty() {
518            self.lattice.reset(text);
519            self.build_lattice();
520        }
521        &self.lattice
522    }
523
524    /// 표면형만 추출 (wakati)
525    ///
526    /// # Arguments
527    ///
528    /// * `text` - 분석할 텍스트
529    ///
530    /// # Returns
531    ///
532    /// 분리된 표면형 목록 (wakati gaki)
533    ///
534    /// 일본어 형태소 분석기의 wakati gaki 모드와 동일합니다.
535    /// 형태소로 분리된 표면형만 반환합니다.
536    ///
537    /// # Arguments
538    ///
539    /// * `text` - 분석할 텍스트
540    ///
541    /// # Returns
542    ///
543    /// 분리된 표면형 목록
544    ///
545    /// # Example
546    ///
547    /// ```rust,no_run
548    /// use mecab_ko_core::Tokenizer;
549    ///
550    /// let mut tokenizer = Tokenizer::new().unwrap();
551    /// let surfaces = tokenizer.wakati("아버지가방에들어가신다");
552    /// // ["아버지", "가", "방", "에", "들어가", "신다"]
553    /// ```
554    pub fn wakati(&mut self, text: &str) -> Vec<String> {
555        self.tokenize(text).into_iter().map(|t| t.surface).collect()
556    }
557
558    /// 명사만 추출
559    ///
560    /// # Arguments
561    ///
562    /// * `text` - 분석할 텍스트
563    ///
564    /// # Returns
565    ///
566    /// 명사 목록
567    pub fn nouns(&mut self, text: &str) -> Vec<String> {
568        self.tokenize(text)
569            .into_iter()
570            .filter(|t| t.pos.starts_with("NN"))
571            .map(|t| t.surface)
572            .collect()
573    }
574
575    /// 형태소 목록 추출
576    ///
577    /// [`wakati`](Self::wakati)와 동일한 기능입니다.
578    /// Python의 `KoNLPy` 인터페이스와 호환됩니다.
579    ///
580    /// # Arguments
581    ///
582    /// * `text` - 분석할 텍스트
583    ///
584    /// # Returns
585    ///
586    /// 형태소 목록
587    pub fn morphs(&mut self, text: &str) -> Vec<String> {
588        self.wakati(text)
589    }
590
591    /// 품사 태깅
592    ///
593    /// 형태소와 품사 태그 쌍을 반환합니다.
594    /// Python의 `KoNLPy` 인터페이스와 호환됩니다.
595    ///
596    /// # Arguments
597    ///
598    /// * `text` - 분석할 텍스트
599    ///
600    /// # Returns
601    ///
602    /// `(표면형, 품사)` 쌍의 벡터
603    ///
604    /// # Example
605    ///
606    /// ```rust,no_run
607    /// use mecab_ko_core::Tokenizer;
608    ///
609    /// let mut tokenizer = Tokenizer::new().unwrap();
610    /// let tagged = tokenizer.pos("아버지가방에들어가신다");
611    /// // [("아버지", "NNG"), ("가", "JKS"), ("방", "NNG"), ...]
612    /// ```
613    pub fn pos(&mut self, text: &str) -> Vec<(String, String)> {
614        self.tokenize(text)
615            .into_iter()
616            .map(|t| (t.surface, t.pos))
617            .collect()
618    }
619
620    /// 시스템 사전 참조 반환
621    ///
622    /// 내부 시스템 사전에 대한 읽기 전용 참조를 반환합니다.
623    /// 사전 정보 조회나 디버깅에 유용합니다.
624    #[must_use]
625    pub const fn dictionary(&self) -> &SystemDictionary {
626        &self.dictionary
627    }
628
629    /// Lattice 통계 정보
630    ///
631    /// 마지막 분석에서 생성된 Lattice의 통계 정보를 반환합니다.
632    /// 노드 수, 엣지 수 등 디버깅 및 프로파일링에 유용합니다.
633    #[must_use]
634    pub fn lattice_stats(&self) -> crate::lattice::LatticeStats {
635        self.lattice.stats()
636    }
637
638    /// 메모리 풀 통계 정보
639    ///
640    /// 메모리 풀의 사용 현황을 반환합니다.
641    #[must_use]
642    pub fn pool_stats(&self) -> PoolStats {
643        self.pool_manager.stats()
644    }
645
646    /// 메모리 사용량 통계
647    ///
648    /// 토크나이저의 메모리 사용 현황을 반환합니다.
649    #[must_use]
650    pub fn memory_stats(&self) -> crate::memory::MemoryStats {
651        crate::memory::MemoryStats {
652            dictionary_bytes: 0, // 사전 크기는 별도 측정 필요
653            lattice_bytes: self.lattice.memory_usage(),
654            pool_bytes: self.pool_manager.total_memory_usage(),
655            cache_bytes: 0,
656            interner_bytes: 0,
657            token_bytes: 0,
658        }
659    }
660
661    /// 메모리 풀 초기화
662    ///
663    /// 모든 풀을 비워 메모리를 해제합니다.
664    /// 장기 실행 프로세스에서 주기적으로 호출하여 메모리 누수 방지.
665    pub fn clear_pools(&self) {
666        self.pool_manager.clear_all();
667    }
668
669    /// 외래어 정규화 활성화
670    ///
671    /// # Arguments
672    ///
673    /// * `enable` - 정규화 활성화 여부
674    /// * `config` - 정규화 설정 (None이면 기본 설정 사용)
675    ///
676    /// # Errors
677    ///
678    /// 정규화기 초기화 실패 시 에러 반환
679    pub fn set_normalization(
680        &mut self,
681        enable: bool,
682        config: Option<NormalizationConfig>,
683    ) -> Result<()> {
684        self.enable_normalization = enable;
685
686        if enable {
687            let normalizer_config = config.unwrap_or_default();
688            self.normalizer = Some(Normalizer::new(normalizer_config)?);
689        } else {
690            self.normalizer = None;
691        }
692
693        Ok(())
694    }
695
696    /// 외래어 정규화기 참조 반환
697    #[must_use]
698    pub const fn normalizer(&self) -> Option<&Normalizer> {
699        self.normalizer.as_ref()
700    }
701
702    /// 정규화가 활성화되어 있는지 확인
703    #[must_use]
704    pub const fn is_normalization_enabled(&self) -> bool {
705        self.enable_normalization
706    }
707
708    /// 정규화 적용 형태소 분석
709    ///
710    /// 토큰의 표면형에 대해 정규화를 적용하고, 정규화된 형태도 함께 반환합니다.
711    ///
712    /// # Arguments
713    ///
714    /// * `text` - 분석할 텍스트
715    ///
716    /// # Returns
717    ///
718    /// 정규화 정보가 포함된 토큰 목록
719    pub fn tokenize_with_normalization(&mut self, text: &str) -> Vec<Token> {
720        let mut tokens = self.tokenize(text);
721
722        // 정규화 적용
723        if let Some(normalizer) = &self.normalizer {
724            for token in &mut tokens {
725                token.normalized = Some(normalizer.normalize(&token.surface));
726            }
727        }
728
729        tokens
730    }
731
732    /// 변이형 확장 검색
733    ///
734    /// 입력 단어의 변이형들을 모두 고려하여 사전 검색을 수행합니다.
735    ///
736    /// # Arguments
737    ///
738    /// * `word` - 검색할 단어
739    ///
740    /// # Returns
741    ///
742    /// `(표준형, [변이형들])` 튜플
743    #[must_use]
744    pub fn get_word_variants(&self, word: &str) -> (String, Vec<String>) {
745        self.normalizer.as_ref().map_or_else(
746            || (word.to_string(), Vec::new()),
747            |normalizer| {
748                let standard = normalizer.normalize(word);
749                let variants = normalizer.get_variants(&standard);
750                (standard, variants)
751            },
752        )
753    }
754}
755
756// Note: Default implementation is not provided for Tokenizer because initialization
757// can fail (dictionary loading, etc.). Use Tokenizer::new() explicitly instead.
758
759#[cfg(test)]
760#[allow(clippy::expect_used, clippy::vec_init_then_push)]
761mod tests {
762    use super::*;
763    use mecab_ko_dict::{matrix::DenseMatrix, trie::TrieBuilder, DictEntry};
764
765    /// 테스트용 토크나이저 생성
766    fn create_test_tokenizer() -> Tokenizer {
767        // 테스트용 Trie 생성
768        let mut trie_entries = vec![
769            ("아버지", 0u32),
770            ("가", 1),
771            ("방", 2),
772            ("에", 3),
773            ("들어가", 4),
774            ("신다", 5),
775        ];
776        let trie_bytes = TrieBuilder::build_unsorted(&mut trie_entries).expect("should build trie");
777        let trie = mecab_ko_dict::Trie::from_vec(trie_bytes);
778
779        // 테스트용 Matrix 생성
780        let matrix = DenseMatrix::new(10, 10, 100);
781        let matrix = mecab_ko_dict::matrix::ConnectionMatrix::Dense(matrix);
782
783        // 테스트용 엔트리 생성
784        let mut entries = Vec::new();
785        entries.push(DictEntry::new(
786            "아버지",
787            1,
788            1,
789            1000,
790            "NNG,*,T,아버지,*,*,*,*",
791        ));
792        entries.push(DictEntry::new("가", 5, 5, 500, "JKS,*,F,가,*,*,*,*"));
793        entries.push(DictEntry::new("방", 2, 2, 2000, "NNG,*,T,방,*,*,*,*"));
794        entries.push(DictEntry::new("에", 6, 6, 400, "JKB,*,F,에,*,*,*,*"));
795        entries.push(DictEntry::new(
796            "들어가",
797            3,
798            3,
799            1500,
800            "VV,*,F,들어가다,*,*,*,*",
801        ));
802        entries.push(DictEntry::new("신다", 4, 4, 1800, "VV+EP,*,F,신다,*,*,*,*"));
803
804        let dictionary = SystemDictionary::new_test(
805            std::path::PathBuf::from("./test_dic"),
806            trie,
807            matrix,
808            entries,
809        );
810
811        let unknown_handler = UnknownHandler::korean_default();
812        let viterbi_searcher = ViterbiSearcher::new();
813        let lattice = Lattice::new("");
814
815        Tokenizer {
816            dictionary,
817            unknown_handler,
818            viterbi_searcher,
819            lattice,
820            normalizer: None,
821            enable_normalization: false,
822            pool_manager: PoolManager::new(),
823        }
824    }
825
826    #[test]
827    fn test_token_creation() {
828        let token = Token::new("안녕".to_string(), "NNG".to_string(), 0, 2, 0, 6);
829
830        assert_eq!(token.surface, "안녕");
831        assert_eq!(token.pos, "NNG");
832        assert_eq!(token.start_pos, 0);
833        assert_eq!(token.end_pos, 2);
834        assert_eq!(token.char_len(), 2);
835        assert_eq!(token.byte_len(), 6);
836    }
837
838    #[test]
839    fn test_parse_features() {
840        let features = "NNG,*,T,안녕,*,*,*,*";
841        let (pos, reading, lemma) = parse_features(features);
842
843        assert_eq!(pos, "NNG");
844        assert_eq!(reading, Some("안녕".to_string()));
845        assert_eq!(lemma, Some("안녕".to_string()));
846    }
847
848    #[test]
849    fn test_parse_features_no_reading() {
850        let features = "JKS,*,F,*,*,*,*,*";
851        let (pos, reading, _lemma) = parse_features(features);
852
853        assert_eq!(pos, "JKS");
854        assert_eq!(reading, None);
855    }
856
857    #[test]
858    fn test_tokenize_simple() {
859        let mut tokenizer = create_test_tokenizer();
860        let tokens = tokenizer.tokenize("아버지");
861
862        assert!(!tokens.is_empty());
863        assert_eq!(tokens[0].surface, "아버지");
864        assert_eq!(tokens[0].pos, "NNG");
865    }
866
867    #[test]
868    fn test_tokenize_with_particle() {
869        let mut tokenizer = create_test_tokenizer();
870        let tokens = tokenizer.tokenize("아버지가");
871
872        assert_eq!(tokens.len(), 2);
873        assert_eq!(tokens[0].surface, "아버지");
874        assert_eq!(tokens[0].pos, "NNG");
875        assert_eq!(tokens[1].surface, "가");
876        assert_eq!(tokens[1].pos, "JKS");
877    }
878
879    #[test]
880    fn test_tokenize_complex() {
881        let mut tokenizer = create_test_tokenizer();
882        let tokens = tokenizer.tokenize("아버지가방에들어가신다");
883
884        // 최소한 "아버지", "가", "방", "에", ... 등이 분석되어야 함
885        assert!(!tokens.is_empty());
886
887        // 첫 토큰은 "아버지"
888        assert_eq!(tokens[0].surface, "아버지");
889    }
890
891    #[test]
892    fn test_tokenize_empty() {
893        let mut tokenizer = create_test_tokenizer();
894        let tokens = tokenizer.tokenize("");
895
896        assert!(tokens.is_empty());
897    }
898
899    #[test]
900    fn test_tokenize_with_spaces() {
901        let mut tokenizer = create_test_tokenizer();
902        let tokens = tokenizer.tokenize("아버지 가방");
903
904        // 공백은 제거되고 "아버지가방"으로 분석됨
905        assert!(!tokens.is_empty());
906    }
907
908    #[test]
909    fn test_wakati() {
910        let mut tokenizer = create_test_tokenizer();
911        let surfaces = tokenizer.wakati("아버지가");
912
913        assert_eq!(surfaces.len(), 2);
914        assert_eq!(surfaces[0], "아버지");
915        assert_eq!(surfaces[1], "가");
916    }
917
918    #[test]
919    fn test_nouns() {
920        let mut tokenizer = create_test_tokenizer();
921        let nouns = tokenizer.nouns("아버지가방에");
922
923        // "아버지"와 "방"이 명사 (NNG)
924        assert!(nouns.contains(&"아버지".to_string()));
925        assert!(nouns.contains(&"방".to_string()));
926        assert!(!nouns.contains(&"가".to_string())); // 조사는 제외
927    }
928
929    #[test]
930    fn test_pos() {
931        let mut tokenizer = create_test_tokenizer();
932        let pos_tags = tokenizer.pos("아버지가");
933
934        assert_eq!(pos_tags.len(), 2);
935        assert_eq!(pos_tags[0], ("아버지".to_string(), "NNG".to_string()));
936        assert_eq!(pos_tags[1], ("가".to_string(), "JKS".to_string()));
937    }
938
939    #[test]
940    fn test_tokenize_to_lattice() {
941        let mut tokenizer = create_test_tokenizer();
942        let lattice = tokenizer.tokenize_to_lattice("아버지가");
943
944        // Lattice에 노드가 추가되었는지 확인
945        assert!(lattice.node_count() > 2); // BOS, EOS 외에 최소 1개 이상
946
947        // 통계 확인
948        let stats = lattice.stats();
949        assert!(stats.total_nodes > 2);
950    }
951
952    #[test]
953    fn test_lattice_stats() {
954        let mut tokenizer = create_test_tokenizer();
955        tokenizer.tokenize("아버지가");
956
957        let stats = tokenizer.lattice_stats();
958        assert!(stats.total_nodes > 0);
959        assert!(stats.char_length > 0);
960    }
961
962    #[test]
963    fn test_token_positions() {
964        let mut tokenizer = create_test_tokenizer();
965        let tokens = tokenizer.tokenize("아버지가");
966
967        // 첫 번째 토큰: "아버지"
968        assert_eq!(tokens[0].start_pos, 0);
969        assert_eq!(tokens[0].end_pos, 3);
970
971        // 두 번째 토큰: "가"
972        assert_eq!(tokens[1].start_pos, 3);
973        assert_eq!(tokens[1].end_pos, 4);
974    }
975
976    #[test]
977    fn test_multiple_tokenize_calls() {
978        let mut tokenizer = create_test_tokenizer();
979
980        // 첫 번째 분석
981        let tokens1 = tokenizer.tokenize("아버지");
982        assert!(!tokens1.is_empty());
983
984        // 두 번째 분석 (Lattice 재사용)
985        let tokens2 = tokenizer.tokenize("가방");
986        assert!(!tokens2.is_empty());
987
988        // 각 분석이 독립적으로 동작해야 함
989        assert_ne!(tokens1[0].surface, tokens2[0].surface);
990    }
991
992    #[test]
993    fn test_token_from_node() {
994        use crate::lattice::Node;
995        use std::borrow::Cow;
996
997        let node = Node {
998            id: 1,
999            surface: Cow::Borrowed("테스트"),
1000            start_pos: 0,
1001            end_pos: 3,
1002            start_byte: 0,
1003            end_byte: 9,
1004            left_id: 1,
1005            right_id: 1,
1006            word_cost: 1000,
1007            total_cost: 1500,
1008            prev_node_id: 0,
1009            node_type: NodeType::Known,
1010            feature: Cow::Borrowed("NNG,*,T,테스트,*,*,*,*"),
1011            has_space_before: false,
1012        };
1013
1014        let token = Token::from_node(&node);
1015
1016        assert_eq!(token.surface, "테스트");
1017        assert_eq!(token.pos, "NNG");
1018        assert_eq!(token.start_pos, 0);
1019        assert_eq!(token.end_pos, 3);
1020        assert_eq!(token.reading, Some("테스트".to_string()));
1021        assert_eq!(token.cost, 1500);
1022    }
1023
1024    #[test]
1025    fn test_with_user_dict() {
1026        let mut tokenizer = create_test_tokenizer();
1027
1028        let mut user_dict = UserDictionary::new();
1029        user_dict.add_entry("딥러닝", "NNG", Some(-1000), None);
1030
1031        tokenizer.set_user_dict(user_dict);
1032
1033        // 사용자 사전이 설정되었는지 확인
1034        assert!(tokenizer.dictionary().user_dictionary().is_some());
1035    }
1036}