Skip to main content

mecab_ko_core/
cache.rs

1//! 토큰화 캐싱
2//!
3//! 반복되는 입력에 대해 토큰화 결과를 캐싱하여 성능을 향상시킵니다.
4//!
5//! # 특징
6//!
7//! - **LRU 캐시**: Least Recently Used 방식으로 오래된 항목 자동 제거
8//! - **스레드 안전**: `RwLock` 기반 동시 접근 지원
9//! - **통계 추적**: 히트/미스 비율 모니터링
10//!
11//! # 예제
12//!
13//! ```rust,no_run
14//! use mecab_ko_core::cache::{TokenCache, CacheConfig};
15//!
16//! let config = CacheConfig::default();
17//! let cache = TokenCache::new(config);
18//!
19//! // 캐시 키 생성 (문자열 해시)
20//! let key = cache.make_key("안녕하세요");
21//!
22//! // 캐시 조회 또는 계산
23//! let tokens = cache.get_or_insert(key, || {
24//!     vec![] // 실제로는 토큰화 수행
25//! });
26//!
27//! // 통계 확인
28//! let stats = cache.stats();
29//! println!("Hit rate: {:.1}%", stats.hit_rate() * 100.0);
30//! ```
31
32use std::collections::HashMap;
33use std::hash::{DefaultHasher, Hash, Hasher};
34use std::sync::atomic::{AtomicU64, Ordering};
35use std::sync::RwLock;
36
37/// 캐시된 토큰 정보
38#[derive(Debug, Clone)]
39pub struct CachedToken {
40    /// 표면형
41    pub surface: String,
42    /// 품사 태그
43    pub pos: String,
44    /// 시작 바이트 위치
45    pub start_byte: usize,
46    /// 끝 바이트 위치
47    pub end_byte: usize,
48}
49
50/// 캐시 키 타입
51pub type CacheKey = u64;
52
53/// 캐시 설정
54#[derive(Debug, Clone)]
55pub struct CacheConfig {
56    /// 최대 캐시 항목 수
57    pub max_entries: usize,
58    /// 최대 키 길이 (바이트)
59    pub max_key_length: usize,
60    /// 통계 추적 활성화
61    pub track_stats: bool,
62}
63
64impl Default for CacheConfig {
65    fn default() -> Self {
66        Self {
67            max_entries: 10_000,
68            max_key_length: 1024,
69            track_stats: true,
70        }
71    }
72}
73
74impl CacheConfig {
75    /// 새 설정 생성
76    #[must_use]
77    pub const fn new() -> Self {
78        Self {
79            max_entries: 10_000,
80            max_key_length: 1024,
81            track_stats: true,
82        }
83    }
84
85    /// 최대 항목 수 설정
86    #[must_use]
87    pub const fn with_max_entries(mut self, max: usize) -> Self {
88        self.max_entries = max;
89        self
90    }
91
92    /// 최대 키 길이 설정
93    #[must_use]
94    pub const fn with_max_key_length(mut self, max: usize) -> Self {
95        self.max_key_length = max;
96        self
97    }
98
99    /// 통계 추적 설정
100    #[must_use]
101    pub const fn with_track_stats(mut self, track: bool) -> Self {
102        self.track_stats = track;
103        self
104    }
105}
106
107/// 캐시 통계
108#[derive(Debug, Default)]
109pub struct CacheStats {
110    /// 캐시 히트 횟수
111    hits: AtomicU64,
112    /// 캐시 미스 횟수
113    misses: AtomicU64,
114    /// 제거된 항목 수
115    evictions: AtomicU64,
116}
117
118impl CacheStats {
119    /// 히트 횟수
120    #[must_use]
121    pub fn hits(&self) -> u64 {
122        self.hits.load(Ordering::Relaxed)
123    }
124
125    /// 미스 횟수
126    #[must_use]
127    pub fn misses(&self) -> u64 {
128        self.misses.load(Ordering::Relaxed)
129    }
130
131    /// 총 요청 횟수
132    #[must_use]
133    pub fn total_requests(&self) -> u64 {
134        self.hits() + self.misses()
135    }
136
137    /// 히트율 (0.0 ~ 1.0)
138    #[must_use]
139    #[allow(clippy::cast_precision_loss)]
140    pub fn hit_rate(&self) -> f64 {
141        let total = self.total_requests();
142        if total == 0 {
143            0.0
144        } else {
145            self.hits() as f64 / total as f64
146        }
147    }
148
149    /// 제거된 항목 수
150    #[must_use]
151    pub fn evictions(&self) -> u64 {
152        self.evictions.load(Ordering::Relaxed)
153    }
154
155    fn record_hit(&self) {
156        self.hits.fetch_add(1, Ordering::Relaxed);
157    }
158
159    fn record_miss(&self) {
160        self.misses.fetch_add(1, Ordering::Relaxed);
161    }
162
163    fn record_eviction(&self) {
164        self.evictions.fetch_add(1, Ordering::Relaxed);
165    }
166
167    /// 통계 리셋
168    pub fn reset(&self) {
169        self.hits.store(0, Ordering::Relaxed);
170        self.misses.store(0, Ordering::Relaxed);
171        self.evictions.store(0, Ordering::Relaxed);
172    }
173}
174
175/// LRU 캐시 항목
176struct CacheEntry {
177    /// 캐시된 토큰들
178    tokens: Vec<CachedToken>,
179    /// 마지막 접근 시간 (순서 카운터)
180    last_access: u64,
181}
182
183/// 토큰화 캐시
184pub struct TokenCache {
185    config: CacheConfig,
186    entries: RwLock<HashMap<CacheKey, CacheEntry>>,
187    stats: CacheStats,
188    access_counter: AtomicU64,
189}
190
191impl TokenCache {
192    /// 새 캐시 생성
193    #[must_use]
194    pub fn new(config: CacheConfig) -> Self {
195        Self {
196            config,
197            entries: RwLock::new(HashMap::new()),
198            stats: CacheStats::default(),
199            access_counter: AtomicU64::new(0),
200        }
201    }
202
203    /// 기본 설정으로 캐시 생성
204    #[must_use]
205    pub fn with_defaults() -> Self {
206        Self::new(CacheConfig::default())
207    }
208
209    /// 문자열에서 캐시 키 생성
210    #[must_use]
211    pub fn make_key(&self, text: &str) -> CacheKey {
212        let mut hasher = DefaultHasher::new();
213        text.hash(&mut hasher);
214        hasher.finish()
215    }
216
217    /// 캐시에서 조회
218    #[must_use]
219    pub fn get(&self, key: CacheKey) -> Option<Vec<CachedToken>> {
220        let mut entries = self.entries.write().ok()?;
221
222        if let Some(entry) = entries.get_mut(&key) {
223            entry.last_access = self.access_counter.fetch_add(1, Ordering::Relaxed);
224            if self.config.track_stats {
225                self.stats.record_hit();
226            }
227            Some(entry.tokens.clone())
228        } else {
229            if self.config.track_stats {
230                self.stats.record_miss();
231            }
232            None
233        }
234    }
235
236    /// 캐시에 저장
237    pub fn insert(&self, key: CacheKey, tokens: Vec<CachedToken>) {
238        let Ok(mut entries) = self.entries.write() else {
239            return;
240        };
241
242        // 캐시 용량 초과 시 LRU 제거
243        while entries.len() >= self.config.max_entries {
244            self.evict_lru(&mut entries);
245        }
246
247        let access = self.access_counter.fetch_add(1, Ordering::Relaxed);
248        entries.insert(key, CacheEntry {
249            tokens,
250            last_access: access,
251        });
252    }
253
254    /// 캐시 조회 또는 계산 후 삽입
255    pub fn get_or_insert<F>(&self, key: CacheKey, compute: F) -> Vec<CachedToken>
256    where
257        F: FnOnce() -> Vec<CachedToken>,
258    {
259        // 먼저 읽기 시도
260        if let Some(tokens) = self.get(key) {
261            return tokens;
262        }
263
264        // 없으면 계산 후 삽입
265        let tokens = compute();
266        self.insert(key, tokens.clone());
267        tokens
268    }
269
270    /// 캐시에서 텍스트로 조회 또는 계산 후 삽입
271    pub fn get_or_insert_with_text<F>(&self, text: &str, compute: F) -> Vec<CachedToken>
272    where
273        F: FnOnce() -> Vec<CachedToken>,
274    {
275        // 너무 긴 텍스트는 캐시하지 않음
276        if text.len() > self.config.max_key_length {
277            return compute();
278        }
279
280        let key = self.make_key(text);
281        self.get_or_insert(key, compute)
282    }
283
284    /// LRU 항목 제거
285    fn evict_lru(&self, entries: &mut HashMap<CacheKey, CacheEntry>) {
286        if entries.is_empty() {
287            return;
288        }
289
290        // 가장 오래된 항목 찾기
291        let oldest_key = entries
292            .iter()
293            .min_by_key(|(_, entry)| entry.last_access)
294            .map(|(key, _)| *key);
295
296        if let Some(key) = oldest_key {
297            entries.remove(&key);
298            if self.config.track_stats {
299                self.stats.record_eviction();
300            }
301        }
302    }
303
304    /// 캐시 전체 삭제
305    pub fn clear(&self) {
306        if let Ok(mut entries) = self.entries.write() {
307            entries.clear();
308        }
309    }
310
311    /// 현재 캐시 항목 수
312    #[must_use]
313    pub fn len(&self) -> usize {
314        self.entries.read().map(|e| e.len()).unwrap_or(0)
315    }
316
317    /// 캐시가 비어있는지 확인
318    #[must_use]
319    pub fn is_empty(&self) -> bool {
320        self.len() == 0
321    }
322
323    /// 캐시 통계 참조
324    #[must_use]
325    pub const fn stats(&self) -> &CacheStats {
326        &self.stats
327    }
328
329    /// 설정 참조
330    #[must_use]
331    pub const fn config(&self) -> &CacheConfig {
332        &self.config
333    }
334}
335
336impl Default for TokenCache {
337    fn default() -> Self {
338        Self::with_defaults()
339    }
340}
341
342/// 캐싱 가능한 토크나이저 래퍼
343pub struct CachingTokenizer<T> {
344    inner: T,
345    cache: TokenCache,
346}
347
348impl<T> CachingTokenizer<T> {
349    /// 새 캐싱 토크나이저 생성
350    pub fn new(inner: T, config: CacheConfig) -> Self {
351        Self {
352            inner,
353            cache: TokenCache::new(config),
354        }
355    }
356
357    /// 기본 캐시 설정으로 생성
358    pub fn with_defaults(inner: T) -> Self {
359        Self::new(inner, CacheConfig::default())
360    }
361
362    /// 내부 토크나이저 참조
363    #[must_use]
364    pub const fn inner(&self) -> &T {
365        &self.inner
366    }
367
368    /// 내부 토크나이저 가변 참조
369    pub fn inner_mut(&mut self) -> &mut T {
370        &mut self.inner
371    }
372
373    /// 캐시 참조
374    #[must_use]
375    pub const fn cache(&self) -> &TokenCache {
376        &self.cache
377    }
378
379    /// 캐시 통계
380    #[must_use]
381    pub const fn stats(&self) -> &CacheStats {
382        self.cache.stats()
383    }
384
385    /// 캐시 삭제
386    pub fn clear_cache(&self) {
387        self.cache.clear();
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    #[test]
396    fn test_cache_config_default() {
397        let config = CacheConfig::default();
398        assert_eq!(config.max_entries, 10_000);
399        assert_eq!(config.max_key_length, 1024);
400        assert!(config.track_stats);
401    }
402
403    #[test]
404    fn test_cache_config_builder() {
405        let config = CacheConfig::new()
406            .with_max_entries(1000)
407            .with_max_key_length(512)
408            .with_track_stats(false);
409
410        assert_eq!(config.max_entries, 1000);
411        assert_eq!(config.max_key_length, 512);
412        assert!(!config.track_stats);
413    }
414
415    #[test]
416    fn test_cache_basic_operations() {
417        let cache = TokenCache::with_defaults();
418
419        let key = cache.make_key("테스트");
420
421        // 처음에는 없음
422        assert!(cache.get(key).is_none());
423        assert_eq!(cache.stats().misses(), 1);
424
425        // 삽입
426        let tokens = vec![CachedToken {
427            surface: "테스트".to_string(),
428            pos: "NNG".to_string(),
429            start_byte: 0,
430            end_byte: 9,
431        }];
432        cache.insert(key, tokens.clone());
433
434        // 조회
435        let cached = cache.get(key).unwrap();
436        assert_eq!(cached.len(), 1);
437        assert_eq!(cached[0].surface, "테스트");
438        assert_eq!(cache.stats().hits(), 1);
439    }
440
441    #[test]
442    fn test_cache_get_or_insert() {
443        let cache = TokenCache::with_defaults();
444
445        let key = cache.make_key("안녕");
446        let mut call_count = 0;
447
448        // 첫 번째 호출 - compute 실행
449        let tokens1 = cache.get_or_insert(key, || {
450            call_count += 1;
451            vec![CachedToken {
452                surface: "안녕".to_string(),
453                pos: "IC".to_string(),
454                start_byte: 0,
455                end_byte: 6,
456            }]
457        });
458        assert_eq!(call_count, 1);
459        assert_eq!(tokens1.len(), 1);
460
461        // 두 번째 호출 - 캐시에서 반환
462        let tokens2 = cache.get_or_insert(key, || {
463            call_count += 1;
464            vec![]
465        });
466        assert_eq!(call_count, 1); // compute가 호출되지 않음
467        assert_eq!(tokens2.len(), 1);
468    }
469
470    #[test]
471    fn test_cache_lru_eviction() {
472        let config = CacheConfig::new().with_max_entries(3);
473        let cache = TokenCache::new(config);
474
475        // 3개 삽입
476        for i in 0..3 {
477            let key = cache.make_key(&format!("text{i}"));
478            cache.insert(key, vec![]);
479        }
480        assert_eq!(cache.len(), 3);
481
482        // 첫 번째 항목 접근 (LRU 갱신)
483        let key0 = cache.make_key("text0");
484        let _ = cache.get(key0);
485
486        // 4번째 삽입 시 text1이 제거됨 (가장 오래됨)
487        let key3 = cache.make_key("text3");
488        cache.insert(key3, vec![]);
489        assert_eq!(cache.len(), 3);
490        assert_eq!(cache.stats().evictions(), 1);
491
492        // text0은 여전히 존재 (최근 접근)
493        assert!(cache.get(key0).is_some());
494
495        // text1은 제거됨
496        let key1 = cache.make_key("text1");
497        assert!(cache.get(key1).is_none());
498    }
499
500    #[test]
501    fn test_cache_stats() {
502        let cache = TokenCache::with_defaults();
503
504        let key = cache.make_key("test");
505
506        // 미스
507        let _ = cache.get(key);
508        assert_eq!(cache.stats().misses(), 1);
509        assert_eq!(cache.stats().hits(), 0);
510        assert!((cache.stats().hit_rate() - 0.0).abs() < f64::EPSILON);
511
512        // 삽입 후 히트
513        cache.insert(key, vec![]);
514        let _ = cache.get(key);
515        assert_eq!(cache.stats().hits(), 1);
516        assert!((cache.stats().hit_rate() - 0.5).abs() < f64::EPSILON);
517
518        // 리셋
519        cache.stats().reset();
520        assert_eq!(cache.stats().total_requests(), 0);
521    }
522
523    #[test]
524    fn test_cache_clear() {
525        let cache = TokenCache::with_defaults();
526
527        for i in 0..10 {
528            let key = cache.make_key(&format!("text{i}"));
529            cache.insert(key, vec![]);
530        }
531        assert_eq!(cache.len(), 10);
532
533        cache.clear();
534        assert_eq!(cache.len(), 0);
535        assert!(cache.is_empty());
536    }
537
538    #[test]
539    fn test_cache_skip_long_text() {
540        let config = CacheConfig::new().with_max_key_length(10);
541        let cache = TokenCache::new(config);
542
543        let mut call_count = 0;
544
545        // 짧은 텍스트 - 캐시됨
546        let short = "짧은";
547        cache.get_or_insert_with_text(short, || {
548            call_count += 1;
549            vec![]
550        });
551        cache.get_or_insert_with_text(short, || {
552            call_count += 1;
553            vec![]
554        });
555        assert_eq!(call_count, 1);
556
557        // 긴 텍스트 - 캐시되지 않음
558        let long = "이것은 아주 긴 텍스트입니다";
559        cache.get_or_insert_with_text(long, || {
560            call_count += 1;
561            vec![]
562        });
563        cache.get_or_insert_with_text(long, || {
564            call_count += 1;
565            vec![]
566        });
567        assert_eq!(call_count, 3); // 매번 compute 호출
568    }
569
570    #[test]
571    fn test_caching_tokenizer() {
572        struct DummyTokenizer;
573
574        let caching = CachingTokenizer::with_defaults(DummyTokenizer);
575
576        assert!(caching.cache().is_empty());
577        assert_eq!(caching.stats().total_requests(), 0);
578
579        // 캐시에 항목 추가
580        let key = caching.cache().make_key("test");
581        caching.cache().insert(key, vec![]);
582
583        assert_eq!(caching.cache().len(), 1);
584
585        // 캐시 삭제
586        caching.clear_cache();
587        assert!(caching.cache().is_empty());
588    }
589}