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}