use super::{
error::check_limit, filters::CompiledFilter, CompletionMode, JapaneseFilter, JapaneseTokenizer,
KuromojiDictionary, KuromojiLimits, KuromojiMode, KuromojiOptions, UserDictionary,
};
use crate::{AnalysisResult, AnalyzedText, CharFilter, FilteredText};
use std::sync::Arc;
use uqa_core::memory::{Budgeted, BudgetedVec, MemoryBudget};
#[derive(Debug)]
pub struct JapaneseAnalyzer {
model: Arc<KuromojiDictionary>,
tokenizer: JapaneseTokenizer,
filters: Budgeted<Vec<CompiledFilter>>,
normalization: Normalization,
}
#[derive(Debug)]
enum Normalization {
WidthAndLowercase,
Width,
}
impl JapaneseAnalyzer {
pub fn new(
model: Arc<KuromojiDictionary>,
user: Option<Arc<UserDictionary>>,
mode: KuromojiMode,
) -> AnalysisResult<Self> {
Self::with_filters(
model,
user,
KuromojiOptions {
mode,
..KuromojiOptions::default()
},
&[
JapaneseFilter::BaseForm,
JapaneseFilter::PartOfSpeech { stop_tags: None },
JapaneseFilter::Stop {
words: None,
ignore_case: true,
},
JapaneseFilter::KatakanaStem { minimum_length: 4 },
JapaneseFilter::SimpleLowercase,
],
)
}
pub fn completion(
model: Arc<KuromojiDictionary>,
user: Option<Arc<UserDictionary>>,
mode: CompletionMode,
) -> AnalysisResult<Self> {
Self::completion_budgeted(
model,
user,
mode,
KuromojiLimits::default(),
&MemoryBudget::new(usize::MAX),
&mut || Ok(()),
)
}
pub fn completion_budgeted(
model: Arc<KuromojiDictionary>,
user: Option<Arc<UserDictionary>>,
mode: CompletionMode,
limits: KuromojiLimits,
budget: &MemoryBudget,
poll: &mut impl FnMut() -> AnalysisResult<()>,
) -> AnalysisResult<Self> {
let mut analyzer = Self::with_filters_budgeted(
model,
user,
KuromojiOptions {
mode: KuromojiMode::Normal,
..KuromojiOptions::default()
},
&[
JapaneseFilter::Completion { mode },
JapaneseFilter::SimpleLowercase,
],
limits,
budget,
poll,
)?;
analyzer.normalization = Normalization::Width;
Ok(analyzer)
}
pub fn with_filters(
model: Arc<KuromojiDictionary>,
user: Option<Arc<UserDictionary>>,
options: KuromojiOptions,
filters: &[JapaneseFilter],
) -> AnalysisResult<Self> {
Self::with_filters_budgeted(
model,
user,
options,
filters,
KuromojiLimits::default(),
&MemoryBudget::new(usize::MAX),
&mut || Ok(()),
)
}
pub fn with_filters_budgeted(
model: Arc<KuromojiDictionary>,
user: Option<Arc<UserDictionary>>,
options: KuromojiOptions,
filters: &[JapaneseFilter],
limits: KuromojiLimits,
budget: &MemoryBudget,
poll: &mut impl FnMut() -> AnalysisResult<()>,
) -> AnalysisResult<Self> {
poll()?;
check_limit(
"Kuromoji filter stages",
filters.len(),
limits.max_filter_entries,
)?;
let tokenizer = JapaneseTokenizer::new(model.clone(), user, options)?;
let mut output = BudgetedVec::new(budget);
output.reserve(filters.len())?;
let mut memory = budget.empty_reservation();
for filter in filters {
let (filter, allocation) = filter
.compile(Some(&model), limits, budget, poll)?
.into_parts();
output.push(filter)?;
memory.absorb(allocation);
}
let (filters, allocation) = output.into_parts();
memory.absorb(allocation);
poll()?;
Ok(Self {
model,
tokenizer,
filters: Budgeted::new(filters, memory),
normalization: Normalization::WidthAndLowercase,
})
}
pub fn analyze(&self, input: &str) -> AnalysisResult<AnalyzedText> {
self.analyze_mapped(&FilteredText::new(input))
}
pub fn analyze_mapped(&self, input: &FilteredText<'_>) -> AnalysisResult<AnalyzedText> {
Ok(self
.analyze_mapped_budgeted(
input,
KuromojiLimits::default(),
&MemoryBudget::new(usize::MAX),
&mut || Ok(()),
)?
.into_parts()
.0)
}
pub fn analyze_budgeted(
&self,
input: &str,
limits: KuromojiLimits,
budget: &MemoryBudget,
poll: &mut impl FnMut() -> AnalysisResult<()>,
) -> AnalysisResult<Budgeted<AnalyzedText>> {
self.analyze_mapped_budgeted(&FilteredText::new(input), limits, budget, poll)
}
pub fn analyze_mapped_budgeted(
&self,
input: &FilteredText<'_>,
limits: KuromojiLimits,
budget: &MemoryBudget,
poll: &mut impl FnMut() -> AnalysisResult<()>,
) -> AnalysisResult<Budgeted<AnalyzedText>> {
input_length(input.as_str(), limits, poll)?;
let input = CharFilter::CJKWidth.filter_mapped_budgeted(input.clone(), budget, poll)?;
let mut output = self
.tokenizer
.tokenize_mapped_for_filters_budgeted(&input, limits, budget, poll)?;
for filter in self.filters.iter() {
output = filter.filter_analyzed_budgeted(output, Some(&self.model), limits, poll)?;
}
output.validate_japanese_attributes(poll)?;
poll()?;
Ok(output)
}
pub fn normalize(&self, input: &str) -> AnalysisResult<String> {
Ok(self
.normalize_budgeted(
input,
KuromojiLimits::default(),
&MemoryBudget::new(usize::MAX),
&mut || Ok(()),
)?
.into_parts()
.0)
}
pub fn normalize_budgeted(
&self,
input: &str,
limits: KuromojiLimits,
budget: &MemoryBudget,
poll: &mut impl FnMut() -> AnalysisResult<()>,
) -> AnalysisResult<Budgeted<String>> {
super::normalization::normalize_budgeted(
input,
true,
matches!(self.normalization, Normalization::WidthAndLowercase)
.then_some(self.model.as_ref()),
limits,
budget,
poll,
)
}
}
fn input_length(
input: &str,
limits: KuromojiLimits,
poll: &mut dyn FnMut() -> AnalysisResult<()>,
) -> AnalysisResult<usize> {
crate::allocation::input::utf16_len(input, poll, |length| {
check_limit(
"Kuromoji input UTF-16 units",
length,
limits.max_input_utf16,
)
.map_err(Into::into)
})
}