use std::cmp::Reverse;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::str::FromStr;
use encoding_rs::{Encoding, UTF_8};
use encoding_rs_io::DecodeReaderBytesBuilder;
use glob::glob;
use crate::LinderaResult;
use crate::builder::connection_cost_matrix::read_matrix_header;
use crate::dictionary::context_id_map::ContextIdMap;
use crate::dictionary::metadata::Metadata;
use crate::error::LinderaErrorKind;
pub fn compute_context_id_remap(
input_dir: &Path,
metadata: &Metadata,
freq_file: Option<&Path>,
) -> LinderaResult<ContextIdMap> {
let (forward_size, backward_size) = read_matrix_header(input_dir, &metadata.encoding)?;
if forward_size as usize > u16::MAX as usize + 1
|| backward_size as usize > u16::MAX as usize + 1
{
return Err(LinderaErrorKind::Content.with_error(anyhow::anyhow!(
"connection matrix axis exceeds u16 range: forward={forward_size}, backward={backward_size}"
)));
}
let env_override = std::env::var_os("LINDERA_CTX_FREQ_FILE").map(std::path::PathBuf::from);
let corpus_freq: Option<&Path> = env_override.as_deref().or(freq_file);
if let Some(freq_path) = corpus_freq {
let (hist_left, hist_right) =
load_freq_file(freq_path, backward_size as usize, forward_size as usize)?;
return Ok(ContextIdMap {
left: build_perm(&hist_left),
right: build_perm(&hist_right),
});
}
log::warn!(
"connection_id_mapping is enabled but no context-id frequency file was found; \
falling back to the entry-count proxy, which measured ~0% improvement. \
Bundle a corpus-derived histogram (see the ctxfreq_dump example) for the real gain."
);
let left_index = metadata
.dictionary_schema
.get_field_index("left_context_id")
.ok_or_else(|| {
LinderaErrorKind::Content.with_error(anyhow::anyhow!(
"schema has no left_context_id field; cannot compute context-id remap"
))
})?;
let right_index = metadata
.dictionary_schema
.get_field_index("right_context_id")
.ok_or_else(|| {
LinderaErrorKind::Content.with_error(anyhow::anyhow!(
"schema has no right_context_id field; cannot compute context-id remap"
))
})?;
let mut hist_left = vec![0u64; backward_size as usize];
let mut hist_right = vec![0u64; forward_size as usize];
let encoding =
Encoding::for_label_no_replacement(metadata.encoding.as_bytes()).ok_or_else(|| {
LinderaErrorKind::Decode
.with_error(anyhow::anyhow!("Invalid encoding: {}", metadata.encoding))
})?;
let pattern = input_dir
.to_str()
.map(|p| format!("{p}/*.csv"))
.ok_or_else(|| {
LinderaErrorKind::Io.with_error(anyhow::anyhow!(
"Input directory path contains invalid characters: {input_dir:?}"
))
})?;
for entry in
glob(&pattern).map_err(|err| LinderaErrorKind::Io.with_error(anyhow::anyhow!(err)))?
{
let path =
entry.map_err(|err| LinderaErrorKind::Content.with_error(anyhow::anyhow!(err)))?;
let file = File::open(&path)
.map_err(|err| LinderaErrorKind::Io.with_error(anyhow::anyhow!(err)))?;
let reader: Box<dyn Read> = if encoding == UTF_8 {
Box::new(file)
} else {
Box::new(
DecodeReaderBytesBuilder::new()
.encoding(Some(encoding))
.build(file),
)
};
let mut rdr = csv::ReaderBuilder::new()
.has_headers(false)
.flexible(metadata.flexible_csv)
.from_reader(reader);
for result in rdr.records() {
let record =
result.map_err(|err| LinderaErrorKind::Content.with_error(anyhow::anyhow!(err)))?;
if let Some(value) = record.get(left_index)
&& let Ok(id) = u16::from_str(value.trim())
&& (id as usize) < hist_left.len()
{
hist_left[id as usize] += 1;
}
if let Some(value) = record.get(right_index)
&& let Ok(id) = u16::from_str(value.trim())
&& (id as usize) < hist_right.len()
{
hist_right[id as usize] += 1;
}
}
}
Ok(ContextIdMap {
left: build_perm(&hist_left),
right: build_perm(&hist_right),
})
}
fn build_perm(hist: &[u64]) -> Vec<u16> {
let n = hist.len();
if n == 0 {
return Vec::new();
}
let mut ids: Vec<usize> = (1..n).collect();
ids.sort_by_key(|&id| (Reverse(hist[id]), id));
let mut perm = vec![0u16; n];
for (rank, &old) in ids.iter().enumerate() {
perm[old] = (rank + 1) as u16;
}
perm
}
fn load_freq_file(
path: &Path,
backward_size: usize,
forward_size: usize,
) -> LinderaResult<(Vec<u64>, Vec<u64>)> {
let content = std::fs::read_to_string(path).map_err(|err| {
LinderaErrorKind::Io
.with_error(anyhow::anyhow!(err))
.add_context(format!(
"Failed to read context-id frequency file: {path:?}"
))
})?;
let mut lines = content.lines();
lines.next();
let parse_line = |line: Option<&str>| -> Vec<u64> {
line.map(|l| {
l.split_whitespace()
.map(|s| s.parse::<u64>().unwrap_or(0))
.collect()
})
.unwrap_or_default()
};
let mut left = parse_line(lines.next());
let mut right = parse_line(lines.next());
left.resize(backward_size, 0);
right.resize(forward_size, 0);
Ok((left, right))
}
#[cfg(feature = "ctxfreq")]
mod ctxfreq {
use std::cell::RefCell;
use std::path::Path;
thread_local! {
static HIST_LEFT: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
static HIST_RIGHT: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
}
fn bump(hist: &RefCell<Vec<u64>>, idx: usize) {
let mut v = hist.borrow_mut();
if idx >= v.len() {
v.resize(idx + 1, 0);
}
v[idx] += 1;
}
pub fn record_access(forward_id: u32, backward_id: u32) {
HIST_RIGHT.with(|h| bump(h, forward_id as usize));
HIST_LEFT.with(|h| bump(h, backward_id as usize));
}
pub fn dump(path: &Path, forward_size: usize, backward_size: usize) -> std::io::Result<()> {
use std::io::Write;
let take = |h: &'static std::thread::LocalKey<RefCell<Vec<u64>>>, n: usize| -> Vec<u64> {
h.with(|c| {
let mut v = c.borrow().clone();
v.resize(n, 0);
v
})
};
let left = take(&HIST_LEFT, backward_size);
let right = take(&HIST_RIGHT, forward_size);
let mut f = std::io::BufWriter::new(std::fs::File::create(path)?);
writeln!(f, "{backward_size} {forward_size}")?;
let write_row =
|f: &mut std::io::BufWriter<std::fs::File>, row: &[u64]| -> std::io::Result<()> {
let mut first = true;
for c in row {
if !first {
write!(f, " ")?;
}
write!(f, "{c}")?;
first = false;
}
writeln!(f)
};
write_row(&mut f, &left)?;
write_row(&mut f, &right)?;
f.flush()
}
}
#[cfg(feature = "ctxfreq")]
pub use ctxfreq::{dump as dump_ctx_freq, record_access};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_perm_orders_by_frequency() {
let hist = [99u64, 5, 50, 50, 0];
let perm = build_perm(&hist);
assert_eq!(perm[0], 0); assert_eq!(perm[2], 1);
assert_eq!(perm[3], 2);
assert_eq!(perm[1], 3); assert_eq!(perm[4], 4); let mut seen = perm.clone();
seen.sort_unstable();
assert_eq!(seen, vec![0, 1, 2, 3, 4]);
}
#[test]
fn test_build_perm_empty() {
assert!(build_perm(&[]).is_empty());
}
#[test]
fn test_build_perm_single() {
assert_eq!(build_perm(&[7]), vec![0]);
}
}