litsea 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12e321b2284507d1ac91d3a77c6a5a2fa77440da6f655c3591d6535a1226ddbd
4
- data.tar.gz: df71d843a1ae5ea741fcbc124d202200ee41f62ef56d340af43c8430cbfb7e0a
3
+ metadata.gz: 48bde143045210bc8ebc4dacba04155649772c88a97b88d4cbfbbf9d1f4670fa
4
+ data.tar.gz: d45ddd6b6b9875ab645cd5e20dc539974e189953ecd1c4851c5f90a5c6d4d76c
5
5
  SHA512:
6
- metadata.gz: 710a2a9a938b22309d001bdbacc36c2035a7c6aaefa364f39fd052ca69c2f3fda7b6b7b8f8d924b723e9dda7b40cb965f981b5625bb02abdd5d4afad0a9b57fa
7
- data.tar.gz: 884330a6bbab450ed6f51442d5614edb7640dca5b40b3ed5d8588fe219f9fa6fd51f6ab1770564ae368b6b6d42de9c17b673634a9afec74e91bb7e413b634ef6
6
+ metadata.gz: 6bf4331b0bd88b94921d492429504761f5ec4a783c020080e9af6012d1e544fb7daae41b38e2580ebe1c3498bf9b49a6c0922c66c1355aef678821b4d2175865
7
+ data.tar.gz: a8b264e872e9e7a04601dd5f4e964893b0cfc3b2af2880b3328d7ad84c7ece3e499a7c615e972a66f78182f0522b62e5f372c5ce101a233b24f433c5085a883d
data/Cargo.toml CHANGED
@@ -1,25 +1,58 @@
1
+ # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
2
+ #
3
+ # When uploading crates to the registry Cargo will automatically
4
+ # "normalize" Cargo.toml files for maximal compatibility
5
+ # with all versions of Cargo and also rewrite `path` dependencies
6
+ # to registry (e.g., crates.io) dependencies.
7
+ #
8
+ # If you are reading this file be aware that the original Cargo.toml
9
+ # will likely look very different (and much more reasonable).
10
+ # See Cargo.toml.orig for the original contents.
11
+
1
12
  [package]
13
+ edition = "2024"
14
+ rust-version = "1.87"
2
15
  name = "litsea-ruby"
3
- version.workspace = true
4
- edition.workspace = true
5
- rust-version.workspace = true
16
+ version = "0.15.0"
17
+ build = false
18
+ autolib = false
19
+ autobins = false
20
+ autoexamples = false
21
+ autotests = false
22
+ autobenches = false
6
23
  description = "Ruby binding for Litsea."
24
+ homepage = "https://github.com/mosuka/litsea"
7
25
  documentation = "https://docs.rs/litsea-ruby"
8
- homepage.workspace = true
9
- repository.workspace = true
10
26
  readme = "README.md"
11
- keywords = ["word", "segmentation", "nlp", "ruby", "binding"]
12
- categories.workspace = true
13
- license.workspace = true
27
+ keywords = [
28
+ "word",
29
+ "segmentation",
30
+ "nlp",
31
+ "ruby",
32
+ "binding",
33
+ ]
34
+ categories = ["text-processing"]
35
+ license = "MIT"
36
+ repository = "https://github.com/mosuka/litsea"
14
37
 
15
38
  [lib]
16
39
  name = "litsea_ruby"
17
- crate-type = ["cdylib", "lib"]
40
+ crate-type = [
41
+ "cdylib",
42
+ "lib",
43
+ ]
44
+ path = "src/lib.rs"
45
+
46
+ [dependencies.litsea]
47
+ version = "=0.15.0"
48
+
49
+ [dependencies.litsea-binding-core]
50
+ version = "=0.15.0"
51
+ features = ["remote_model"]
52
+
53
+ [dependencies.magnus]
54
+ version = "0.9.0"
55
+ features = ["rb-sys"]
18
56
 
