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    /// Immediately shrinks the worker's internal buffers to what an input
222    /// of `text_len_hint` bytes needs.
223    ///
224    /// # 引数
225    ///
226    /// * `text_len_hint` - Expected typical input length in bytes.
227    pub fn shrink_to(&mut self, text_len_hint: usize) {
228        self.segment_worker.shrink_to(text_len_hint);
229        self.text_buf.shrink_to(text_len_hint);
230        self.offset_mappings.shrink_to_fit();
231    }
232
233    /// Discards all internal buffers, replacing them with fresh ones.
234    ///
235    /// Intended for recovery paths (e.g. after a panic poisoned a mutex
236    /// holding this worker) where the buffers may be in an inconsistent
237    /// intermediate state; configuration (dictionary, filters, mode) is
238    /// preserved.
239    pub fn reset(&mut self) {
240        self.segment_worker.reset();
241        self.text_buf = String::new();
242        self.offset_mappings = Vec::new();
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    #[cfg(feature = "embed-ipadic")]
249    mod with_ipadic {
250        use std::collections::HashMap;
251
252        use lindera::dictionary::load_dictionary;
253        use lindera::mode::Mode;
254        use lindera::segmenter::Segmenter;
255
256        use crate::character_filter::BoxCharacterFilter;
257        use crate::character_filter::mapping::MappingCharacterFilter;
258        use crate::token_filter::BoxTokenFilter;
259        use crate::token_filter::lowercase::LowercaseTokenFilter;
260        use crate::tokenizer::Tokenizer;
261
262        fn ipadic_tokenizer(with_filters: bool) -> Tokenizer {
263            let dictionary = match load_dictionary("embedded://ipadic") {
264                Ok(dictionary) => dictionary,
265                Err(err) => panic!("failed to load embedded IPADIC: {err}"),
266            };
267            let segmenter = Segmenter::new(Mode::Normal, dictionary, None);
268            let mut tokenizer = Tokenizer::new(segmenter);
269            if with_filters {
270                let mut mapping = HashMap::new();
271                mapping.insert("リンデラ".to_string(), "Lindera".to_string());
272                let filter = match MappingCharacterFilter::new(mapping) {
273                    Ok(filter) => filter,
274                    Err(err) => panic!("failed to build mapping filter: {err}"),
275                };
276                tokenizer
277                    .character_filters
278                    .push(BoxCharacterFilter::from(filter));
279                tokenizer
280                    .token_filters
281                    .push(BoxTokenFilter::from(LowercaseTokenFilter::new()));
282            }
283            tokenizer
284        }
285
286        fn assert_same_tokens(
287            tokenizer: &Tokenizer,
288            worker_tokens: &[(String, usize, usize)],
289            text: &str,
290        ) {
291            let expected = match tokenizer.tokenize(text) {
292                Ok(tokens) => tokens,
293                Err(err) => panic!("tokenize failed: {err}"),
294            };
295            let expected: Vec<(String, usize, usize)> = expected
296                .iter()
297                .map(|t| (t.surface.to_string(), t.byte_start, t.byte_end))
298                .collect();
299            assert_eq!(expected, worker_tokens, "mismatch for {text:?}");
300        }
301
302        #[test]
303        fn test_worker_matches_tokenize_without_filters() {
304            let tokenizer = ipadic_tokenizer(false);
305            let mut worker = tokenizer.new_worker();
306
307            let texts = [
308                "すもももももももものうち",
309                "関西国際空港限定トートバッグ",
310                "",
311            ];
312            for _ in 0..3 {
313                for text in texts {
314                    let actual: Vec<(String, usize, usize)> = {
315                        let tokens = match worker.tokenize(text) {
316                            Ok(tokens) => tokens,
317                            Err(err) => panic!("worker.tokenize failed: {err}"),
318                        };
319                        tokens
320                            .iter()
321                            .map(|t| (t.surface.to_string(), t.byte_start, t.byte_end))
322                            .collect()
323                    };
324                    assert_same_tokens(&tokenizer, &actual, text);
325                }
326            }
327        }
328
329        #[test]
330        fn test_worker_matches_tokenize_with_filters() {
331            let tokenizer = ipadic_tokenizer(true);
332            let mut worker = tokenizer.new_worker();
333
334            // The mapping filter rewrites リンデラ -> Lindera (changing byte
335            // lengths, so offset corrections are exercised), and the
336            // lowercase token filter forces owned surfaces.
337            let texts = [
338                "リンデラは形態素解析器です。",
339                "TOKYO と リンデラ",
340                "すもももももももものうち",
341            ];
342            for _ in 0..3 {
343                for text in texts {
344                    let actual: Vec<(String, usize, usize)> = {
345                        let tokens = match worker.tokenize(text) {
346                            Ok(tokens) => tokens,
347                            Err(err) => panic!("worker.tokenize failed: {err}"),
348                        };
349                        tokens
350                            .iter()
351                            .map(|t| (t.surface.to_string(), t.byte_start, t.byte_end))
352                            .collect()
353                    };
354                    assert_same_tokens(&tokenizer, &actual, text);
355                }
356            }
357        }
358
359        #[test]
360        fn test_worker_matches_tokenize_nbest() {
361            let tokenizer = ipadic_tokenizer(true);
362            let mut worker = tokenizer.new_worker();
363            let text = "リンデラとすもももももももものうち";
364
365            let expected = match tokenizer.tokenize_nbest(text, 3, false, None) {
366                Ok(results) => results,
367                Err(err) => panic!("tokenize_nbest failed: {err}"),
368            };
369            for _ in 0..2 {
370                let actual = match worker.tokenize_nbest(text, 3, false, None) {
371                    Ok(results) => results,
372                    Err(err) => panic!("worker.tokenize_nbest failed: {err}"),
373                };
374                assert_eq!(expected.len(), actual.len());
375                for ((e_tokens, e_cost), (a_tokens, a_cost)) in expected.iter().zip(actual.iter()) {
376                    assert_eq!(e_cost, a_cost);
377                    let e: Vec<(String, usize, usize)> = e_tokens
378                        .iter()
379                        .map(|t| (t.surface.to_string(), t.byte_start, t.byte_end))
380                        .collect();
381                    let a: Vec<(String, usize, usize)> = a_tokens
382                        .iter()
383                        .map(|t| (t.surface.to_string(), t.byte_start, t.byte_end))
384                        .collect();
385                    assert_eq!(e, a);
386                }
387            }
388        }
389
390        #[test]
391        fn test_worker_reset_and_shrink_keep_output() {
392            let tokenizer = ipadic_tokenizer(true);
393            let mut worker = tokenizer.new_worker();
394            let text = "リンデラは形態素解析器です。";
395
396            let before: Vec<String> = {
397                let tokens = match worker.tokenize(text) {
398                    Ok(tokens) => tokens,
399                    Err(err) => panic!("worker.tokenize failed: {err}"),
400                };
401                tokens.iter().map(|t| t.surface.to_string()).collect()
402            };
403
404            worker.reset();
405            let after_reset: Vec<String> = {
406                let tokens = match worker.tokenize(text) {
407                    Ok(tokens) => tokens,
408                    Err(err) => panic!("worker.tokenize failed: {err}"),
409                };
410                tokens.iter().map(|t| t.surface.to_string()).collect()
411            };
412            assert_eq!(before, after_reset, "output changed after reset()");
413
414            worker.shrink_to(0);
415            let after_shrink: Vec<String> = {
416                let tokens = match worker.tokenize(text) {
417                    Ok(tokens) => tokens,
418                    Err(err) => panic!("worker.tokenize failed: {err}"),
419                };
420                tokens.iter().map(|t| t.surface.to_string()).collect()
421            };
422            assert_eq!(before, after_shrink, "output changed after shrink_to()");
423        }
424    }
425}