Skip to main content

lindera_dictionary/
dictionary.rs

1pub mod character_definition;
2pub mod connection_cost_matrix;
3pub mod context_id_map;
4pub mod metadata;
5pub mod prefix_dictionary;
6pub mod schema;
7pub mod unknown_dictionary;
8
9use std::fs;
10use std::path::Path;
11use std::str;
12use std::sync::Arc;
13
14use byteorder::{ByteOrder, LittleEndian};
15use once_cell::sync::Lazy;
16use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
17use serde::{Deserialize, Serialize};
18
19use crate::LinderaResult;
20use crate::dictionary::character_definition::CharacterDefinition;
21use crate::dictionary::connection_cost_matrix::ConnectionCostMatrix;
22use crate::dictionary::context_id_map::ContextIdMap;
23use crate::dictionary::metadata::Metadata;
24use crate::dictionary::prefix_dictionary::{PrefixDictionary, UserPrefixDictionary};
25use crate::dictionary::unknown_dictionary::UnknownDictionary;
26use crate::error::LinderaErrorKind;
27use crate::loader::character_definition::CharacterDefinitionLoader;
28use crate::loader::connection_cost_matrix::ConnectionCostMatrixLoader;
29use crate::loader::metadata::MetadataLoader;
30use crate::loader::prefix_dictionary::PrefixDictionaryLoader;
31use crate::loader::unknown_dictionary::UnknownDictionaryLoader;
32use crate::util::Data;
33use crate::viterbi::WordEntry;
34
35pub static UNK: Lazy<Vec<&str>> = Lazy::new(|| vec!["UNK"]);
36
37/// `prefix_dictionary` and `connection_cost_matrix` are `Arc`-wrapped so that
38/// `Dictionary::clone()` is O(1) regardless of load method (embedded, mmap,
39/// or plain filesystem read) -- these two components dominate a dictionary's
40/// memory footprint (tens to hundreds of MB), and nothing in this codebase
41/// mutates them after construction.
42#[derive(Clone)]
43pub struct Dictionary {
44    pub prefix_dictionary: Arc<PrefixDictionary>,
45    pub connection_cost_matrix: Arc<ConnectionCostMatrix>,
46    pub character_definition: Arc<CharacterDefinition>,
47    pub unknown_dictionary: Arc<UnknownDictionary>,
48    pub metadata: Arc<Metadata>,
49}
50
51impl Dictionary {
52    /// Retrieve the detail fields (POS, etc.) for an unknown word entry.
53    pub fn unknown_word_details(&self, word_id: usize) -> Vec<&str> {
54        match self.unknown_dictionary.word_details(word_id as u32) {
55            Some(details) => details,
56            None => UNK.to_vec(),
57        }
58    }
59
60    pub fn word_details(&self, word_id: usize) -> Vec<&str> {
61        if 4 * word_id >= self.prefix_dictionary.words_idx_data.len() {
62            return vec![];
63        }
64
65        let idx: usize = match LittleEndian::read_u32(
66            &self.prefix_dictionary.words_idx_data[4 * word_id..][..4],
67        )
68        .try_into()
69        {
70            Ok(value) => value,
71            Err(_) => return UNK.to_vec(), // return empty vector if conversion fails
72        };
73        let data = &self.prefix_dictionary.words_data[idx..];
74        let joined_details_len: usize = match LittleEndian::read_u32(data).try_into() {
75            Ok(value) => value,
76            Err(_) => return UNK.to_vec(), // return empty vector if conversion fails
77        };
78        let joined_details_bytes: &[u8] =
79            &self.prefix_dictionary.words_data[idx + 4..idx + 4 + joined_details_len];
80
81        let mut details = Vec::new();
82        for bytes in joined_details_bytes.split(|&b| b == 0) {
83            let detail = match str::from_utf8(bytes) {
84                Ok(s) => s,
85                Err(_) => return UNK.to_vec(), // return empty vector if conversion fails
86            };
87            details.push(detail);
88        }
89        details
90    }
91
92    /// Load dictionary from a directory containing dictionary files.
93    ///
94    /// When the `mmap` feature is compiled in, the connection-cost matrix
95    /// and word list are routed through memory-mapped reads by default
96    /// (#879); use [`Dictionary::load_from_path_with_options`] with
97    /// `use_mmap = false` to force eager reads.
98    pub fn load_from_path(dict_path: &Path) -> LinderaResult<Self> {
99        Self::load_from_path_with_options(dict_path, cfg!(feature = "mmap"))
100    }
101
102    /// Load dictionary from a directory with options
103    ///
104    /// `use_mmap` (when the `mmap` feature is enabled) routes
105    /// `connection_cost_matrix` and `prefix_dictionary` through memory-mapped
106    /// reads instead of plain file reads. What that buys differs per
107    /// component:
108    ///
109    /// - `ConnectionCostMatrix` reads its costs **in place**, with no copy and
110    ///   no anonymous memory: `matrix.mtx` already stores the values in the
111    ///   in-memory layout, and an mmap base is page-aligned, so the payload
112    ///   can be viewed as `[i16]` directly. Loading it is O(1) and the pages
113    ///   are faulted in lazily during tokenization. (Costs are also borrowed
114    ///   from a plain read's buffer whenever it happens to be `i16`-aligned;
115    ///   the alignment is only *guaranteed* under mmap and for embedded data.)
116    /// - `PrefixDictionary`'s `vals_data`/`words_idx_data`/`words_data` are
117    ///   likewise mmap-backed and read lazily at lookup time.
118    /// - `PrefixDictionary`'s double-array trie (`da`) is still eagerly
119    ///   deserialized into owned daachorse structures, so for that component
120    ///   `use_mmap` only avoids the initial file-read syscall/allocation.
121    ///
122    /// `metadata`, `character_definition` and `unknown_dictionary` are always
123    /// plain-read regardless of this flag. Separately, `Dictionary::clone()`
124    /// is O(1) regardless of `use_mmap`, since
125    /// `prefix_dictionary`/`connection_cost_matrix` are `Arc`-wrapped.
126    pub fn load_from_path_with_options(dict_path: &Path, use_mmap: bool) -> LinderaResult<Self> {
127        // Verify that the dictionary directory exists
128        if !dict_path.exists() {
129            return Err(LinderaErrorKind::Io.with_error(anyhow::anyhow!(
130                "Dictionary path does not exist: {}",
131                dict_path.display()
132            )));
133        }
134
135        if !dict_path.is_dir() {
136            return Err(LinderaErrorKind::Io.with_error(anyhow::anyhow!(
137                "Dictionary path is not a directory: {}",
138                dict_path.display()
139            )));
140        }
141
142        // Load each component from the dictionary directory. The format check
143        // comes first: the remaining artifacts are headerless raw arrays, so a
144        // stale dictionary decodes into garbage rather than failing, and the
145        // error would surface far from its cause.
146        let metadata = MetadataLoader::load(dict_path)?;
147        metadata.validate_format_version()?;
148
149        let character_definition = CharacterDefinitionLoader::load(dict_path)?;
150
151        let connection_cost_matrix = {
152            #[cfg(feature = "mmap")]
153            if use_mmap {
154                ConnectionCostMatrixLoader::load_mmap(dict_path)?
155            } else {
156                ConnectionCostMatrixLoader::load(dict_path)?
157            }
158            #[cfg(not(feature = "mmap"))]
159            ConnectionCostMatrixLoader::load(dict_path)?
160        };
161
162        let prefix_dictionary = {
163            #[cfg(feature = "mmap")]
164            if use_mmap {
165                PrefixDictionaryLoader::load_mmap(dict_path)?
166            } else {
167                PrefixDictionaryLoader::load(dict_path)?
168            }
169            #[cfg(not(feature = "mmap"))]
170            PrefixDictionaryLoader::load(dict_path)?
171        };
172
173        let unknown_dictionary = UnknownDictionaryLoader::load(dict_path)?;
174
175        Ok(Dictionary {
176            prefix_dictionary: Arc::new(prefix_dictionary),
177            connection_cost_matrix: Arc::new(connection_cost_matrix),
178            character_definition: Arc::new(character_definition),
179            unknown_dictionary: Arc::new(unknown_dictionary),
180            metadata: Arc::new(metadata),
181        })
182    }
183
184    /// Save dictionary to a directory
185    pub fn save_to_path(&self, dict_path: &Path) -> LinderaResult<()> {
186        // Create directory if it doesn't exist
187        fs::create_dir_all(dict_path)
188            .map_err(|err| LinderaErrorKind::Io.with_error(anyhow::anyhow!(err)))?;
189
190        // For now, we'll implement this as needed
191        // This would require implementing save methods for each component
192        todo!("Dictionary saving will be implemented when needed")
193    }
194}
195
196/// `dict` archives with the exact field sequence the pre-v6
197/// `PrefixDictionary` used, which is what keeps previously-built user
198/// dictionary `.bin` files loading across the v6 system-dictionary format
199/// break -- rkyv 0.8 archives structurally, without type names. See
200/// [`UserPrefixDictionary`]'s type-level comment before touching either type.
201#[derive(Clone, Serialize, Deserialize, Archive, RkyvSerialize, RkyvDeserialize)]
202
203pub struct UserDictionary {
204    pub dict: UserPrefixDictionary,
205}
206
207impl UserDictionary {
208    /// Relabel this dictionary's context IDs with a system dictionary's permutation.
209    ///
210    /// User dictionaries are always compiled in the *original* context-ID space, which
211    /// keeps a built `.bin` portable across remapped and un-remapped system
212    /// dictionaries. When one is attached to a system dictionary built with
213    /// `connection_id_mapping`, its `left_id`/`right_id` must be moved into the same
214    /// space, or every connection cost it participates in would address the wrong
215    /// matrix cell — silently, since the IDs stay in range.
216    ///
217    /// Entries live in `vals_data` as a flat [`WordEntry::SERIALIZED_LEN`]-byte stride
218    /// with `left_id` at offset 6 and `right_id` at offset 8 (little endian), so this
219    /// rewrites those two `u16`s in place. IDs outside the permutation are left
220    /// untouched, matching the builder's behaviour for malformed IDs.
221    ///
222    /// # Arguments
223    ///
224    /// * `map` - The permutation persisted in the system dictionary's metadata.
225    pub fn remap_context_ids(&mut self, map: &ContextIdMap) {
226        const LEFT_ID_OFFSET: usize = 6;
227        const RIGHT_ID_OFFSET: usize = 8;
228
229        let mut vals = self.dict.vals_data.to_vec();
230        for entry in vals.chunks_exact_mut(WordEntry::SERIALIZED_LEN) {
231            let left = LittleEndian::read_u16(&entry[LEFT_ID_OFFSET..][..2]);
232            let right = LittleEndian::read_u16(&entry[RIGHT_ID_OFFSET..][..2]);
233            LittleEndian::write_u16(&mut entry[LEFT_ID_OFFSET..][..2], map.map_left(left));
234            LittleEndian::write_u16(&mut entry[RIGHT_ID_OFFSET..][..2], map.map_right(right));
235        }
236        self.dict.vals_data = Data::Vec(vals);
237    }
238
239    pub fn load(user_dict_data: &[u8]) -> LinderaResult<UserDictionary> {
240        let mut aligned = rkyv::util::AlignedVec::<16>::new();
241        aligned.extend_from_slice(user_dict_data);
242        rkyv::from_bytes::<UserDictionary, rkyv::rancor::Error>(&aligned).map_err(|err| {
243            LinderaErrorKind::Deserialize.with_error(anyhow::anyhow!(err.to_string()))
244        })
245    }
246
247    pub fn word_details(&self, word_id: usize) -> Vec<&str> {
248        if 4 * word_id >= self.dict.words_idx_data.len() {
249            return UNK.to_vec(); // return empty vector if conversion fails
250        }
251        let idx = LittleEndian::read_u32(&self.dict.words_idx_data[4 * word_id..][..4]);
252        let data = &self.dict.words_data[idx as usize..];
253
254        // Parse the data in the same format as main Dictionary
255        let joined_details_len: usize = match LittleEndian::read_u32(data).try_into() {
256            Ok(value) => value,
257            Err(_) => return UNK.to_vec(), // return empty vector if conversion fails
258        };
259        let joined_details_bytes: &[u8] =
260            &self.dict.words_data[idx as usize + 4..idx as usize + 4 + joined_details_len];
261
262        let mut details = Vec::new();
263        for bytes in joined_details_bytes.split(|&b| b == 0) {
264            let detail = match str::from_utf8(bytes) {
265                Ok(s) => s,
266                Err(_) => return UNK.to_vec(), // return empty vector if conversion fails
267            };
268            details.push(detail);
269        }
270        details
271    }
272}