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 {
73 use crate::tokenizer::Tokenizer;
74 let terms: Vec<Vec<u8>> = crate::tokenizer::SimpleTokenizer
75 .tokenize(phrase)
76 .into_iter()
77 .map(|token| token.text.into_bytes())
78 .collect();
79 Self {
80 field,
81 terms,
82 slop: 0,
83 global_stats: None,
84 }
85 }
86
87 pub fn with_slop(mut self, slop: u32) -> Self {
89 self.slop = slop;
90 self
91 }
92
93 pub fn with_global_stats(mut self, stats: Arc<GlobalStats>) -> Self {
95 self.global_stats = Some(stats);
96 self
97 }
98}
99
100fn build_phrase_scorer<'a>(
102 term_data: Vec<(BlockPostingList, PositionPostingList)>,
103 slop: u32,
104 reader: &SegmentReader,
105 field: Field,
106) -> Box<dyn Scorer + 'a> {
107 let idf: f32 = term_data
108 .iter()
109 .map(|(p, _)| {
110 let num_docs = reader.num_docs() as f32;
111 let doc_freq = p.doc_count() as f32;
112 super::bm25_idf(doc_freq, num_docs)
113 })
114 .sum();
115 let avg_field_len = reader.avg_field_len(field);
116 let (postings, positions): (Vec<_>, Vec<_>) = term_data.into_iter().unzip();
117 Box::new(PhraseScorer::new(
118 postings,
119 positions,
120 slop,
121 idf,
122 avg_field_len,
123 ))
124}
125
126macro_rules! phrase_early_returns {
131 ($field:expr, $terms:expr, $reader:expr, $limit:expr,
132 $scorer_fn:ident, $options:expr $(, $aw:tt)*) => {
133 if $terms.is_empty() {
134 return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + '_>);
135 }
136 if $terms.len() == 1 {
137 let tq = super::TermQuery::new($field, $terms[0].clone());
138 return tq.$scorer_fn($reader, $limit, $options) $(. $aw)* ;
139 }
140 if !$reader.has_positions($field) {
141 let mut bq = super::BooleanQuery::new();
142 for t in $terms.iter() {
143 bq = bq.must(super::TermQuery::new($field, t.clone()));
144 }
145 return bq.$scorer_fn($reader, $limit, $options) $(. $aw)* ;
146 }
147 };
148}
149
150impl Query for PhraseQuery {
151 fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
152 self.scorer_with_options(reader, limit, super::ScorerOptions::with_positions())
153 }
154
155 fn scorer_with_options<'a>(
156 &self,
157 reader: &'a SegmentReader,
158 limit: usize,
159 options: super::ScorerOptions,
160 ) -> ScorerFuture<'a> {
161 let field = self.field;
162 let terms = self.terms.clone();
163 let slop = self.slop;
164
165 Box::pin(async move {
166 phrase_early_returns!(
167 field,
168 terms,
169 reader,
170 limit,
171 scorer_with_options,
172 options,
173 await
174 );
175
176 let mut term_data = Vec::with_capacity(terms.len());
178 for term in &terms {
179 let (postings, positions) = futures::join!(
180 reader.get_postings(field, term),
181 reader.get_positions(field, term)
182 );
183 match (postings?, positions?) {
184 (Some(p), Some(pos)) => term_data.push((p, pos)),
185 _ => return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + 'a>),
186 }
187 }
188
189 Ok(build_phrase_scorer(term_data, slop, reader, field))
190 })
191 }
192
193 #[cfg(feature = "sync")]
194 fn scorer_sync<'a>(
195 &self,
196 reader: &'a SegmentReader,
197 limit: usize,
198 ) -> crate::Result<Box<dyn Scorer + 'a>> {
199 self.scorer_sync_with_options(reader, limit, super::ScorerOptions::with_positions())
200 }
201
202 #[cfg(feature = "sync")]
203 fn scorer_sync_with_options<'a>(
204 &self,
205 reader: &'a SegmentReader,
206 limit: usize,
207 options: super::ScorerOptions,
208 ) -> crate::Result<Box<dyn Scorer + 'a>> {
209 phrase_early_returns!(
210 self.field,
211 self.terms,
212 reader,
213 limit,
214 scorer_sync_with_options,
215 options
216 );
217
218 use rayon::prelude::*;
220 let pairs: crate::Result<Vec<Option<(BlockPostingList, PositionPostingList)>>> = self
221 .terms
222 .par_iter()
223 .map(|term| {
224 let postings = reader.get_postings_sync(self.field, term)?;
225 let positions = reader.get_positions_sync(self.field, term)?;
226 Ok(match (postings, positions) {
227 (Some(p), Some(pos)) => Some((p, pos)),
228 _ => None,
229 })
230 })
231 .collect();
232 let mut term_data = Vec::with_capacity(self.terms.len());
233 for entry in pairs? {
234 match entry {
235 Some(pair) => term_data.push(pair),
236 None => return Ok(Box::new(EmptyScorer) as Box<dyn Scorer + 'a>),
237 }
238 }
239
240 Ok(build_phrase_scorer(
241 term_data, self.slop, reader, self.field,
242 ))
243 }
244
245 fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
246 let field = self.field;
247 let terms = self.terms.clone();
248
249 Box::pin(async move {
250 if terms.is_empty() {
251 return Ok(0);
252 }
253
254 let mut min_count = u32::MAX;
256 for term in &terms {
257 match reader.get_postings(field, term).await? {
258 Some(list) => min_count = min_count.min(list.doc_count()),
259 None => return Ok(0),
260 }
261 }
262
263 Ok((min_count / 10).max(1))
266 })
267 }
268}
269
270struct PhraseScorer {
272 posting_iters: Vec<BlockPostingIterator<'static>>,
274 position_lists: Vec<PositionPostingList>,
276 slop: u32,
278 current_doc: DocId,
280 idf: f32,
282 avg_field_len: f32,
284 position_bufs: Vec<Vec<u32>>,
286}
287
288impl PhraseScorer {
289 fn new(
290 posting_lists: Vec<BlockPostingList>,
291 position_lists: Vec<PositionPostingList>,
292 slop: u32,
293 idf: f32,
294 avg_field_len: f32,
295 ) -> Self {
296 let posting_iters: Vec<_> = posting_lists
297 .into_iter()
298 .map(|p| p.into_iterator())
299 .collect();
300
301 let num_terms = position_lists.len();
302 let mut scorer = Self {
303 posting_iters,
304 position_lists,
305 slop,
306 current_doc: 0,
307 idf,
308 avg_field_len,
309 position_bufs: (0..num_terms).map(|_| Vec::new()).collect(),
310 };
311
312 scorer.find_next_phrase_match();
313 scorer
314 }
315
316 fn find_next_phrase_match(&mut self) {
318 loop {
319 let doc = self.find_next_and_match();
321 if doc == TERMINATED {
322 self.current_doc = TERMINATED;
323 return;
324 }
325
326 if self.check_phrase_positions(doc) {
328 self.current_doc = doc;
329 return;
330 }
331
332 self.posting_iters[0].advance();
334 }
335 }
336
337 fn find_next_and_match(&mut self) -> DocId {
339 if self.posting_iters.is_empty() {
340 return TERMINATED;
341 }
342
343 loop {
344 let max_doc = self.posting_iters.iter().map(|it| it.doc()).max().unwrap();
345
346 if max_doc == TERMINATED {
347 return TERMINATED;
348 }
349
350 let mut all_match = true;
351 for it in &mut self.posting_iters {
352 let doc = it.seek(max_doc);
353 if doc != max_doc {
354 all_match = false;
355 if doc == TERMINATED {
356 return TERMINATED;
357 }
358 }
359 }
360
361 if all_match {
362 return max_doc;
363 }
364 }
365 }
366
367 fn check_phrase_positions(&mut self, doc_id: DocId) -> bool {
369 for (i, pos_list) in self.position_lists.iter().enumerate() {
371 if !pos_list.get_positions_into(doc_id, &mut self.position_bufs[i]) {
372 return false;
373 }
374 }
375
376 self.find_phrase_match_from_bufs()
379 }
380
381 fn find_phrase_match_from_bufs(&self) -> bool {
383 if self.position_bufs.is_empty() || self.position_bufs[0].is_empty() {
384 return false;
385 }
386
387 for &first_pos in &self.position_bufs[0] {
388 if self.check_phrase_from_position(first_pos, &self.position_bufs) {
389 return true;
390 }
391 }
392
393 false
394 }
395
396 fn check_phrase_from_position(&self, start_pos: u32, term_positions: &[Vec<u32>]) -> bool {
398 let mut expected_pos = start_pos;
399
400 for (i, positions) in term_positions.iter().enumerate() {
401 if i == 0 {
402 continue; }
404
405 expected_pos += 1;
406
407 let found = positions.iter().any(|&pos| {
409 if self.slop == 0 {
410 pos == expected_pos
411 } else {
412 let diff = pos.abs_diff(expected_pos);
413 diff <= self.slop
414 }
415 });
416
417 if !found {
418 return false;
419 }
420 }
421
422 true
423 }
424}
425
426impl super::docset::DocSet for PhraseScorer {
427 fn doc(&self) -> DocId {
428 self.current_doc
429 }
430
431 fn advance(&mut self) -> DocId {
432 if self.current_doc == TERMINATED {
433 return TERMINATED;
434 }
435
436 self.posting_iters[0].advance();
437 self.find_next_phrase_match();
438 self.current_doc
439 }
440
441 fn seek(&mut self, target: DocId) -> DocId {
442 if target == TERMINATED {
443 self.current_doc = TERMINATED;
444 return TERMINATED;
445 }
446
447 self.posting_iters[0].seek(target);
448 self.find_next_phrase_match();
449 self.current_doc
450 }
451
452 fn size_hint(&self) -> u32 {
453 0
454 }
455}
456
457impl Scorer for PhraseScorer {
458 fn score(&self) -> Score {
459 if self.current_doc == TERMINATED {
460 return 0.0;
461 }
462
463 let tf: f32 = self
465 .posting_iters
466 .iter()
467 .map(|it| it.term_freq() as f32)
468 .sum();
469
470 super::bm25_score(tf, self.idf, tf, self.avg_field_len) * 1.5
472 }
473}