Skip to main content

lindera_analysis/
worker.rs

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