1use std::sync::Arc;
4
5use crate::dsl::Field;
6use crate::segment::SegmentReader;
7use crate::structures::{BlockPostingIterator, BlockPostingList, PositionPostingList, TERMINATED};
8use crate::{DocId, Score};
9
10use super::{CountFuture, EmptyScorer, GlobalStats, Query, Scorer, ScorerFuture};
11
12#[derive(Clone)]
17pub struct PhraseQuery {
18 pub field: Field,
19 pub terms: Vec<Vec<u8>>,
21 pub slop: u32,
23 global_stats: Option<Arc<GlobalStats>>,
25}
26
27impl std::fmt::Display for PhraseQuery {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 let terms: Vec<_> = self
30 .terms
31 .iter()
32 .map(|t| String::from_utf8_lossy(t))
33 .collect();
34 write!(f, "Phrase({}:\"{}\"", self.field.0, terms.join(" "))?;
35 if self.slop > 0 {
36 write!(f, "~{}", self.slop)?;
37 }
38 write!(f, ")")
39 }
40}
41
42impl std::fmt::Debug for PhraseQuery {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 let terms: Vec<_> = self
45 .terms
46 .iter()
47 .map(|t| String::from_utf8_lossy(t).to_string())
48 .collect();
49 f.debug_struct("PhraseQuery")
50 .field("field", &self.field)
51 .field("terms", &terms)
52 .field("slop", &self.slop)
53 .finish()
54 }
55}
56
57impl PhraseQuery {
58 pub fn new(field: Field, terms: Vec<Vec<u8>>) -> Self {
60 Self {
61 field,
62 terms,
63 slop: 0,
64 global_stats: None,
65 }
66 }
67
68 pub fn text(field: Field, phrase: &str) -> Self {
70 let terms: Vec<Vec<u8>> = phrase
71 .split_whitespace()
72 .map(|w| w.to_lowercase().into_bytes())
73 .collect();
74 Self {
75 field,
76 terms,
77 slop: 0,
78 global_stats: None,
79 }
80 }
81
82 pub fn with_slop(mut self, slop: u32) -> Self {
84 self.slop = slop;
85 self
86 }
87
88 pub fn with_global_stats(mut self, stats: Arc<GlobalStats>) -> Self {
90 self.global_stats = Some(stats);
91 self
92 }
93}
94
95fn build_phrase_scorer<'a>(
97 term_data: Vec<(BlockPostingList, PositionPostingList)>,
98 slop: u32,
99 reader: &SegmentReader,
100 field: Field,
101) -> Box<dyn Scorer + 'a> {
102 let idf: f32 = term_data
103 .iter()
104 .map(|(p, _)| {
105 let num_docs = reader.num_docs() as f32;
106 let doc_freq = p.doc_count() as f32;
107 super::bm25_idf(doc_freq, num_docs)
108 })
109 .sum();
110 let avg_field_len = reader.avg_field_len(field);
111 let (postings, positions): (Vec<_>, Vec<_>) = term_data.into_iter().unzip();
112 Box::new(PhraseScorer::new(
113 postings,
114 positions,
115 slop,
116 idf,
117 avg_field_len,
118 ))
119}
120
121macro_rules! phrase_early_returns {
126 ($field:expr, $terms:expr, $reader:expr, $limit:expr,
127 $scorer_fn:ident, $options:expr $(, $aw:tt)*) => {
128 if $terms.is_empty() {
129 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
130 }
131 if $terms.len() == 1 {
132 let tq = super::TermQuery::new($field, $terms[0].clone());
133 return tq.$scorer_fn($reader, $limit, $options) $(. $aw)* ;
134 }
135 if !$reader.has_positions($field) {
136 let mut bq = super::BooleanQuery::new();
137 for t in $terms.iter() {
138 bq = bq.must(super::TermQuery::new($field, t.clone()));
139 }
140 return bq.$scorer_fn($reader, $limit, $options) $(. $aw)* ;
141 }
142 };
143}
144
145impl Query for PhraseQuery {
146 fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
147 self.scorer_with_options(reader, limit, super::ScorerOptions::with_positions())
148 }
149
150 fn scorer_with_options<'a>(
151 &self,
152 reader: &'a SegmentReader,
153 limit: usize,
154 options: super::ScorerOptions,
155 ) -> ScorerFuture<'a> {
156 let field = self.field;
157 let terms = self.terms.clone();
158 let slop = self.slop;
159
160 Box::pin(async move {
161 phrase_early_returns!(
162 field,
163 terms,
164 reader,
165 limit,
166 scorer_with_options,
167 options,
168 await
169 );
170
171 let mut term_data = Vec::with_capacity(terms.len());
173 for term in &terms {
174 let (postings, positions) = futures::join!(
175 reader.get_postings(field, term),
176 reader.get_positions(field, term)
177 );
178 match (postings?, positions?) {
179 (Some(p), Some(pos)) => term_data.push((p, pos)),
180 _ => return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + 'a>),
181 }
182 }
183
184 Ok(build_phrase_scorer(term_data, slop, reader, field))
185 })
186 }
187
188 #[cfg(feature = "sync")]
189 fn scorer_sync<'a>(
190 &self,
191 reader: &'a SegmentReader,
192 limit: usize,
193 ) -> crate::Result<Box<dyn Scorer + 'a>> {
194 self.scorer_sync_with_options(reader, limit, super::ScorerOptions::with_positions())
195 }
196
197 #[cfg(feature = "sync")]
198 fn scorer_sync_with_options<'a>(
199 &self,
200 reader: &'a SegmentReader,
201 limit: usize,
202 options: super::ScorerOptions,
203 ) -> crate::Result<Box<dyn Scorer + 'a>> {
204 phrase_early_returns!(
205 self.field,
206 self.terms,
207 reader,
208 limit,
209 scorer_sync_with_options,
210 options
211 );
212
213 use rayon::prelude::*;
215 let pairs: crate::Result<Vec<Option<(BlockPostingList, PositionPostingList)>>> = self
216 .terms
217 .par_iter()
218 .map(|term| {
219 let postings = reader.get_postings_sync(self.field, term)?;
220 let positions = reader.get_positions_sync(self.field, term)?;
221 Ok(match (postings, positions) {
222 (Some(p), Some(pos)) => Some((p, pos)),
223 _ => None,
224 })
225 })
226 .collect();
227 let mut term_data = Vec::with_capacity(self.terms.len());
228 for entry in pairs? {
229 match entry {
230 Some(pair) => term_data.push(pair),
231 None => return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + 'a>),
232 }
233 }
234
235 Ok(build_phrase_scorer(
236 term_data, self.slop, reader, self.field,
237 ))
238 }
239
240 fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
241 let field = self.field;
242 let terms = self.terms.clone();
243
244 Box::pin(async move {
245 if terms.is_empty() {
246 return Ok(0);
247 }
248
249 let mut min_count = u32::MAX;
251 for term in &terms {
252 match reader.get_postings(field, term).await? {
253 Some(list) => min_count = min_count.min(list.doc_count()),
254 None => return Ok(0),
255 }
256 }
257
258 Ok((min_count / 10).max(1))
261 })
262 }
263}
264
265struct PhraseScorer {
267 posting_iters: Vec<BlockPostingIterator<'static>>,
269 position_lists: Vec<PositionPostingList>,
271 slop: u32,
273 current_doc: DocId,
275 idf: f32,
277 avg_field_len: f32,
279 position_bufs: Vec<Vec<u32>>,
281}
282
283impl PhraseScorer {
284 fn new(
285 posting_lists: Vec<BlockPostingList>,
286 position_lists: Vec<PositionPostingList>,
287 slop: u32,
288 idf: f32,
289 avg_field_len: f32,
290 ) -> Self {
291 let posting_iters: Vec<_> = posting_lists
292 .into_iter()
293 .map(|p| p.into_iterator())
294 .collect();
295
296 let num_terms = position_lists.len();
297 let mut scorer = Self {
298 posting_iters,
299 position_lists,
300 slop,
301 current_doc: 0,
302 idf,
303 avg_field_len,
304 position_bufs: (0..num_terms).map(|_| Vec::new()).collect(),
305 };
306
307 scorer.find_next_phrase_match();
308 scorer
309 }
310
311 fn find_next_phrase_match(&mut self) {
313 loop {
314 let doc = self.find_next_and_match();
316 if doc == TERMINATED {
317 self.current_doc = TERMINATED;
318 return;
319 }
320
321 if self.check_phrase_positions(doc) {
323 self.current_doc = doc;
324 return;
325 }
326
327 self.posting_iters[0].advance();
329 }
330 }
331
332 fn find_next_and_match(&mut self) -> DocId {
334 if self.posting_iters.is_empty() {
335 return TERMINATED;
336 }
337
338 loop {
339 let max_doc = self.posting_iters.iter().map(|it| it.doc()).max().unwrap();
340
341 if max_doc == TERMINATED {
342 return TERMINATED;
343 }
344
345 let mut all_match = true;
346 for it in &mut self.posting_iters {
347 let doc = it.seek(max_doc);
348 if doc != max_doc {
349 all_match = false;
350 if doc == TERMINATED {
351 return TERMINATED;
352 }
353 }
354 }
355
356 if all_match {
357 return max_doc;
358 }
359 }
360 }
361
362 fn check_phrase_positions(&mut self, doc_id: DocId) -> bool {
364 for (i, pos_list) in self.position_lists.iter().enumerate() {
366 if !pos_list.get_positions_into(doc_id, &mut self.position_bufs[i]) {
367 return false;
368 }
369 }
370
371 self.find_phrase_match_from_bufs()
374 }
375
376 fn find_phrase_match_from_bufs(&self) -> bool {
378 if self.position_bufs.is_empty() || self.position_bufs[0].is_empty() {
379 return false;
380 }
381
382 for &first_pos in &self.position_bufs[0] {
383 if self.check_phrase_from_position(first_pos, &self.position_bufs) {
384 return true;
385 }
386 }
387
388 false
389 }
390
391 fn check_phrase_from_position(&self, start_pos: u32, term_positions: &[Vec<u32>]) -> bool {
393 let mut expected_pos = start_pos;
394
395 for (i, positions) in term_positions.iter().enumerate() {
396 if i == 0 {
397 continue; }
399
400 expected_pos += 1;
401
402 let found = positions.iter().any(|&pos| {
404 if self.slop == 0 {
405 pos == expected_pos
406 } else {
407 let diff = pos.abs_diff(expected_pos);
408 diff <= self.slop
409 }
410 });
411
412 if !found {
413 return false;
414 }
415 }
416
417 true
418 }
419}
420
421impl super::docset::DocSet for PhraseScorer {
422 fn doc(&self) -> DocId {
423 self.current_doc
424 }
425
426 fn advance(&mut self) -> DocId {
427 if self.current_doc == TERMINATED {
428 return TERMINATED;
429 }
430
431 self.posting_iters[0].advance();
432 self.find_next_phrase_match();
433 self.current_doc
434 }
435
436 fn seek(&mut self, target: DocId) -> DocId {
437 if target == TERMINATED {
438 self.current_doc = TERMINATED;
439 return TERMINATED;
440 }
441
442 self.posting_iters[0].seek(target);
443 self.find_next_phrase_match();
444 self.current_doc
445 }
446
447 fn size_hint(&self) -> u32 {
448 0
449 }
450}
451
452impl Scorer for PhraseScorer {
453 fn score(&self) -> Score {
454 if self.current_doc == TERMINATED {
455 return 0.0;
456 }
457
458 let tf: f32 = self
460 .posting_iters
461 .iter()
462 .map(|it| it.term_freq() as f32)
463 .sum();
464
465 super::bm25_score(tf, self.idf, tf, self.avg_field_len) * 1.5
467 }
468}