use crate::index::text_types::TermId;
use crate::{Result, StorageError};
use lru::LruCache;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{create_dir_all, File};
use std::io::{Read, Write};
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DictionaryChunk {
entries: HashMap<String, TermId>,
chunk_id: usize,
#[serde(skip)]
dirty: bool,
}
impl DictionaryChunk {
fn new(chunk_id: usize) -> Self {
Self {
entries: HashMap::new(),
chunk_id,
dirty: false,
}
}
fn insert(&mut self, token: String, term_id: TermId) {
self.entries.insert(token, term_id);
self.dirty = true;
}
fn get(&self, token: &str) -> Option<TermId> {
self.entries.get(token).copied()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DictionaryMetadata {
total_terms: usize,
num_chunks: usize,
next_term_id: TermId,
chunk_index: HashMap<String, usize>,
}
impl DictionaryMetadata {
fn new() -> Self {
Self {
total_terms: 0,
num_chunks: 0,
next_term_id: 0,
chunk_index: HashMap::new(),
}
}
}
pub struct ChunkedDictionary {
storage_dir: PathBuf,
metadata: Arc<RwLock<DictionaryMetadata>>,
cache: Arc<RwLock<LruCache<usize, DictionaryChunk>>>,
}
impl ChunkedDictionary {
pub fn new(storage_dir: PathBuf, cache_size: usize) -> Result<Self> {
create_dir_all(&storage_dir)?;
let meta_path = storage_dir.join("dict_meta.bin");
let metadata = if meta_path.exists() {
Self::load_metadata(&meta_path)?
} else {
DictionaryMetadata::new()
};
Ok(Self {
storage_dir,
metadata: Arc::new(RwLock::new(metadata)),
cache: Arc::new(RwLock::new(LruCache::new(
NonZeroUsize::new(cache_size.max(1)).unwrap(),
))),
})
}
pub fn get_or_insert(&self, token: &str) -> TermId {
let chunk_id = self.guess_chunk_id(token);
{
let cache = self.cache.read();
if let Some(chunk) = cache.peek(&chunk_id) {
if let Some(term_id) = chunk.get(token) {
return term_id;
}
}
}
{
let mut cache = self.cache.write();
if let Some(chunk) = cache.get_mut(&chunk_id) {
if let Some(term_id) = chunk.get(token) {
return term_id;
}
} else {
if let Ok(chunk) = self.load_chunk(chunk_id) {
if let Some(term_id) = chunk.get(token) {
cache.put(chunk_id, chunk);
return term_id;
}
cache.put(chunk_id, chunk);
}
}
}
self.insert_new_token(token, chunk_id)
}
pub fn get(&self, token: &str) -> Option<TermId> {
let chunk_id = self.guess_chunk_id(token);
{
let cache = self.cache.read();
if let Some(chunk) = cache.peek(&chunk_id) {
if let Some(term_id) = chunk.get(token) {
return Some(term_id);
}
}
}
{
let mut cache = self.cache.write();
if let Some(chunk) = cache.get(&chunk_id) {
let result = chunk.get(token);
return result;
}
}
if let Ok(chunk) = self.load_chunk(chunk_id) {
let term_id = chunk.get(token);
self.cache.write().put(chunk_id, chunk);
return term_id;
}
None
}
pub fn len(&self) -> usize {
self.metadata.read().total_terms
}
pub fn flush(&self) -> Result<()> {
let mut cache = self.cache.write();
for (chunk_id, chunk) in cache.iter() {
if chunk.dirty {
self.save_chunk(*chunk_id, chunk)?;
}
}
for (_, chunk) in cache.iter_mut() {
chunk.dirty = false; }
let meta = self.metadata.read();
self.save_metadata(&meta)?;
Ok(())
}
fn guess_chunk_id(&self, token: &str) -> usize {
let meta = self.metadata.read();
if let Some(prefix) = token
.chars()
.take(2)
.collect::<String>()
.is_empty()
.then_some(token)
{
if let Some(&chunk_id) = meta.chunk_index.get(prefix) {
return chunk_id;
}
}
let hash = self.hash_token(token);
hash % meta.num_chunks.max(1)
}
fn hash_token(&self, token: &str) -> usize {
token.bytes().fold(0usize, |acc, b| {
acc.wrapping_mul(31).wrapping_add(b as usize)
})
}
fn insert_new_token(&self, token: &str, chunk_id: usize) -> TermId {
let term_id = {
let mut meta = self.metadata.write();
let term_id = meta.next_term_id;
meta.next_term_id += 1;
meta.total_terms += 1;
if chunk_id >= meta.num_chunks {
meta.num_chunks = chunk_id + 1;
}
if token.len() >= 2 {
let prefix = token.chars().take(2).collect::<String>();
meta.chunk_index.entry(prefix).or_insert(chunk_id);
}
term_id
};
let mut target_chunk = None;
{
let cache = self.cache.read();
if !cache.contains(&chunk_id) {
drop(cache);
target_chunk = Some(
self.load_chunk(chunk_id)
.unwrap_or_else(|_| DictionaryChunk::new(chunk_id)),
);
}
}
{
let mut cache = self.cache.write();
let chunk = if let Some(loaded_chunk) = target_chunk {
cache.put(chunk_id, loaded_chunk);
let result = cache.get_mut(&chunk_id).unwrap();
result
} else {
let result = cache.get_mut(&chunk_id).unwrap();
result
};
chunk.insert(token.to_string(), term_id);
}
term_id
}
fn load_chunk(&self, chunk_id: usize) -> Result<DictionaryChunk> {
let path = self.chunk_path(chunk_id);
if !path.exists() {
return Ok(DictionaryChunk::new(chunk_id));
}
let mut file = File::open(&path)?;
let mut data = Vec::new();
file.read_to_end(&mut data)?;
let mut chunk: DictionaryChunk =
bincode::deserialize(&data).map_err(|e| StorageError::Serialization(e.to_string()))?;
chunk.dirty = false; Ok(chunk)
}
fn save_chunk(&self, chunk_id: usize, chunk: &DictionaryChunk) -> Result<()> {
let path = self.chunk_path(chunk_id);
let data =
bincode::serialize(chunk).map_err(|e| StorageError::Serialization(e.to_string()))?;
let mut file = File::create(&path)?;
file.write_all(&data)?;
file.sync_all()?;
Ok(())
}
fn chunk_path(&self, chunk_id: usize) -> PathBuf {
self.storage_dir
.join(format!("dict_chunk_{:04}.bin", chunk_id))
}
fn load_metadata(path: &PathBuf) -> Result<DictionaryMetadata> {
let mut file = File::open(path)?;
let mut data = Vec::new();
file.read_to_end(&mut data)?;
bincode::deserialize(&data).map_err(|e| StorageError::Serialization(e.to_string()))
}
fn save_metadata(&self, meta: &DictionaryMetadata) -> Result<()> {
let path = self.storage_dir.join("dict_meta.bin");
let data =
bincode::serialize(meta).map_err(|e| StorageError::Serialization(e.to_string()))?;
let mut file = File::create(&path)?;
file.write_all(&data)?;
file.sync_all()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_chunked_dictionary_basic() {
let temp_dir = TempDir::new().unwrap();
let dict = ChunkedDictionary::new(temp_dir.path().to_path_buf(), 4).unwrap();
let id1 = dict.get_or_insert("hello");
let id2 = dict.get_or_insert("world");
let id3 = dict.get_or_insert("hello");
assert_eq!(id1, id3); assert_ne!(id1, id2);
assert_eq!(dict.len(), 2);
}
#[test]
fn test_chunked_dictionary_persistence() {
let temp_dir = TempDir::new().unwrap();
{
let dict = ChunkedDictionary::new(temp_dir.path().to_path_buf(), 4).unwrap();
dict.get_or_insert("apple");
dict.get_or_insert("banana");
dict.flush().unwrap();
}
{
let dict = ChunkedDictionary::new(temp_dir.path().to_path_buf(), 4).unwrap();
assert_eq!(dict.len(), 2);
assert!(dict.get("apple").is_some());
assert!(dict.get("banana").is_some());
}
}
#[test]
fn test_chunked_dictionary_large_scale() {
let temp_dir = TempDir::new().unwrap();
let dict = ChunkedDictionary::new(temp_dir.path().to_path_buf(), 8).unwrap();
for i in 0..50_000 {
let token = format!("token_{}", i);
dict.get_or_insert(&token);
}
assert_eq!(dict.len(), 50_000);
dict.flush().unwrap();
assert!(dict.get("token_12345").is_some());
}
}