1use crate::prelude::*;
2use secrecy::ExposeSecret;
3use serde_json::json;
4
5const API_VERSION: &str = "2024-07-01";
6
7#[derive(Debug, Deserialize)]
8struct IndexerStatusResponse {
9 pub status: String,
10 #[serde(rename = "lastResult")]
11 pub last_result: Option<LastResult>,
12}
13
14#[derive(Debug, Deserialize)]
15struct LastResult {
16 pub status: String,
17 pub errors: Vec<IndexerError>,
18 pub warnings: Vec<IndexerWarning>,
19}
20
21#[derive(Debug, Deserialize)]
22struct IndexerError {
23 pub key: Option<String>,
24 pub name: Option<String>,
25 pub message: Option<String>,
26 pub details: Option<String>,
27 #[serde(rename = "documentationLink")]
28 pub documentation_link: Option<String>,
29}
30
31#[derive(Debug, Deserialize)]
32struct IndexerWarning {
33 key: Option<String>,
34 name: Option<String>,
35 message: Option<String>,
36 details: Option<String>,
37 #[serde(rename = "documentationLink")]
38 documentation_link: Option<String>,
39}
40
41pub async fn does_search_indexer_exist(
42 indexer_name: &str,
43 app_config: &ApplicationConfiguration,
44) -> ChatbotResult<bool> {
45 let azure_config = app_config.azure_configuration.as_ref().ok_or_else(|| {
46 chatbot_err!(
47 AzureRequestBuildError,
48 "Azure configuration is missing from the application configuration"
49 )
50 })?;
51
52 let search_config = azure_config.search_config.as_ref().ok_or_else(|| {
53 chatbot_err!(
54 AzureRequestBuildError,
55 "Azure search configuration is missing from the Azure configuration"
56 )
57 })?;
58 let mut url = search_config.search_endpoint.clone();
59 url.set_path(&format!("indexers('{}')", indexer_name));
60 url.set_query(Some(&format!("api-version={}", API_VERSION)));
61
62 let response = REQWEST_CLIENT
63 .get(url)
64 .header("Content-Type", "application/json")
65 .header("api-key", search_config.search_api_key.expose_secret())
66 .send()
67 .await?;
68
69 if response.status().is_success() {
70 Ok(true)
71 } else if response.status() == 404 {
72 Ok(false)
73 } else {
74 let status = response.status();
75 let error_text = response.text().await?;
76 Err(chatbot_err!(
77 FailedAzureResponse,
78 format!(
79 "Error checking if index exists. Status: {}. Error: {}",
80 status, error_text
81 )
82 ))
83 }
84}
85
86pub async fn create_search_indexer(
87 indexer_name: &str,
88 data_source_name: &str,
89 skillset_name: &str,
90 target_index_name: &str,
91 app_config: &ApplicationConfiguration,
92) -> ChatbotResult<()> {
93 let azure_config = app_config.azure_configuration.as_ref().ok_or_else(|| {
94 chatbot_err!(
95 AzureRequestBuildError,
96 "Azure configuration is missing from the application configuration"
97 )
98 })?;
99
100 let search_config = azure_config.search_config.as_ref().ok_or_else(|| {
101 chatbot_err!(
102 AzureRequestBuildError,
103 "Azure search configuration is missing from the Azure configuration"
104 )
105 })?;
106
107 let mut url = search_config.search_endpoint.clone();
108 url.set_path(&format!("indexers/{}", indexer_name));
109 url.set_query(Some(&format!("api-version={}", API_VERSION)));
110
111 let indexer_definition = json!({
112 "name": indexer_name,
113 "description": null,
114 "dataSourceName": data_source_name,
115 "skillsetName": skillset_name,
116 "targetIndexName": target_index_name,
117 "disabled": null,
118 "schedule": null,
119 "parameters": {
120 "batchSize": null,
121 "maxFailedItems": null,
122 "maxFailedItemsPerBatch": null,
123 "base64EncodeKeys": null,
124 "configuration": {
125 "dataToExtract": "contentAndMetadata"
126 }
127 },
128 "fieldMappings": [
129 {
130 "sourceFieldName": "metadata_storage_path",
131 "targetFieldName": "chunk_id",
132 "mappingFunction": { "name": "base64Encode" }
133 },
134 ],
135 "outputFieldMappings": [
136
137 ],
138 "encryptionKey": null
139 });
140
141 let response = REQWEST_CLIENT
142 .put(url)
143 .header("Content-Type", "application/json")
144 .header("api-key", search_config.search_api_key.expose_secret())
145 .json(&indexer_definition)
146 .send()
147 .await?;
148
149 if response.status().is_success() {
150 Ok(())
151 } else {
152 let status = response.status();
153 let error_text = response.text().await?;
154 Err(chatbot_err!(
155 FailedAzureResponse,
156 format!(
157 "Error creating search indexer. Status: {}. Error: {}",
158 status, error_text
159 )
160 ))
161 }
162}
163
164pub async fn run_search_indexer_now(
165 indexer_name: &str,
166 app_config: &ApplicationConfiguration,
167) -> ChatbotResult<()> {
168 let azure_config = app_config.azure_configuration.as_ref().ok_or_else(|| {
169 chatbot_err!(
170 AzureRequestBuildError,
171 "Azure configuration is missing from the application configuration"
172 )
173 })?;
174
175 let search_config = azure_config.search_config.as_ref().ok_or_else(|| {
176 chatbot_err!(
177 AzureRequestBuildError,
178 "Azure search configuration is missing from the Azure configuration"
179 )
180 })?;
181
182 let mut url = search_config.search_endpoint.clone();
183 url.set_path(&format!("indexers/{}/run", indexer_name));
184 url.set_query(Some(&format!("api-version={}", API_VERSION)));
185
186 let response = REQWEST_CLIENT
187 .post(url)
188 .header("Content-Type", "application/json")
189 .header("api-key", search_config.search_api_key.expose_secret())
190 .send()
191 .await?;
192
193 if response.status().is_success() {
194 Ok(())
195 } else {
196 let status = response.status();
197 let error_text = response.text().await?;
198 Err(chatbot_err!(
199 FailedAzureResponse,
200 format!(
201 "Error triggering search indexer. Status: {}. Error: {}",
202 status, error_text
203 )
204 ))
205 }
206}
207
208pub async fn check_search_indexer_status(
222 indexer_name: &str,
223 app_config: &ApplicationConfiguration,
224) -> ChatbotResult<bool> {
225 let azure_config = app_config.azure_configuration.as_ref().ok_or_else(|| {
226 chatbot_err!(
227 AzureRequestBuildError,
228 "Azure configuration is missing from the application configuration"
229 )
230 })?;
231
232 let search_config = azure_config.search_config.as_ref().ok_or_else(|| {
233 chatbot_err!(
234 AzureRequestBuildError,
235 "Azure search configuration is missing from the Azure configuration"
236 )
237 })?;
238
239 let mut url = search_config.search_endpoint.clone();
240 url.set_path(&format!("indexers('{}')/search.status", indexer_name));
241 url.set_query(Some(&format!("api-version={}", API_VERSION)));
242
243 let response = REQWEST_CLIENT
244 .get(url)
245 .header("Content-Type", "application/json")
246 .header("api-key", search_config.search_api_key.expose_secret())
247 .send()
248 .await?;
249
250 if response.status().is_success() {
251 let response_text = response.text().await?;
252 let indexer_status: IndexerStatusResponse = match serde_json::from_str(&response_text) {
253 Ok(status) => status,
254 Err(e) => {
255 error!("Failed to parse indexer status JSON: {}", e);
256 error!(
257 "{}",
258 serde_json::to_string_pretty(&response_text)
259 .unwrap_or_else(|_| "Invalid JSON".to_string())
260 );
261 return Err(chatbot_err!(
262 SerdeJson,
263 "Failed to parse indexer status JSON.",
264 e
265 ));
266 }
267 };
268
269 let is_running = indexer_status.status.eq_ignore_ascii_case("running");
271
272 let last_result_in_progress = indexer_status
274 .last_result
275 .as_ref()
276 .is_some_and(|lr| lr.status.eq_ignore_ascii_case("inprogress"));
277
278 if !is_running {
279 info!("Indexer '{}' is not running normally.", indexer_name);
280 }
281
282 if last_result_in_progress {
283 warn!(
284 "Last execution of indexer '{}' is in progress.",
285 indexer_name
286 );
287 }
288
289 if let Some(last_result) = &indexer_status.last_result {
290 if !last_result.errors.is_empty() {
291 error!("Errors in the last execution:");
292 for error in &last_result.errors {
293 error!(
294 " - **Key**: {}\n **Name**: {}\n **Message**: {}\n **Details**: {}\n **Documentation**: {}\n",
295 error.key.as_deref().unwrap_or("N/A"),
296 error.name.as_deref().unwrap_or("N/A"),
297 error.message.as_deref().unwrap_or("N/A"),
298 error.details.as_deref().unwrap_or("N/A"),
299 error.documentation_link.as_deref().unwrap_or("N/A"),
300 );
301 }
302 }
303
304 if !last_result.warnings.is_empty() {
305 warn!("Warnings in the last execution:");
306 for warning in &last_result.warnings {
307 warn!(
308 " - **Key**: {}\n **Name**: {}\n **Message**: {}\n **Details**: {}\n **Documentation**: {}\n",
309 warning.key.as_deref().unwrap_or("N/A"),
310 warning.name.as_deref().unwrap_or("N/A"),
311 warning.message.as_deref().unwrap_or("N/A"),
312 warning.details.as_deref().unwrap_or("N/A"),
313 warning.documentation_link.as_deref().unwrap_or("N/A"),
314 );
315 }
316 }
317 } else {
318 warn!(
319 "No last result information available for indexer '{}'. Assuming the index is not ready yet.",
320 indexer_name
321 );
322 return Ok(false);
323 }
324
325 if is_running && !last_result_in_progress {
326 Ok(true)
327 } else {
328 Ok(false)
329 }
330 } else if response.status() == reqwest::StatusCode::NOT_FOUND {
331 error!("Indexer '{}' does not exist.", indexer_name);
332 Ok(false)
333 } else {
334 let status = response.status();
335 let error_text = response
336 .text()
337 .await
338 .unwrap_or_else(|_| "No error text".to_string());
339 error!(
340 "Error fetching indexer status. Status: {}. Error: {}",
341 status, error_text
342 );
343 Err(chatbot_err!(
344 FailedAzureResponse,
345 format!(
346 "Error fetching indexer status. Status: {}. Error: {}",
347 status, error_text
348 )
349 ))
350 }
351}