use std::borrow::Cow;
use lindera_dictionary::mode::Mode;
use lindera_dictionary::space_penalty::SpacePenaltyConfig;
use lindera_dictionary::viterbi::{Lattice, WordId};
use crate::LinderaResult;
use crate::segmenter::{MAX_SENTENCE_BYTES, Segmenter};
use crate::token::Token;
const SHRINK_WINDOW_CALLS: u32 = 64;
const SHRINK_HYSTERESIS: usize = 2;
const SHRINK_FLOOR_SLOTS: usize = 4 * 1024;
pub struct SegmentWorker {
segmenter: Segmenter,
lattice: Lattice,
offsets: Vec<(usize, WordId)>,
window_max_needed: usize,
calls_in_window: u32,
}
impl Segmenter {
pub fn new_worker(&self) -> SegmentWorker {
SegmentWorker::new(self.clone())
}
pub fn into_worker(self) -> SegmentWorker {
SegmentWorker::new(self)
}
}
impl SegmentWorker {
fn new(segmenter: Segmenter) -> Self {
Self {
segmenter,
lattice: Lattice::default(),
offsets: Vec::new(),
window_max_needed: 0,
calls_in_window: 0,
}
}
pub fn segment<'w>(&'w mut self, text: &'w str) -> LinderaResult<Vec<Token<'w>>> {
self.maybe_auto_shrink();
let Self {
segmenter,
lattice,
offsets,
..
} = self;
segmenter.segment_with_buffers(Cow::Borrowed(text), lattice, offsets)
}
pub fn segment_nbest<'w>(
&'w mut self,
text: &'w str,
n: usize,
unique: bool,
cost_threshold: Option<i64>,
) -> LinderaResult<Vec<(Vec<Token<'w>>, i64)>> {
self.maybe_auto_shrink();
let Self {
segmenter, lattice, ..
} = self;
segmenter.segment_nbest_with_lattice(
Cow::Borrowed(text),
lattice,
n,
unique,
cost_threshold,
)
}
pub fn set_mode(&mut self, mode: Mode) {
self.segmenter.mode = mode;
}
pub fn set_keep_whitespace(&mut self, keep: bool) {
self.segmenter.keep_whitespace = keep;
}
pub fn set_max_grouping_len(&mut self, max_grouping_len: Option<usize>) {
self.segmenter.max_grouping_len = max_grouping_len;
}
pub fn set_unknown_word_ladder(&mut self, unknown_word_ladder: bool) {
self.segmenter.unknown_word_ladder = unknown_word_ladder;
}
pub fn set_space_penalty(
&mut self,
space_penalty: Option<SpacePenaltyConfig>,
) -> LinderaResult<()> {
self.segmenter.set_space_penalty(space_penalty)
}
pub fn set_space_penalty_from_dictionary(&mut self) -> LinderaResult<()> {
self.segmenter.set_space_penalty_from_dictionary()
}
pub fn segmenter(&self) -> &Segmenter {
&self.segmenter
}
pub fn shrink_to(&mut self, text_len_hint: usize) {
self.lattice
.shrink_to(text_len_hint.min(MAX_SENTENCE_BYTES));
self.offsets.shrink_to_fit();
self.window_max_needed = 0;
self.calls_in_window = 0;
}
pub fn reset(&mut self) {
self.lattice = Lattice::default();
self.offsets = Vec::new();
self.window_max_needed = 0;
self.calls_in_window = 0;
}
fn maybe_auto_shrink(&mut self) {
self.window_max_needed = self.window_max_needed.max(self.lattice.take_max_char_len());
self.calls_in_window += 1;
if self.calls_in_window >= SHRINK_WINDOW_CALLS {
let target = self.window_max_needed.max(SHRINK_FLOOR_SLOTS);
if self.lattice.capacity() > target.saturating_mul(SHRINK_HYSTERESIS) {
self.lattice.shrink_to(target);
}
self.window_max_needed = 0;
self.calls_in_window = 0;
}
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "embed-ipadic")]
mod with_ipadic {
use std::borrow::Cow;
use lindera_dictionary::mode::{Mode, Penalty};
use crate::dictionary::load_dictionary;
use crate::segmenter::Segmenter;
fn ipadic_segmenter() -> Segmenter {
let dictionary = match load_dictionary("embedded://ipadic") {
Ok(dictionary) => dictionary,
Err(err) => panic!("failed to load embedded IPADIC: {err}"),
};
Segmenter::new(Mode::Normal, dictionary, None)
}
#[test]
fn test_worker_matches_segment_output() {
let segmenter = ipadic_segmenter();
let mut worker = segmenter.new_worker();
let texts = [
"すもももももももものうち",
"関西国際空港限定トートバッグ",
"Lindera is a morphological analysis library.",
"",
"検索エンジン(けんさくエンジン、英語: search engine)は、狭義にはインターネットに存在する情報を検索する機能およびそのプログラム。",
];
for _ in 0..3 {
for text in texts {
let expected = match segmenter.segment(Cow::Borrowed(text)) {
Ok(tokens) => tokens,
Err(err) => panic!("segment failed: {err}"),
};
let actual = match worker.segment(text) {
Ok(tokens) => tokens,
Err(err) => panic!("worker.segment failed: {err}"),
};
assert_eq!(expected.len(), actual.len(), "token count for {text:?}");
for (e, a) in expected.iter().zip(actual.iter()) {
assert_eq!(e.surface, a.surface);
assert_eq!(e.byte_start, a.byte_start);
assert_eq!(e.byte_end, a.byte_end);
assert_eq!(e.position, a.position);
assert_eq!(e.word_id, a.word_id);
}
}
}
}
#[test]
fn test_worker_matches_segment_nbest_output() {
let segmenter = ipadic_segmenter();
let mut worker = segmenter.new_worker();
let text = "すもももももももものうち";
let expected = match segmenter.segment_nbest(Cow::Borrowed(text), 5, false, None) {
Ok(results) => results,
Err(err) => panic!("segment_nbest failed: {err}"),
};
for _ in 0..2 {
let actual = match worker.segment_nbest(text, 5, false, None) {
Ok(results) => results,
Err(err) => panic!("worker.segment_nbest failed: {err}"),
};
assert_eq!(expected.len(), actual.len());
for ((e_tokens, e_cost), (a_tokens, a_cost)) in expected.iter().zip(actual.iter()) {
assert_eq!(e_cost, a_cost);
let e_surfaces: Vec<_> = e_tokens.iter().map(|t| t.surface.clone()).collect();
let a_surfaces: Vec<_> = a_tokens.iter().map(|t| t.surface.clone()).collect();
assert_eq!(e_surfaces, a_surfaces);
}
}
}
#[test]
fn test_worker_mode_and_whitespace_switching() {
let segmenter = ipadic_segmenter();
let mut worker = segmenter.new_worker();
let text = "関西国際空港へ 行く";
worker.set_mode(Mode::Decompose(Penalty::default()));
let decompose_segmenter = Segmenter::new(
Mode::Decompose(Penalty::default()),
segmenter.dictionary.clone(),
None,
);
let expected = match decompose_segmenter.segment(Cow::Borrowed(text)) {
Ok(tokens) => tokens,
Err(err) => panic!("segment failed: {err}"),
};
let actual = match worker.segment(text) {
Ok(tokens) => tokens,
Err(err) => panic!("worker.segment failed: {err}"),
};
let expected_surfaces: Vec<_> = expected.iter().map(|t| t.surface.clone()).collect();
let actual_surfaces: Vec<_> = actual.iter().map(|t| t.surface.clone()).collect();
assert_eq!(expected_surfaces, actual_surfaces);
worker.set_mode(Mode::Normal);
let normal = match worker.segment(text) {
Ok(tokens) => tokens,
Err(err) => panic!("worker.segment failed: {err}"),
};
let fresh = match segmenter.segment(Cow::Borrowed(text)) {
Ok(tokens) => tokens,
Err(err) => panic!("segment failed: {err}"),
};
assert_eq!(normal.len(), fresh.len());
worker.set_keep_whitespace(true);
let kept = match worker.segment(text) {
Ok(tokens) => tokens,
Err(err) => panic!("worker.segment failed: {err}"),
};
assert!(
kept.iter().any(|t| t.surface == " "),
"whitespace token missing with keep_whitespace=true"
);
worker.set_keep_whitespace(false);
}
#[test]
fn test_unknown_word_ladder_toggle() {
let segmenter = ipadic_segmenter();
let mut worker = segmenter.new_worker();
let text = "龘龍";
let with_ladder = match worker.segment(text) {
Ok(tokens) => tokens
.iter()
.map(|t| t.surface.to_string())
.collect::<Vec<_>>(),
Err(err) => panic!("worker.segment failed: {err}"),
};
assert_eq!(
with_ladder,
vec![text.to_string()],
"the ladder's 2-char candidate must win the best path"
);
worker.set_unknown_word_ladder(false);
let without_ladder = match worker.segment(text) {
Ok(tokens) => tokens
.iter()
.map(|t| t.surface.to_string())
.collect::<Vec<_>>(),
Err(err) => panic!("worker.segment failed: {err}"),
};
assert_eq!(
without_ladder,
vec!["龘".to_string(), "龍".to_string()],
"disabling the ladder must fall back to single-char unknowns"
);
worker.set_unknown_word_ladder(true);
assert_eq!(
match worker.segment(text) {
Ok(tokens) => tokens
.iter()
.map(|t| t.surface.to_string())
.collect::<Vec<_>>(),
Err(err) => panic!("worker.segment failed: {err}"),
},
with_ladder,
"re-enabling the ladder must restore the default output"
);
}
#[test]
fn test_unknown_word_ladder_decompose_mode() {
use lindera_dictionary::mode::Penalty;
let segmenter = ipadic_segmenter();
let mut worker = segmenter.new_worker();
worker.set_mode(Mode::Decompose(Penalty::default()));
let text = "龘龍";
let with_ladder = match worker.segment(text) {
Ok(tokens) => tokens
.iter()
.map(|t| t.surface.to_string())
.collect::<Vec<_>>(),
Err(err) => panic!("worker.segment failed: {err}"),
};
assert_eq!(with_ladder, vec![text.to_string()]);
worker.set_unknown_word_ladder(false);
let without_ladder = match worker.segment(text) {
Ok(tokens) => tokens
.iter()
.map(|t| t.surface.to_string())
.collect::<Vec<_>>(),
Err(err) => panic!("worker.segment failed: {err}"),
};
assert_eq!(without_ladder, vec!["龘".to_string(), "龍".to_string()]);
}
#[test]
fn test_max_grouping_len_caps_unknown_grouping() {
let segmenter = ipadic_segmenter();
let mut worker = segmenter.new_worker();
worker.set_unknown_word_ladder(false);
let text = "ヷヸヹヺヷ";
let surfaces = |worker: &mut super::super::SegmentWorker| -> Vec<String> {
match worker.segment(text) {
Ok(tokens) => tokens.iter().map(|t| t.surface.to_string()).collect(),
Err(err) => panic!("worker.segment failed: {err}"),
}
};
let unbounded = surfaces(&mut worker);
assert_eq!(
unbounded,
vec![text.to_string()],
"unbounded grouping must span the whole run"
);
worker.set_max_grouping_len(Some(2));
assert_eq!(
surfaces(&mut worker),
vec!["ヷ".to_string(), "ヸ".to_string(), "ヹヺヷ".to_string()],
"cap must reject over-long runs and admit fitting tails"
);
worker.set_max_grouping_len(Some(4));
assert_eq!(surfaces(&mut worker), vec![text.to_string()]);
worker.set_max_grouping_len(None);
assert_eq!(surfaces(&mut worker), unbounded);
}
#[test]
fn test_auto_shrink_fires_after_window_of_short_inputs() {
use super::super::{SHRINK_FLOOR_SLOTS, SHRINK_HYSTERESIS, SHRINK_WINDOW_CALLS};
let segmenter = ipadic_segmenter();
let mut worker = segmenter.new_worker();
let long_chars = 9000;
let long_text = "あ".repeat(long_chars);
assert!(long_chars > SHRINK_FLOOR_SLOTS * SHRINK_HYSTERESIS);
if let Err(err) = worker.segment(&long_text) {
panic!("worker.segment failed: {err}");
}
assert!(
worker.lattice.capacity() >= long_chars,
"long sentence did not grow the lattice"
);
let short_text = "すもももももももものうち";
for _ in 0..(2 * SHRINK_WINDOW_CALLS + 1) {
if let Err(err) = worker.segment(short_text) {
panic!("worker.segment failed: {err}");
}
}
assert!(
worker.lattice.capacity() <= SHRINK_FLOOR_SLOTS,
"auto shrink did not fire: capacity {} > floor {}",
worker.lattice.capacity(),
SHRINK_FLOOR_SLOTS
);
let tokens = match worker.segment(short_text) {
Ok(tokens) => tokens,
Err(err) => panic!("worker.segment failed: {err}"),
};
assert!(!tokens.is_empty());
}
#[test]
fn test_worker_shrink_to_keeps_output_identical() {
let segmenter = ipadic_segmenter();
let mut worker = segmenter.new_worker();
let text = "すもももももももものうち";
let before = match worker.segment(text) {
Ok(tokens) => tokens.len(),
Err(err) => panic!("worker.segment failed: {err}"),
};
worker.shrink_to(0);
let after = match worker.segment(text) {
Ok(tokens) => tokens.len(),
Err(err) => panic!("worker.segment failed: {err}"),
};
assert_eq!(before, after, "output changed after shrink_to");
}
}
}