Skip to main content

zeppelin_embed/planner/
lexical.rs

1//! Selectivity-driven exact lexical filtering.
2
3use crate::fts::bm25::Bm25Params;
4use crate::fts::index::{IndexError, LexicalIndex};
5use crate::fts::prune::{search_pruned_filtered_controlled, select_strategy};
6use crate::fts::search::{
7    ControlledSearchError, SearchResult, TermQuery, search_allow_list_driven_controlled,
8};
9use crate::meta::DocBitmap;
10
11/// Corpus divisor below which the allow-list drives lexical iteration.
12///
13/// PLACEHOLDER -- NOT YET MEASURED. Both branches are exact; a poor value is
14/// a latency defect only.
15pub const LEXICAL_ALLOW_LIST_DIVISOR: u64 = 64;
16
17/// Exact lexical filter execution branch.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum LexicalBranch {
20    /// Enumerate allow-listed rows and seek terms to each row.
21    AllowListDrive,
22    /// Preserve pruning traversal and post-check every scored candidate.
23    PostCheck,
24}
25
26/// Filtered lexical result and the branch that produced it.
27#[derive(Clone, Debug, PartialEq)]
28pub struct LexicalSearchOutcome {
29    /// Exact BM25 hits and counters.
30    pub result: SearchResult,
31    /// Actual exact filter branch.
32    pub branch: LexicalBranch,
33}
34
35/// Typed lexical filter planning or scoring failure.
36#[derive(Debug)]
37pub enum LexicalFilterError {
38    /// One bitmap per lexical segment is required.
39    SegmentCount {
40        /// Lexical segment count.
41        expected: usize,
42        /// Supplied bitmap count.
43        actual: usize,
44    },
45    /// An allow-list row exceeded its segment's dense row space.
46    RowOutOfRange {
47        /// Segment ordinal.
48        segment: usize,
49        /// Invalid row.
50        row: u32,
51        /// Segment row count.
52        row_count: u32,
53    },
54    /// The lexical scorer rejected the index or query.
55    Index(IndexError),
56}
57
58impl std::fmt::Display for LexicalFilterError {
59    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        match self {
61            Self::SegmentCount { expected, actual } => write!(
62                formatter,
63                "lexical filter supplied {actual} segment bitmaps, expected {expected}"
64            ),
65            Self::RowOutOfRange {
66                segment,
67                row,
68                row_count,
69            } => write!(
70                formatter,
71                "lexical filter row {row} is outside segment {segment} row count {row_count}"
72            ),
73            Self::Index(error) => error.fmt(formatter),
74        }
75    }
76}
77
78impl std::error::Error for LexicalFilterError {
79    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
80        match self {
81            Self::Index(error) => Some(error),
82            Self::SegmentCount { .. } | Self::RowOutOfRange { .. } => None,
83        }
84    }
85}
86
87impl From<IndexError> for LexicalFilterError {
88    fn from(error: IndexError) -> Self {
89        Self::Index(error)
90    }
91}
92
93/// Plans and executes one exact lexical query over segment-local allow-lists.
94pub fn search_lexical_filtered(
95    index: &LexicalIndex,
96    query: &TermQuery,
97    k: usize,
98    params: Bm25Params,
99    allow_lists: &[DocBitmap],
100    forced: Option<LexicalBranch>,
101) -> Result<LexicalSearchOutcome, LexicalFilterError> {
102    let references = allow_lists.iter().collect::<Vec<_>>();
103    search_lexical_filtered_refs(index, query, k, params, &references, forced)
104}
105
106pub(crate) fn search_lexical_filtered_refs(
107    index: &LexicalIndex,
108    query: &TermQuery,
109    k: usize,
110    params: Bm25Params,
111    allow_lists: &[&DocBitmap],
112    forced: Option<LexicalBranch>,
113) -> Result<LexicalSearchOutcome, LexicalFilterError> {
114    search_lexical_filtered_refs_controlled(index, query, k, params, allow_lists, forced, || Ok(()))
115}
116
117pub(crate) fn search_lexical_filtered_refs_controlled<E: From<LexicalFilterError>>(
118    index: &LexicalIndex,
119    query: &TermQuery,
120    k: usize,
121    params: Bm25Params,
122    allow_lists: &[&DocBitmap],
123    forced: Option<LexicalBranch>,
124    mut check: impl FnMut() -> Result<(), E>,
125) -> Result<LexicalSearchOutcome, E> {
126    let mut work = crate::fts::control::WorkCheck::new(&mut check);
127    work.check_now()?;
128    validate_allow_lists(index, allow_lists, &mut work)?;
129    let mut allowed = 0_u64;
130    for allow_list in allow_lists {
131        work.step()?;
132        allowed = allowed.saturating_add(allow_list.cardinality());
133    }
134    work.check_now()?;
135    let corpus = index.document_count();
136    let branch = forced.unwrap_or_else(|| {
137        if allowed.saturating_mul(LEXICAL_ALLOW_LIST_DIVISOR) <= corpus {
138            LexicalBranch::AllowListDrive
139        } else {
140            LexicalBranch::PostCheck
141        }
142    });
143    // No live term can match; BM25's average length is undefined here.
144    if index.total_tokens() == 0 {
145        check()?;
146        return Ok(LexicalSearchOutcome {
147            result: SearchResult::default(),
148            branch,
149        });
150    }
151    let result = match branch {
152        LexicalBranch::AllowListDrive => {
153            search_allow_list_driven_controlled(index, query, k, params, allow_lists, &mut check)
154        }
155        LexicalBranch::PostCheck => search_pruned_filtered_controlled(
156            index,
157            query,
158            k,
159            params,
160            select_strategy(query.terms.len(), k),
161            allow_lists,
162            &mut check,
163        ),
164    }
165    .map_err(|error| match error {
166        ControlledSearchError::Index(error) => E::from(LexicalFilterError::Index(error)),
167        ControlledSearchError::Control(error) => error,
168    })?;
169    check()?;
170    Ok(LexicalSearchOutcome { result, branch })
171}
172
173// Keep this loop's stack layout independent of the larger query caller.
174#[inline(never)]
175fn validate_allow_lists<E: From<LexicalFilterError>>(
176    index: &LexicalIndex,
177    allow_lists: &[&DocBitmap],
178    work: &mut crate::fts::control::WorkCheck<impl FnMut() -> Result<(), E>>,
179) -> Result<(), E> {
180    if index.segments().len() != allow_lists.len() {
181        return Err(LexicalFilterError::SegmentCount {
182            expected: index.segments().len(),
183            actual: allow_lists.len(),
184        }
185        .into());
186    }
187    for (segment, (sealed, allow_list)) in index.segments().iter().zip(allow_lists).enumerate() {
188        work.step()?;
189        if let Some(row) = allow_list.first_at_or_after(sealed.row_count()) {
190            return Err(LexicalFilterError::RowOutOfRange {
191                segment,
192                row,
193                row_count: sealed.row_count(),
194            }
195            .into());
196        }
197    }
198    Ok(())
199}