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, $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
114 let is_indexed = reader.schema().get_field_entry(field).is_none_or(|e| e.indexed);
116 if !is_indexed {
117 let term_str = String::from_utf8_lossy(term);
118 if let Some(scorer) = FastFieldTextScorer::try_new(reader, field, &term_str) {
119 return Ok(Box::new(scorer) as Box<dyn Scorer + '_>);
120 }
121 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
122 }
123
124 let postings = reader.$get_postings_fn(field, term) $(. $aw)* ?;
125
126 match postings {
127 Some(posting_list) if reader.is_chunked_field(field) => {
130 let num_chunks = reader.text_corpus_size(field);
131 let idf = super::bm25_idf(posting_list.doc_count() as f32, num_chunks);
132 super::planner::finish_chunked_text_maxscore(
133 vec![(posting_list, idf)],
134 limit,
135 reader,
136 field,
137 None,
138 None,
139 1.0,
140 None,
141 )
142 }
143 Some(posting_list) => {
144 let (idf, avg_field_len) =
145 compute_term_idf(&posting_list, field, reader, global_stats, term);
146
147 let positions = if $load_positions {
148 reader.$get_positions_fn(field, term) $(. $aw)* ?
149 } else {
150 None
151 };
152
153 let mut scorer = TermScorer::new(posting_list, idf, avg_field_len, 1.0)
154 .with_params(super::Bm25Params::for_field(reader.schema(), field));
155 if let Some(lengths) = reader.doc_lengths(field) {
156 scorer = scorer.with_doc_lengths(lengths.clone());
157 }
158 if let Some(pos) = positions {
159 scorer = scorer.with_positions(field.0, pos);
160 }
161 Ok(Box::new(scorer) as Box<dyn Scorer + '_>)
162 }
163 None => {
164 let term_str = String::from_utf8_lossy(term);
165 if let Some(scorer) = FastFieldTextScorer::try_new(reader, field, &term_str) {
166 Ok(Box::new(scorer) as Box<dyn Scorer + '_>)
167 } else {
168 Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>)
169 }
170 }
171 }
172 }};
173}
174
175impl Query for TermQuery {
176 fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
177 self.scorer_with_options(reader, limit, super::ScorerOptions::with_positions())
178 }
179
180 fn scorer_with_options<'a>(
181 &self,
182 reader: &'a SegmentReader,
183 limit: usize,
184 options: super::ScorerOptions,
185 ) -> ScorerFuture<'a> {
186 let field = self.field;
187 let term = self.term.clone();
188 let global_stats = self
189 .global_stats
190 .clone()
191 .or_else(|| options.global_stats.clone());
192 let load_positions = options.collect_positions;
193 Box::pin(async move {
194 term_plan!(
195 field,
196 &term,
197 global_stats.as_ref(),
198 reader,
199 limit,
200 load_positions,
201 get_postings,
202 get_positions,
203 await
204 )
205 })
206 }
207
208 fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
209 let field = self.field;
210 let term = self.term.clone();
211 Box::pin(async move {
212 match reader.get_postings(field, &term).await? {
213 Some(list) => Ok(list.doc_count()),
214 None => Ok(0),
215 }
216 })
217 }
218
219 #[cfg(feature = "sync")]
220 fn scorer_sync<'a>(
221 &self,
222 reader: &'a SegmentReader,
223 limit: usize,
224 ) -> crate::Result<Box<dyn Scorer + 'a>> {
225 self.scorer_sync_with_options(reader, limit, super::ScorerOptions::with_positions())
226 }
227
228 #[cfg(feature = "sync")]
229 fn scorer_sync_with_options<'a>(
230 &self,
231 reader: &'a SegmentReader,
232 limit: usize,
233 options: super::ScorerOptions,
234 ) -> crate::Result<Box<dyn Scorer + 'a>> {
235 let global_stats = self
236 .global_stats
237 .clone()
238 .or_else(|| options.global_stats.clone());
239 term_plan!(
240 self.field,
241 &self.term,
242 global_stats.as_ref(),
243 reader,
244 limit,
245 options.collect_positions,
246 get_postings_sync,
247 get_positions_sync
248 )
249 }
250
251 fn as_doc_predicate<'a>(&self, reader: &'a SegmentReader) -> Option<super::DocPredicate<'a>> {
252 let fast_field = reader.fast_field(self.field.0)?;
253 let term_str = String::from_utf8_lossy(&self.term);
254 match fast_field.text_ordinal(&term_str) {
255 Some(target_ordinal) => Some(Box::new(move |doc_id: DocId| -> bool {
256 fast_field.get_u64(doc_id) == target_ordinal
257 })),
258 None => Some(Box::new(|_| false)),
260 }
261 }
262
263 #[cfg(feature = "sync")]
264 fn bitset_cardinality_estimate(&self, reader: &SegmentReader) -> Option<u64> {
265 if reader.is_chunked_field(self.field) {
267 return None;
268 }
269 let pl = reader.get_postings_sync(self.field, &self.term).ok()??;
271 Some(pl.doc_count() as u64)
272 }
273
274 #[cfg(feature = "sync")]
275 fn as_doc_bitset(&self, reader: &SegmentReader) -> Option<super::DocBitset> {
276 if reader.is_chunked_field(self.field) {
279 return None;
280 }
281 let pl = reader.get_postings_sync(self.field, &self.term).ok()??;
284 let mut bitset = super::DocBitset::new(reader.num_docs());
285 let mut iter = pl.iterator();
286 loop {
287 let doc = iter.doc();
288 if doc == crate::structures::TERMINATED {
289 break;
290 }
291 bitset.set(doc);
292 iter.advance();
293 }
294 Some(bitset)
295 }
296
297 fn text_terms(&self, out: &mut Vec<(Field, Vec<u8>)>) {
298 out.push((self.field, self.term.clone()));
299 }
300
301 fn decompose(&self) -> super::QueryDecomposition {
302 super::QueryDecomposition::TextTerm(TermQueryInfo {
303 weight: 1.0,
304 field: self.field,
305 term: self.term.clone(),
306 })
307 }
308}
309
310struct TermScorer {
311 iterator: crate::structures::BlockPostingIterator<'static>,
312 idf: f32,
313 avg_field_len: f32,
315 field_boost: f32,
317 field_id: u32,
319 positions: Option<crate::structures::TermPositions>,
321 lengths: Option<crate::segment::chunk_map::DocLengths>,
323 params: super::Bm25Params,
325}
326
327impl TermScorer {
328 pub fn new(
329 posting_list: BlockPostingList,
330 idf: f32,
331 avg_field_len: f32,
332 field_boost: f32,
333 ) -> Self {
334 Self {
335 iterator: posting_list.into_iterator(),
336 idf,
337 avg_field_len,
338 field_boost,
339 field_id: 0,
340 positions: None,
341 lengths: None,
342 params: super::Bm25Params::default(),
343 }
344 }
345
346 pub fn with_params(mut self, params: super::Bm25Params) -> Self {
348 self.params = params;
349 self
350 }
351
352 pub fn with_doc_lengths(mut self, lengths: crate::segment::chunk_map::DocLengths) -> Self {
354 self.lengths = Some(lengths);
355 self
356 }
357
358 pub fn with_positions(
359 mut self,
360 field_id: u32,
361 positions: crate::structures::TermPositions,
362 ) -> Self {
363 self.field_id = field_id;
364 self.positions = Some(positions);
365 self
366 }
367}
368
369impl super::docset::DocSet for TermScorer {
370 fn doc(&self) -> DocId {
371 self.iterator.doc()
372 }
373
374 fn advance(&mut self) -> DocId {
375 self.iterator.advance()
376 }
377
378 fn seek(&mut self, target: DocId) -> DocId {
379 self.iterator.seek(target)
380 }
381
382 fn size_hint(&self) -> u32 {
383 0
384 }
385}
386
387struct FastFieldTextScorer<'a> {
393 fast_field: &'a crate::structures::fast_field::FastFieldReader,
394 target_ordinal: u64,
395 current: u32,
396 num_docs: u32,
397}
398
399impl<'a> FastFieldTextScorer<'a> {
400 fn try_new(reader: &'a SegmentReader, field: Field, text: &str) -> Option<Self> {
401 let fast_field = reader.fast_field(field.0)?;
402 let target_ordinal = fast_field.text_ordinal(text)?;
403 let num_docs = reader.num_docs();
404 let mut scorer = Self {
405 fast_field,
406 target_ordinal,
407 current: 0,
408 num_docs,
409 };
410 if num_docs > 0 && fast_field.get_u64(0) != target_ordinal {
412 scorer.scan_forward();
413 }
414 Some(scorer)
415 }
416
417 fn scan_forward(&mut self) {
418 loop {
419 self.current += 1;
420 if self.current >= self.num_docs {
421 self.current = self.num_docs;
422 return;
423 }
424 if self.fast_field.get_u64(self.current) == self.target_ordinal {
425 return;
426 }
427 }
428 }
429}
430
431impl super::docset::DocSet for FastFieldTextScorer<'_> {
432 fn doc(&self) -> DocId {
433 if self.current >= self.num_docs {
434 TERMINATED
435 } else {
436 self.current
437 }
438 }
439
440 fn advance(&mut self) -> DocId {
441 self.scan_forward();
442 self.doc()
443 }
444
445 fn seek(&mut self, target: DocId) -> DocId {
446 if target > self.current {
447 self.current = target;
448 if self.current < self.num_docs
449 && self.fast_field.get_u64(self.current) != self.target_ordinal
450 {
451 self.scan_forward();
452 }
453 }
454 self.doc()
455 }
456
457 fn size_hint(&self) -> u32 {
458 0
459 }
460}
461
462impl Scorer for FastFieldTextScorer<'_> {
463 fn score(&self) -> Score {
464 1.0
465 }
466}
467
468impl Scorer for TermScorer {
469 fn score(&self) -> Score {
470 let tf = self.iterator.term_freq() as f32;
471 let doc_len = self
474 .lengths
475 .as_ref()
476 .map(|lengths| lengths.length(self.iterator.doc()) as f32)
477 .filter(|len| *len > 0.0)
478 .unwrap_or(tf);
479 self.params
480 .score_boosted(tf, self.idf, doc_len, self.avg_field_len, self.field_boost)
481 }
482
483 fn matched_positions(&self) -> Option<super::MatchedPositions> {
484 let positions = self.positions.as_ref()?;
485 let doc_id = self.iterator.doc();
486 let pos = positions.positions(
487 doc_id,
488 self.iterator.position_cursor(),
489 self.iterator.term_freq(),
490 )?;
491 let score = self.score();
492 let per_position_score = if pos.is_empty() {
494 0.0
495 } else {
496 score / pos.len() as f32
497 };
498 let scored_positions: Vec<super::ScoredPosition> = pos
499 .iter()
500 .map(|&p| super::ScoredPosition::new(p, per_position_score))
501 .collect();
502 Some(vec![(self.field_id, scored_positions)])
503 }
504}