1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum Field {
8 Title,
10 Author,
12 Abstract,
14 Keyword,
16}
17
18impl Field {
19 pub fn name(self) -> &'static str {
21 match self {
22 Field::Title => "title_t",
23 Field::Author => "authFullName_t",
24 Field::Abstract => "abstract_t",
25 Field::Keyword => "keyword_t",
26 }
27 }
28}
29
30#[derive(Clone, Debug)]
46pub struct SearchQuery {
47 q: String,
48 fields: Vec<String>,
49 rows: Option<u32>,
50 start: Option<u32>,
51 sort: Option<String>,
52 facet_fields: Vec<String>,
53}
54
55impl SearchQuery {
56 #[must_use]
60 pub fn basic(query: impl Into<String>) -> Self {
61 SearchQuery {
62 q: query.into(),
63 fields: Vec::new(),
64 rows: None,
65 start: None,
66 sort: None,
67 facet_fields: Vec::new(),
68 }
69 }
70
71 #[must_use]
74 pub fn field(field: &str, value: &str) -> Self {
75 Self::basic(format!("{field}:{value}"))
76 }
77
78 #[must_use]
84 pub fn in_field(field: Field, value: &str) -> Self {
85 let words: Vec<&str> = value.split_whitespace().collect();
86 if words.is_empty() {
87 Self::basic(value.to_owned())
88 } else {
89 Self::field_terms(field.name(), words)
90 }
91 }
92
93 #[must_use]
97 pub fn field_terms<I, S>(field: &str, terms: I) -> Self
98 where
99 I: IntoIterator<Item = S>,
100 S: AsRef<str>,
101 {
102 let joined = terms
103 .into_iter()
104 .map(|t| t.as_ref().to_owned())
105 .collect::<Vec<_>>()
106 .join(" ");
107 Self::basic(format!("{field}:({joined})"))
108 }
109
110 #[must_use]
114 pub fn field_all_terms<I, S>(field: &str, terms: I) -> Self
115 where
116 I: IntoIterator<Item = S>,
117 S: AsRef<str>,
118 {
119 let joined = terms
120 .into_iter()
121 .map(|t| t.as_ref().to_owned())
122 .collect::<Vec<_>>()
123 .join(" AND ");
124 Self::basic(format!("{field}:({joined})"))
125 }
126
127 #[must_use]
131 pub fn proximity(field: &str, phrase: &str, distance: u32) -> Self {
132 Self::basic(format!("{field}:\"{phrase}\"~{distance}"))
133 }
134
135 #[must_use]
137 pub fn fields<I, S>(mut self, fields: I) -> Self
138 where
139 I: IntoIterator<Item = S>,
140 S: AsRef<str>,
141 {
142 self.fields = fields.into_iter().map(|f| f.as_ref().to_owned()).collect();
143 self
144 }
145
146 #[must_use]
148 pub fn rows(mut self, rows: u32) -> Self {
149 self.rows = Some(rows);
150 self
151 }
152
153 #[must_use]
155 pub fn start(mut self, start: u32) -> Self {
156 self.start = Some(start);
157 self
158 }
159
160 #[must_use]
162 pub fn page(mut self, page: u32, per_page: u32) -> Self {
163 self.rows = Some(per_page);
164 self.start = Some(page * per_page);
165 self
166 }
167
168 #[must_use]
170 pub fn sort(mut self, sort: impl Into<String>) -> Self {
171 self.sort = Some(sort.into());
172 self
173 }
174
175 #[must_use]
177 pub fn facet(mut self, field: impl Into<String>) -> Self {
178 self.facet_fields.push(field.into());
179 self
180 }
181
182 #[must_use]
186 pub fn to_params(&self) -> Vec<(String, String)> {
187 let mut params = vec![
188 ("q".to_owned(), self.q.clone()),
189 ("wt".to_owned(), "json".to_owned()),
190 ];
191
192 if !self.fields.is_empty() {
193 params.push(("fl".to_owned(), self.fields.join(",")));
194 }
195 if let Some(rows) = self.rows {
196 params.push(("rows".to_owned(), rows.to_string()));
197 }
198 if let Some(start) = self.start {
199 params.push(("start".to_owned(), start.to_string()));
200 }
201 if let Some(sort) = &self.sort {
202 params.push(("sort".to_owned(), sort.clone()));
203 }
204 if !self.facet_fields.is_empty() {
205 params.push(("facet".to_owned(), "true".to_owned()));
206 for field in &self.facet_fields {
207 params.push(("facet.field".to_owned(), field.clone()));
208 }
209 }
210
211 params
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 fn param<'a>(params: &'a [(String, String)], key: &str) -> Option<&'a str> {
220 params
221 .iter()
222 .find(|(k, _)| k == key)
223 .map(|(_, v)| v.as_str())
224 }
225
226 #[test]
227 fn basic_query() {
228 let params = SearchQuery::basic("europe").to_params();
229 assert_eq!(param(¶ms, "q"), Some("europe"));
230 assert_eq!(param(¶ms, "wt"), Some("json"));
231 }
232
233 #[test]
234 fn field_query() {
235 let params = SearchQuery::field("title_t", "europe").to_params();
236 assert_eq!(param(¶ms, "q"), Some("title_t:europe"));
237 }
238
239 #[test]
240 fn field_terms_query() {
241 let params = SearchQuery::field_terms("title_t", ["economic", "policy"]).to_params();
242 assert_eq!(param(¶ms, "q"), Some("title_t:(economic policy)"));
243 }
244
245 #[test]
246 fn proximity_query() {
247 let params = SearchQuery::proximity("title_t", "economic policy", 3).to_params();
248 assert_eq!(param(¶ms, "q"), Some("title_t:\"economic policy\"~3"));
249 }
250
251 #[test]
252 fn pagination_and_fields() {
253 let params = SearchQuery::basic("europe")
254 .fields(["docid", "label_s"])
255 .page(2, 10)
256 .to_params();
257 assert_eq!(param(¶ms, "fl"), Some("docid,label_s"));
258 assert_eq!(param(¶ms, "rows"), Some("10"));
259 assert_eq!(param(¶ms, "start"), Some("20"));
260 }
261
262 #[test]
263 fn field_all_terms_uses_and() {
264 let params = SearchQuery::field_all_terms("title_t", ["economic", "policy"]).to_params();
265 assert_eq!(param(¶ms, "q"), Some("title_t:(economic AND policy)"));
266 }
267
268 #[test]
269 fn in_field_groups_words() {
270 let params = SearchQuery::in_field(Field::Title, "open science").to_params();
271 assert_eq!(param(¶ms, "q"), Some("title_t:(open science)"));
272
273 let single = SearchQuery::in_field(Field::Author, "dupont").to_params();
274 assert_eq!(param(&single, "q"), Some("authFullName_t:(dupont)"));
275 }
276
277 #[test]
278 fn facets_enabled() {
279 let params = SearchQuery::basic("europe").facet("docType_s").to_params();
280 assert_eq!(param(¶ms, "facet"), Some("true"));
281 assert_eq!(param(¶ms, "facet.field"), Some("docType_s"));
282 }
283}