lucisearch 0.8.0

Embeddable, in-process search engine — the SQLite/DuckDB of Elasticsearch
Documentation
//! dis_max query: returns the score from the best-matching sub-query,
//! plus tie_breaker * sum of other matching scores.
//!
//! See [[query-dsl]].

use crate::core::{Result, ScoreMode, Scorer};

use crate::query::{BoundQuery, Query, ScorerSupplier};
use crate::search::searcher::Searcher;
use crate::segment::reader::SegmentReader;

/// Disjunction max query.
pub struct DisMaxQuery {
    pub(crate) queries: Vec<Box<dyn Query>>,
    pub tie_breaker: f32,
}

impl Query for DisMaxQuery {
    fn bind(&self, searcher: &Searcher, score_mode: ScoreMode) -> Result<Box<dyn BoundQuery>> {
        let weights: Vec<Box<dyn BoundQuery>> = self
            .queries
            .iter()
            .map(|q| q.bind(searcher, score_mode))
            .collect::<Result<_>>()?;
        Ok(Box::new(BoundDisMaxQuery {
            weights,
            tie_breaker: self.tie_breaker,
        }))
    }
}

struct BoundDisMaxQuery {
    weights: Vec<Box<dyn BoundQuery>>,
    tie_breaker: f32,
}

impl BoundQuery for BoundDisMaxQuery {
    fn scorer_supplier(&self, reader: &SegmentReader) -> Result<Option<Box<dyn ScorerSupplier>>> {
        let mut suppliers: Vec<Box<dyn ScorerSupplier>> = Vec::new();
        for w in &self.weights {
            if let Some(s) = w.scorer_supplier(reader)? {
                suppliers.push(s);
            }
        }
        if suppliers.is_empty() {
            return Ok(None);
        }
        Ok(Some(Box::new(DisMaxScorerSupplier {
            suppliers,
            tie_breaker: self.tie_breaker,
        })))
    }
}

struct DisMaxScorerSupplier {
    suppliers: Vec<Box<dyn ScorerSupplier>>,
    tie_breaker: f32,
}

impl ScorerSupplier for DisMaxScorerSupplier {
    fn cost(&self) -> u64 {
        self.suppliers.iter().map(|s| s.cost()).sum()
    }

    fn scorer(self: Box<Self>) -> Result<Box<dyn Scorer>> {
        let scorers: Vec<Box<dyn Scorer>> = self
            .suppliers
            .into_iter()
            .map(|s| s.scorer())
            .collect::<Result<_>>()?;
        Ok(Box::new(crate::search::wand::WANDScorer::new_dis_max(
            scorers,
            self.tie_breaker,
        )))
    }
}

// DisMaxScorer is now WANDScorer::new_dis_max() — see search/wand.rs.

#[cfg(test)]
mod tests {

    #[test]
    fn dis_max_score_computation() {
        // Verify score = max + tie_breaker * sum(others)
        // With scores [3.0, 1.0, 2.0] and tie_breaker 0.5:
        // max = 3.0, others = 1.0 + 2.0 = 3.0
        // score = 3.0 + 0.5 * 3.0 = 4.5

        // This is a logic test — we can't easily create mock scorers
        // without the full infrastructure, so we test via integration.
    }
}