uqa_scoring/text/
candidates.rs1use super::TextSearchError;
10use crate::{BM25Params, BayesianBM25Params, ScoringMode};
11use uqa_core::{
12 memory::{BudgetedVec, MemoryBudget},
13 IndexStats,
14};
15
16enum CandidateMode {
17 BM25(BM25Params),
18 Bayesian(BayesianBM25Params),
19}
20impl CandidateMode {
21 fn params(&self) -> &BM25Params {
22 match self {
23 Self::BM25(params) => params,
24 Self::Bayesian(params) => ¶ms.bm25,
25 }
26 }
27 fn finalize(&self, sum: f64) -> f64 {
28 match self {
29 Self::BM25(_) => sum,
30 Self::Bayesian(scorer) => scorer.calibrate_raw_value(sum),
31 }
32 }
33}
34
35pub struct TextCandidateScorer {
37 scorer: CandidateMode,
38 avg_doc_length: f64,
39 idfs: BudgetedVec<f64>,
40}
41
42impl TextCandidateScorer {
43 pub fn new(
44 mode: &ScoringMode,
45 stats: IndexStats,
46 document_frequencies: &[u64],
47 ) -> Result<Self, TextSearchError> {
48 Self::new_budgeted(
49 mode,
50 stats,
51 document_frequencies,
52 &MemoryBudget::new(usize::MAX),
53 || Ok(()),
54 )
55 }
56
57 pub fn new_budgeted(
61 mode: &ScoringMode,
62 stats: IndexStats,
63 document_frequencies: &[u64],
64 budget: &MemoryBudget,
65 mut poll: impl FnMut() -> Result<(), TextSearchError>,
66 ) -> Result<Self, TextSearchError> {
67 poll()?;
68 let total_docs = stats.total_docs;
69 let avg_doc_length = stats.avg_doc_length;
70 drop(stats);
71 let scorer = match mode {
72 ScoringMode::BM25(params) => {
73 params.validate()?;
74 CandidateMode::BM25(*params)
75 }
76 ScoringMode::BayesianBM25(params) => {
77 let params = params.scaled_for_query_terms(document_frequencies.len());
78 crate::bayesian_bm25::validate_params(params, avg_doc_length)?;
79 CandidateMode::Bayesian(params)
80 }
81 };
82 let mut output = Self {
83 scorer,
84 avg_doc_length,
85 idfs: BudgetedVec::new(budget),
86 };
87 output.idfs.reserve(document_frequencies.len())?;
88 for frequency in document_frequencies {
89 poll()?;
90 output.idfs.push(crate::bm25::idf(total_docs, *frequency))?;
91 }
92 poll()?;
93 Ok(output)
94 }
95
96 pub fn score_document(
97 &mut self,
98 document_length: u64,
99 term_frequencies: &[u64],
100 ) -> Result<f64, TextSearchError> {
101 self.score_document_with_control(document_length, term_frequencies, || Ok(()))
102 }
103
104 pub fn score_document_with_control(
106 &self,
107 document_length: u64,
108 term_frequencies: &[u64],
109 mut poll: impl FnMut() -> Result<(), TextSearchError>,
110 ) -> Result<f64, TextSearchError> {
111 poll()?;
112 if term_frequencies.len() != self.idfs.len() {
113 return Err(TextSearchError::InvalidIndex(
114 "candidate frequencies do not match the emitted query term count".into(),
115 ));
116 }
117 let mut sum = -0.0;
119 for (idf, frequency) in self.idfs.iter().zip(term_frequencies) {
120 poll()?;
121 sum += self.scorer.params().score_with_idf(
122 self.avg_doc_length,
123 *frequency,
124 document_length,
125 *idf,
126 );
127 }
128 poll()?;
129 Ok(self.scorer.finalize(sum))
130 }
131}
132
133#[cfg(test)]
134mod tests;