Skip to main content

sonic/query/
builder.rs

1// Sonic
2//
3// Fast, lightweight and schema-less search backend
4// Copyright: 2019, Valerian Saliou <valerian@valeriansaliou.name>
5// Copyright: 2026, Rémi Bardon <remi@remibardon.name>
6// License: Mozilla Public License v2.0 (MPL v2.0)
7
8use super::Query;
9use super::types::{QueryGenericLang, QuerySearchLimit, QuerySearchOffset};
10use crate::config::{ConfigNormalization, ConfigStopwords, ConfigTokenization};
11use crate::lexer::{TokenLexerBuilder, TokenLexerMode};
12use crate::store::StoreItemBuilder;
13
14impl<'a> Query<'a> {
15    #[allow(clippy::too_many_arguments)] // This will be reworked at some point.
16    pub fn search(
17        query_id: &'a str,
18        collection: &'a str,
19        bucket: &'a str,
20        terms: &'a str,
21        limit: QuerySearchLimit,
22        offset: QuerySearchOffset,
23        lang: Option<QueryGenericLang>,
24        normalization_config: ConfigNormalization,
25        tokenization_config: ConfigTokenization,
26        stopwords_config: &'a ConfigStopwords,
27    ) -> Result<Self, ()> {
28        match (
29            StoreItemBuilder::from_depth_2(collection, bucket),
30            TokenLexerBuilder::from(
31                TokenLexerMode::from_query_lang(&lang),
32                lang.and_then(QueryGenericLang::into_lang_opt),
33                terms,
34                normalization_config,
35                tokenization_config,
36                stopwords_config,
37            ),
38        ) {
39            (Ok(store), Ok(text_lexed)) => {
40                Ok(Query::Search(store, query_id, text_lexed, limit, offset))
41            }
42            _ => Err(()),
43        }
44    }
45
46    #[allow(clippy::too_many_arguments)] // This will be reworked at some point.
47    pub fn suggest(
48        query_id: &'a str,
49        collection: &'a str,
50        bucket: &'a str,
51        terms: &'a str,
52        limit: QuerySearchLimit,
53        normalization_config: ConfigNormalization,
54        tokenization_config: ConfigTokenization,
55        stopwords_config: &'a ConfigStopwords,
56    ) -> Result<Self, ()> {
57        match (
58            StoreItemBuilder::from_depth_2(collection, bucket),
59            TokenLexerBuilder::from(
60                TokenLexerMode::NormalizeOnly,
61                None,
62                terms,
63                normalization_config,
64                tokenization_config,
65                stopwords_config,
66            ),
67        ) {
68            (Ok(store), Ok(text_lexed)) => Ok(Query::Suggest(store, query_id, text_lexed, limit)),
69            _ => Err(()),
70        }
71    }
72
73    pub fn list(
74        query_id: &'a str,
75        collection: &'a str,
76        bucket: &'a str,
77        limit: QuerySearchLimit,
78        offset: QuerySearchOffset,
79    ) -> Result<Self, ()> {
80        match StoreItemBuilder::from_depth_2(collection, bucket) {
81            Ok(store) => Ok(Query::List(store, query_id, limit, offset)),
82            _ => Err(()),
83        }
84    }
85
86    #[allow(clippy::too_many_arguments)] // This will be reworked at some point.
87    pub fn push(
88        collection: &'a str,
89        bucket: &'a str,
90        object: &'a str,
91        text: &'a str,
92        lang: Option<QueryGenericLang>,
93        normalization_config: ConfigNormalization,
94        tokenization_config: ConfigTokenization,
95        stopwords_config: &'a ConfigStopwords,
96    ) -> Result<Self, ()> {
97        match (
98            StoreItemBuilder::from_depth_3(collection, bucket, object),
99            TokenLexerBuilder::from(
100                TokenLexerMode::from_query_lang(&lang),
101                lang.and_then(QueryGenericLang::into_lang_opt),
102                text,
103                normalization_config,
104                tokenization_config,
105                stopwords_config,
106            ),
107        ) {
108            (Ok(store), Ok(text_lexed)) => Ok(Query::Push(store, text_lexed)),
109            _ => Err(()),
110        }
111    }
112
113    pub fn pop(
114        collection: &'a str,
115        bucket: &'a str,
116        object: &'a str,
117        text: &'a str,
118        normalization_config: ConfigNormalization,
119        tokenization_config: ConfigTokenization,
120        stopwords_config: &'a ConfigStopwords,
121    ) -> Result<Self, ()> {
122        match (
123            StoreItemBuilder::from_depth_3(collection, bucket, object),
124            TokenLexerBuilder::from(
125                TokenLexerMode::NormalizeOnly,
126                None,
127                text,
128                normalization_config,
129                tokenization_config,
130                stopwords_config,
131            ),
132        ) {
133            (Ok(store), Ok(text_lexed)) => Ok(Query::Pop(store, text_lexed)),
134            _ => Err(()),
135        }
136    }
137
138    pub fn count(
139        collection: &'a str,
140        bucket: Option<&'a str>,
141        object: Option<&'a str>,
142    ) -> Result<Self, ()> {
143        let store_result = match (bucket, object) {
144            (Some(bucket_inner), Some(object_inner)) => {
145                StoreItemBuilder::from_depth_3(collection, bucket_inner, object_inner)
146            }
147            (Some(bucket_inner), None) => StoreItemBuilder::from_depth_2(collection, bucket_inner),
148            _ => StoreItemBuilder::from_depth_1(collection),
149        };
150
151        match store_result {
152            Ok(store) => Ok(Query::Count(store)),
153            _ => Err(()),
154        }
155    }
156
157    pub fn flushc(collection: &'a str) -> Result<Self, ()> {
158        match StoreItemBuilder::from_depth_1(collection) {
159            Ok(store) => Ok(Query::FlushC(store)),
160            _ => Err(()),
161        }
162    }
163
164    pub fn flushb(collection: &'a str, bucket: &'a str) -> Result<Self, ()> {
165        match StoreItemBuilder::from_depth_2(collection, bucket) {
166            Ok(store) => Ok(Query::FlushB(store)),
167            _ => Err(()),
168        }
169    }
170
171    pub fn flusho(collection: &'a str, bucket: &'a str, object: &'a str) -> Result<Self, ()> {
172        match StoreItemBuilder::from_depth_3(collection, bucket, object) {
173            Ok(store) => Ok(Query::FlushO(store)),
174            _ => Err(()),
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use std::sync::LazyLock;
182
183    use super::*;
184
185    const NORMALIZATION_CONFIG: ConfigNormalization = ConfigNormalization {
186        unicode_normalization: None,
187        diacritic_folding_enabled: false,
188        stemming_enabled: false,
189    };
190    const TOKENIZATION_CONFIG: ConfigTokenization = ConfigTokenization {
191        detect_special_patterns: true,
192        compat_split_special_patterns: false,
193    };
194    static STOPWORDS_CONFIG: LazyLock<ConfigStopwords> = LazyLock::new(|| ConfigStopwords {
195        allow: Default::default(),
196        deny: Default::default(),
197    });
198
199    #[test]
200    fn it_builds_search_query() {
201        #[rustfmt::skip]
202        assert!(Query::search(
203            "id1", "c:test:1", "b:test:1", "Michael Dake", 10, 20, None,
204            NORMALIZATION_CONFIG, TOKENIZATION_CONFIG, &STOPWORDS_CONFIG,
205        ).is_ok());
206
207        #[rustfmt::skip]
208        assert!(Query::search(
209            "id2", "c:test:1", "", "Michael Dake", 1, 0, None,
210            NORMALIZATION_CONFIG, TOKENIZATION_CONFIG, &STOPWORDS_CONFIG,
211        ).is_err());
212    }
213
214    #[test]
215    fn it_builds_suggest_query() {
216        #[rustfmt::skip]
217        assert!(Query::suggest(
218            "id1", "c:test:2", "b:test:2", "Micha", 5,
219            NORMALIZATION_CONFIG, TOKENIZATION_CONFIG, &STOPWORDS_CONFIG,
220        ).is_ok());
221
222        #[rustfmt::skip]
223        assert!(Query::suggest(
224            "id2", "c:test:2", "", "Micha", 1,
225            NORMALIZATION_CONFIG, TOKENIZATION_CONFIG, &STOPWORDS_CONFIG,
226        ).is_err());
227    }
228
229    #[test]
230    fn it_builds_list_query() {
231        assert!(Query::list("id1", "c:test:2", "b:test:2", 100, 0).is_ok());
232        assert!(Query::list("id2", "c:test:2", "", 10, 0).is_err());
233    }
234
235    #[test]
236    fn it_builds_push_query() {
237        #[rustfmt::skip]
238        assert!(Query::push(
239            "c:test:3", "b:test:3", "o:test:3", "My name is Michael Dake. I'm ordering in the US.", None,
240            NORMALIZATION_CONFIG, TOKENIZATION_CONFIG, &STOPWORDS_CONFIG,
241        ).is_ok());
242
243        #[rustfmt::skip]
244        assert!(Query::push(
245            "c:test:3", "", "o:test:3", "My name is Michael Dake.", None,
246            NORMALIZATION_CONFIG, TOKENIZATION_CONFIG, &STOPWORDS_CONFIG,
247        ).is_err());
248    }
249
250    #[test]
251    fn it_builds_pop_query() {
252        #[rustfmt::skip]
253        assert!(Query::pop(
254            "c:test:4", "b:test:4", "o:test:4", "ordering US",
255            NORMALIZATION_CONFIG, TOKENIZATION_CONFIG, &STOPWORDS_CONFIG,
256        ).is_ok());
257
258        #[rustfmt::skip]
259        assert!(Query::pop(
260            "c:test:4", "", "o:test:4", "ordering US",
261            NORMALIZATION_CONFIG, TOKENIZATION_CONFIG, &STOPWORDS_CONFIG,
262        ).is_err());
263    }
264
265    #[test]
266    fn it_builds_count_query() {
267        assert!(Query::count("c:test:5", None, None).is_ok());
268        assert!(Query::count("c:test:5", Some("b:test:5"), None).is_ok());
269        assert!(Query::count("c:test:5", Some("b:test:5"), Some("o:test:5")).is_ok());
270        assert!(Query::count("c:test:5", Some(""), Some("o:test:5")).is_err());
271    }
272
273    #[test]
274    fn it_builds_flushc_query() {
275        assert!(Query::flushc("c:test:6").is_ok());
276        assert!(Query::flushc("").is_err());
277    }
278
279    #[test]
280    fn it_builds_flushb_query() {
281        assert!(Query::flushb("c:test:7", "b:test:7").is_ok());
282        assert!(Query::flushb("c:test:7", "").is_err());
283    }
284
285    #[test]
286    fn it_builds_flusho_query() {
287        assert!(Query::flusho("c:test:8", "b:test:8", "o:test:8").is_ok());
288        assert!(Query::flusho("c:test:8", "b:test:8", "").is_err());
289    }
290}