use std::{ops::Range, sync::Arc};
use serde::Deserialize;
use crate::superfile::fts::{bm25, tokenize::Tokenizer};
#[derive(Debug, Clone)]
pub struct NormTable {
bytes: Vec<u8>,
lut: Box<[f32; 256]>,
}
impl NormTable {
pub(super) fn new(doc_lengths: impl Iterator<Item = u32>, n_docs: usize, avgdl: f32) -> Self {
if avgdl <= 0.0 {
return Self::empty();
}
let inv_avgdl = 1.0_f32 / avgdl;
let mut lut = Box::new([0.0_f32; 256]);
for (b, slot) in lut.iter_mut().enumerate() {
let dl = bm25::dequantize_len(b as u8) as f32;
*slot = bm25::K1 * (1.0 - bm25::B + bm25::B * dl * inv_avgdl);
}
let mut bytes = Vec::with_capacity(n_docs);
for dl in doc_lengths {
bytes.push(bm25::quantize_len(dl));
}
Self { bytes, lut }
}
#[inline(always)]
pub(super) fn get(&self, doc: u32) -> f32 {
self.lut[self.bytes[doc as usize] as usize]
}
#[cfg(test)]
fn len(&self) -> usize {
self.bytes.len()
}
pub(super) fn empty() -> Self {
Self {
bytes: Vec::new(),
lut: Box::new([0.0; 256]),
}
}
}
#[derive(Debug, Clone)]
pub struct ColumnMeta {
pub name: String,
pub doc_lengths_range: Range<usize>,
pub avgdl: f32,
pub dl_norm_k1: NormTable,
pub positions: bool,
pub tokenizer: Arc<dyn Tokenizer>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FtsColumnConfig {
pub name: String,
#[serde(default = "default_tokenizer")]
pub tokenizer: String,
#[serde(default)]
pub positions: bool,
}
pub(super) fn default_tokenizer() -> String {
"ascii_lower".to_string()
}
#[derive(Debug, Clone, Copy)]
pub struct OpenOptions {
pub verify_crc: bool,
}
impl Default for OpenOptions {
fn default() -> Self {
Self { verify_crc: true }
}
}
impl OpenOptions {
pub fn for_object_store() -> Self {
Self { verify_crc: false }
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use super::{super::test_util::*, *};
use crate::superfile::fts::{
builder::FtsBuilder, reader::FtsReader, tokenize::AsciiLowerTokenizer,
};
#[test]
fn open_with_verify_crc_off_succeeds() {
let (blob, json) = build_blob();
let r = FtsReader::open_with(blob, &json, OpenOptions { verify_crc: false })
.expect("open with crc off");
assert_eq!(r.n_docs(), 3);
assert_eq!(r.fts_columns().collect::<Vec<_>>(), vec!["body"]);
}
#[test]
fn open_with_object_store_options_matches_crc_off() {
let opts = OpenOptions::for_object_store();
assert!(!opts.verify_crc);
let (blob, json) = build_blob();
FtsReader::open_with(blob, &json, opts).expect("open object-store options");
}
#[test]
fn default_open_options_verifies_crc() {
assert!(OpenOptions::default().verify_crc);
}
#[test]
fn default_tokenizer_helper_is_ascii_lower() {
assert_eq!(default_tokenizer(), "ascii_lower");
}
#[test]
fn fts_column_config_missing_tokenizer_defaults() {
let (blob, _) = build_blob();
let json = r#"[{"name":"body"}]"#;
let r = FtsReader::open(blob, json).expect("open with terse json");
let cfg = r.fts_columns_config().next().expect("one column");
assert_eq!(cfg.name, "body");
}
#[test]
fn fts_columns_config_exposes_per_column_metadata() {
let (blob, json) = build_blob();
let r = FtsReader::open(blob, &json).expect("open");
let cols: Vec<&ColumnMeta> = r.fts_columns_config().collect();
assert_eq!(cols.len(), 1);
assert_eq!(cols[0].name, "body");
assert!(cols[0].avgdl > 0.0);
assert_eq!(cols[0].dl_norm_k1.len(), 3);
}
#[test]
fn norm_table_footprint_is_one_byte_per_doc() {
const N: u32 = 5_000;
let tok = Arc::new(AsciiLowerTokenizer);
let mut b = FtsBuilder::new(tok);
b.register_column("body".into(), false)
.expect("register column");
for d in 0..N {
let words = (d % 40) + 1;
let text: String = (0..words).map(|w| format!("t{}x{w} ", d % 97)).collect();
b.add_doc(0, d, text.trim()).expect("add doc");
}
let bytes = b.finish().expect("finish");
let json = r#"[{"name":"body","tokenizer":"ascii_lower"}]"#;
let r = FtsReader::open(Bytes::from(bytes), json).expect("open");
let nt = &r.columns[0].dl_norm_k1;
let per_doc = nt.bytes.capacity(); let lut = std::mem::size_of_val(&*nt.lut); let m2_bytes = per_doc + lut;
let f32_baseline = N as usize * std::mem::size_of::<f32>();
assert_eq!(nt.bytes.len(), N as usize, "one bucket byte per doc");
assert_eq!(nt.lut.len(), 256, "fixed 256-entry decode table");
assert!(
m2_bytes < f32_baseline,
"norm table {m2_bytes} B not smaller than f32 baseline {f32_baseline} B"
);
assert_eq!(
per_doc * 4,
f32_baseline,
"per-doc term is exactly 4× smaller"
);
}
}