Skip to main content

hoist_core/resources/
index.rs

1//! Index resource definition
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use super::traits::{Resource, ResourceKind};
7
8/// Azure AI Search Index definition
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct Index {
12    pub name: String,
13    pub fields: Vec<Field>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub scoring_profiles: Option<Vec<ScoringProfile>>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub default_scoring_profile: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub cors_options: Option<CorsOptions>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub suggesters: Option<Vec<Suggester>>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub analyzers: Option<Vec<Value>>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub tokenizers: Option<Vec<Value>>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub token_filters: Option<Vec<Value>>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub char_filters: Option<Vec<Value>>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub similarity: Option<Value>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub semantic: Option<SemanticConfiguration>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub vector_search: Option<VectorSearch>,
36    /// Catch-all for additional fields from Azure API
37    #[serde(flatten)]
38    pub extra: std::collections::HashMap<String, Value>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct Field {
44    pub name: String,
45    #[serde(rename = "type")]
46    pub field_type: String,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub key: Option<bool>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub searchable: Option<bool>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub filterable: Option<bool>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub sortable: Option<bool>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub facetable: Option<bool>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub retrievable: Option<bool>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub stored: Option<bool>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub analyzer: Option<String>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub search_analyzer: Option<String>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub index_analyzer: Option<String>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub synonym_maps: Option<Vec<String>>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub fields: Option<Vec<Field>>,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub dimensions: Option<i32>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub vector_search_profile: Option<String>,
75    #[serde(flatten)]
76    pub extra: std::collections::HashMap<String, Value>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80#[serde(rename_all = "camelCase")]
81pub struct ScoringProfile {
82    pub name: String,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub text: Option<Value>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub functions: Option<Vec<Value>>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub function_aggregation: Option<String>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct CorsOptions {
94    pub allowed_origins: Vec<String>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub max_age_in_seconds: Option<i64>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct Suggester {
102    pub name: String,
103    pub search_mode: String,
104    pub source_fields: Vec<String>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108#[serde(rename_all = "camelCase")]
109pub struct SemanticConfiguration {
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub default_configuration: Option<String>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub configurations: Option<Vec<Value>>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct VectorSearch {
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub algorithms: Option<Vec<Value>>,
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub profiles: Option<Vec<Value>>,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub vectorizers: Option<Vec<Value>>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub compressions: Option<Vec<Value>>,
127}
128
129impl Resource for Index {
130    fn kind() -> ResourceKind {
131        ResourceKind::Index
132    }
133
134    fn name(&self) -> &str {
135        &self.name
136    }
137
138    fn immutable_fields() -> &'static [&'static str] {
139        // Index fields cannot be modified after creation (only added)
140        &["fields"]
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_index_kind() {
150        assert_eq!(Index::kind(), ResourceKind::Index);
151    }
152
153    #[test]
154    fn test_index_immutable_fields() {
155        assert_eq!(Index::immutable_fields(), &["fields"]);
156    }
157
158    #[test]
159    fn test_index_deserialize_minimal() {
160        let json = r#"{
161            "name": "my-index",
162            "fields": [
163                { "name": "id", "type": "Edm.String", "key": true }
164            ]
165        }"#;
166        let index: Index = serde_json::from_str(json).unwrap();
167        assert_eq!(index.name, "my-index");
168        assert_eq!(index.fields.len(), 1);
169        assert_eq!(index.fields[0].name, "id");
170        assert_eq!(index.fields[0].key, Some(true));
171    }
172
173    #[test]
174    fn test_index_deserialize_with_vector_search() {
175        let json = r#"{
176            "name": "vec-index",
177            "fields": [],
178            "vectorSearch": {
179                "algorithms": [{"name": "hnsw"}],
180                "profiles": [{"name": "default"}]
181            }
182        }"#;
183        let index: Index = serde_json::from_str(json).unwrap();
184        assert!(index.vector_search.is_some());
185    }
186
187    #[test]
188    fn test_index_extra_fields_preserved() {
189        let json = r#"{
190            "name": "idx",
191            "fields": [],
192            "customField": "hello"
193        }"#;
194        let index: Index = serde_json::from_str(json).unwrap();
195        assert_eq!(
196            index.extra.get("customField").and_then(|v| v.as_str()),
197            Some("hello")
198        );
199    }
200}