1use 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
11pub const LEXICAL_ALLOW_LIST_DIVISOR: u64 = 64;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum LexicalBranch {
20 AllowListDrive,
22 PostCheck,
24}
25
26#[derive(Clone, Debug, PartialEq)]
28pub struct LexicalSearchOutcome {
29 pub result: SearchResult,
31 pub branch: LexicalBranch,
33}
34
35#[derive(Debug)]
37pub enum LexicalFilterError {
38 SegmentCount {
40 expected: usize,
42 actual: usize,
44 },
45 RowOutOfRange {
47 segment: usize,
49 row: u32,
51 row_count: u32,
53 },
54 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
93pub 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 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#[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}