Skip to main content

summa_core/query/
boost.rs

1//! Boost query - multiplies the score of the inner query
2
3use std::sync::Arc;
4
5use crate::segment::SegmentReader;
6use crate::{DocId, Score};
7
8use super::{CountFuture, Query, Scorer, ScorerFuture};
9
10/// Boost query - multiplies the score of the inner query
11#[derive(Clone)]
12pub struct BoostQuery {
13    pub inner: Arc<dyn Query>,
14    pub boost: f32,
15}
16
17impl std::fmt::Debug for BoostQuery {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        f.debug_struct("BoostQuery")
20            .field("boost", &self.boost)
21            .finish()
22    }
23}
24
25impl std::fmt::Display for BoostQuery {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(f, "{}^{}", self.inner, self.boost)
28    }
29}
30
31/// Options for the inner scorer of a boosted query. A boost other than one
32/// changes the score space, so the outer threshold must not be applied. A
33/// non-positive boost reverses or flattens the inner order, so a ranked
34/// handoff pre-truncated by positive BM25 order would return the wrong
35/// candidates; request the complete stream instead.
36fn boosted_inner_options(boost: f32, options: super::ScorerOptions) -> super::ScorerOptions {
37    if boost == 1.0 {
38        options
39    } else if boost <= 0.0 {
40        options.for_required_clause()
41    } else {
42        options.without_threshold()
43    }
44}
45
46impl BoostQuery {
47    pub fn new(query: impl Query + 'static, boost: f32) -> Self {
48        Self {
49            inner: Arc::new(query),
50            boost,
51        }
52    }
53}
54
55impl Query for BoostQuery {
56    fn physical_text_field(&self, reader: &SegmentReader, complete: bool) -> Option<crate::Field> {
57        self.inner
58            .physical_text_field(reader, complete || self.boost <= 0.0)
59    }
60    fn candidate_query(&self) -> crate::Result<crate::query::CandidateQuery> {
61        self.inner.candidate_query()?.boosted(self.boost)
62    }
63
64    fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
65        self.scorer_with_options(reader, limit, super::ScorerOptions::with_positions())
66    }
67
68    fn scorer_with_options<'a>(
69        &self,
70        reader: &'a SegmentReader,
71        limit: usize,
72        options: super::ScorerOptions,
73    ) -> ScorerFuture<'a> {
74        let inner = self.inner.clone();
75        let boost = self.boost;
76        Box::pin(async move {
77            if !boost.is_finite() {
78                return Err(crate::Error::Query(
79                    "boost must be a finite number".to_string(),
80                ));
81            }
82            let inner_options = boosted_inner_options(boost, options);
83            let inner_scorer = inner
84                .scorer_with_options(reader, limit, inner_options)
85                .await?;
86            Ok(Box::new(BoostScorer {
87                inner: inner_scorer,
88                boost,
89            }) as Box<dyn Scorer + 'a>)
90        })
91    }
92
93    #[cfg(feature = "sync")]
94    fn scorer_sync<'a>(
95        &self,
96        reader: &'a SegmentReader,
97        limit: usize,
98    ) -> crate::Result<Box<dyn Scorer + 'a>> {
99        self.scorer_sync_with_options(reader, limit, super::ScorerOptions::with_positions())
100    }
101
102    #[cfg(feature = "sync")]
103    fn scorer_sync_with_options<'a>(
104        &self,
105        reader: &'a SegmentReader,
106        limit: usize,
107        options: super::ScorerOptions,
108    ) -> crate::Result<Box<dyn Scorer + 'a>> {
109        if !self.boost.is_finite() {
110            return Err(crate::Error::Query(
111                "boost must be a finite number".to_string(),
112            ));
113        }
114        let inner_options = boosted_inner_options(self.boost, options);
115        let inner_scorer = self
116            .inner
117            .scorer_sync_with_options(reader, limit, inner_options)?;
118        Ok(Box::new(BoostScorer {
119            inner: inner_scorer,
120            boost: self.boost,
121        }) as Box<dyn Scorer + 'a>)
122    }
123
124    fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
125        let inner = self.inner.clone();
126        Box::pin(async move { inner.count_estimate(reader).await })
127    }
128
129    fn is_filter(&self) -> bool {
130        self.boost == 1.0 && self.inner.is_filter()
131    }
132
133    fn as_doc_predicate<'a>(&self, reader: &'a SegmentReader) -> Option<super::DocPredicate<'a>> {
134        (self.boost == 1.0)
135            .then(|| self.inner.as_doc_predicate(reader))
136            .flatten()
137    }
138
139    fn text_terms(&self, out: &mut Vec<(crate::dsl::Field, Vec<u8>)>) {
140        self.inner.text_terms(out);
141    }
142
143    fn decompose(&self) -> super::QueryDecomposition {
144        match self.inner.decompose() {
145            // A boosted text term stays on the text MaxScore path: the
146            // weight scales its idf, so scores and bounds scale together.
147            super::QueryDecomposition::TextTerm(mut info) => {
148                info.weight *= self.boost;
149                super::QueryDecomposition::TextTerm(info)
150            }
151            other if self.boost == 1.0 => other,
152            _ => super::QueryDecomposition::Opaque,
153        }
154    }
155}
156
157struct BoostScorer<'a> {
158    inner: Box<dyn Scorer + 'a>,
159    boost: f32,
160}
161
162impl super::docset::DocSet for BoostScorer<'_> {
163    fn doc(&self) -> DocId {
164        self.inner.doc()
165    }
166
167    fn advance(&mut self) -> DocId {
168        self.inner.advance()
169    }
170
171    fn seek(&mut self, target: DocId) -> DocId {
172        self.inner.seek(target)
173    }
174
175    fn size_hint(&self) -> u32 {
176        self.inner.size_hint()
177    }
178}
179
180impl Scorer for BoostScorer<'_> {
181    fn score(&self) -> Score {
182        self.inner.score() * self.boost
183    }
184
185    fn matched_positions(&self) -> Option<super::MatchedPositions> {
186        let mut positions = self.inner.matched_positions()?;
187        for (_, scored_positions) in &mut positions {
188            for position in scored_positions {
189                position.score *= self.boost;
190            }
191        }
192        Some(positions)
193    }
194}