Skip to main content

lindera_analysis/
worker.rs

1use lindera::LinderaResult;
2use lindera::mode::Mode;
3use lindera::space_penalty::SpacePenaltyConfig;
4use lindera::token::Token;
5use lindera::worker::SegmentWorker;
6
7use crate::character_filter::{BoxCharacterFilter, OffsetMapping};
8use crate::token_filter::BoxTokenFilter;
9use crate::tokenizer::{Tokenizer, correct_offsets};
10
11/// A reusable tokenization session over a [`Tokenizer`]'s full analysis
12/// chain (character filters → segmentation → token filters), owning every
13/// per-call buffer: the Viterbi lattice and backtrace scratch (via
14/// [`SegmentWorker`]), the normalized-text buffer the character filters
15/// operate on, and the offset-mapping scratch.
16///
17/// Compared to [`Tokenizer::tokenize`], repeated calls avoid the fresh
18/// lattice allocation, and — when character filters are configured — both
19/// the per-call `String` promotion of the input and the per-token surface
20/// `String` allocations (surfaces borrow the worker's buffer instead).
21///
22/// Create a worker with [`Tokenizer::new_worker`] or
23/// [`Tokenizer::into_worker`]; it stays permanently bound to that
24/// tokenizer's dictionary and filter chain. Returned tokens borrow the
25/// worker, so they must be consumed before the next call — the usual
26/// per-line loop compiles as-is:
27///
28/// ```ignore
29/// let mut worker = tokenizer.new_worker();
30/// for line in lines {
31///     let tokens = worker.tokenize(line)?;
32///     for token in &tokens {
33///         // use token.surface, ...
34///     }
35/// }
36/// ```
37///
38/// The worker is `Send + Sync`; the `&mut self` API means cross-thread
39/// sharing requires external synchronization (one worker per thread, or a
40/// `Mutex` as `lindera-binding-core` does).
41pub struct AnalysisWorker {
42    /// Character filters applied to the text before segmentation.
43    character_filters: Vec<BoxCharacterFilter>,
44    /// Token filters applied to the tokens after segmentation.
45    token_filters: Vec<BoxTokenFilter>,
46    /// The reusable segmentation session (lattice + backtrace scratch).
47    segment_worker: SegmentWorker,
48    /// Persistent buffer holding the character-filtered (normalized) text;
49    /// token surfaces borrow from it when character filters are configured.
50    text_buf: String,
51    /// Scratch for the per-call offset mappings produced by the character
52    /// filters (the `Vec` is reused; mappings themselves are per-call).
53    offset_mappings: Vec<OffsetMapping>,
54}
55
56impl Tokenizer {
57    /// Creates a reusable [`AnalysisWorker`] bound to a deep clone of this
58    /// tokenizer (filters are cloned via `box_clone`; the dictionary clone
59    /// is `Arc`-cheap, but a configured user dictionary is deep-copied —
60    /// prefer [`Tokenizer::into_worker`] when the tokenizer itself is no
61    /// longer needed).
62    ///
63    /// # 戻り値
64    ///
65    /// A fresh worker with empty internal buffers.
66    pub fn new_worker(&self) -> AnalysisWorker {
67        self.clone().into_worker()
68    }
69
70    /// Consumes this tokenizer and creates a reusable [`AnalysisWorker`]
71    /// from it without cloning any of its parts.
72    ///
73    /// # 戻り値
74    ///
75    /// A fresh worker with empty internal buffers.
76    pub fn into_worker(self) -> AnalysisWorker {
77        AnalysisWorker {
78            character_filters: self.character_filters,
79            token_filters: self.token_filters,
80            segment_worker: self.segmenter.into_worker(),
81            text_buf: String::new(),
82            offset_mappings: Vec::new(),
83        }
84    }
85}
86
87impl AnalysisWorker {
88    /// Tokenizes `text` through the full analysis chain, reusing this
89    /// worker's internal buffers.
90    ///
91    /// Produces exactly the same tokens as [`Tokenizer::tokenize`] for the
92    /// same input and configuration. The returned tokens borrow both
93    /// `text` and this worker, so they must be consumed before the next
94    /// call.
95    ///
96    /// # 引数
97    ///
98    /// * `text` - The input text to tokenize.
99    ///
100    /// # 戻り値
101    ///
102    /// The filtered tokens, in reading order, with byte offsets corrected
103    /// back to the original (pre-filter) text.
104    pub fn tokenize<'w>(&'w mut self, text: &'w str) -> LinderaResult<Vec<Token<'w>>> {
105        let Self {
106            character_filters,
107            token_filters,
108            segment_worker,
109            text_buf,
110            offset_mappings,
111        } = self;
112        offset_mappings.clear();
113
114        let (mut tokens, final_text_len) = if character_filters.is_empty() {
115            (segment_worker.segment(text)?, text.len())
116        } else {
117            text_buf.clear();
118            text_buf.push_str(text);
119            for character_filter in character_filters.iter() {
120                let mapping = character_filter.apply(text_buf)?;
121                if !mapping.is_empty() {
122                    offset_mappings.push(mapping);
123                }
124            }
125            // Read the length before segmenting: the tokens returned below
126            // hold a shared borrow of `text_buf` for the rest of the call.
127            let final_text_len = text_buf.len();
128            (segment_worker.segment(text_buf.as_str())?, final_text_len)
129        };
130
131        for token_filter in token_filters.iter() {
132            token_filter.apply(&mut tokens)?;
133        }
134
135        correct_offsets(&mut tokens, offset_mappings, final_text_len);
136
137        Ok(tokens)
138    }
139
140    /// Tokenizes `text` and returns the top-N results with costs, through
141    /// the full analysis chain, reusing this worker's internal buffers.
142    ///
143    /// Produces exactly the same results as [`Tokenizer::tokenize_nbest`]
144    /// for the same input and configuration.
145    ///
146    /// # 引数
147    ///
148    /// * `text` - The input text to tokenize.
149    /// * `n` - Maximum number of tokenizations to return.
150    /// * `unique` - Deduplicate results with identical word boundaries.
151    /// * `cost_threshold` - Discard paths costing more than best + threshold.
152    ///
153    /// # 戻り値
154    ///
155    /// Up to `n` `(tokens, cost)` pairs ordered by cost (best first).
156    pub fn tokenize_nbest<'w>(
157        &'w mut self,
158        text: &'w str,
159        n: usize,
160        unique: bool,
161        cost_threshold: Option<i64>,
162    ) -> LinderaResult<Vec<(Vec<Token<'w>>, i64)>> {
163        let Self {
164            character_filters,
165            token_filters,
166            segment_worker,
167            text_buf,
168            offset_mappings,
169        } = self;
170        offset_mappings.clear();
171
172        let (mut all_results, final_text_len) = if character_filters.is_empty() {
173            (
174                segment_worker.segment_nbest(text, n, unique, cost_threshold)?,
175                text.len(),
176            )
177        } else {
178            text_buf.clear();
179            text_buf.push_str(text);
180            for character_filter in character_filters.iter() {
181                let mapping = character_filter.apply(text_buf)?;
182                if !mapping.is_empty() {
183                    offset_mappings.push(mapping);
184                }
185            }
186            let final_text_len = text_buf.len();
187            (
188                segment_worker.segment_nbest(text_buf.as_str(), n, unique, cost_threshold)?,
189                final_text_len,
190            )
191        };
192
193        for (tokens, _cost) in &mut all_results {
194            for token_filter in token_filters.iter() {
195                token_filter.apply(tokens)?;
196            }
197            correct_offsets(tokens, offset_mappings, final_text_len);
198        }
199
200        Ok(all_results)
201    }
202
203    /// Sets the segmentation mode for subsequent calls.
204    ///
205    /// # 引数
206    ///
207    /// * `mode` - The mode to use from the next call on.
208    pub fn set_mode(&mut self, mode: Mode) {
209        self.segment_worker.set_mode(mode);
210    }
211
212    /// Sets whether whitespace tokens are kept in the output for
213    /// subsequent calls.
214    ///
215    /// # 引数
216    ///
217    /// * `keep` - `true` to keep whitespace tokens, `false` to drop them.
218    pub fn set_keep_whitespace(&mut self, keep: bool) {
219        self.segment_worker.set_keep_whitespace(keep);
220    }
221
222    /// Sets the unknown-word grouping cap for subsequent calls (MeCab's
223    /// `max-grouping-size` semantics; `None` = unbounded, the default).
224    ///
225    /// # 引数
226    ///
227    /// * `max_grouping_len` - Maximum grouped characters beyond the first.
228    pub fn set_max_grouping_len(&mut self, max_grouping_len: Option<usize>) {
229        self.segment_worker.set_max_grouping_len(max_grouping_len);
230    }
231
232    /// Enables/disables the unknown-word length ladder for subsequent
233    /// calls (see `Segmenter::unknown_word_ladder`; defaults to `true`).
234    ///
235    /// # 引数
236    ///
237    /// * `unknown_word_ladder` - Whether to emit the length ladder.
238    pub fn set_unknown_word_ladder(&mut self, unknown_word_ladder: bool) {
239        self.segment_worker
240            .set_unknown_word_ladder(unknown_word_ladder);
241    }
242
243    /// Sets the left-space penalty rules for subsequent calls (see
244    /// `Segmenter::space_penalty`; `None` disables it, overriding the rules
245    /// the dictionary ships and applies by default).
246    ///
247    /// # 引数
248    ///
249    /// * `space_penalty` - The rules, or `None` to disable the penalty.
250    ///
251    /// # 戻り値
252    ///
253    /// `Ok(())`, or an error when the dictionary schema has no
254    /// part-of-speech field; the previous setting is kept on error.
255    pub fn set_space_penalty(
256        &mut self,
257        space_penalty: Option<SpacePenaltyConfig>,
258    ) -> LinderaResult<()> {
259        self.segment_worker.set_space_penalty(space_penalty)
260    }
261
262    /// Enables the left-space penalty with the rules the dictionary ships in
263    /// its metadata (see `Segmenter::space_penalty_from_dictionary`), e.g.
264    /// to restore the default after `set_space_penalty(None)`.
265    ///
266    /// # 戻り値
267    ///
268    /// `Ok(())`, or an error when the dictionary ships no rules; the
269    /// previous setting is kept on error.
270    pub fn set_space_penalty_from_dictionary(&mut self) -> LinderaResult<()> {
271        self.segment_worker.set_space_penalty_from_dictionary()
272    }
273
274    /// Immediately shrinks the worker's internal buffers to what an input
275    /// of `text_len_hint` bytes needs.
276    ///
277    /// # 引数
278    ///
279    /// * `text_len_hint` - Expected typical input length in bytes.
280    pub fn shrink_to(&mut self, text_len_hint: usize) {
281        self.segment_worker.shrink_to(text_len_hint);
282        self.text_buf.shrink_to(text_len_hint);
283        self.offset_mappings.shrink_to_fit();
284    }
285
286    /// Discards all internal buffers, replacing them with fresh ones.
287    ///
288    /// Intended for recovery paths (e.g. after a panic poisoned a mutex
289    /// holding this worker) where the buffers may be in an inconsistent
290    /// intermediate state; configuration (dictionary, filters, mode) is
291    /// preserved.
292    pub fn reset(&mut self) {
293        self.segment_worker.reset();
294        self.text_buf = String::new();
295        self.offset_mappings = Vec::new();
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    #[cfg(feature = "embed-ipadic")]
302    mod with_ipadic {
303        use std::collections::HashMap;
304
305        use lindera::dictionary::load_dictionary;
306        use lindera::mode::Mode;
307        use lindera::segmenter::Segmenter;
308
309        use crate::character_filter::BoxCharacterFilter;
310        use crate::character_filter::mapping::MappingCharacterFilter;
311        use crate::token_filter::BoxTokenFilter;
312        use crate::token_filter::lowercase::LowercaseTokenFilter;
313        use crate::tokenizer::Tokenizer;
314
315        fn ipadic_tokenizer(with_filters: bool) -> Tokenizer {
316            let dictionary = match load_dictionary("embedded://ipadic") {
317                Ok(dictionary) => dictionary,
318                Err(err) => panic!("failed to load embedded IPADIC: {err}"),
319            };
320            let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
321            let mut tokenizer = Tokenizer::new(segmenter);
322            if with_filters {
323                let mut mapping = HashMap::new();
324                mapping.insert("リンデラ".to_string(), "Lindera".to_string());
325                let filter = match MappingCharacterFilter::new(mapping) {
326                    Ok(filter) => filter,
327                    Err(err) => panic!("failed to build mapping filter: {err}"),
328                };
329                tokenizer
330                    .character_filters
331                    .push(BoxCharacterFilter::from(filter));
332                tokenizer
333                    .token_filters
334                    .push(BoxTokenFilter::from(LowercaseTokenFilter::new()));
335            }
336            tokenizer
337        }
338
339        fn assert_same_tokens(
340            tokenizer: &Tokenizer,
341            worker_tokens: &[(String, usize, usize)],
342            text: &str,
343        ) {
344            let expected = match tokenizer.tokenize(text) {
345                Ok(tokens) => tokens,
346                Err(err) => panic!("tokenize failed: {err}"),
347            };
348            let expected: Vec<(String, usize, usize)> = expected
349                .iter()
350                .map(|t| (t.surface.to_string(), t.byte_start, t.byte_end))
351                .collect();
352            assert_eq!(expected, worker_tokens, "mismatch for {text:?}");
353        }
354
355        #[test]
356        fn test_worker_matches_tokenize_without_filters() {
357            let tokenizer = ipadic_tokenizer(false);
358            let mut worker = tokenizer.new_worker();
359
360            let texts = [
361                "すもももももももものうち",
362                "関西国際空港限定トートバッグ",
363                "",
364            ];
365            for _ in 0..3 {
366                for text in texts {
367                    let actual: Vec<(String, usize, usize)> = {
368                        let tokens = match worker.tokenize(text) {
369                            Ok(tokens) => tokens,
370                            Err(err) => panic!("worker.tokenize failed: {err}"),
371                        };
372                        tokens
373                            .iter()
374                            .map(|t| (t.surface.to_string(), t.byte_start, t.byte_end))
375                            .collect()
376                    };
377                    assert_same_tokens(&tokenizer, &actual, text);
378                }
379            }
380        }
381
382        #[test]
383        fn test_worker_matches_tokenize_with_filters() {
384            let tokenizer = ipadic_tokenizer(true);
385            let mut worker = tokenizer.new_worker();
386
387            // The mapping filter rewrites リンデラ -> Lindera (changing byte
388            // lengths, so offset corrections are exercised), and the
389            // lowercase token filter forces owned surfaces.
390            let texts = [
391                "リンデラは形態素解析器です。",
392                "TOKYO と リンデラ",
393                "すもももももももものうち",
394            ];
395            for _ in 0..3 {
396                for text in texts {
397                    let actual: Vec<(String, usize, usize)> = {
398                        let tokens = match worker.tokenize(text) {
399                            Ok(tokens) => tokens,
400                            Err(err) => panic!("worker.tokenize failed: {err}"),
401                        };
402                        tokens
403                            .iter()
404                            .map(|t| (t.surface.to_string(), t.byte_start, t.byte_end))
405                            .collect()
406                    };
407                    assert_same_tokens(&tokenizer, &actual, text);
408                }
409            }
410        }
411
412        #[test]
413        fn test_worker_matches_tokenize_nbest() {
414            let tokenizer = ipadic_tokenizer(true);
415            let mut worker = tokenizer.new_worker();
416            let text = "リンデラとすもももももももものうち";
417
418            let expected = match tokenizer.tokenize_nbest(text, 3, false, None) {
419                Ok(results) => results,
420                Err(err) => panic!("tokenize_nbest failed: {err}"),
421            };
422            for _ in 0..2 {
423                let actual = match worker.tokenize_nbest(text, 3, false, None) {
424                    Ok(results) => results,
425                    Err(err) => panic!("worker.tokenize_nbest failed: {err}"),
426                };
427                assert_eq!(expected.len(), actual.len());
428                for ((e_tokens, e_cost), (a_tokens, a_cost)) in expected.iter().zip(actual.iter()) {
429                    assert_eq!(e_cost, a_cost);
430                    let e: Vec<(String, usize, usize)> = e_tokens
431                        .iter()
432                        .map(|t| (t.surface.to_string(), t.byte_start, t.byte_end))
433                        .collect();
434                    let a: Vec<(String, usize, usize)> = a_tokens
435                        .iter()
436                        .map(|t| (t.surface.to_string(), t.byte_start, t.byte_end))
437                        .collect();
438                    assert_eq!(e, a);
439                }
440            }
441        }
442
443        #[test]
444        fn test_worker_reset_and_shrink_keep_output() {
445            let tokenizer = ipadic_tokenizer(true);
446            let mut worker = tokenizer.new_worker();
447            let text = "リンデラは形態素解析器です。";
448
449            let before: Vec<String> = {
450                let tokens = match worker.tokenize(text) {
451                    Ok(tokens) => tokens,
452                    Err(err) => panic!("worker.tokenize failed: {err}"),
453                };
454                tokens.iter().map(|t| t.surface.to_string()).collect()
455            };
456
457            worker.reset();
458            let after_reset: Vec<String> = {
459                let tokens = match worker.tokenize(text) {
460                    Ok(tokens) => tokens,
461                    Err(err) => panic!("worker.tokenize failed: {err}"),
462                };
463                tokens.iter().map(|t| t.surface.to_string()).collect()
464            };
465            assert_eq!(before, after_reset, "output changed after reset()");
466
467            worker.shrink_to(0);
468            let after_shrink: Vec<String> = {
469                let tokens = match worker.tokenize(text) {
470                    Ok(tokens) => tokens,
471                    Err(err) => panic!("worker.tokenize failed: {err}"),
472                };
473                tokens.iter().map(|t| t.surface.to_string()).collect()
474            };
475            assert_eq!(before, after_shrink, "output changed after shrink_to()");
476        }
477    }
478}