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