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