use crate::fts::bm25::Bm25Params;
use crate::fts::index::{IndexError, LexicalIndex};
use crate::fts::prune::{search_pruned_filtered_controlled, select_strategy};
use crate::fts::search::{
ControlledSearchError, SearchResult, TermQuery, search_allow_list_driven_controlled,
};
use crate::meta::DocBitmap;
pub const LEXICAL_ALLOW_LIST_DIVISOR: u64 = 64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LexicalBranch {
AllowListDrive,
PostCheck,
}
#[derive(Clone, Debug, PartialEq)]
pub struct LexicalSearchOutcome {
pub result: SearchResult,
pub branch: LexicalBranch,
}
#[derive(Debug)]
pub enum LexicalFilterError {
SegmentCount {
expected: usize,
actual: usize,
},
RowOutOfRange {
segment: usize,
row: u32,
row_count: u32,
},
Index(IndexError),
}
impl std::fmt::Display for LexicalFilterError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SegmentCount { expected, actual } => write!(
formatter,
"lexical filter supplied {actual} segment bitmaps, expected {expected}"
),
Self::RowOutOfRange {
segment,
row,
row_count,
} => write!(
formatter,
"lexical filter row {row} is outside segment {segment} row count {row_count}"
),
Self::Index(error) => error.fmt(formatter),
}
}
}
impl std::error::Error for LexicalFilterError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Index(error) => Some(error),
Self::SegmentCount { .. } | Self::RowOutOfRange { .. } => None,
}
}
}
impl From<IndexError> for LexicalFilterError {
fn from(error: IndexError) -> Self {
Self::Index(error)
}
}
pub fn search_lexical_filtered(
index: &LexicalIndex,
query: &TermQuery,
k: usize,
params: Bm25Params,
allow_lists: &[DocBitmap],
forced: Option<LexicalBranch>,
) -> Result<LexicalSearchOutcome, LexicalFilterError> {
let references = allow_lists.iter().collect::<Vec<_>>();
search_lexical_filtered_refs(index, query, k, params, &references, forced)
}
pub(crate) fn search_lexical_filtered_refs(
index: &LexicalIndex,
query: &TermQuery,
k: usize,
params: Bm25Params,
allow_lists: &[&DocBitmap],
forced: Option<LexicalBranch>,
) -> Result<LexicalSearchOutcome, LexicalFilterError> {
search_lexical_filtered_refs_controlled(index, query, k, params, allow_lists, forced, || Ok(()))
}
pub(crate) fn search_lexical_filtered_refs_controlled<E: From<LexicalFilterError>>(
index: &LexicalIndex,
query: &TermQuery,
k: usize,
params: Bm25Params,
allow_lists: &[&DocBitmap],
forced: Option<LexicalBranch>,
mut check: impl FnMut() -> Result<(), E>,
) -> Result<LexicalSearchOutcome, E> {
let mut work = crate::fts::control::WorkCheck::new(&mut check);
work.check_now()?;
validate_allow_lists(index, allow_lists, &mut work)?;
let mut allowed = 0_u64;
for allow_list in allow_lists {
work.step()?;
allowed = allowed.saturating_add(allow_list.cardinality());
}
work.check_now()?;
let corpus = index.document_count();
let branch = forced.unwrap_or_else(|| {
if allowed.saturating_mul(LEXICAL_ALLOW_LIST_DIVISOR) <= corpus {
LexicalBranch::AllowListDrive
} else {
LexicalBranch::PostCheck
}
});
if index.total_tokens() == 0 {
check()?;
return Ok(LexicalSearchOutcome {
result: SearchResult::default(),
branch,
});
}
let result = match branch {
LexicalBranch::AllowListDrive => {
search_allow_list_driven_controlled(index, query, k, params, allow_lists, &mut check)
}
LexicalBranch::PostCheck => search_pruned_filtered_controlled(
index,
query,
k,
params,
select_strategy(query.terms.len(), k),
allow_lists,
&mut check,
),
}
.map_err(|error| match error {
ControlledSearchError::Index(error) => E::from(LexicalFilterError::Index(error)),
ControlledSearchError::Control(error) => error,
})?;
check()?;
Ok(LexicalSearchOutcome { result, branch })
}
#[inline(never)]
fn validate_allow_lists<E: From<LexicalFilterError>>(
index: &LexicalIndex,
allow_lists: &[&DocBitmap],
work: &mut crate::fts::control::WorkCheck<impl FnMut() -> Result<(), E>>,
) -> Result<(), E> {
if index.segments().len() != allow_lists.len() {
return Err(LexicalFilterError::SegmentCount {
expected: index.segments().len(),
actual: allow_lists.len(),
}
.into());
}
for (segment, (sealed, allow_list)) in index.segments().iter().zip(allow_lists).enumerate() {
work.step()?;
if let Some(row) = allow_list.first_at_or_after(sealed.row_count()) {
return Err(LexicalFilterError::RowOutOfRange {
segment,
row,
row_count: sealed.row_count(),
}
.into());
}
}
Ok(())
}