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 let result = match branch {
144 LexicalBranch::AllowListDrive => {
145 search_allow_list_driven_controlled(index, query, k, params, allow_lists, &mut check)
146 }
147 LexicalBranch::PostCheck => search_pruned_filtered_controlled(
148 index,
149 query,
150 k,
151 params,
152 select_strategy(query.terms.len(), k),
153 allow_lists,
154 &mut check,
155 ),
156 }
157 .map_err(|error| match error {
158 ControlledSearchError::Index(error) => E::from(LexicalFilterError::Index(error)),
159 ControlledSearchError::Control(error) => error,
160 })?;
161 check()?;
162 Ok(LexicalSearchOutcome { result, branch })
163}
164
165#[inline(never)]
167fn validate_allow_lists<E: From<LexicalFilterError>>(
168 index: &LexicalIndex,
169 allow_lists: &[&DocBitmap],
170 work: &mut crate::fts::control::WorkCheck<impl FnMut() -> Result<(), E>>,
171) -> Result<(), E> {
172 if index.segments().len() != allow_lists.len() {
173 return Err(LexicalFilterError::SegmentCount {
174 expected: index.segments().len(),
175 actual: allow_lists.len(),
176 }
177 .into());
178 }
179 for (segment, (sealed, allow_list)) in index.segments().iter().zip(allow_lists).enumerate() {
180 work.step()?;
181 if let Some(row) = allow_list.first_at_or_after(sealed.row_count()) {
182 return Err(LexicalFilterError::RowOutOfRange {
183 segment,
184 row,
185 row_count: sealed.row_count(),
186 }
187 .into());
188 }
189 }
190 Ok(())
191}