1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
//! Search configuration domain models
//!
//! Defines searchable field configuration, facets, synonyms, and boost rules
//! for product search customization.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use stateset_primitives::SearchConfigId;
use strum::{Display, EnumString};
/// Tokenizer strategy for a searchable field
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Display, EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[non_exhaustive]
pub enum Tokenizer {
/// Standard word-boundary tokenizer
#[default]
Standard,
/// N-gram tokenizer (for substring matching)
Ngram,
/// Edge n-gram tokenizer (for autocomplete)
Edge,
/// Keyword tokenizer (exact match only)
Keyword,
}
/// Facet type for search refinement
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Display, EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[non_exhaustive]
pub enum FacetType {
/// Distinct value facet (e.g., brand, color)
#[default]
Value,
/// Numeric range facet (e.g., price ranges)
Range,
/// Hierarchical facet (e.g., category tree)
Hierarchical,
}
/// A search configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchConfig {
/// Unique config ID
pub id: SearchConfigId,
/// Configuration name
pub name: String,
/// Description
pub description: Option<String>,
/// Fields available for search
pub searchable_fields: Vec<SearchField>,
/// Facet definitions for filtering
pub facets: Vec<FacetConfig>,
/// Synonym groups
pub synonyms: Vec<SynonymGroup>,
/// Boost rules for relevance tuning
pub boost_rules: Vec<BoostRule>,
/// Whether this is the active configuration
pub is_active: bool,
/// When the config was created
pub created_at: DateTime<Utc>,
/// When the config was last updated
pub updated_at: DateTime<Utc>,
}
/// A searchable field configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchField {
/// Field name (e.g., "title", "description", "sku")
pub field_name: String,
/// Relative search weight (higher = more important)
pub weight: f64,
/// Tokenizer to use for this field
pub tokenizer: Tokenizer,
/// Whether this field is enabled for search
pub enabled: bool,
}
/// Facet configuration for search filtering
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FacetConfig {
/// Field to facet on
pub field_name: String,
/// Facet type
pub facet_type: FacetType,
/// Display name in UI
pub display_name: String,
/// Sort order in the facet panel
pub sort_order: i32,
/// Maximum number of facet values to return
pub max_values: Option<u32>,
}
/// A group of synonymous terms
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SynonymGroup {
/// The canonical/primary term
pub canonical: String,
/// Synonymous terms that should match the canonical
pub synonyms: Vec<String>,
}
/// A boost rule for search relevance tuning
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BoostRule {
/// Field to apply the boost to
pub field: String,
/// Value pattern to match (exact or wildcard)
pub value_match: String,
/// Boost factor (> 1.0 increases relevance, < 1.0 decreases)
pub boost_factor: f64,
}
/// Input for creating a search configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateSearchConfig {
/// Configuration name
pub name: String,
/// Description
pub description: Option<String>,
/// Searchable fields
pub searchable_fields: Vec<SearchField>,
/// Facets
pub facets: Vec<FacetConfig>,
/// Synonyms
pub synonyms: Vec<SynonymGroup>,
/// Boost rules
pub boost_rules: Vec<BoostRule>,
}
/// Input for updating a search configuration
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UpdateSearchConfig {
/// Updated name
pub name: Option<String>,
/// Updated description
pub description: Option<Option<String>>,
/// Updated searchable fields (replaces all)
pub searchable_fields: Option<Vec<SearchField>>,
/// Updated facets (replaces all)
pub facets: Option<Vec<FacetConfig>>,
/// Updated synonyms (replaces all)
pub synonyms: Option<Vec<SynonymGroup>>,
/// Updated boost rules (replaces all)
pub boost_rules: Option<Vec<BoostRule>>,
/// Updated active status
pub is_active: Option<bool>,
}
/// Filter for listing search configurations
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SearchConfigFilter {
/// Filter by active status
pub is_active: Option<bool>,
/// Search by name
pub name: Option<String>,
/// Maximum results
pub limit: Option<u32>,
/// Offset for pagination
pub offset: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::*;
// ---- Defaults ----
#[test]
fn tokenizer_default_is_standard() {
assert_eq!(Tokenizer::default(), Tokenizer::Standard);
}
#[test]
fn facet_type_default_is_value() {
assert_eq!(FacetType::default(), FacetType::Value);
}
// ---- enum Display / FromStr round-trips ----
#[test]
fn tokenizer_display_fromstr_roundtrip() {
for tokenizer in
[Tokenizer::Standard, Tokenizer::Ngram, Tokenizer::Edge, Tokenizer::Keyword]
{
let s = tokenizer.to_string();
let parsed: Tokenizer = s.parse().unwrap();
assert_eq!(parsed, tokenizer, "round-trip failed for {s}");
}
}
#[test]
fn facet_type_display_fromstr_roundtrip() {
for facet_type in [FacetType::Value, FacetType::Range, FacetType::Hierarchical] {
let s = facet_type.to_string();
let parsed: FacetType = s.parse().unwrap();
assert_eq!(parsed, facet_type, "round-trip failed for {s}");
}
}
// ---- basic struct construction ----
#[test]
fn search_field_construction() {
let field = SearchField {
field_name: "title".to_string(),
weight: 2.0,
tokenizer: Tokenizer::Standard,
enabled: true,
};
assert_eq!(field.field_name, "title");
assert!(field.enabled);
}
#[test]
fn synonym_group_construction() {
let group = SynonymGroup {
canonical: "shirt".to_string(),
synonyms: vec!["tee".to_string(), "top".to_string()],
};
assert_eq!(group.synonyms.len(), 2);
}
}