1use std::sync::Arc;
4
5use crate::dsl::Field;
6use crate::segment::SegmentReader;
7use crate::structures::BlockPostingList;
8use crate::structures::TERMINATED;
9use crate::{DocId, Score};
10
11use super::{CountFuture, EmptyScorer, GlobalStats, Query, Scorer, ScorerFuture, TermQueryInfo};
12
13#[derive(Clone)]
15pub struct TermQuery {
16 pub field: Field,
17 pub term: Vec<u8>,
18 global_stats: Option<Arc<GlobalStats>>,
20}
21
22impl std::fmt::Debug for TermQuery {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 f.debug_struct("TermQuery")
25 .field("field", &self.field)
26 .field("term", &String::from_utf8_lossy(&self.term))
27 .field("has_global_stats", &self.global_stats.is_some())
28 .finish()
29 }
30}
31
32impl std::fmt::Display for TermQuery {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 write!(
35 f,
36 "Term({}:\"{}\")",
37 self.field.0,
38 String::from_utf8_lossy(&self.term)
39 )
40 }
41}
42
43impl TermQuery {
44 pub fn new(field: Field, term: impl Into<Vec<u8>>) -> Self {
45 Self {
46 field,
47 term: term.into(),
48 global_stats: None,
49 }
50 }
51
52 pub fn text(field: Field, text: &str) -> Self {
53 Self {
54 field,
55 term: text.to_lowercase().into_bytes(),
56 global_stats: None,
57 }
58 }
59
60 pub fn with_global_stats(field: Field, text: &str, stats: Arc<GlobalStats>) -> Self {
62 Self {
63 field,
64 term: text.to_lowercase().into_bytes(),
65 global_stats: Some(stats),
66 }
67 }
68
69 pub fn set_global_stats(&mut self, stats: Arc<GlobalStats>) {
71 self.global_stats = Some(stats);
72 }
73}
74
75fn compute_term_idf(
77 posting_list: &BlockPostingList,
78 field: Field,
79 reader: &SegmentReader,
80 global_stats: Option<&Arc<GlobalStats>>,
81 term: &[u8],
82) -> (f32, f32) {
83 if let Some(stats) = global_stats {
84 let term_str = String::from_utf8_lossy(term);
85 let global_idf = stats.text_idf(field, &term_str);
86 if global_idf > 0.0 {
87 return (global_idf, stats.avg_field_len(field));
88 }
89 }
90 let num_docs = reader.num_docs() as f32;
91 let doc_freq = posting_list.doc_count() as f32;
92 (
93 super::bm25_idf(doc_freq, num_docs),
94 reader.avg_field_len(field),
95 )
96}
97
98macro_rules! term_plan {
105 ($field:expr, $term:expr, $global_stats:expr, $reader:expr, $limit:expr,
106 $load_positions:expr, $budget:expr, $get_postings_fn:ident, $get_positions_fn:ident
107 $(, $aw:tt)*) => {{
108 let field: Field = $field;
109 let term: &[u8] = $term;
110 let global_stats: Option<&Arc<GlobalStats>> = $global_stats;
111 let reader: &SegmentReader = $reader;
112 let limit: usize = $limit;
113 let budget: Option<&super::SharedThreshold> = $budget;
114 if budget.is_some_and(super::SharedThreshold::stop_if_expired) {
115 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
116 }
117
118 let is_indexed = reader.schema().get_field_entry(field).is_none_or(|e| e.indexed);
120 if !is_indexed {
121 let term_str = String::from_utf8_lossy(term);
122 if let Some(scorer) = FastFieldTextScorer::try_new(reader, field, &term_str) {
123 return Ok(Box::new(scorer) as Box<dyn Scorer + '_>);
124 }
125 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
126 }
127
128 let postings = reader.$get_postings_fn(field, term) $(. $aw)* ?;
129
130 match postings {
131 Some(posting_list) if reader.is_chunked_field(field) => {
134 let num_chunks = reader.text_corpus_size(field);
135 let idf = super::bm25_idf(posting_list.doc_count() as f32, num_chunks);
136 super::planner::finish_chunked_text_maxscore(
137 vec![(posting_list, idf)],
138 limit,
139 reader,
140 field,
141 None,
142 None,
143 1.0,
144 budget,
145 )
146 }
147 Some(posting_list) => {
148 let (idf, avg_field_len) =
149 compute_term_idf(&posting_list, field, reader, global_stats, term);
150
151 let positions = if $load_positions {
152 reader.$get_positions_fn(field, term) $(. $aw)* ?
153 } else {
154 None
155 };
156
157 let mut scorer = TermScorer::new(posting_list, idf, avg_field_len, 1.0)
158 .with_params(super::Bm25Params::for_field(reader.schema(), field));
159 scorer.budget = budget.filter(|b| b.deadline().is_some()).cloned();
160 if let Some(lengths) = reader.doc_lengths(field) {
161 scorer = scorer.with_doc_lengths(lengths.clone());
162 }
163 if let Some(pos) = positions {
164 scorer = scorer.with_positions(field.0, pos);
165 }
166 Ok(Box::new(scorer) as Box<dyn Scorer + '_>)
167 }
168 None => {
169 let term_str = String::from_utf8_lossy(term);
170 if let Some(scorer) = FastFieldTextScorer::try_new(reader, field, &term_str) {
171 Ok(Box::new(scorer) as Box<dyn Scorer + '_>)
172 } else {
173 Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>)
174 }
175 }
176 }
177 }};
178}
179
180impl Query for TermQuery {
181 fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
182 self.scorer_with_options(reader, limit, super::ScorerOptions::with_positions())
183 }
184
185 fn scorer_with_options<'a>(
186 &self,
187 reader: &'a SegmentReader,
188 limit: usize,
189 options: super::ScorerOptions,
190 ) -> ScorerFuture<'a> {
191 let field = self.field;
192 let term = self.term.clone();
193 let global_stats = self
194 .global_stats
195 .clone()
196 .or_else(|| options.global_stats.clone());
197 let load_positions = options.collect_positions;
198 Box::pin(async move {
199 term_plan!(
200 field,
201 &term,
202 global_stats.as_ref(),
203 reader,
204 limit,
205 load_positions,
206 options.shared_threshold.as_ref(),
207 get_postings,
208 get_positions,
209 await
210 )
211 })
212 }
213
214 fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
215 let field = self.field;
216 let term = self.term.clone();
217 Box::pin(async move {
218 match reader.get_postings(field, &term).await? {
219 Some(list) => Ok(list.doc_count()),
220 None => Ok(0),
221 }
222 })
223 }
224
225 #[cfg(feature = "sync")]
226 fn scorer_sync<'a>(
227 &self,
228 reader: &'a SegmentReader,
229 limit: usize,
230 ) -> crate::Result<Box<dyn Scorer + 'a>> {
231 self.scorer_sync_with_options(reader, limit, super::ScorerOptions::with_positions())
232 }
233
234 #[cfg(feature = "sync")]
235 fn scorer_sync_with_options<'a>(
236 &self,
237 reader: &'a SegmentReader,
238 limit: usize,
239 options: super::ScorerOptions,
240 ) -> crate::Result<Box<dyn Scorer + 'a>> {
241 let global_stats = self
242 .global_stats
243 .clone()
244 .or_else(|| options.global_stats.clone());
245 term_plan!(
246 self.field,
247 &self.term,
248 global_stats.as_ref(),
249 reader,
250 limit,
251 options.collect_positions,
252 options.shared_threshold.as_ref(),
253 get_postings_sync,
254 get_positions_sync
255 )
256 }
257
258 fn as_doc_predicate<'a>(&self, reader: &'a SegmentReader) -> Option<super::DocPredicate<'a>> {
259 let fast_field = reader.fast_field(self.field.0)?;
260 let term_str = String::from_utf8_lossy(&self.term);
261 match fast_field.text_ordinal(&term_str) {
262 Some(target_ordinal) => Some(Box::new(move |doc_id: DocId| -> bool {
263 fast_field.get_u64(doc_id) == target_ordinal
264 })),
265 None => Some(Box::new(|_| false)),
267 }
268 }
269
270 #[cfg(feature = "sync")]
271 fn bitset_cardinality_estimate(&self, reader: &SegmentReader) -> Option<u64> {
272 if reader.is_chunked_field(self.field) {
274 return None;
275 }
276 let pl = reader.get_postings_sync(self.field, &self.term).ok()??;
278 Some(pl.doc_count() as u64)
279 }
280
281 #[cfg(feature = "sync")]
282 fn as_doc_bitset(&self, reader: &SegmentReader) -> Option<super::DocBitset> {
283 if reader.is_chunked_field(self.field) {
286 return None;
287 }
288 let pl = reader.get_postings_sync(self.field, &self.term).ok()??;
291 let mut bitset = super::DocBitset::new(reader.num_docs());
292 let mut iter = pl.iterator();
293 loop {
294 let doc = iter.doc();
295 if doc == crate::structures::TERMINATED {
296 break;
297 }
298 bitset.set(doc);
299 iter.advance();
300 }
301 Some(bitset)
302 }
303
304 fn text_terms(&self, out: &mut Vec<(Field, Vec<u8>)>) {
305 out.push((self.field, self.term.clone()));
306 }
307
308 fn decompose(&self) -> super::QueryDecomposition {
309 super::QueryDecomposition::TextTerm(TermQueryInfo {
310 weight: 1.0,
311 field: self.field,
312 term: self.term.clone(),
313 })
314 }
315}
316
317struct TermScorer {
318 budget: Option<super::SharedThreshold>,
319 iterator: crate::structures::BlockPostingIterator<'static>,
320 idf: f32,
321 avg_field_len: f32,
323 field_boost: f32,
325 field_id: u32,
327 positions: Option<crate::structures::TermPositions>,
329 lengths: Option<crate::segment::chunk_map::DocLengths>,
331 params: super::Bm25Params,
333}
334
335impl TermScorer {
336 pub fn new(
337 posting_list: BlockPostingList,
338 idf: f32,
339 avg_field_len: f32,
340 field_boost: f32,
341 ) -> Self {
342 Self {
343 budget: None,
344 iterator: posting_list.into_iterator(),
345 idf,
346 avg_field_len,
347 field_boost,
348 field_id: 0,
349 positions: None,
350 lengths: None,
351 params: super::Bm25Params::default(),
352 }
353 }
354
355 pub fn with_params(mut self, params: super::Bm25Params) -> Self {
357 self.params = params;
358 self
359 }
360
361 pub fn with_doc_lengths(mut self, lengths: crate::segment::chunk_map::DocLengths) -> Self {
363 self.lengths = Some(lengths);
364 self
365 }
366
367 pub fn with_positions(
368 mut self,
369 field_id: u32,
370 positions: crate::structures::TermPositions,
371 ) -> Self {
372 self.field_id = field_id;
373 self.positions = Some(positions);
374 self
375 }
376}
377
378impl super::docset::DocSet for TermScorer {
379 fn doc(&self) -> DocId {
380 if self
381 .budget
382 .as_ref()
383 .is_some_and(super::SharedThreshold::stop_if_expired)
384 {
385 return TERMINATED;
386 }
387 self.iterator.doc()
388 }
389
390 fn advance(&mut self) -> DocId {
391 if self.doc() == TERMINATED {
392 return TERMINATED;
393 }
394 self.iterator.advance()
395 }
396
397 fn seek(&mut self, target: DocId) -> DocId {
398 if self.doc() == TERMINATED {
399 return TERMINATED;
400 }
401 self.iterator.seek(target)
402 }
403
404 fn size_hint(&self) -> u32 {
405 0
406 }
407}
408
409struct FastFieldTextScorer<'a> {
415 fast_field: &'a crate::structures::fast_field::FastFieldReader,
416 target_ordinal: u64,
417 current: u32,
418 num_docs: u32,
419}
420
421impl<'a> FastFieldTextScorer<'a> {
422 fn try_new(reader: &'a SegmentReader, field: Field, text: &str) -> Option<Self> {
423 let fast_field = reader.fast_field(field.0)?;
424 let target_ordinal = fast_field.text_ordinal(text)?;
425 let num_docs = reader.num_docs();
426 let mut scorer = Self {
427 fast_field,
428 target_ordinal,
429 current: 0,
430 num_docs,
431 };
432 if num_docs > 0 && fast_field.get_u64(0) != target_ordinal {
434 scorer.scan_forward();
435 }
436 Some(scorer)
437 }
438
439 fn scan_forward(&mut self) {
440 loop {
441 self.current += 1;
442 if self.current >= self.num_docs {
443 self.current = self.num_docs;
444 return;
445 }
446 if self.fast_field.get_u64(self.current) == self.target_ordinal {
447 return;
448 }
449 }
450 }
451}
452
453impl super::docset::DocSet for FastFieldTextScorer<'_> {
454 fn doc(&self) -> DocId {
455 if self.current >= self.num_docs {
456 TERMINATED
457 } else {
458 self.current
459 }
460 }
461
462 fn advance(&mut self) -> DocId {
463 self.scan_forward();
464 self.doc()
465 }
466
467 fn seek(&mut self, target: DocId) -> DocId {
468 if target > self.current {
469 self.current = target;
470 if self.current < self.num_docs
471 && self.fast_field.get_u64(self.current) != self.target_ordinal
472 {
473 self.scan_forward();
474 }
475 }
476 self.doc()
477 }
478
479 fn size_hint(&self) -> u32 {
480 0
481 }
482}
483
484impl Scorer for FastFieldTextScorer<'_> {
485 fn score(&self) -> Score {
486 1.0
487 }
488}
489
490impl Scorer for TermScorer {
491 fn score(&self) -> Score {
492 let tf = self.iterator.term_freq() as f32;
493 let doc_len = self
496 .lengths
497 .as_ref()
498 .map(|lengths| lengths.length(self.iterator.doc()) as f32)
499 .filter(|len| *len > 0.0)
500 .unwrap_or(tf);
501 self.params
502 .score_boosted(tf, self.idf, doc_len, self.avg_field_len, self.field_boost)
503 }
504
505 fn matched_positions(&self) -> Option<super::MatchedPositions> {
506 let positions = self.positions.as_ref()?;
507 let doc_id = self.iterator.doc();
508 let pos = positions.positions(
509 doc_id,
510 self.iterator.position_cursor(),
511 self.iterator.term_freq(),
512 )?;
513 let score = self.score();
514 let per_position_score = if pos.is_empty() {
516 0.0
517 } else {
518 score / pos.len() as f32
519 };
520 let scored_positions: Vec<super::ScoredPosition> = pos
521 .iter()
522 .map(|&p| super::ScoredPosition::new(p, per_position_score))
523 .collect();
524 Some(vec![(self.field_id, scored_positions)])
525 }
526}