Skip to main content

lindera/
tokenizer.rs

1//! Tokenizer implementation for morphological analysis.
2//!
3//! This module provides a builder pattern for creating tokenizers and the tokenizer itself.
4//! The build-flow orchestration is delegated to
5//! [`lindera_binding_core::CoreTokenizerBuilder`] / [`lindera_binding_core::CoreTokenizer`];
6//! this module only adds the PyO3 wrappers and the PyDict-to-JSON conversion.
7//!
8//! # Examples
9//!
10//! ```python
11//! # Create a tokenizer with custom configuration
12//! tokenizer = (lindera.TokenizerBuilder()
13//!     .set_mode("normal")
14//!     .append_token_filter("japanese_stop_tags", {"tags": ["助詞"]})
15//!     .build())
16//!
17//! # Tokenize text
18//! tokens = tokenizer.tokenize("すもももももももものうち")
19//! ```
20
21use std::path::Path;
22
23use pyo3::prelude::*;
24use pyo3::types::PyDict;
25
26use lindera_binding_core::{CoreTokenizer, CoreTokenizerBuilder};
27
28use crate::dictionary::{PyDictionary, PyUserDictionary};
29use crate::error::to_py_error;
30use crate::token::PyToken;
31use crate::util::pydict_to_value;
32
33/// Converts the optional filter-argument dict into a JSON value.
34fn filter_args(args: Option<&Bound<'_, PyDict>>) -> PyResult<serde_json::Value> {
35    match args {
36        Some(dict) => pydict_to_value(dict),
37        None => Ok(serde_json::Value::Object(serde_json::Map::new())),
38    }
39}
40
41/// Builder for creating a `Tokenizer` with custom configuration.
42///
43/// The builder pattern allows for fluent configuration of tokenizer parameters including
44/// dictionaries, modes, and filter pipelines.
45///
46/// # Examples
47///
48/// ```python
49/// builder = lindera.TokenizerBuilder()
50/// builder.set_mode("normal")
51/// builder.set_dictionary("/path/to/dict")
52/// tokenizer = builder.build()
53/// ```
54#[pyclass(name = "TokenizerBuilder")]
55pub struct PyTokenizerBuilder {
56    pub inner: CoreTokenizerBuilder,
57}
58
59#[pymethods]
60impl PyTokenizerBuilder {
61    /// Creates a new `TokenizerBuilder` with default configuration.
62    ///
63    /// # Returns
64    ///
65    /// A new instance of `TokenizerBuilder`.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if the builder cannot be initialized.
70    #[new]
71    #[pyo3(signature = ())]
72    fn new() -> PyResult<Self> {
73        let inner = CoreTokenizerBuilder::new().map_err(to_py_error)?;
74        Ok(Self { inner })
75    }
76
77    /// Loads configuration from a file.
78    ///
79    /// # Arguments
80    ///
81    /// * `file_path` - Path to the configuration file.
82    ///
83    /// # Returns
84    ///
85    /// A new `TokenizerBuilder` with the loaded configuration.
86    #[pyo3(signature = (file_path))]
87    #[allow(clippy::wrong_self_convention)]
88    fn from_file(&self, file_path: &str) -> PyResult<Self> {
89        let inner = CoreTokenizerBuilder::from_file(Path::new(file_path)).map_err(to_py_error)?;
90        Ok(Self { inner })
91    }
92
93    /// Sets the tokenization mode.
94    ///
95    /// # Arguments
96    ///
97    /// * `mode` - Mode string ("normal" or "decompose").
98    ///
99    /// # Returns
100    ///
101    /// Self for method chaining.
102    #[pyo3(signature = (mode))]
103    fn set_mode<'a>(mut slf: PyRefMut<'a, Self>, mode: &str) -> PyResult<PyRefMut<'a, Self>> {
104        slf.inner.set_mode(mode).map_err(to_py_error)?;
105        Ok(slf)
106    }
107
108    /// Sets the dictionary path.
109    ///
110    /// # Arguments
111    ///
112    /// * `path` - Path to the dictionary directory.
113    ///
114    /// # Returns
115    ///
116    /// Self for method chaining.
117    #[pyo3(signature = (path))]
118    fn set_dictionary<'a>(mut slf: PyRefMut<'a, Self>, path: &str) -> PyResult<PyRefMut<'a, Self>> {
119        slf.inner.set_dictionary(path);
120        Ok(slf)
121    }
122
123    /// Sets the user dictionary URI.
124    ///
125    /// # Arguments
126    ///
127    /// * `uri` - URI to the user dictionary.
128    ///
129    /// # Returns
130    ///
131    /// Self for method chaining.
132    #[pyo3(signature = (uri))]
133    fn set_user_dictionary<'a>(
134        mut slf: PyRefMut<'a, Self>,
135        uri: &str,
136    ) -> PyResult<PyRefMut<'a, Self>> {
137        slf.inner.set_user_dictionary(uri);
138        Ok(slf)
139    }
140
141    /// Sets whether to keep whitespace in tokenization results.
142    ///
143    /// # Arguments
144    ///
145    /// * `keep_whitespace` - If true, whitespace tokens will be included in results.
146    ///
147    /// # Returns
148    ///
149    /// Self for method chaining.
150    #[pyo3(signature = (keep_whitespace))]
151    fn set_keep_whitespace<'a>(
152        mut slf: PyRefMut<'a, Self>,
153        keep_whitespace: bool,
154    ) -> PyResult<PyRefMut<'a, Self>> {
155        slf.inner.set_keep_whitespace(keep_whitespace);
156        Ok(slf)
157    }
158
159    /// Appends a character filter to the filter pipeline.
160    ///
161    /// # Arguments
162    ///
163    /// * `kind` - Type of character filter to add.
164    /// * `args` - Optional dictionary of filter arguments.
165    ///
166    /// # Returns
167    ///
168    /// Self for method chaining.
169    #[pyo3(signature = (kind, args=None))]
170    fn append_character_filter<'a>(
171        mut slf: PyRefMut<'a, Self>,
172        kind: &str,
173        args: Option<&Bound<'_, PyDict>>,
174    ) -> PyResult<PyRefMut<'a, Self>> {
175        let args = filter_args(args)?;
176        slf.inner.append_character_filter(kind, &args);
177        Ok(slf)
178    }
179
180    /// Appends a token filter to the filter pipeline.
181    ///
182    /// # Arguments
183    ///
184    /// * `kind` - Type of token filter to add.
185    /// * `args` - Optional dictionary of filter arguments.
186    ///
187    /// # Returns
188    ///
189    /// Self for method chaining.
190    #[pyo3(signature = (kind, args=None))]
191    fn append_token_filter<'a>(
192        mut slf: PyRefMut<'a, Self>,
193        kind: &str,
194        args: Option<&Bound<'_, PyDict>>,
195    ) -> PyResult<PyRefMut<'a, Self>> {
196        let args = filter_args(args)?;
197        slf.inner.append_token_filter(kind, &args);
198        Ok(slf)
199    }
200
201    /// Builds the tokenizer with the configured settings.
202    ///
203    /// # Returns
204    ///
205    /// A configured `Tokenizer` instance ready for use.
206    ///
207    /// # Errors
208    ///
209    /// Returns an error if the tokenizer cannot be built with the current configuration.
210    #[pyo3(signature = ())]
211    fn build(&self) -> PyResult<PyTokenizer> {
212        let inner = self.inner.build().map_err(to_py_error)?;
213        Ok(PyTokenizer { inner })
214    }
215}
216
217/// Tokenizer for performing morphological analysis.
218///
219/// The tokenizer processes text and returns tokens with their morphological features.
220///
221/// # Examples
222///
223/// ```python
224/// # Using TokenizerBuilder (recommended)
225/// tokenizer = lindera.TokenizerBuilder().build()
226///
227/// # Or create directly with a dictionary
228/// dictionary = lindera.load_dictionary("ipadic")
229/// tokenizer = lindera.Tokenizer(dictionary, mode="normal")
230/// ```
231#[pyclass(name = "Tokenizer")]
232pub struct PyTokenizer {
233    inner: CoreTokenizer,
234}
235
236#[pymethods]
237impl PyTokenizer {
238    /// Creates a new tokenizer with the given dictionary and mode.
239    ///
240    /// # Arguments
241    ///
242    /// * `dictionary` - Dictionary to use for tokenization.
243    /// * `mode` - Tokenization mode ("normal" or "decompose"). Default: "normal".
244    /// * `user_dictionary` - Optional user dictionary for custom words.
245    ///
246    /// # Returns
247    ///
248    /// A new `Tokenizer` instance.
249    #[new]
250    #[pyo3(signature = (dictionary, mode="normal", user_dictionary=None))]
251    fn new(
252        dictionary: PyDictionary,
253        mode: &str,
254        user_dictionary: Option<PyUserDictionary>,
255    ) -> PyResult<Self> {
256        let inner =
257            CoreTokenizer::from_segmenter(mode, dictionary.inner, user_dictionary.map(|d| d.inner))
258                .map_err(to_py_error)?;
259
260        Ok(Self { inner })
261    }
262
263    /// Tokenizes the given text.
264    ///
265    /// # Arguments
266    ///
267    /// * `text` - Text to tokenize.
268    ///
269    /// # Returns
270    ///
271    /// A list of Token objects containing morphological features.
272    ///
273    /// # Errors
274    ///
275    /// Returns an error if tokenization fails.
276    #[pyo3(signature = (text))]
277    fn tokenize(&self, text: &str) -> PyResult<Vec<PyToken>> {
278        let views = self.inner.tokenize(text).map_err(to_py_error)?;
279        Ok(views.into_iter().map(PyToken::from_view).collect())
280    }
281
282    /// Tokenizes the given text and returns N-best results.
283    ///
284    /// # Arguments
285    ///
286    /// * `text` - Text to tokenize.
287    /// * `n` - Number of N-best results to return.
288    ///
289    /// # Returns
290    ///
291    /// A list of lists of Token objects, ordered by cost (best first).
292    ///
293    /// # Errors
294    ///
295    /// Returns an error if tokenization fails.
296    #[pyo3(signature = (text, n, unique=false, cost_threshold=None))]
297    fn tokenize_nbest(
298        &self,
299        text: &str,
300        n: usize,
301        unique: bool,
302        cost_threshold: Option<i64>,
303    ) -> PyResult<Vec<(Vec<PyToken>, i64)>> {
304        let results = self
305            .inner
306            .tokenize_nbest(text, n, unique, cost_threshold)
307            .map_err(to_py_error)?;
308
309        let py_results: Vec<(Vec<PyToken>, i64)> = results
310            .into_iter()
311            .map(|(views, cost)| (views.into_iter().map(PyToken::from_view).collect(), cost))
312            .collect();
313
314        Ok(py_results)
315    }
316}
317
318pub fn register(parent_module: &Bound<'_, PyModule>) -> PyResult<()> {
319    let py = parent_module.py();
320    let m = PyModule::new(py, "tokenizer")?;
321    m.add_class::<PyTokenizerBuilder>()?;
322    m.add_class::<PyTokenizer>()?;
323    parent_module.add_submodule(&m)?;
324    Ok(())
325}