19
- [dependencies]
20
- litsea.workspace = true
21
- litsea-binding-core = { workspace = true, features = ["remote_model"] }
22
- magnus = { version = "0.8.2", features = ["rb-sys"] }
23
- # Used only to link against libruby; the GVL entry point is declared by
24
- # hand in `gvl.rs` because neither magnus nor rb-sys exposes it.
25
- rb-sys = "0.9.130"
57
+ [dependencies.rb-sys]
58
+ version = "0.9.130"
data/LICENSE ADDED
@@ -0,0 +1,52 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Minoru OSUKA
4
+ Copyright (c) 2022 ICHINOSE Shogo
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+
24
+ --------------------------------------------------------------------
25
+
26
+ Copyright (c) 2008, Taku Kudo
27
+
28
+ All rights reserved.
29
+
30
+ Redistribution and use in source and binary forms, with or without
31
+ modification, are permitted provided that the following conditions are met:
32
+
33
+ * Redistributions of source code must retain the above copyright notice,
34
+ this list of conditions and the following disclaimer.
35
+ * Redistributions in binary form must reproduce the above copyright
36
+ notice, this list of conditions and the following disclaimer in the
37
+ documentation and/or other materials provided with the distribution.
38
+ * Neither the name of the <ORGANIZATION> nor the names of its
39
+ contributors may be used to endorse or promote products derived from this
40
+ software without specific prior written permission.
41
+
42
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
43
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
44
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
45
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
46
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
47
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
48
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
49
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
50
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
51
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
52
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md CHANGED
@@ -10,7 +10,13 @@ Ruby binding for [Litsea](https://github.com/mosuka/litsea), a compact word segm
10
10
  gem install litsea
11
11
  ```
12
12
 
13
- The gem compiles the native extension on install, so a Rust toolchain is required. Ruby 3.1 or later.
13
+ The gem compiles the native extension on install, which takes a minute or two. It needs:
14
+
15
+ - Ruby 3.1 or later, with a C compiler and `make` for building native gems
16
+ - a Rust toolchain, 1.87 or later
17
+ - libclang (`libclang-dev` on Debian and Ubuntu, the Xcode Command Line Tools on macOS)
18
+
19
+ It compiles against the crates.io releases of `litsea` and `litsea-binding-core` of its own version. The gems released from 0.13.0 until the fix for [#247](https://github.com/mosuka/litsea/issues/247) fail to compile during installation; `gem install litsea` picks the newest release, so this matters only when you pin one of them.
14
20
 
15
21
  ## Models are not bundled
16
22
 
@@ -18,8 +24,8 @@ The gem ships code only. Download a pre-trained model from the [Litsea repositor
18
24
 
19
25
  | Model | Purpose | Size |
20
26
  |-------|---------|------|
21
- | `japanese.model`, `chinese.model`, `korean.model`, `english.model` | Segmentation | 84 KB – 2.0 MB |
22
- | `japanese_pos.model`, `chinese_pos.model`, `korean_pos.model`, `english_pos.model` | Segmentation + POS | 3.0 – 8.0 MB |
27
+ | `japanese.model`, `chinese.model`, `korean.model`, `english.model` | Segmentation | 86 KB – 2.0 MB |
28
+ | `japanese_pos.model`, `chinese_pos.model`, `korean_pos.model`, `english_pos.model` | Segmentation + POS | 3.1 – 8.3 MB |
23
29
 
24
30
  You never have to say which kind you have: the model file identifies itself, and `has_pos?` reports what the loaded model can do.
25
31
 
@@ -70,6 +76,28 @@ text.byteslice(token.start, token.end - token.start) # == token.surface
70
76
 
71
77
  Calling `segment_with_pos` on a segmentation-only model raises `Litsea::PosUnavailableError`.
72
78
 
79
+ ### User dictionary
80
+
81
+ Words passed as `user_words:` are emitted as single tokens wherever they occur, without retraining; every constructor accepts the keyword.
82
+
83
+ ```ruby
84
+ seg = Litsea::Segmenter.open(:japanese, "models/japanese.model", user_words: ["雪だるま", "新幹線"])
85
+
86
+ seg.segment("雪だるまを作る")
87
+ # => ["雪だるま", "を", "作る"] (without the dictionary: ["雪", "だる", "ま", "を", "作る"])
88
+ ```
89
+
90
+ Matching is context-free, so a short fragment registered as a word breaks every other word it occurs in. `user_words_gate:` (`0.0`-`1.0`) applies a word only where the model's margin against it is small enough: `0.0` only where the model already agrees with it.
91
+
92
+ With a POS model, `user_word_tags:` (a Hash of words and UPOS tags, as Strings) registers further words whose tokens `segment_with_pos` gives the registered tag, ahead of the model's own tagging:
93
+
94
+ ```ruby
95
+ tagged = Litsea::Segmenter.open(:japanese, "models/japanese_pos.model", user_word_tags: { "チャットGPT" => "PROPN" })
96
+
97
+ tagged.segment_with_pos("チャットGPTに聞く").map { |t| "#{t.surface}/#{t.pos}" }
98
+ # => ["チャットGPT/PROPN", "に/ADP", "聞く/VERB"] (in user_words: instead: "チャットGPT/NOUN")
99
+ ```
100
+
73
101
  ### Other model sources
74
102
 
75
103
  ```ruby
@@ -116,7 +144,7 @@ Every error derives from `Litsea::Error`, so one `rescue` handles them all.
116
144
 
117
145
  | Error | Raised when |
118
146
  |-------|-------------|
119
- | `Litsea::InvalidArgumentError` | Unknown language name, unknown feature set, reused trainer |
147
+ | `Litsea::InvalidArgumentError` | Unknown language name, unknown feature set, reused trainer, `user_words_gate:` outside `0.0`-`1.0`, a `user_word_tags:` value that is not a UPOS tag |
120
148
  | `Litsea::ModelError` | Download failed, or the file is a legacy joint POS model |
121
149
  | `Litsea::IoError` | A file could not be read or written |
122
150
  | `Litsea::ParseError` | The model or training data is malformed |
@@ -126,10 +154,14 @@ Every error derives from `Litsea::Error`, so one `rescue` handles them all.
126
154
  ## Development
127
155
 
128
156
  ```sh
129
- make test-litsea-ruby # cargo test + rake compile + rake test
130
- make build-litsea-ruby # release build
157
+ make test-litsea-ruby # cargo test + rake compile + rake test
158
+ make build-litsea-ruby # release build
159
+ make package-litsea-ruby # source gem into pkg/
160
+ make test-litsea-ruby-gem # gem install that gem in a clean container (needs Docker)
131
161
  ```
132
162
 
163
+ Build the gem with `make package-litsea-ruby` (`bundle exec rake build`), not `gem build`: this directory's `Cargo.toml` only resolves inside the Cargo workspace, so the task builds the gem from the crate as `cargo package` normalizes it.
164
+
133
165
  The parity tests build the `litsea` CLI and compare the binding's output against it. Note that `bundle` must be available for the active Ruby; with rbenv, `rbenv local 3.4.9` (or any installed 3.1+) is enough.
134
166
 
135
167
  ## License
data/README_ja.md CHANGED
@@ -10,7 +10,13 @@
10
10
  gem install litsea
11
11
  ```
12
12
 
13
- インストール時にネイティブ拡張をコンパイルするため、Rust ツールチェーンが必要です。Ruby 3.1 以降に対応しています。
13
+ インストール時にネイティブ拡張をコンパイルします(1〜2 分かかります)。次のものが必要です。
14
+
15
+ - Ruby 3.1 以降と、ネイティブ gem のビルドに使う C コンパイラと `make`
16
+ - Rust ツールチェーン 1.87 以降
17
+ - libclang(Debian / Ubuntu は `libclang-dev`、macOS は Xcode Command Line Tools)
18
+
19
+ gem は crates.io 上の同じバージョンの `litsea` と `litsea-binding-core` に対してコンパイルされます。0.13.0 から [#247](https://github.com/mosuka/litsea/issues/247) の修正までにリリースされた gem は、インストール時のコンパイルに失敗します。`gem install litsea` は最新のリリースを選ぶため、影響があるのはそれらのバージョンを指定した場合だけです。
14
20
 
15
21
  ## モデルは同梱されません
16
22
 
@@ -18,8 +24,8 @@ gem にはコードのみが含まれます。事前学習済みモデルは [Li
18
24
 
19
25
  | モデル | 用途 | サイズ |
20
26
  |-------|------|-------|
21
- | `japanese.model`, `chinese.model`, `korean.model`, `english.model` | 分割 | 84KB〜2.0MB |
22
- | `japanese_pos.model`, `chinese_pos.model`, `korean_pos.model`, `english_pos.model` | 分割 + POS | 3.0〜8.0MB |
27
+ | `japanese.model`, `chinese.model`, `korean.model`, `english.model` | 分割 | 86KB〜2.0MB |
28
+ | `japanese_pos.model`, `chinese_pos.model`, `korean_pos.model`, `english_pos.model` | 分割 + POS | 3.1〜8.3MB |
23
29
 
24
30
  どちらの種別かを指定する必要はありません。モデルファイル自身が種別を持っており、読み込んだモデルで何ができるかは `has_pos?` が示します。
25
31
 
@@ -70,6 +76,28 @@ text.byteslice(token.start, token.end - token.start) # == token.surface
70
76
 
71
77
  分割専用モデルに対して `segment_with_pos` を呼ぶと `Litsea::PosUnavailableError` が発生します。
72
78
 
79
+ ### ユーザー辞書
80
+
81
+ `user_words:` に渡した語は、再学習なしで、現れた場所で必ず 1 トークンとして出力されます。すべてのコンストラクタがこのキーワードを受け取ります。
82
+
83
+ ```ruby
84
+ seg = Litsea::Segmenter.open(:japanese, "models/japanese.model", user_words: ["雪だるま", "新幹線"])
85
+
86
+ seg.segment("雪だるまを作る")
87
+ # => ["雪だるま", "を", "作る"] (辞書なしでは ["雪", "だる", "ま", "を", "作る"])
88
+ ```
89
+
90
+ 照合は文脈を見ないため、短い断片を語として登録すると、それを含む他の語をすべて壊します。`user_words_gate:`(`0.0`〜`1.0`)は、モデルの margin が十分小さい場所にだけ語を適用します。`0.0` では、モデルがすでに同意している場所だけに適用します。
91
+
92
+ POS モデルでは、`user_word_tags:`(語と UPOS タグの Hash。キーと値は String)で、さらに語を登録できます。`segment_with_pos` は、その語のトークンに、モデル自身のタグ付けより優先して、登録したタグを付けます:
93
+
94
+ ```ruby
95
+ tagged = Litsea::Segmenter.open(:japanese, "models/japanese_pos.model", user_word_tags: { "チャットGPT" => "PROPN" })
96
+
97
+ tagged.segment_with_pos("チャットGPTに聞く").map { |t| "#{t.surface}/#{t.pos}" }
98
+ # => ["チャットGPT/PROPN", "に/ADP", "聞く/VERB"] (user_words: に入れた場合は "チャットGPT/NOUN")
99
+ ```
100
+
73
101
  ### その他のモデル読み込み方法
74
102
 
75
103
  ```ruby
@@ -116,7 +144,7 @@ metrics = Litsea::Trainer.new(0.01, 100_000, "features.txt").train("japanese.mod
116
144
 
117
145
  | エラー | 発生条件 |
118
146
  |-------|---------|
119
- | `Litsea::InvalidArgumentError` | 未知の言語名、未知の feature set、使用済みトレーナ |
147
+ | `Litsea::InvalidArgumentError` | 未知の言語名、未知の feature set、使用済みトレーナ、`0.0`〜`1.0` の外の `user_words_gate:`、UPOS タグでない `user_word_tags:` の値 |
120
148
  | `Litsea::ModelError` | ダウンロード失敗、または旧 joint POS モデル |
121
149
  | `Litsea::IoError` | ファイルの読み書き失敗 |
122
150
  | `Litsea::ParseError` | モデルまたは学習データの形式不正 |
@@ -126,10 +154,14 @@ metrics = Litsea::Trainer.new(0.01, 100_000, "features.txt").train("japanese.mod
126
154
  ## 開発
127
155
 
128
156
  ```sh
129
- make test-litsea-ruby # cargo test + rake compile + rake test
130
- make build-litsea-ruby # リリースビルド
157
+ make test-litsea-ruby # cargo test + rake compile + rake test
158
+ make build-litsea-ruby # リリースビルド
159
+ make package-litsea-ruby # ソース gem を pkg/ に出力
160
+ make test-litsea-ruby-gem # その gem をクリーンなコンテナで gem install(Docker が必要)
131
161
  ```
132
162
 
163
+ gem は `gem build` ではなく `make package-litsea-ruby`(`bundle exec rake build`)でビルドしてください。このディレクトリの `Cargo.toml` は Cargo ワークスペースの中でしか解決できないため、このタスクは `cargo package` が正規化したクレートから gem をビルドします。
164
+
133
165
  パリティテストは `litsea` CLI をビルドし、その出力とバインディングの出力を突き合わせます。なお、有効な Ruby に `bundle` が入っている必要があります(rbenv なら `rbenv local 3.4.9` などで 3.1 以降を選択してください)。
134
166
 
135
167
  ## ライセンス
@@ -3,5 +3,5 @@
3
3
  module Litsea
4
4
  # Kept in lockstep with the `litsea` crate; `Litsea.version` reports the
5
5
  # version the native extension was built from.
6
- VERSION = '0.13.0'
6
+ VERSION = '0.15.0'
7
7
  end
data/src/segmenter.rs CHANGED
@@ -1,21 +1,106 @@
1
1
  //! The `Litsea::Segmenter` class.
2
2
 
3
+ use std::collections::HashMap;
3
4
  use std::path::Path;
4
5
 
5
- use litsea_binding_core::{CoreSegmenter, TokenView};
6
- use magnus::{Module, Object, RArray, RModule, RString, Ruby, Value, error::Error};
6
+ use litsea::UserDictionary;
7
+ use litsea_binding_core::{CoreSegmenter, TokenView, build_user_dictionary_with_tags};
8
+ use magnus::scan_args::{get_kwargs, scan_args};
9
+ use magnus::{Module, Object, RArray, RModule, RString, Ruby, TryConvert, Value, error::Error};
7
10
 
8
11
  use crate::error::map_err;
9
12
  use crate::gvl::without_gvl;
10
13
  use crate::language::language_from_value;
11
14
  use crate::token::Token;
12
15
 
16
+ /// The keyword arguments every constructor accepts (issues #263, #264): the
17
+ /// optional `user_words:`, `user_words_gate:` and `user_word_tags:`, each of
18
+ /// which may also be passed as `nil`.
19
+ struct UserWordsArgs {
20
+ /// The words to emit as single tokens.
21
+ user_words: Option<Vec<String>>,
22
+ /// The flip-ratio gate in `0.0..=1.0`.
23
+ user_words_gate: Option<f64>,
24
+ /// Words to emit as single tokens with the UPOS tag `segment_with_pos`
25
+ /// gives them.
26
+ user_word_tags: Option<HashMap<String, String>>,
27
+ }
28
+
29
+ impl UserWordsArgs {
30
+ /// Builds the user dictionary these arguments describe.
31
+ ///
32
+ /// # Returns
33
+ /// `None` when no words were given, otherwise the dictionary.
34
+ ///
35
+ /// # Errors
36
+ /// Raises `Litsea::InvalidArgumentError` if a tag is not one of the 17
37
+ /// UPOS tags, or if the gate is outside `0.0..=1.0`, also when no words
38
+ /// were given.
39
+ fn dictionary(self) -> Result<Option<UserDictionary>, Error> {
40
+ let word_tags: Vec<(String, String)> =
41
+ self.user_word_tags.unwrap_or_default().into_iter().collect();
42
+ map_err(build_user_dictionary_with_tags(
43
+ self.user_words.as_deref().unwrap_or_default(),
44
+ &word_tags,
45
+ self.user_words_gate,
46
+ ))
47
+ }
48
+ }
49
+
50
+ /// Splits the arguments of `open`, `from_bytes`, and `from_uri`: the
51
+ /// language and the model source as required positionals, then the
52
+ /// `user_words:` / `user_words_gate:` / `user_word_tags:` keywords.
53
+ ///
54
+ /// magnus cannot express optional keyword arguments in a `function!`
55
+ /// arity, so the constructors take their arguments through `scan_args`,
56
+ /// as the trainers do.
57
+ ///
58
+ /// # Arguments
59
+ /// * `args` - The raw Ruby arguments.
60
+ ///
61
+ /// # Returns
62
+ /// The language value, the model source, and the keyword arguments.
63
+ ///
64
+ /// # Errors
65
+ /// Returns a Ruby `ArgumentError` for a wrong argument count or an unknown
66
+ /// keyword, or a `TypeError` if an argument has the wrong type.
67
+ fn scan_constructor_args<T: TryConvert>(
68
+ args: &[Value],
69
+ ) -> Result<(Value, T, UserWordsArgs), Error> {
70
+ let args = scan_args::<(Value, T), (), (), (), _, ()>(args)?;
71
+ let (language, source) = args.required;
72
+ let kwargs = get_kwargs::<
73
+ _,
74
+ (),
75
+ (
76
+ Option<Option<Vec<String>>>,
77
+ Option<Option<f64>>,
78
+ Option<Option<HashMap<String, String>>>,
79
+ ),
80
+ (),
81
+ >(args.keywords, &[], &["user_words", "user_words_gate", "user_word_tags"])?;
82
+ let (user_words, user_words_gate, user_word_tags) = kwargs.optional;
83
+ Ok((
84
+ language,
85
+ source,
86
+ UserWordsArgs {
87
+ user_words: user_words.flatten(),
88
+ user_words_gate: user_words_gate.flatten(),
89
+ user_word_tags: user_word_tags.flatten(),
90
+ },
91
+ ))
92
+ }
93
+
13
94
  /// A word segmenter, optionally with POS tagging.
14
95
  ///
15
96
  /// Build one with `Segmenter.open`, `Segmenter.from_bytes`, or
16
97
  /// `Segmenter.from_uri`. The kind of model is detected from the file itself,
17
98
  /// so `has_pos?` describes what was loaded rather than something the caller
18
- /// declares.
99
+ /// declares. Every constructor accepts the keyword arguments `user_words:`,
100
+ /// an array of words to emit as single tokens; `user_words_gate:`, a flip
101
+ /// ratio in `0.0..=1.0` that applies a word only where the model's margin
102
+ /// against it is small enough; and `user_word_tags:`, a Hash mapping further
103
+ /// words to the UPOS tag `segment_with_pos` gives them.
19
104
  #[magnus::wrap(class = "Litsea::Segmenter", free_immediately, size)]
20
105
  pub struct Segmenter {
21
106
  /// The wrapped core segmenter.
@@ -26,20 +111,25 @@ impl Segmenter {
26
111
  /// Loads a model from a filesystem path.
27
112
  ///
28
113
  /// # Arguments
29
- /// * `language` - A language name or ISO 639-1 code, as a String or Symbol.
30
- /// * `path` - Path to the model file.
114
+ /// * `args` - `language` (a language name or ISO 639-1 code, as a String
115
+ /// or Symbol), `path` (the model file), and the optional
116
+ /// `user_words:` / `user_words_gate:` / `user_word_tags:` keywords.
31
117
  ///
32
118
  /// # Returns
33
119
  /// The new segmenter.
34
120
  ///
35
121
  /// # Errors
36
- /// Raises `Litsea::InvalidArgumentError`, `Litsea::IoError`,
122
+ /// Raises `Litsea::InvalidArgumentError` (also for a tag that is not a
123
+ /// UPOS tag or a gate outside `0.0..=1.0`), `Litsea::IoError`,
37
124
  /// `Litsea::ParseError`, or `Litsea::ModelError`.
38
- fn open(language: Value, path: String) -> Result<Self, Error> {
125
+ fn open(args: &[Value]) -> Result<Self, Error> {
126
+ let (language, path, user_words): (_, String, _) = scan_constructor_args(args)?;
39
127
  let language = language_from_value(language)?;
128
+ let dictionary = user_words.dictionary()?;
40
129
  // Reading and compiling a model is measurable work; let other Ruby
41
130
  // threads run while it happens.
42
- let inner = without_gvl(|| CoreSegmenter::from_path(language, Path::new(&path)));
131
+ let inner =
132
+ without_gvl(|| CoreSegmenter::from_path_with(language, Path::new(&path), dictionary));
43
133
  Ok(Self {
44
134
  inner: map_err(inner)?,
45
135
  })
@@ -51,23 +141,27 @@ impl Segmenter {
51
141
  /// (ASCII-8BIT) works as well as one read as UTF-8.
52
142
  ///
53
143
  /// # Arguments
54
- /// * `language` - A language name or ISO 639-1 code, as a String or Symbol.
55
- /// * `data` - The model file contents.
144
+ /// * `args` - `language` (a language name or ISO 639-1 code, as a String
145
+ /// or Symbol), `data` (the model file contents), and the optional
146
+ /// `user_words:` / `user_words_gate:` / `user_word_tags:` keywords.
56
147
  ///
57
148
  /// # Returns
58
149
  /// The new segmenter.
59
150
  ///
60
151
  /// # Errors
61
- /// Raises `Litsea::InvalidArgumentError`, `Litsea::ParseError`, or
152
+ /// Raises `Litsea::InvalidArgumentError` (also for a tag that is not a
153
+ /// UPOS tag or a gate outside `0.0..=1.0`), `Litsea::ParseError`, or
62
154
  /// `Litsea::ModelError`.
63
- fn from_bytes(language: Value, data: RString) -> Result<Self, Error> {
155
+ fn from_bytes(args: &[Value]) -> Result<Self, Error> {
156
+ let (language, data, user_words): (_, RString, _) = scan_constructor_args(args)?;
64
157
  let language = language_from_value(language)?;
158
+ let dictionary = user_words.dictionary()?;
65
159
  // SAFETY: the slice is used only within this call, and nothing here
66
160
  // runs Ruby code that could move or free the string in the meantime
67
- // (`from_bytes` parses into owned Rust structures).
161
+ // (`from_bytes_with` parses into owned Rust structures).
68
162
  let bytes = unsafe { data.as_slice() };
69
163
  Ok(Self {
70
- inner: map_err(CoreSegmenter::from_bytes(language, bytes))?,
164
+ inner: map_err(CoreSegmenter::from_bytes_with(language, bytes, dictionary))?,
71
165
  })
72
166
  }
73
167
 
@@ -77,8 +171,9 @@ impl Segmenter {
77
171
  /// The GVL is released while the model is fetched.
78
172
  ///
79
173
  /// # Arguments
80
- /// * `language` - A language name or ISO 639-1 code, as a String or Symbol.
81
- /// * `uri` - The model URI.
174
+ /// * `args` - `language` (a language name or ISO 639-1 code, as a String
175
+ /// or Symbol), `uri` (the model URI), and the optional `user_words:` /
176
+ /// `user_words_gate:` / `user_word_tags:` keywords.
82
177
  ///
83
178
  /// # Returns
84
179
  /// The new segmenter.
@@ -86,9 +181,12 @@ impl Segmenter {
86
181
  /// # Errors
87
182
  /// Raises `Litsea::ModelError` if the download fails, plus the same
88
183
  /// errors as `open`.
89
- fn from_uri(language: Value, uri: String) -> Result<Self, Error> {
184
+ fn from_uri(args: &[Value]) -> Result<Self, Error> {
185
+ let (language, uri, user_words): (_, String, _) = scan_constructor_args(args)?;
90
186
  let language = language_from_value(language)?;
91
- let inner = without_gvl(|| CoreSegmenter::from_uri_blocking(language, &uri));
187
+ let dictionary = user_words.dictionary()?;
188
+ let inner =
189
+ without_gvl(|| CoreSegmenter::from_uri_blocking_with(language, &uri, dictionary));
92
190
  Ok(Self {
93
191
  inner: map_err(inner)?,
94
192
  })
@@ -224,9 +322,9 @@ fn tokens_to_array(ruby: &Ruby, tokens: Vec<TokenView>) -> Result<RArray, Error>
224
322
  /// Returns a Ruby exception if the class cannot be defined.
225
323
  pub fn define(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
226
324
  let class = module.define_class("Segmenter", ruby.class_object())?;
227
- class.define_singleton_method("open", magnus::function!(Segmenter::open, 2))?;
228
- class.define_singleton_method("from_bytes", magnus::function!(Segmenter::from_bytes, 2))?;
229
- class.define_singleton_method("from_uri", magnus::function!(Segmenter::from_uri, 2))?;
325
+ class.define_singleton_method("open", magnus::function!(Segmenter::open, -1))?;
326
+ class.define_singleton_method("from_bytes", magnus::function!(Segmenter::from_bytes, -1))?;
327
+ class.define_singleton_method("from_uri", magnus::function!(Segmenter::from_uri, -1))?;
230
328
  class.define_method("language", magnus::method!(Segmenter::language, 0))?;
231
329
  class.define_method("has_pos?", magnus::method!(Segmenter::has_pos, 0))?;
232
330
  class.define_method("segment", magnus::method!(Segmenter::segment, 1))?;
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: litsea
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Minoru Osuka
@@ -31,6 +31,7 @@ extensions:
31
31
  extra_rdoc_files: []
32
32
  files:
33
33
  - Cargo.toml
34
+ - LICENSE
34
35
  - README.md
35
36
  - README_ja.md
36
37
  - extconf.rb