use crate::error::Result;
#[derive(Debug, Clone)]
pub struct TermStats {
pub term: String,
pub doc_freq: u64,
pub total_term_freq: u64,
}
pub trait TermsEnum: Send + Sync {
fn next(&mut self) -> Result<Option<TermStats>>;
fn seek(&mut self, target: &str) -> Result<bool>;
fn seek_exact(&mut self, term: &str) -> Result<bool>;
fn current(&self) -> Option<&TermStats>;
fn term_stats(&self) -> Option<TermStats> {
self.current().cloned()
}
}
pub trait Terms: Send + Sync {
fn iterator(&self) -> Result<Box<dyn TermsEnum>>;
fn size(&self) -> Option<u64>;
fn sum_doc_freq(&self) -> Option<u64>;
fn sum_total_term_freq(&self) -> Option<u64>;
fn has_freqs(&self) -> bool {
true
}
fn has_positions(&self) -> bool {
false
}
fn has_offsets(&self) -> bool {
false
}
fn has_payloads(&self) -> bool {
false
}
}
pub trait TermDictionaryAccess {
fn terms(&self, field: &str) -> Result<Option<Box<dyn Terms>>>;
fn term_exists(&self, field: &str, term: &str) -> Result<bool> {
if let Some(terms) = self.terms(field)? {
let mut iter = terms.iterator()?;
iter.seek_exact(term)
} else {
Ok(false)
}
}
}
impl TermsEnum for Box<dyn TermsEnum> {
fn next(&mut self) -> Result<Option<TermStats>> {
(**self).next()
}
fn seek(&mut self, target: &str) -> Result<bool> {
(**self).seek(target)
}
fn seek_exact(&mut self, term: &str) -> Result<bool> {
(**self).seek_exact(term)
}
fn current(&self) -> Option<&TermStats> {
(**self).current()
}
}
use std::sync::Arc;
use crate::lexical::index::structures::dictionary::BlockTermDictionary;
pub struct InvertedIndexTermsEnum {
dict: Arc<BlockTermDictionary>,
prefix: String,
position: usize,
current: Option<TermStats>,
}
impl InvertedIndexTermsEnum {
pub fn new(field: &str, dict: &Arc<BlockTermDictionary>) -> Self {
let prefix = format!("{}:", field);
let position = dict.seek_index(&prefix);
InvertedIndexTermsEnum {
dict: Arc::clone(dict),
prefix,
position,
current: None,
}
}
fn stats_at(&self, idx: usize) -> Option<TermStats> {
let (key, info) = self.dict.entry_at(idx)?;
let term = key.strip_prefix(&self.prefix)?;
Some(TermStats {
term: term.to_string(),
doc_freq: info.doc_frequency,
total_term_freq: info.total_frequency,
})
}
}
impl TermsEnum for InvertedIndexTermsEnum {
fn next(&mut self) -> Result<Option<TermStats>> {
match self.stats_at(self.position) {
Some(stats) => {
self.current = Some(stats.clone());
self.position += 1;
Ok(Some(stats))
}
None => {
self.current = None;
Ok(None)
}
}
}
fn seek(&mut self, target: &str) -> Result<bool> {
let full_key = format!("{}{}", self.prefix, target);
self.position = self.dict.seek_index(&full_key);
match self.stats_at(self.position) {
Some(stats) => {
let exact = stats.term == target;
self.current = Some(stats);
Ok(exact)
}
None => {
self.current = None;
Ok(false)
}
}
}
fn seek_exact(&mut self, term: &str) -> Result<bool> {
let full_key = format!("{}{}", self.prefix, term);
if self.dict.get(&full_key).is_none() {
self.current = None;
return Ok(false);
}
self.position = self.dict.seek_index(&full_key);
self.current = self.stats_at(self.position);
Ok(true)
}
fn current(&self) -> Option<&TermStats> {
self.current.as_ref()
}
}
pub struct InvertedIndexTerms {
field: String,
dict: Arc<BlockTermDictionary>,
size: Option<u64>,
sum_doc_freq: Option<u64>,
sum_total_term_freq: Option<u64>,
}
impl InvertedIndexTerms {
pub fn new(field: &str, dict: Arc<BlockTermDictionary>) -> Self {
let field_prefix = format!("{}:", field);
let mut size = 0u64;
let mut sum_doc_freq = 0u64;
let mut sum_total_term_freq = 0u64;
let mut idx = dict.seek_index(&field_prefix);
while let Some((key, info)) = dict.entry_at(idx) {
if !key.starts_with(&field_prefix) {
break;
}
size += 1;
sum_doc_freq += info.doc_frequency;
sum_total_term_freq += info.total_frequency;
idx += 1;
}
InvertedIndexTerms {
field: field.to_string(),
dict,
size: Some(size),
sum_doc_freq: Some(sum_doc_freq),
sum_total_term_freq: Some(sum_total_term_freq),
}
}
}
impl Terms for InvertedIndexTerms {
fn iterator(&self) -> Result<Box<dyn TermsEnum>> {
Ok(Box::new(InvertedIndexTermsEnum::new(
&self.field,
&self.dict,
)))
}
fn size(&self) -> Option<u64> {
self.size
}
fn sum_doc_freq(&self) -> Option<u64> {
self.sum_doc_freq
}
fn sum_total_term_freq(&self) -> Option<u64> {
self.sum_total_term_freq
}
}
pub struct MergedInvertedIndexTerms {
field: String,
dicts: Vec<Arc<BlockTermDictionary>>,
size: u64,
sum_doc_freq: u64,
sum_total_term_freq: u64,
}
impl MergedInvertedIndexTerms {
pub fn new(field: &str, dicts: &[Arc<BlockTermDictionary>]) -> Self {
let mut stats_cursor = MergedTermsEnum::new(field, dicts);
let mut size = 0u64;
let mut sum_doc_freq = 0u64;
let mut sum_total_term_freq = 0u64;
while let Ok(Some(stats)) = stats_cursor.next() {
size += 1;
sum_doc_freq += stats.doc_freq;
sum_total_term_freq += stats.total_term_freq;
}
MergedInvertedIndexTerms {
field: field.to_string(),
dicts: dicts.to_vec(),
size,
sum_doc_freq,
sum_total_term_freq,
}
}
}
impl Terms for MergedInvertedIndexTerms {
fn iterator(&self) -> Result<Box<dyn TermsEnum>> {
Ok(Box::new(MergedTermsEnum::new(&self.field, &self.dicts)))
}
fn size(&self) -> Option<u64> {
Some(self.size)
}
fn sum_doc_freq(&self) -> Option<u64> {
Some(self.sum_doc_freq)
}
fn sum_total_term_freq(&self) -> Option<u64> {
Some(self.sum_total_term_freq)
}
}
struct MergedTermsEnum {
children: Vec<InvertedIndexTermsEnum>,
heads: Vec<Option<TermStats>>,
current: Option<TermStats>,
}
impl MergedTermsEnum {
fn new(field: &str, dicts: &[Arc<BlockTermDictionary>]) -> Self {
let mut children: Vec<InvertedIndexTermsEnum> = dicts
.iter()
.map(|dict| InvertedIndexTermsEnum::new(field, dict))
.collect();
let heads = children
.iter_mut()
.map(|child| child.next().unwrap_or(None))
.collect();
MergedTermsEnum {
children,
heads,
current: None,
}
}
fn peek_merged(&self) -> Option<TermStats> {
let min_term = self
.heads
.iter()
.flatten()
.map(|stats| stats.term.as_str())
.min()?;
let mut merged = TermStats {
term: min_term.to_string(),
doc_freq: 0,
total_term_freq: 0,
};
for stats in self.heads.iter().flatten() {
if stats.term == merged.term {
merged.doc_freq += stats.doc_freq;
merged.total_term_freq += stats.total_term_freq;
}
}
Some(merged)
}
fn advance_heads_matching(&mut self, term: &str) -> Result<()> {
for (child, head) in self.children.iter_mut().zip(self.heads.iter_mut()) {
if head.as_ref().is_some_and(|stats| stats.term == term) {
*head = child.next()?;
}
}
Ok(())
}
}
impl TermsEnum for MergedTermsEnum {
fn next(&mut self) -> Result<Option<TermStats>> {
match self.peek_merged() {
Some(stats) => {
self.advance_heads_matching(&stats.term)?;
self.current = Some(stats.clone());
Ok(Some(stats))
}
None => {
self.current = None;
Ok(None)
}
}
}
fn seek(&mut self, target: &str) -> Result<bool> {
for (child, head) in self.children.iter_mut().zip(self.heads.iter_mut()) {
child.seek(target)?;
*head = child.next()?;
}
self.current = self.peek_merged();
Ok(self
.current
.as_ref()
.is_some_and(|stats| stats.term == target))
}
fn seek_exact(&mut self, term: &str) -> Result<bool> {
let found = self.seek(term)?;
if !found {
self.current = None;
}
Ok(found)
}
fn current(&self) -> Option<&TermStats> {
self.current.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexical::index::structures::dictionary::{TermDictionaryBuilder, TermInfo};
fn mixed_field_dict() -> Arc<BlockTermDictionary> {
let mut builder = TermDictionaryBuilder::new();
for (key, df, ttf) in [
("alpha:apple", 2, 5),
("alpha:apricot", 1, 1),
("body:program", 3, 7),
("body:programmer", 1, 2),
("body:programming", 4, 9),
("body:python", 2, 3),
("title:zebra", 1, 1),
] {
builder.add_term(key.to_string(), TermInfo::new(0, 0, df, ttf));
}
Arc::new(builder.build().unwrap())
}
#[test]
fn lazy_enum_isolates_field_boundaries() {
let dict = mixed_field_dict();
let mut terms_enum = InvertedIndexTermsEnum::new("body", &dict);
let mut drained = Vec::new();
while let Some(stats) = terms_enum.next().unwrap() {
drained.push((stats.term, stats.doc_freq, stats.total_term_freq));
}
assert_eq!(
drained,
vec![
("program".to_string(), 3, 7),
("programmer".to_string(), 1, 2),
("programming".to_string(), 4, 9),
("python".to_string(), 2, 3),
]
);
let mut empty = InvertedIndexTermsEnum::new("missing", &dict);
assert!(empty.next().unwrap().is_none());
}
#[test]
fn lazy_enum_matches_find_prefix() {
let dict = mixed_field_dict();
let expected: Vec<String> = dict
.find_prefix("body:")
.into_iter()
.map(|(key, _)| key.strip_prefix("body:").unwrap().to_string())
.collect();
let mut terms_enum = InvertedIndexTermsEnum::new("body", &dict);
let mut drained = Vec::new();
while let Some(stats) = terms_enum.next().unwrap() {
drained.push(stats.term);
}
assert_eq!(drained, expected);
}
#[test]
fn lazy_enum_seek_semantics() {
let dict = mixed_field_dict();
let mut terms_enum = InvertedIndexTermsEnum::new("body", &dict);
assert!(terms_enum.seek("programmer").unwrap(), "exact hit");
assert_eq!(terms_enum.current().unwrap().term, "programmer");
assert_eq!(terms_enum.next().unwrap().unwrap().term, "programmer");
assert!(!terms_enum.seek("prog").unwrap(), "miss -> next greater");
assert_eq!(terms_enum.current().unwrap().term, "program");
assert!(!terms_enum.seek("zzz").unwrap(), "past the field range");
assert!(terms_enum.current().is_none());
assert!(terms_enum.seek_exact("python").unwrap());
assert_eq!(terms_enum.current().unwrap().term, "python");
assert!(!terms_enum.seek_exact("nope").unwrap());
}
#[test]
fn merged_enum_aggregates_across_segments() {
let mut b1 = TermDictionaryBuilder::new();
b1.add_term("body:apple".to_string(), TermInfo::new(0, 0, 2, 4));
b1.add_term("body:cherry".to_string(), TermInfo::new(0, 0, 1, 1));
let mut b2 = TermDictionaryBuilder::new();
b2.add_term("body:apple".to_string(), TermInfo::new(0, 0, 3, 5));
b2.add_term("body:banana".to_string(), TermInfo::new(0, 0, 1, 2));
let dicts = vec![Arc::new(b1.build().unwrap()), Arc::new(b2.build().unwrap())];
let merged = MergedInvertedIndexTerms::new("body", &dicts);
assert_eq!(merged.size(), Some(3), "3 distinct terms");
assert_eq!(merged.sum_doc_freq(), Some(2 + 3 + 1 + 1));
assert_eq!(merged.sum_total_term_freq(), Some(4 + 5 + 2 + 1));
let mut iter = merged.iterator().unwrap();
let mut drained = Vec::new();
while let Some(stats) = iter.next().unwrap() {
drained.push((stats.term, stats.doc_freq, stats.total_term_freq));
}
assert_eq!(
drained,
vec![
("apple".to_string(), 5, 9), ("banana".to_string(), 1, 2),
("cherry".to_string(), 1, 1),
]
);
let mut iter = merged.iterator().unwrap();
assert!(iter.seek("banana").unwrap());
assert_eq!(iter.next().unwrap().unwrap().term, "banana");
assert_eq!(iter.next().unwrap().unwrap().term, "cherry");
assert!(!iter.seek("aaa").unwrap(), "miss -> positioned at apple");
assert_eq!(iter.current().unwrap().term, "apple");
}
}