use std::collections::BTreeMap;
use std::sync::Arc;
use ahash::AHashMap;
use fst::Map as FstMap;
use crate::error::{LaurusError, Result};
use crate::storage::structured::{StructReader, StructWriter};
use crate::storage::{StorageInput, StorageOutput};
mod block_max_data;
mod block_reader;
mod builder;
mod front_coding;
mod term_info_block;
const MAGIC_LTDD: u32 = 0x4C544444;
const LEGACY_MAGIC_STDC: u32 = 0x53544443;
const LEGACY_MAGIC_HTDC: u32 = 0x48544443;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BlockMax {
pub last_doc_id: u64,
pub max_factor: f32,
}
pub const BLOCK_SIZE: usize = 128;
#[derive(Debug, Clone, PartialEq)]
pub struct TermInfo {
pub posting_offset: u64,
pub posting_length: u64,
pub doc_frequency: u64,
pub total_frequency: u64,
pub max_score_factor: f32,
pub block_max: Vec<BlockMax>,
}
impl TermInfo {
pub fn new(
posting_offset: u64,
posting_length: u64,
doc_frequency: u64,
total_frequency: u64,
) -> Self {
TermInfo {
posting_offset,
posting_length,
doc_frequency,
total_frequency,
max_score_factor: 0.0,
block_max: Vec::new(),
}
}
pub fn with_max_score_factor(
posting_offset: u64,
posting_length: u64,
doc_frequency: u64,
total_frequency: u64,
max_score_factor: f32,
) -> Self {
TermInfo {
posting_offset,
posting_length,
doc_frequency,
total_frequency,
max_score_factor,
block_max: Vec::new(),
}
}
pub fn with_block_max(
posting_offset: u64,
posting_length: u64,
doc_frequency: u64,
total_frequency: u64,
max_score_factor: f32,
block_max: Vec<BlockMax>,
) -> Self {
TermInfo {
posting_offset,
posting_length,
doc_frequency,
total_frequency,
max_score_factor,
block_max,
}
}
}
#[derive(Clone, Debug)]
pub struct BlockTermDictionary {
fst: Arc<FstMap<Vec<u8>>>,
block_section: Arc<[u8]>,
map: AHashMap<String, u32>,
sorted_terms: Vec<String>,
term_infos: Arc<[TermInfo]>,
total_term_count: u64,
block_count: u32,
posting_format_version: u32,
}
impl BlockTermDictionary {
pub fn get(&self, term: &str) -> Option<&TermInfo> {
let ordinal = *self.map.get(term)? as usize;
Some(&self.term_infos[ordinal])
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &TermInfo)> + '_ {
self.sorted_terms
.iter()
.zip(self.term_infos.iter())
.map(|(term, info)| (term.as_str(), info))
}
pub(crate) fn seek_index(&self, target: &str) -> usize {
self.sorted_terms
.partition_point(|term| term.as_str() < target)
}
pub(crate) fn entry_at(&self, idx: usize) -> Option<(&str, &TermInfo)> {
let term = self.sorted_terms.get(idx)?;
Some((term.as_str(), &self.term_infos[idx]))
}
pub fn find_prefix(&self, prefix: &str) -> Vec<(&str, &TermInfo)> {
let start = self
.sorted_terms
.binary_search_by(|probe| probe.as_str().cmp(prefix))
.unwrap_or_else(|insert_at| insert_at);
let mut result = Vec::new();
for i in start..self.sorted_terms.len() {
let term = &self.sorted_terms[i];
if term.starts_with(prefix) {
result.push((term.as_str(), &self.term_infos[i]));
} else {
break;
}
}
result
}
pub fn find_range(&self, start: &str, end: &str) -> Vec<(&str, &TermInfo)> {
if start >= end {
return Vec::new();
}
let start_idx = self
.sorted_terms
.binary_search_by(|probe| probe.as_str().cmp(start))
.unwrap_or_else(|insert_at| insert_at);
let end_idx = self
.sorted_terms
.binary_search_by(|probe| probe.as_str().cmp(end))
.unwrap_or_else(|insert_at| insert_at);
let mut result = Vec::with_capacity(end_idx.saturating_sub(start_idx));
for i in start_idx..end_idx.min(self.sorted_terms.len()) {
result.push((self.sorted_terms[i].as_str(), &self.term_infos[i]));
}
result
}
pub fn len(&self) -> u64 {
self.total_term_count
}
pub fn is_empty(&self) -> bool {
self.total_term_count == 0
}
pub fn block_count(&self) -> u32 {
self.block_count
}
pub fn read_from_storage<R: StorageInput>(reader: &mut StructReader<R>) -> Result<Self> {
let magic = reader.read_u32()?;
match magic {
MAGIC_LTDD => {} LEGACY_MAGIC_STDC | LEGACY_MAGIC_HTDC => {
return Err(LaurusError::index(
"Unsupported legacy term dictionary format. Rebuild required.",
));
}
_ => {
return Err(LaurusError::index(format!(
"Invalid term dictionary magic: 0x{magic:08X}"
)));
}
}
let version = reader.read_u32()?;
if version != 1 && version != 2 {
return Err(LaurusError::index(format!(
"Unsupported BlockTermDictionary version: {version}"
)));
}
let fst_bytes_len = reader.read_u32()? as usize;
let fst_bytes = reader.read_raw(fst_bytes_len)?;
let fst = FstMap::new(fst_bytes)
.map_err(|e| LaurusError::index(format!("FST parse error: {e}")))?;
let block_section_len = reader.read_u32()? as usize;
let block_section_vec = reader.read_raw(block_section_len)?;
let total_term_count = reader.read_u64()?;
let block_count = reader.read_u32()?;
let _reserved = reader.read_u32()?;
let block_section: Arc<[u8]> = Arc::from(block_section_vec.into_boxed_slice());
let (map, sorted_terms, term_infos) =
populate_in_memory_layer(&block_section, block_count, total_term_count);
Ok(BlockTermDictionary {
fst: Arc::new(fst),
block_section,
map,
sorted_terms,
term_infos,
total_term_count,
block_count,
posting_format_version: version,
})
}
pub fn posting_format_version(&self) -> u32 {
self.posting_format_version
}
pub fn write_to_storage<W: StorageOutput>(&self, writer: &mut StructWriter<W>) -> Result<()> {
writer.write_u32(MAGIC_LTDD)?;
writer.write_u32(2)?;
let fst_bytes = self.fst.as_fst().as_inner();
writer.write_u32(
u32::try_from(fst_bytes.len())
.map_err(|_| LaurusError::index("FST bytes length exceeds u32::MAX"))?,
)?;
writer.write_raw(fst_bytes)?;
writer.write_u32(
u32::try_from(self.block_section.len())
.map_err(|_| LaurusError::index("BlockSection length exceeds u32::MAX"))?,
)?;
writer.write_raw(&self.block_section)?;
writer.write_u64(self.total_term_count)?;
writer.write_u32(self.block_count)?;
writer.write_u32(0)?;
Ok(())
}
pub fn stats(&self) -> DictionaryStats {
let term_count = self.total_term_count as usize;
let mut total_term_length = 0usize;
let mut total_doc_frequency = 0u64;
let mut total_term_frequency = 0u64;
for (term, info) in self.iter() {
total_term_length += term.len();
total_doc_frequency += info.doc_frequency;
total_term_frequency += info.total_frequency;
}
let avg_term_length = if term_count > 0 {
total_term_length as f64 / term_count as f64
} else {
0.0
};
let memory_size = self.fst.as_fst().as_inner().len() + self.block_section.len();
DictionaryStats {
term_count,
memory_size,
avg_term_length,
total_doc_frequency,
total_term_frequency,
}
}
}
pub struct TermDictionaryBuilder {
terms: BTreeMap<String, TermInfo>,
}
impl TermDictionaryBuilder {
pub fn new() -> Self {
TermDictionaryBuilder {
terms: BTreeMap::new(),
}
}
pub fn add_term(&mut self, term: String, info: TermInfo) {
self.terms.insert(term, info);
}
pub fn build(self) -> Result<BlockTermDictionary> {
let total_term_count = self.terms.len() as u64;
if self.terms.is_empty() {
let fst_builder = fst::MapBuilder::memory();
let bytes = fst_builder
.into_inner()
.map_err(|e| LaurusError::index(format!("FST finish error: {e}")))?;
let fst = FstMap::new(bytes)
.map_err(|e| LaurusError::index(format!("FST construct error: {e}")))?;
return Ok(BlockTermDictionary {
fst: Arc::new(fst),
block_section: Arc::from(Vec::<u8>::new().into_boxed_slice()),
map: AHashMap::new(),
sorted_terms: Vec::new(),
term_infos: Arc::from(Vec::<TermInfo>::new().into_boxed_slice()),
total_term_count: 0,
block_count: 0,
posting_format_version: 2,
});
}
let entries: Vec<(String, TermInfo)> = self.terms.into_iter().collect();
let mut block_section: Vec<u8> = Vec::new();
let mut fst_builder = fst::MapBuilder::memory();
let mut block_count: u32 = 0;
let mut map = AHashMap::with_capacity(entries.len());
let mut sorted_terms: Vec<String> = Vec::with_capacity(entries.len());
let mut term_infos: Vec<TermInfo> = Vec::with_capacity(entries.len());
let mut next_ordinal: u32 = 0;
for chunk in entries.chunks(term_info_block::BLOCK_TERM_COUNT) {
let block_offset = block_section.len() as u64;
let term_byte_refs: Vec<&[u8]> = chunk.iter().map(|(t, _)| t.as_bytes()).collect();
let fixed_infos: Vec<term_info_block::FixedTermInfo> = chunk
.iter()
.map(|(_, info)| term_info_block::FixedTermInfo {
posting_offset: info.posting_offset,
posting_length: info.posting_length,
doc_frequency: info.doc_frequency,
total_frequency: info.total_frequency,
max_score_factor: info.max_score_factor,
})
.collect();
let block_max_per_term: Vec<Vec<BlockMax>> = chunk
.iter()
.map(|(_, info)| info.block_max.clone())
.collect();
builder::encode_block_into(
&mut block_section,
&term_byte_refs,
&fixed_infos,
&block_max_per_term,
);
for (term, info) in chunk {
map.insert(term.clone(), next_ordinal);
sorted_terms.push(term.clone());
term_infos.push(info.clone());
next_ordinal += 1;
}
let last_term = chunk.last().expect("chunk non-empty").0.as_bytes();
fst_builder
.insert(last_term, block_offset)
.map_err(|e| LaurusError::index(format!("FST insert error: {e}")))?;
block_count += 1;
}
let fst_bytes = fst_builder
.into_inner()
.map_err(|e| LaurusError::index(format!("FST finish error: {e}")))?;
let fst = FstMap::new(fst_bytes)
.map_err(|e| LaurusError::index(format!("FST construct error: {e}")))?;
Ok(BlockTermDictionary {
fst: Arc::new(fst),
block_section: Arc::from(block_section.into_boxed_slice()),
map,
sorted_terms,
term_infos: Arc::from(term_infos.into_boxed_slice()),
total_term_count,
block_count,
posting_format_version: 2,
})
}
pub fn len(&self) -> usize {
self.terms.len()
}
pub fn is_empty(&self) -> bool {
self.terms.is_empty()
}
}
impl Default for TermDictionaryBuilder {
fn default() -> Self {
Self::new()
}
}
fn populate_in_memory_layer(
block_section: &[u8],
block_count: u32,
total_term_count: u64,
) -> (AHashMap<String, u32>, Vec<String>, Arc<[TermInfo]>) {
let cap = total_term_count as usize;
let mut map = AHashMap::with_capacity(cap);
let mut sorted_terms: Vec<String> = Vec::with_capacity(cap);
let mut term_infos: Vec<TermInfo> = Vec::with_capacity(cap);
let iter = block_reader::BlockSectionIter::new(block_section, block_count);
for (ordinal, (term, info)) in iter.enumerate() {
map.insert(term.clone(), ordinal as u32);
sorted_terms.push(term);
term_infos.push(info);
}
(map, sorted_terms, Arc::from(term_infos.into_boxed_slice()))
}
#[derive(Debug, Clone)]
pub struct DictionaryStats {
pub term_count: usize,
pub memory_size: usize,
pub avg_term_length: f64,
pub total_doc_frequency: u64,
pub total_term_frequency: u64,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::Storage;
use crate::storage::memory::MemoryStorage;
use crate::storage::memory::MemoryStorageConfig;
use std::sync::Arc;
fn create_test_term_info(offset: u64) -> TermInfo {
TermInfo::new(offset, 100, 5, 20)
}
#[test]
fn test_dictionary_builder_basic() {
let mut builder = TermDictionaryBuilder::new();
assert!(builder.is_empty());
builder.add_term("test".to_string(), create_test_term_info(0));
assert_eq!(builder.len(), 1);
let dict = builder.build().unwrap();
assert_eq!(dict.len(), 1);
assert!(dict.get("test").is_some());
}
fn make_test_term_info_with_block_max(offset: u64, block_max: Vec<BlockMax>) -> TermInfo {
TermInfo {
posting_offset: offset,
posting_length: 100,
doc_frequency: 5,
total_frequency: 20,
max_score_factor: 1.0,
block_max,
}
}
#[test]
fn block_dict_empty_builder_yields_empty_dict() {
let builder = TermDictionaryBuilder::new();
let dict = builder.build().unwrap();
assert!(dict.is_empty());
assert_eq!(dict.len(), 0);
assert_eq!(dict.block_count(), 0);
assert!(dict.get("anything").is_none());
assert_eq!(dict.iter().count(), 0);
}
#[test]
fn block_dict_single_term_round_trip() {
let mut builder = TermDictionaryBuilder::new();
builder.add_term("hello".to_string(), create_test_term_info(42));
let dict = builder.build().unwrap();
assert_eq!(dict.len(), 1);
assert_eq!(dict.block_count(), 1);
let info = dict.get("hello").unwrap();
assert_eq!(info.posting_offset, 42);
assert!(dict.get("missing").is_none());
}
#[test]
fn block_dict_get_within_single_block() {
let mut builder = TermDictionaryBuilder::new();
for (i, term) in ["apple", "banana", "cherry", "date"].iter().enumerate() {
builder.add_term(term.to_string(), create_test_term_info(i as u64 * 100));
}
let dict = builder.build().unwrap();
assert_eq!(dict.len(), 4);
assert_eq!(dict.block_count(), 1);
for (i, term) in ["apple", "banana", "cherry", "date"].iter().enumerate() {
assert_eq!(dict.get(term).unwrap().posting_offset, i as u64 * 100);
}
assert!(dict.get("aardvark").is_none());
assert!(dict.get("blueberry").is_none());
assert!(dict.get("zulu").is_none());
}
#[test]
fn block_dict_iter_yields_in_sorted_order() {
let mut builder = TermDictionaryBuilder::new();
for term in ["zulu", "alpha", "mike", "bravo"] {
builder.add_term(term.to_string(), create_test_term_info(0));
}
let dict = builder.build().unwrap();
let collected: Vec<String> = dict.iter().map(|(t, _)| t.to_string()).collect();
assert_eq!(collected, vec!["alpha", "bravo", "mike", "zulu"]);
}
#[test]
fn block_dict_seek_index_and_entry_at() {
let mut builder = TermDictionaryBuilder::new();
for term in ["a:x", "a:y", "b:m", "b:n", "c:z"] {
builder.add_term(term.to_string(), create_test_term_info(1));
}
let dict = builder.build().unwrap();
assert_eq!(dict.seek_index("a:x"), 0);
assert_eq!(dict.seek_index("a:xx"), 1, "between a:x and a:y");
assert_eq!(dict.seek_index("b:"), 2, "field-prefix start of b");
assert_eq!(dict.seek_index("d:"), 5, "past every term");
assert_eq!(dict.entry_at(2).map(|(t, _)| t), Some("b:m"));
assert_eq!(dict.entry_at(4).map(|(t, _)| t), Some("c:z"));
assert!(dict.entry_at(5).is_none(), "past-the-end yields None");
}
#[test]
fn block_dict_multi_block_get_hit_and_miss() {
let mut builder = TermDictionaryBuilder::new();
for i in 0..300 {
builder.add_term(format!("term{i:04}"), create_test_term_info(i as u64));
}
let dict = builder.build().unwrap();
assert_eq!(dict.len(), 300);
assert_eq!(dict.block_count(), 3);
for i in [0, 1, 100, 127, 128, 129, 200, 299] {
let key = format!("term{i:04}");
assert_eq!(
dict.get(&key).unwrap().posting_offset,
i as u64,
"miss on hit probe {key}"
);
}
assert!(dict.get("term0300").is_none());
assert!(dict.get("term9999").is_none());
assert!(dict.get("aaa").is_none());
assert!(dict.get("zzz").is_none());
}
#[test]
fn block_dict_iter_walks_multi_block() {
let mut builder = TermDictionaryBuilder::new();
for i in 0..200 {
builder.add_term(format!("term{i:04}"), create_test_term_info(i as u64));
}
let dict = builder.build().unwrap();
let collected: Vec<(String, u64)> = dict
.iter()
.map(|(t, info)| (t.to_string(), info.posting_offset))
.collect();
assert_eq!(collected.len(), 200);
for (i, (term, offset)) in collected.iter().enumerate() {
assert_eq!(term, &format!("term{i:04}"));
assert_eq!(*offset, i as u64);
}
}
#[test]
fn block_dict_find_prefix_within_block() {
let mut builder = TermDictionaryBuilder::new();
for term in ["alpha", "apple", "apricot", "axis", "banana"] {
builder.add_term(term.to_string(), create_test_term_info(0));
}
let dict = builder.build().unwrap();
let ap = dict.find_prefix("ap");
let ap_terms: Vec<&str> = ap.iter().map(|(t, _)| *t).collect();
assert_eq!(ap_terms, vec!["apple", "apricot"]);
let all = dict.find_prefix("");
assert_eq!(all.len(), 5);
let zzz = dict.find_prefix("zzz");
assert!(zzz.is_empty());
}
#[test]
fn block_dict_find_prefix_across_block_boundary() {
let mut builder = TermDictionaryBuilder::new();
for i in 0..200 {
builder.add_term(format!("term{i:04}"), create_test_term_info(i as u64));
}
let dict = builder.build().unwrap();
let matches = dict.find_prefix("term01");
assert_eq!(matches.len(), 100);
assert_eq!(matches[0].0, "term0100");
assert_eq!(matches[99].0, "term0199");
}
#[test]
fn block_dict_find_range_basic() {
let mut builder = TermDictionaryBuilder::new();
for term in ["apple", "banana", "cherry", "date", "fig"] {
builder.add_term(term.to_string(), create_test_term_info(0));
}
let dict = builder.build().unwrap();
let r = dict.find_range("banana", "fig");
let r_terms: Vec<&str> = r.iter().map(|(t, _)| *t).collect();
assert_eq!(r_terms, vec!["banana", "cherry", "date"]);
assert!(dict.find_range("date", "banana").is_empty());
assert!(dict.find_range("date", "date").is_empty());
}
#[test]
fn block_dict_term_info_with_block_max_round_trip() {
let mut builder = TermDictionaryBuilder::new();
let bm = vec![
BlockMax {
last_doc_id: 5,
max_factor: 0.5,
},
BlockMax {
last_doc_id: 10,
max_factor: 1.5,
},
];
builder.add_term(
"hello".to_string(),
make_test_term_info_with_block_max(99, bm.clone()),
);
let dict = builder.build().unwrap();
let info = dict.get("hello").unwrap();
assert_eq!(info.posting_offset, 99);
assert_eq!(info.block_max.len(), 2);
assert_eq!(info.block_max[0].last_doc_id, 5);
assert_eq!(info.block_max[1].last_doc_id, 10);
}
#[test]
fn block_dict_clone_shares_storage() {
let mut builder = TermDictionaryBuilder::new();
for i in 0..50 {
builder.add_term(format!("term{i:03}"), create_test_term_info(i as u64));
}
let dict = builder.build().unwrap();
let dict2 = dict.clone();
assert_eq!(dict.len(), dict2.len());
assert_eq!(dict.get("term025").unwrap(), dict2.get("term025").unwrap());
assert!(Arc::strong_count(&dict.fst) >= 2);
}
#[test]
fn block_dict_stats() {
let mut builder = TermDictionaryBuilder::new();
builder.add_term("short".to_string(), TermInfo::new(0, 50, 1, 1));
builder.add_term("longer_term".to_string(), TermInfo::new(50, 100, 5, 10));
builder.add_term(
"longest_term_here".to_string(),
TermInfo::new(150, 200, 3, 8),
);
let dict = builder.build().unwrap();
let stats = dict.stats();
assert_eq!(stats.term_count, 3);
assert!(stats.avg_term_length > 0.0);
assert_eq!(stats.total_doc_frequency, 9); assert_eq!(stats.total_term_frequency, 19); assert!(stats.memory_size > 0);
}
#[test]
fn block_dict_round_trip_via_storage() {
let storage = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
let mut builder = TermDictionaryBuilder::new();
for i in 0..200u64 {
let mut info = create_test_term_info(i * 16);
if i.is_multiple_of(5) {
info.block_max = vec![BlockMax {
last_doc_id: i,
max_factor: 1.0 + (i as f32) * 0.01,
}];
}
builder.add_term(format!("term{i:04}"), info);
}
let original_dict = builder.build().unwrap();
{
let output = storage.create_output("test_block_dict.bin").unwrap();
let mut writer = StructWriter::new(output);
original_dict.write_to_storage(&mut writer).unwrap();
writer.close().unwrap();
}
let loaded_dict = {
let input = storage.open_input("test_block_dict.bin").unwrap();
let mut reader = StructReader::new(input).unwrap();
BlockTermDictionary::read_from_storage(&mut reader).unwrap()
};
assert_eq!(loaded_dict.len(), original_dict.len());
assert_eq!(loaded_dict.block_count(), original_dict.block_count());
for i in 0..200 {
let key = format!("term{i:04}");
let orig = original_dict.get(&key).unwrap();
let loaded = loaded_dict.get(&key).unwrap();
assert_eq!(orig, loaded, "mismatch for {key}");
}
let orig_iter: Vec<_> = original_dict.iter().collect();
let loaded_iter: Vec<_> = loaded_dict.iter().collect();
assert_eq!(orig_iter.len(), loaded_iter.len());
for (a, b) in orig_iter.iter().zip(loaded_iter.iter()) {
assert_eq!(a, b);
}
}
#[test]
fn block_dict_read_rejects_legacy_stdc_magic() {
let storage = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
{
let output = storage.create_output("legacy_stdc.bin").unwrap();
let mut writer = StructWriter::new(output);
writer.write_u32(LEGACY_MAGIC_STDC).unwrap();
writer.write_u32(3).unwrap(); writer.close().unwrap();
}
let input = storage.open_input("legacy_stdc.bin").unwrap();
let mut reader = StructReader::new(input).unwrap();
let err = BlockTermDictionary::read_from_storage(&mut reader);
assert!(err.is_err());
let msg = format!("{}", err.unwrap_err());
assert!(
msg.contains("Unsupported legacy term dictionary format"),
"unexpected error message: {msg}"
);
}
#[test]
fn block_dict_read_rejects_legacy_htdc_magic() {
let storage = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
{
let output = storage.create_output("legacy_htdc.bin").unwrap();
let mut writer = StructWriter::new(output);
writer.write_u32(LEGACY_MAGIC_HTDC).unwrap();
writer.write_u32(3).unwrap();
writer.close().unwrap();
}
let input = storage.open_input("legacy_htdc.bin").unwrap();
let mut reader = StructReader::new(input).unwrap();
let err = BlockTermDictionary::read_from_storage(&mut reader);
assert!(err.is_err());
let msg = format!("{}", err.unwrap_err());
assert!(
msg.contains("Unsupported legacy term dictionary format"),
"unexpected error message: {msg}"
);
}
#[test]
fn block_dict_read_rejects_unknown_magic() {
let storage = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
{
let output = storage.create_output("garbage.bin").unwrap();
let mut writer = StructWriter::new(output);
writer.write_u32(0xDEADBEEF).unwrap(); writer.write_u32(0).unwrap();
writer.close().unwrap();
}
let input = storage.open_input("garbage.bin").unwrap();
let mut reader = StructReader::new(input).unwrap();
let err = BlockTermDictionary::read_from_storage(&mut reader);
let msg = format!("{}", err.unwrap_err());
assert!(msg.contains("Invalid term dictionary magic"));
}
#[test]
fn block_dict_round_trip_empty() {
let storage = Arc::new(MemoryStorage::new(MemoryStorageConfig::default()));
let original_dict = TermDictionaryBuilder::new().build().unwrap();
{
let output = storage.create_output("empty.bin").unwrap();
let mut writer = StructWriter::new(output);
original_dict.write_to_storage(&mut writer).unwrap();
writer.close().unwrap();
}
let input = storage.open_input("empty.bin").unwrap();
let mut reader = StructReader::new(input).unwrap();
let loaded = BlockTermDictionary::read_from_storage(&mut reader).unwrap();
assert!(loaded.is_empty());
assert_eq!(loaded.block_count(), 0);
}
#[test]
fn block_dict_exact_block_boundary_term_count() {
let mut builder = TermDictionaryBuilder::new();
for i in 0..128 {
builder.add_term(format!("term{i:03}"), create_test_term_info(i as u64));
}
let dict = builder.build().unwrap();
assert_eq!(dict.len(), 128);
assert_eq!(dict.block_count(), 1);
let mut builder2 = TermDictionaryBuilder::new();
for i in 0..129 {
builder2.add_term(format!("term{i:03}"), create_test_term_info(i as u64));
}
let dict2 = builder2.build().unwrap();
assert_eq!(dict2.len(), 129);
assert_eq!(dict2.block_count(), 2);
assert_eq!(dict2.get("term128").unwrap().posting_offset, 128);
}
}