use crate::rowid::RowId;
use std::collections::HashSet;
const SA_SAMPLE_RATE: usize = 4;
const RANK_BLOCK_BITS: usize = 512;
const RANK_BLOCK_WORDS: usize = RANK_BLOCK_BITS / 64;
struct WaveletTree {
root: WtNode,
}
enum WtNode {
Leaf,
Inner {
lo: u32,
hi: u32,
bits: Vec<u64>,
len: usize,
rank_prefix: Vec<usize>,
left: Box<WtNode>,
right: Box<WtNode>,
},
}
impl WaveletTree {
fn build(symbols: &[u8]) -> Self {
Self {
root: build_node(symbols, 0, 256),
}
}
fn rank(&self, c: u8, i: usize) -> usize {
rank_node(&self.root, c as u32, i)
}
}
fn build_node(symbols: &[u8], lo: u32, hi: u32) -> WtNode {
if lo + 1 == hi {
return WtNode::Leaf; }
let mid = (lo + hi) / 2;
let mut bits = vec![0u64; symbols.len().div_ceil(64)];
let mut left_syms = Vec::new();
let mut right_syms = Vec::new();
for (i, &s) in symbols.iter().enumerate() {
if (s as u32) < mid {
left_syms.push(s);
} else {
right_syms.push(s);
bits[i / 64] |= 1u64 << (i % 64);
}
}
let rank_prefix = build_rank_prefix(&bits, symbols.len());
WtNode::Inner {
lo,
hi,
bits,
len: symbols.len(),
rank_prefix,
left: Box::new(build_node(&left_syms, lo, mid)),
right: Box::new(build_node(&right_syms, mid, hi)),
}
}
fn build_rank_prefix(bits: &[u64], len: usize) -> Vec<usize> {
let n_blocks = len.div_ceil(RANK_BLOCK_BITS) + 1;
let mut prefix = Vec::with_capacity(n_blocks);
prefix.push(0);
let mut acc = 0usize;
let n_words = bits.len();
let total_blocks = len.div_ceil(RANK_BLOCK_BITS);
for b in 0..total_blocks {
let w_start = b * RANK_BLOCK_WORDS;
let w_end = (w_start + RANK_BLOCK_WORDS).min(n_words);
for w in &bits[w_start..w_end] {
acc += w.count_ones() as usize;
}
prefix.push(acc);
}
prefix
}
fn rank1(bits: &[u64], len: usize, rank_prefix: &[usize], i: usize) -> usize {
let i = i.min(len);
let block = i / RANK_BLOCK_BITS;
let base = rank_prefix[block];
let w_start = block * RANK_BLOCK_WORDS;
let full = i / 64;
let mut ones = base;
for w in &bits[w_start..full.min(w_start + RANK_BLOCK_WORDS)] {
ones += w.count_ones() as usize;
}
if i % 64 != 0 && full < bits.len() && (full >= w_start && full < w_start + RANK_BLOCK_WORDS) {
ones += (bits[full] & ((1u64 << (i % 64)) - 1)).count_ones() as usize;
}
ones
}
fn rank_node(node: &WtNode, c: u32, i: usize) -> usize {
match node {
WtNode::Leaf => i,
WtNode::Inner {
lo,
hi,
bits,
len,
rank_prefix,
left,
right,
} => {
let mid = (lo + hi) / 2;
let ones = rank1(bits, *len, rank_prefix, i);
if c < mid {
rank_node(left, c, i - ones)
} else {
rank_node(right, c, ones)
}
}
}
}
struct Built {
doc_start: Vec<usize>,
bwt: Vec<u8>,
c: [usize; 256],
wave: WaveletTree,
sa_sample: Vec<Option<usize>>,
n: usize,
}
pub struct FmIndex {
docs: Vec<(Vec<u8>, RowId)>,
built: parking_lot::Mutex<Option<Built>>,
}
impl Default for FmIndex {
fn default() -> Self {
Self::new()
}
}
impl FmIndex {
pub fn new() -> Self {
Self {
docs: Vec::new(),
built: parking_lot::Mutex::new(None),
}
}
pub fn insert(&mut self, text: Vec<u8>, row_id: RowId) {
self.docs.push((text, row_id));
*self.built.lock() = None;
}
pub fn doc_count(&self) -> usize {
self.docs.len()
}
pub fn docs(&self) -> Vec<(Vec<u8>, RowId)> {
self.docs.clone()
}
pub fn from_docs(docs: Vec<(Vec<u8>, RowId)>) -> Self {
let mut idx = Self::new();
idx.docs = docs;
idx
}
fn ensure_built(&self) {
if self.built.lock().is_some() {
return;
}
let b = self.build_inner();
*self.built.lock() = Some(b);
}
pub fn flush_build(&self) {
self.ensure_built();
}
fn build_inner(&self) -> Built {
let used: HashSet<u8> = self
.docs
.iter()
.flat_map(|(t, _)| t.iter().copied())
.collect();
let mut reserved = (0u16..256).filter(|b| !used.contains(&(*b as u8)));
let term = reserved.next().unwrap_or(0) as u8; let sep = reserved.next().unwrap_or(1) as u8;
let mut text = Vec::new();
let mut doc_start = Vec::with_capacity(self.docs.len());
for (t, _) in &self.docs {
doc_start.push(text.len());
text.extend_from_slice(t);
text.push(sep);
}
text.push(term);
let n = text.len();
let mut sa: Vec<usize> = (0..n).collect();
sa.sort_by(|&a, &b| text[a..].cmp(&text[b..]));
let mut bwt = Vec::with_capacity(n);
for &suff in &sa {
if suff == 0 {
bwt.push(term);
} else {
bwt.push(text[suff - 1]);
}
}
let mut freq = [0usize; 256];
for &b in &text {
freq[b as usize] += 1;
}
let mut c = [0usize; 256];
let mut acc = 0;
for s in 0..256 {
c[s] = acc;
acc += freq[s];
}
let wave = WaveletTree::build(&bwt);
let sa_sample: Vec<Option<usize>> = (0..n)
.map(|i| {
if i % SA_SAMPLE_RATE == 0 {
Some(sa[i])
} else {
None
}
})
.collect();
Built {
doc_start,
bwt,
c,
wave,
sa_sample,
n,
}
}
fn backward(&self, pattern: &[u8]) -> (usize, usize) {
self.ensure_built();
let b = self.built.lock();
let b = b.as_ref().expect("fm built");
let mut lo = 0usize;
let mut hi = b.n;
for &c in pattern.iter().rev() {
let occ_lo = b.wave.rank(c, lo);
let occ_hi = b.wave.rank(c, hi);
lo = b.c[c as usize] + occ_lo;
hi = b.c[c as usize] + occ_hi;
if lo >= hi {
return (lo, lo);
}
}
(lo, hi)
}
fn locate_range(&self, lo: usize, hi: usize) -> Vec<RowId> {
self.ensure_built();
let b = self.built.lock();
let b = b.as_ref().expect("fm built");
let mut hits: HashSet<u64> = HashSet::new();
let mut out = Vec::new();
for row in lo..hi {
let tpos = Self::locate_row(b, row);
let d = match b.doc_start.partition_point(|&o| o <= tpos) {
0 => 0,
k => k - 1,
};
if let Some((_, rid)) = self.docs.get(d) {
if hits.insert(rid.0) {
out.push(*rid);
}
}
}
out
}
fn locate_row(b: &Built, row: usize) -> usize {
let mut r = row;
let mut steps = 0usize;
loop {
if let Some(Some(pos)) = b.sa_sample.get(r) {
return (*pos + steps) % b.n;
}
let c = b.bwt[r];
r = b.c[c as usize] + b.wave.rank(c, r);
steps += 1;
if steps > b.n {
return b.n; }
}
}
pub fn count(&self, pattern: &[u8]) -> usize {
if pattern.is_empty() {
return self.docs.len();
}
let (lo, hi) = self.backward(pattern);
if lo >= hi {
return 0;
}
self.locate_range(lo, hi).len()
}
pub fn locate(&self, pattern: &[u8]) -> Vec<RowId> {
if pattern.is_empty() {
return self.docs.iter().map(|(_, r)| *r).collect();
}
let (lo, hi) = self.backward(pattern);
if lo >= hi {
return Vec::new();
}
self.locate_range(lo, hi)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn substring_count_and_locate() {
let mut idx = FmIndex::new();
idx.insert(b"the quick brown fox".to_vec(), RowId(1));
idx.insert(b"the lazy dog".to_vec(), RowId(2));
idx.insert(b"fox in socks".to_vec(), RowId(3));
idx.insert(b"box of frogs".to_vec(), RowId(4));
let mut fox = idx.locate(b"fox");
fox.sort_by_key(|r| r.0);
assert_eq!(fox, vec![RowId(1), RowId(3)]);
assert_eq!(idx.count(b"the"), 2);
assert_eq!(idx.count(b"ox"), 3);
assert_eq!(idx.count(b""), 4);
assert_eq!(idx.count(b"missing"), 0);
}
#[test]
fn matches_overlap_and_repeats() {
let mut idx = FmIndex::new();
idx.insert(b"aaaa".to_vec(), RowId(9)); assert_eq!(idx.count(b"aa"), 1);
assert_eq!(idx.locate(b"aa"), vec![RowId(9)]);
}
#[test]
fn lazy_inserts_rebuild_once_per_query_batch() {
let mut idx = FmIndex::new();
for i in 0..200u64 {
idx.insert(format!("doc number {i}").into_bytes(), RowId(i));
}
assert_eq!(idx.doc_count(), 200);
assert_eq!(idx.count(b"doc number"), 200);
let expected_ones = idx
.docs()
.iter()
.filter(|(t, _)| t.windows(1).any(|w| w == b"1"))
.count();
let mut hits = idx.locate(b"1");
hits.sort_by_key(|r| r.0);
assert_eq!(hits.len(), expected_ones);
assert_eq!(idx.count(b""), 200);
}
#[test]
fn rank_is_correct_on_a_large_symbol_set() {
let n = 20_000usize;
let symbols: Vec<u8> = (0..n as u32)
.map(|i| (i.wrapping_mul(2654435761) >> 16) as u8)
.collect();
let wave = WaveletTree::build(&symbols);
for &c in &[0u8, 1, 17, 64, 128, 200, 255] {
let mut brute = 0usize;
for (i, &s) in symbols.iter().enumerate() {
assert_eq!(
wave.rank(c, i),
brute,
"rank({c}, {i}) mismatch at brute={brute}"
);
if s == c {
brute += 1;
}
}
assert_eq!(wave.rank(c, n), brute, "rank at tail");
}
}
}