mod execution;
mod formula;
mod model;
mod retrieved;
pub use formula::RankingModel;
use super::{MultiValueCombiner, PhraseQuery, QueryDecomposition};
use crate::{Error, Field, Result};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScoreScope {
Document,
Chunk,
}
#[derive(Clone, Debug)]
pub(crate) enum ScoreComponent {
Text(Vec<(Vec<u8>, f32)>),
Phrase(PhraseQuery),
Sparse(Vec<(u32, f32)>),
Dense(Vec<f32>),
Binary(Vec<u8>),
}
#[derive(Clone, Debug)]
pub struct CandidateQuery {
pub(crate) field: Field,
pub(crate) components: Vec<(ScoreComponent, f32)>,
document: DocumentExpression,
}
#[derive(Clone, Debug)]
enum DocumentExpression {
Component(usize, MultiValueCombiner),
Sum(Vec<Self>),
Boost(Box<Self>, f32),
}
impl DocumentExpression {
fn validate(&self) -> Result<()> {
match self {
Self::Component(_, combiner) => combiner.validate().map_err(Error::Query),
Self::Sum(children) => children.iter().try_for_each(Self::validate),
Self::Boost(child, boost) => {
if !boost.is_finite() {
return Err(Error::Query("L1 query boost must be finite".into()));
}
child.validate()
}
}
}
fn rebase(&mut self, offset: usize) {
match self {
Self::Component(index, _) => *index += offset,
Self::Sum(children) => children.iter_mut().for_each(|child| child.rebase(offset)),
Self::Boost(child, _) => child.rebase(offset),
}
}
fn score(&self, components: &[Vec<f32>], locations: &[(u32, usize)]) -> Result<f32> {
let value = match self {
Self::Component(index, combiner) => {
let values: smallvec::SmallVec<[(u32, f32); 16]> = locations
.iter()
.map(|&(ordinal, position)| (ordinal, components[*index][position]))
.collect();
combiner.combine(&values)
}
Self::Sum(children) => {
let mut value = 0.0;
for child in children {
value += child.score(components, locations)?;
}
value
}
Self::Boost(child, boost) => child.score(components, locations)? * boost,
};
if !value.is_finite() {
return Err(Error::Query(
"L1 document feature reduction overflow".into(),
));
}
Ok(value)
}
}
impl CandidateQuery {
pub fn field(&self) -> Field {
self.field
}
pub(crate) fn new(field: Field, component: ScoreComponent) -> Self {
Self {
field,
components: vec![(component, 1.0)],
document: DocumentExpression::Component(0, MultiValueCombiner::Max),
}
}
pub(crate) fn with_combiner(mut self, combiner: MultiValueCombiner) -> Self {
self.document = DocumentExpression::Component(0, combiner);
self
}
pub(crate) fn from_decomposition(decomposition: QueryDecomposition) -> Result<Self> {
match decomposition {
QueryDecomposition::TextTerm(term) if term.global_stats.is_none() => Ok(Self::new(term.field, ScoreComponent::Text(vec![(term.term, term.weight)]))),
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())),
QueryDecomposition::SparseTerms(infos) if !infos.is_empty() => {
let field = infos[0].field;
if infos.iter().any(|info| info.field != field) { return Err(Error::Query("L1 branch mixes sparse fields".into())); }
let combiner = infos[0].combiner;
if infos.iter().any(|info| info.combiner != combiner) { return Err(Error::Query("L1 sparse composition mixes document combiners".into())); }
Ok(Self::new(field, ScoreComponent::Sparse(infos.into_iter().map(|info| (info.dim_id, info.weight)).collect())).with_combiner(combiner))
}
_ => 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())),
}
}
pub(crate) fn boosted(mut self, weight: f32) -> Result<Self> {
self.document = DocumentExpression::Boost(Box::new(self.document), weight);
for (_, boost) in &mut self.components {
*boost *= weight;
if !boost.is_finite() {
return Err(Error::Query("L1 query boost overflow".into()));
}
}
Ok(self)
}
pub(crate) fn sum(queries: impl IntoIterator<Item = Result<Self>>) -> Result<Self> {
let mut result: Option<Self> = None;
for query in queries {
let mut query = query?;
if let Some(current) = &mut result {
if current.field != query.field {
return Err(Error::Query("L1 scoring branch must use one field; name separate branches for separate fields".into()));
}
query.document.rebase(current.components.len());
current.components.extend(query.components);
let previous =
std::mem::replace(&mut current.document, DocumentExpression::Sum(Vec::new()));
current.document = match previous {
DocumentExpression::Sum(mut children) => {
children.push(query.document);
DocumentExpression::Sum(children)
}
previous => DocumentExpression::Sum(vec![previous, query.document]),
};
} else {
result = Some(query);
}
}
result.ok_or_else(|| Error::Query("L1 scoring branch is empty".into()))
}
pub fn text_terms(&self, out: &mut Vec<(Field, Vec<u8>)>) {
for (component, _) in &self.components {
match component {
ScoreComponent::Text(terms) => {
out.extend(terms.iter().map(|(term, _)| (self.field, term.clone())))
}
ScoreComponent::Phrase(query) => {
out.extend(query.terms.iter().map(|term| (self.field, term.clone())))
}
_ => {}
}
}
}
}
#[derive(Clone, Debug)]
pub struct CandidateFeature {
pub name: String,
pub scope: ScoreScope,
pub query: CandidateQuery,
}
#[derive(Clone, Debug)]
pub struct CandidateScoringPlan {
pub features: Vec<CandidateFeature>,
pub backfill: bool,
pub model: Option<RankingModel>,
pub export_passages: usize,
pub all_passages: bool,
pub seed_document_passages: bool,
pub document_combiner: MultiValueCombiner,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct PassageFeatures {
pub ordinal: u16,
pub score: f32,
pub values: Vec<Option<f32>>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct CandidateScores {
pub document: Vec<Option<f32>>,
pub passages: Vec<PassageFeatures>,
pub scored_passages: usize,
}
#[derive(Clone, Debug)]
pub struct ScoredCandidate {
pub result: super::SearchResult,
pub features: CandidateScores,
}
#[cfg(test)]
mod tests;