laurus 0.4.0

Unified search library for lexical, vector, and semantic retrieval
//! Searcher trait for lexical search execution.

use std::collections::HashMap;
use std::sync::Arc;

use crate::error::Result;
use crate::lexical::query::{LexicalSearchResults, Query};

/// Sort order for search results.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortOrder {
    /// Ascending order (lowest to highest).
    #[default]
    Asc,
    /// Descending order (highest to lowest).
    Desc,
}

/// Field to sort search results by.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum SortField {
    /// Sort by relevance score (default).
    #[default]
    Score,
    /// Sort by a document field value.
    Field {
        /// Field name to sort by.
        name: String,
        /// Sort order.
        order: SortOrder,
    },
}

/// Configuration for search operations.
#[derive(Debug, Clone)]
pub struct LexicalSearchParams {
    /// Maximum number of results to return.
    pub limit: usize,
    /// Minimum score threshold.
    pub min_score: f32,
    /// Whether to load document content.
    pub load_documents: bool,
    /// Timeout for search operations in milliseconds.
    pub timeout_ms: Option<u64>,
    /// Enable parallel search for better performance on multi-core systems.
    pub parallel: bool,
    /// Sort results by field or score.
    pub sort_by: SortField,
}

impl Default for LexicalSearchParams {
    fn default() -> Self {
        LexicalSearchParams {
            limit: 10,
            min_score: 0.0,
            load_documents: true,
            timeout_ms: None,
            parallel: false,
            sort_by: SortField::default(),
        }
    }
}

/// Query representation that can be either a DSL string or a Query object.
#[derive(Debug)]
pub enum LexicalSearchQuery {
    /// Query specified as a DSL string (will be parsed at search time).
    Dsl(String),
    /// Query specified as a Query object.
    Obj(Box<dyn Query>),
}

/// Search request containing query and configuration.
#[derive(Debug)]
pub struct LexicalSearchRequest {
    /// The query to execute.
    pub query: LexicalSearchQuery,
    /// Search configuration.
    pub params: LexicalSearchParams,
    /// Field-level boosts for lexical scoring.
    /// Applied at the Engine level before search execution.
    pub field_boosts: HashMap<String, f32>,
}

impl Clone for LexicalSearchQuery {
    fn clone(&self) -> Self {
        match self {
            LexicalSearchQuery::Dsl(s) => LexicalSearchQuery::Dsl(s.clone()),
            LexicalSearchQuery::Obj(q) => LexicalSearchQuery::Obj(q.clone_box()),
        }
    }
}

impl Clone for LexicalSearchRequest {
    fn clone(&self) -> Self {
        LexicalSearchRequest {
            query: self.query.clone(),
            params: self.params.clone(),
            field_boosts: self.field_boosts.clone(),
        }
    }
}

impl From<String> for LexicalSearchQuery {
    fn from(s: String) -> Self {
        LexicalSearchQuery::Dsl(s)
    }
}

impl From<&str> for LexicalSearchQuery {
    fn from(s: &str) -> Self {
        LexicalSearchQuery::Dsl(s.to_string())
    }
}

impl From<Box<dyn Query>> for LexicalSearchQuery {
    fn from(q: Box<dyn Query>) -> Self {
        LexicalSearchQuery::Obj(q)
    }
}

impl LexicalSearchQuery {
    /// Parse DSL string into Query object using the given analyzer.
    pub fn into_query(
        self,
        analyzer: &Arc<dyn crate::analysis::analyzer::analyzer::Analyzer>,
    ) -> crate::error::Result<Box<dyn Query>> {
        match self {
            LexicalSearchQuery::Dsl(dsl_string) => {
                let parser =
                    crate::lexical::query::parser::LexicalQueryParser::new(analyzer.clone());
                parser.parse(&dsl_string)
            }
            LexicalSearchQuery::Obj(query) => Ok(query),
        }
    }

    /// Extract the Query object.
    ///
    /// Returns an error if this is a DSL string variant.
    pub fn unwrap_query(self) -> crate::error::Result<Box<dyn Query>> {
        match self {
            LexicalSearchQuery::Obj(query) => Ok(query),
            LexicalSearchQuery::Dsl(_) => Err(crate::error::LaurusError::invalid_argument(
                "Expected Query object, found DSL string",
            )),
        }
    }
}

impl LexicalSearchRequest {
    /// Create a new search request from a query object.
    pub fn new(query: Box<dyn Query>) -> Self {
        LexicalSearchRequest {
            query: LexicalSearchQuery::Obj(query),
            params: LexicalSearchParams::default(),
            field_boosts: HashMap::new(),
        }
    }

    /// Create a new search request from a DSL query string.
    pub fn from_dsl(dsl: impl Into<String>) -> Self {
        LexicalSearchRequest {
            query: LexicalSearchQuery::Dsl(dsl.into()),
            params: LexicalSearchParams::default(),
            field_boosts: HashMap::new(),
        }
    }

    /// Set the maximum number of results to return.
    pub fn limit(mut self, limit: usize) -> Self {
        self.params.limit = limit;
        self
    }

    /// Set the minimum score threshold.
    pub fn min_score(mut self, min_score: f32) -> Self {
        self.params.min_score = min_score;
        self
    }

    /// Set whether to load document content.
    pub fn load_documents(mut self, load_documents: bool) -> Self {
        self.params.load_documents = load_documents;
        self
    }

    /// Set the search timeout.
    pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
        self.params.timeout_ms = Some(timeout_ms);
        self
    }

    /// Enable parallel search.
    pub fn parallel(mut self, parallel: bool) -> Self {
        self.params.parallel = parallel;
        self
    }

    /// Sort results by a field in ascending order.
    pub fn sort_by_field_asc(mut self, field: &str) -> Self {
        self.params.sort_by = SortField::Field {
            name: field.to_string(),
            order: SortOrder::Asc,
        };
        self
    }

    /// Sort results by a field in descending order.
    pub fn sort_by_field_desc(mut self, field: &str) -> Self {
        self.params.sort_by = SortField::Field {
            name: field.to_string(),
            order: SortOrder::Desc,
        };
        self
    }

    /// Sort results by relevance score (default).
    pub fn sort_by_score(mut self) -> Self {
        self.params.sort_by = SortField::Score;
        self
    }

    /// Add a field-level boost for lexical scoring.
    pub fn with_field_boost(mut self, field: impl Into<String>, boost: f32) -> Self {
        self.field_boosts.insert(field.into(), boost);
        self
    }
}

/// Trait for lexical search implementations.
///
/// This trait defines the interface for executing searches against lexical indexes.
pub trait LexicalSearcher: Send + Sync + std::fmt::Debug {
    /// Execute a search with the given request.
    fn search(&self, request: LexicalSearchRequest) -> Result<LexicalSearchResults>;

    /// Count the number of matching documents for a request.
    ///
    /// Returns the number of documents that match the given search request,
    /// applying the min_score threshold if specified in the request parameters.
    fn count(&self, request: LexicalSearchRequest) -> Result<u64>;
}