use std::borrow::Cow;
use lindera_dictionary::mode::Mode;
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 = 4;
const SHRINK_FLOOR_BYTES: 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(text.len());
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(text.len());
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 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, text_len: usize) {
self.window_max_needed = self.window_max_needed.max(text_len.min(MAX_SENTENCE_BYTES));
self.calls_in_window += 1;
if self.calls_in_window >= SHRINK_WINDOW_CALLS {
let target = self.window_max_needed.max(SHRINK_FLOOR_BYTES);
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_auto_shrink_fires_after_window_of_short_inputs() {
use super::super::{SHRINK_FLOOR_BYTES, SHRINK_HYSTERESIS, SHRINK_WINDOW_CALLS};
let segmenter = ipadic_segmenter();
let mut worker = segmenter.new_worker();
let long_text = "あ".repeat(7000);
assert!(long_text.len() > SHRINK_FLOOR_BYTES * SHRINK_HYSTERESIS);
if let Err(err) = worker.segment(&long_text) {
panic!("worker.segment failed: {err}");
}
assert!(
worker.lattice.capacity() >= long_text.len(),
"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_BYTES,
"auto shrink did not fire: capacity {} > floor {}",
worker.lattice.capacity(),
SHRINK_FLOOR_BYTES
);
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");
}
}
}