summa_core/query/candidate_scoring/
mod.rs1mod execution;
4mod formula;
5mod model;
6mod retrieved;
7pub use formula::RankingModel;
8
9use super::{MultiValueCombiner, PhraseQuery, QueryDecomposition};
10use crate::{Error, Field, Result};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum ScoreScope {
16 Document,
17 Chunk,
18}
19
20#[derive(Clone, Debug)]
21pub(crate) enum ScoreComponent {
22 Text(Vec<(Vec<u8>, f32)>),
23 Phrase(PhraseQuery),
24 Sparse(Vec<(u32, f32)>),
25 Dense(Vec<f32>),
26 Binary(Vec<u8>),
27}
28
29#[derive(Clone, Debug)]
33pub struct CandidateQuery {
34 pub(crate) field: Field,
35 pub(crate) components: Vec<(ScoreComponent, f32)>,
36 document: DocumentExpression,
37}
38
39#[derive(Clone, Debug)]
42enum DocumentExpression {
43 Component(usize, MultiValueCombiner),
44 Sum(Vec<Self>),
45 Boost(Box<Self>, f32),
46}
47impl DocumentExpression {
48 fn validate(&self) -> Result<()> {
49 match self {
50 Self::Component(_, combiner) => combiner.validate().map_err(Error::Query),
51 Self::Sum(children) => children.iter().try_for_each(Self::validate),
52 Self::Boost(child, boost) => {
53 if !boost.is_finite() {
54 return Err(Error::Query("L1 query boost must be finite".into()));
55 }
56 child.validate()
57 }
58 }
59 }
60 fn rebase(&mut self, offset: usize) {
61 match self {
62 Self::Component(index, _) => *index += offset,
63 Self::Sum(children) => children.iter_mut().for_each(|child| child.rebase(offset)),
64 Self::Boost(child, _) => child.rebase(offset),
65 }
66 }
67 fn score(&self, components: &[Vec<f32>], locations: &[(u32, usize)]) -> Result<f32> {
68 let value = match self {
69 Self::Component(index, combiner) => {
70 let values: smallvec::SmallVec<[(u32, f32); 16]> = locations
71 .iter()
72 .map(|&(ordinal, position)| (ordinal, components[*index][position]))
73 .collect();
74 combiner.combine(&values)
75 }
76 Self::Sum(children) => {
77 let mut value = 0.0;
78 for child in children {
79 value += child.score(components, locations)?;
80 }
81 value
82 }
83 Self::Boost(child, boost) => child.score(components, locations)? * boost,
84 };
85 if !value.is_finite() {
86 return Err(Error::Query(
87 "L1 document feature reduction overflow".into(),
88 ));
89 }
90 Ok(value)
91 }
92}
93impl CandidateQuery {
94 pub fn field(&self) -> Field {
95 self.field
96 }
97 pub(crate) fn new(field: Field, component: ScoreComponent) -> Self {
98 Self {
99 field,
100 components: vec![(component, 1.0)],
101 document: DocumentExpression::Component(0, MultiValueCombiner::Max),
102 }
103 }
104 pub(crate) fn with_combiner(mut self, combiner: MultiValueCombiner) -> Self {
105 self.document = DocumentExpression::Component(0, combiner);
106 self
107 }
108 pub(crate) fn from_decomposition(decomposition: QueryDecomposition) -> Result<Self> {
109 match decomposition {
110 QueryDecomposition::TextTerm(term) if term.global_stats.is_none() => Ok(Self::new(term.field, ScoreComponent::Text(vec![(term.term, term.weight)]))),
111 QueryDecomposition::TextTerm(_) => Err(Error::Query("term carries query-owned global statistics; L1 backfill scores with index statistics, so drop `with_global_stats` on the backfilled term".into())),
112 QueryDecomposition::SparseTerms(infos) if !infos.is_empty() => {
113 let field = infos[0].field;
114 if infos.iter().any(|info| info.field != field) { return Err(Error::Query("L1 branch mixes sparse fields".into())); }
115 let combiner = infos[0].combiner;
116 if infos.iter().any(|info| info.combiner != combiner) { return Err(Error::Query("L1 sparse composition mixes document combiners".into())); }
117 Ok(Self::new(field, ScoreComponent::Sparse(infos.into_iter().map(|info| (info.dim_id, info.weight)).collect())).with_combiner(combiner))
118 }
119 _ => Err(Error::Query("query cannot be backfilled as an L1 score; use text/phrase/vector scoring branches and the common fusion filter for eligibility".into())),
120 }
121 }
122 pub(crate) fn boosted(mut self, weight: f32) -> Result<Self> {
123 self.document = DocumentExpression::Boost(Box::new(self.document), weight);
124 for (_, boost) in &mut self.components {
125 *boost *= weight;
126 if !boost.is_finite() {
127 return Err(Error::Query("L1 query boost overflow".into()));
128 }
129 }
130 Ok(self)
131 }
132 pub(crate) fn sum(queries: impl IntoIterator<Item = Result<Self>>) -> Result<Self> {
133 let mut result: Option<Self> = None;
134 for query in queries {
135 let mut query = query?;
136 if let Some(current) = &mut result {
137 if current.field != query.field {
138 return Err(Error::Query("L1 scoring branch must use one field; name separate branches for separate fields".into()));
139 }
140 query.document.rebase(current.components.len());
141 current.components.extend(query.components);
142 let previous =
143 std::mem::replace(&mut current.document, DocumentExpression::Sum(Vec::new()));
144 current.document = match previous {
145 DocumentExpression::Sum(mut children) => {
146 children.push(query.document);
147 DocumentExpression::Sum(children)
148 }
149 previous => DocumentExpression::Sum(vec![previous, query.document]),
150 };
151 } else {
152 result = Some(query);
153 }
154 }
155 result.ok_or_else(|| Error::Query("L1 scoring branch is empty".into()))
156 }
157 pub fn text_terms(&self, out: &mut Vec<(Field, Vec<u8>)>) {
158 for (component, _) in &self.components {
159 match component {
160 ScoreComponent::Text(terms) => {
161 out.extend(terms.iter().map(|(term, _)| (self.field, term.clone())))
162 }
163 ScoreComponent::Phrase(query) => {
164 out.extend(query.terms.iter().map(|term| (self.field, term.clone())))
165 }
166 _ => {}
167 }
168 }
169 }
170}
171
172#[derive(Clone, Debug)]
173pub struct CandidateFeature {
174 pub name: String,
175 pub scope: ScoreScope,
176 pub query: CandidateQuery,
177}
178
179#[derive(Clone, Debug)]
180pub struct CandidateScoringPlan {
181 pub features: Vec<CandidateFeature>,
182 pub backfill: bool,
184 pub model: Option<RankingModel>,
186 pub export_passages: usize,
189 pub all_passages: bool,
192 pub seed_document_passages: bool,
194 pub document_combiner: MultiValueCombiner,
196}
197
198#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
199pub struct PassageFeatures {
200 pub ordinal: u16,
201 pub score: f32,
202 pub values: Vec<Option<f32>>,
203}
204#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
205pub struct CandidateScores {
206 pub document: Vec<Option<f32>>,
208 pub passages: Vec<PassageFeatures>,
209 pub scored_passages: usize,
210}
211#[derive(Clone, Debug)]
212pub struct ScoredCandidate {
213 pub result: super::SearchResult,
214 pub features: CandidateScores,
215}
216
217#[cfg(test)]
218mod tests;