1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use stateset_primitives::SearchConfigId;
9use strum::{Display, EnumString};
10
11#[derive(
13 Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Display, EnumString,
14)]
15#[serde(rename_all = "snake_case")]
16#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
17#[non_exhaustive]
18pub enum Tokenizer {
19 #[default]
21 Standard,
22 Ngram,
24 Edge,
26 Keyword,
28}
29
30#[derive(
32 Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Display, EnumString,
33)]
34#[serde(rename_all = "snake_case")]
35#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
36#[non_exhaustive]
37pub enum FacetType {
38 #[default]
40 Value,
41 Range,
43 Hierarchical,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct SearchConfig {
50 pub id: SearchConfigId,
52 pub name: String,
54 pub description: Option<String>,
56 pub searchable_fields: Vec<SearchField>,
58 pub facets: Vec<FacetConfig>,
60 pub synonyms: Vec<SynonymGroup>,
62 pub boost_rules: Vec<BoostRule>,
64 pub is_active: bool,
66 pub created_at: DateTime<Utc>,
68 pub updated_at: DateTime<Utc>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct SearchField {
75 pub field_name: String,
77 pub weight: f64,
79 pub tokenizer: Tokenizer,
81 pub enabled: bool,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct FacetConfig {
88 pub field_name: String,
90 pub facet_type: FacetType,
92 pub display_name: String,
94 pub sort_order: i32,
96 pub max_values: Option<u32>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SynonymGroup {
103 pub canonical: String,
105 pub synonyms: Vec<String>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct BoostRule {
112 pub field: String,
114 pub value_match: String,
116 pub boost_factor: f64,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct CreateSearchConfig {
123 pub name: String,
125 pub description: Option<String>,
127 pub searchable_fields: Vec<SearchField>,
129 pub facets: Vec<FacetConfig>,
131 pub synonyms: Vec<SynonymGroup>,
133 pub boost_rules: Vec<BoostRule>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, Default)]
139pub struct UpdateSearchConfig {
140 pub name: Option<String>,
142 pub description: Option<Option<String>>,
144 pub searchable_fields: Option<Vec<SearchField>>,
146 pub facets: Option<Vec<FacetConfig>>,
148 pub synonyms: Option<Vec<SynonymGroup>>,
150 pub boost_rules: Option<Vec<BoostRule>>,
152 pub is_active: Option<bool>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, Default)]
158pub struct SearchConfigFilter {
159 pub is_active: Option<bool>,
161 pub name: Option<String>,
163 pub limit: Option<u32>,
165 pub offset: Option<u32>,
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
176 fn tokenizer_default_is_standard() {
177 assert_eq!(Tokenizer::default(), Tokenizer::Standard);
178 }
179
180 #[test]
181 fn facet_type_default_is_value() {
182 assert_eq!(FacetType::default(), FacetType::Value);
183 }
184
185 #[test]
188 fn tokenizer_display_fromstr_roundtrip() {
189 for tokenizer in
190 [Tokenizer::Standard, Tokenizer::Ngram, Tokenizer::Edge, Tokenizer::Keyword]
191 {
192 let s = tokenizer.to_string();
193 let parsed: Tokenizer = s.parse().unwrap();
194 assert_eq!(parsed, tokenizer, "round-trip failed for {s}");
195 }
196 }
197
198 #[test]
199 fn facet_type_display_fromstr_roundtrip() {
200 for facet_type in [FacetType::Value, FacetType::Range, FacetType::Hierarchical] {
201 let s = facet_type.to_string();
202 let parsed: FacetType = s.parse().unwrap();
203 assert_eq!(parsed, facet_type, "round-trip failed for {s}");
204 }
205 }
206
207 #[test]
210 fn search_field_construction() {
211 let field = SearchField {
212 field_name: "title".to_string(),
213 weight: 2.0,
214 tokenizer: Tokenizer::Standard,
215 enabled: true,
216 };
217 assert_eq!(field.field_name, "title");
218 assert!(field.enabled);
219 }
220
221 #[test]
222 fn synonym_group_construction() {
223 let group = SynonymGroup {
224 canonical: "shirt".to_string(),
225 synonyms: vec!["tee".to_string(), "top".to_string()],
226 };
227 assert_eq!(group.synonyms.len(), 2);
228 }
229}