Skip to main content

laurus/engine/
search.rs

1use crate::lexical::query::Query;
2use crate::lexical::search::searcher::LexicalSearchRequest;
3use crate::vector::store::request::VectorSearchRequest;
4
5/// Unified search request.
6pub struct SearchRequest {
7    /// Lexical search request.
8    pub lexical_search_request: Option<LexicalSearchRequest>,
9
10    /// Vector search request.
11    pub vector_search_request: Option<VectorSearchRequest>,
12
13    /// Maximum number of results to return.
14    pub limit: usize,
15
16    /// Number of results to skip before returning (for pagination).
17    pub offset: usize,
18
19    /// Hybrid fusion algorithm to use (if both queries are present).
20    pub fusion_algorithm: Option<FusionAlgorithm>,
21
22    /// Filter query (lexical) to restrict search space.
23    /// Documents matching this query will be candidates for vector search.
24    pub filter_query: Option<Box<dyn Query>>,
25}
26
27/// Algorithm used to combine lexical and vector scores.
28#[derive(Debug, Clone, Copy)]
29pub enum FusionAlgorithm {
30    /// Reciprocal Rank Fusion (RRF).
31    /// Good when scores are not comparable (e.g. BM25 vs Cosine).
32    RRF {
33        /// Constant k (default 60).
34        k: f64,
35    },
36
37    /// Weighted Sum.
38    /// Requires normalized scores.
39    WeightedSum {
40        /// Weight for lexical score (0.0 - 1.0).
41        lexical_weight: f32,
42        /// Weight for vector score (0.0 - 1.0).
43        vector_weight: f32,
44    },
45}
46
47impl Default for SearchRequest {
48    fn default() -> Self {
49        Self {
50            lexical_search_request: None,
51            vector_search_request: None,
52            limit: 10,
53            offset: 0,
54            fusion_algorithm: None,
55            filter_query: None,
56        }
57    }
58}
59
60pub struct SearchRequestBuilder {
61    request: SearchRequest,
62}
63
64impl Default for SearchRequestBuilder {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl SearchRequestBuilder {
71    pub fn new() -> Self {
72        Self {
73            request: SearchRequest::default(),
74        }
75    }
76
77    /// Set the lexical search request.
78    pub fn lexical_search_request(mut self, request: LexicalSearchRequest) -> Self {
79        self.request.lexical_search_request = Some(request);
80        self
81    }
82
83    /// Set the vector search request.
84    pub fn vector_search_request(mut self, request: VectorSearchRequest) -> Self {
85        self.request.vector_search_request = Some(request);
86        self
87    }
88
89    pub fn limit(mut self, limit: usize) -> Self {
90        self.request.limit = limit;
91        self
92    }
93
94    pub fn offset(mut self, offset: usize) -> Self {
95        self.request.offset = offset;
96        self
97    }
98
99    pub fn fusion_algorithm(mut self, fusion: FusionAlgorithm) -> Self {
100        // Clamp weights to valid range to prevent NaN/Inf propagation.
101        let fusion = match fusion {
102            FusionAlgorithm::WeightedSum {
103                lexical_weight,
104                vector_weight,
105            } => FusionAlgorithm::WeightedSum {
106                lexical_weight: lexical_weight.clamp(0.0, 1.0),
107                vector_weight: vector_weight.clamp(0.0, 1.0),
108            },
109            other => other,
110        };
111        self.request.fusion_algorithm = Some(fusion);
112        self
113    }
114
115    pub fn filter_query(mut self, query: Box<dyn Query>) -> Self {
116        self.request.filter_query = Some(query);
117        self
118    }
119
120    /// Add a field-level boost for lexical search.
121    ///
122    /// Note: This requires `with_lexical()` to have been called first.
123    /// If no lexical query has been set, this is a no-op.
124    pub fn add_field_boost(mut self, field: impl Into<String>, boost: f32) -> Self {
125        if let Some(ref mut lex) = self.request.lexical_search_request {
126            lex.field_boosts.insert(field.into(), boost);
127        }
128        self
129    }
130
131    pub fn build(self) -> SearchRequest {
132        self.request
133    }
134}
135
136#[derive(Debug, Clone)]
137pub struct SearchResult {
138    /// External document ID.
139    pub id: String,
140    pub score: f32,
141    pub document: Option<crate::data::Document>,
142}