|
| 1 | +use crate::ast_parsing::query_info::{ |
| 2 | + AggregationType, ElasticDSLQueryInfo, FieldName, GroupBySpec, Predicate, TermValue, |
| 3 | +}; |
| 4 | +use crate::helpers::strip_keyword_suffix; |
| 5 | +use elasticsearch_dsl_ast::{self as dsl}; |
| 6 | +use serde_json; |
| 7 | + |
| 8 | +pub fn extract_query_info(query: &str) -> Option<ElasticDSLQueryInfo> { |
| 9 | + // Main entry point for extracting relevant information from the parsed query pattern. |
| 10 | + let search_request = serde_json::from_str(query).ok()?; |
| 11 | + walk_ast_and_extract_info(&search_request) |
| 12 | +} |
| 13 | + |
| 14 | +pub fn parse_query_to_ast(query: &str) -> Option<dsl::Search> { |
| 15 | + serde_json::from_str(query).ok()? |
| 16 | +} |
| 17 | + |
| 18 | +pub fn walk_ast_and_extract_info(ast: &dsl::Search) -> Option<ElasticDSLQueryInfo> { |
| 19 | + // Traverses the AST and extracts relevant information for answering sketchable aggregations within ASAPQuery. |
| 20 | + // This would involve traversing the AST nodes and applying logic to determine query patterns, labels, statistics, etc. |
| 21 | + let query = ast.query.clone(); |
| 22 | + let predicates = match query { |
| 23 | + Some(dsl::Query::Bool(bool_query)) => { |
| 24 | + // Extract information from the bool query |
| 25 | + walk_bool_query_and_extract_info(&bool_query) |
| 26 | + } |
| 27 | + Some(other) => { |
| 28 | + // Predicates may just be specified directly without enclosing bool context. |
| 29 | + if let Some(predicate) = extract_predicates_from_query(&other) { |
| 30 | + vec![predicate] |
| 31 | + } else { |
| 32 | + Vec::new() |
| 33 | + } |
| 34 | + } |
| 35 | + None => Vec::new(), // Return an empty vector of predicates if no query is specified |
| 36 | + }; |
| 37 | + let (target_field, aggregation_type, group_by_spec) = |
| 38 | + walk_aggregations_and_extract_info(&ast.aggs)?; |
| 39 | + Some(ElasticDSLQueryInfo::new( |
| 40 | + target_field, |
| 41 | + predicates, |
| 42 | + group_by_spec, |
| 43 | + aggregation_type, |
| 44 | + )) |
| 45 | +} |
| 46 | + |
| 47 | +fn walk_bool_query_and_extract_info(bool_query: &dsl::BoolQuery) -> Vec<Predicate> { |
| 48 | + // Placeholder for walking the filter context of the AST and extracting relevant information |
| 49 | + // This would involve traversing the filter nodes and applying logic to determine label filters, time ranges, etc. |
| 50 | + let dsl::QueryCollection(filters) = bool_query.filter.clone(); |
| 51 | + let mut predicates = Vec::new(); |
| 52 | + for query in filters { |
| 53 | + if let Some(predicate) = extract_predicates_from_query(&query) { |
| 54 | + predicates.push(predicate); |
| 55 | + } |
| 56 | + } |
| 57 | + predicates |
| 58 | +} |
| 59 | + |
| 60 | +fn extract_predicates_from_query(query: &dsl::Query) -> Option<Predicate> { |
| 61 | + // Extract predicate information from a given query node, if it matches supported patterns (term or range queries). |
| 62 | + match query { |
| 63 | + dsl::Query::Term(term_query) => { |
| 64 | + // Extract information from the term query |
| 65 | + let field = strip_keyword_suffix(&term_query.field).to_owned(); |
| 66 | + let Some(value) = term_query.value.clone() else { |
| 67 | + return None; // Skip if term query does not have a value |
| 68 | + }; |
| 69 | + let Some(term_value) = map_term_to_json_value(&value) else { |
| 70 | + return None; // Skip if term query value cannot be mapped to a JSON value |
| 71 | + }; |
| 72 | + // Process the term query information as needed |
| 73 | + Some(Predicate::Term { |
| 74 | + field, |
| 75 | + value: term_value, |
| 76 | + }) |
| 77 | + } |
| 78 | + |
| 79 | + dsl::Query::Range(range_query) => { |
| 80 | + // Extract information from the range query |
| 81 | + let field = strip_keyword_suffix(&range_query.field).to_owned(); |
| 82 | + let gte = range_query.gte.clone(); |
| 83 | + let lte = range_query.lte.clone(); |
| 84 | + // Process the range query information as needed |
| 85 | + let gte_value = gte.as_ref().and_then(map_term_to_json_value); |
| 86 | + let lte_value = lte.as_ref().and_then(map_term_to_json_value); |
| 87 | + Some(Predicate::Range { |
| 88 | + field, |
| 89 | + gte: gte_value, |
| 90 | + lte: lte_value, |
| 91 | + }) |
| 92 | + } |
| 93 | + _ => { |
| 94 | + // Handle other query types |
| 95 | + None // Skip unsupported query types |
| 96 | + } |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +fn walk_aggregations_and_extract_info( |
| 101 | + aggregations: &dsl::Aggregations, |
| 102 | +) -> Option<(FieldName, AggregationType, Option<GroupBySpec>)> { |
| 103 | + // Traverse the aggregations in the AST and extracting relevant information. Extract the first valid aggregation type found, along with any associated group by specifications. |
| 104 | + for agg in aggregations.values() { |
| 105 | + match agg { |
| 106 | + dsl::Aggregation::MultiTerms(terms_agg) => { |
| 107 | + // Extract information from the terms aggregation |
| 108 | + let field_names: Vec<String> = terms_agg |
| 109 | + .multi_terms |
| 110 | + .terms |
| 111 | + .iter() |
| 112 | + .filter_map(|multi_term| multi_term.field.clone()) |
| 113 | + .collect(); |
| 114 | + let field_names: Vec<String> = field_names |
| 115 | + .iter() |
| 116 | + .map(|s| strip_keyword_suffix(s).to_owned()) |
| 117 | + .collect(); |
| 118 | + if field_names.is_empty() { |
| 119 | + return None; // Return None if no valid field names are found in the multi-terms aggregation. |
| 120 | + } |
| 121 | + let group_by_spec = Some(GroupBySpec::Fields(field_names)); |
| 122 | + let (target_field, aggregation_type) = |
| 123 | + find_aggregation_info(&terms_agg.aggs.clone())?; |
| 124 | + return Some((target_field, aggregation_type, group_by_spec)); |
| 125 | + } |
| 126 | + dsl::Aggregation::Terms(terms_agg) => { |
| 127 | + // Extract information from the terms aggregation |
| 128 | + if let Some(field) = terms_agg.terms.field.clone() { |
| 129 | + let field = strip_keyword_suffix(&field).to_owned(); |
| 130 | + // Process the terms aggregation information as needed |
| 131 | + let group_by_spec = Some(GroupBySpec::Fields(vec![field])); |
| 132 | + let (target_field, aggregation_type) = |
| 133 | + find_aggregation_info(&terms_agg.aggs.clone())?; |
| 134 | + return Some((target_field, aggregation_type, group_by_spec)); |
| 135 | + } |
| 136 | + } |
| 137 | + other => { |
| 138 | + // Handle other aggregation types |
| 139 | + let (target_field, aggregation_type) = extract_aggregation_info(other)?; |
| 140 | + return Some((target_field, aggregation_type, None)); |
| 141 | + } |
| 142 | + } |
| 143 | + } |
| 144 | + None // Return None if no relevant aggregation information is found |
| 145 | +} |
| 146 | + |
| 147 | +fn find_aggregation_info(aggregations: &dsl::Aggregations) -> Option<(FieldName, AggregationType)> { |
| 148 | + // Placeholder for extracting specific information from an aggregation node |
| 149 | + if let Some((_, agg)) = aggregations.iter().next() { |
| 150 | + let (field, aggregation_type) = extract_aggregation_info(agg)?; |
| 151 | + return Some((field, aggregation_type)); |
| 152 | + } |
| 153 | + None // Return None if no relevant aggregation information is found |
| 154 | +} |
| 155 | + |
| 156 | +fn extract_aggregation_info(agg: &dsl::Aggregation) -> Option<(FieldName, AggregationType)> { |
| 157 | + // Extracts the specific aggregation type and target field from the given aggregation node, if it matches supported types (avg, sum, min, max, percentiles). |
| 158 | + match agg { |
| 159 | + dsl::Aggregation::Avg(avg_agg) => { |
| 160 | + let field = strip_keyword_suffix(&avg_agg.avg.field).to_owned(); |
| 161 | + let aggregation_type = AggregationType::Avg; |
| 162 | + Some((field, aggregation_type)) |
| 163 | + } |
| 164 | + dsl::Aggregation::Sum(sum_agg) => { |
| 165 | + let field = strip_keyword_suffix(&sum_agg.sum.field.clone()?).to_owned(); |
| 166 | + let aggregation_type = AggregationType::Sum; |
| 167 | + Some((field, aggregation_type)) |
| 168 | + } |
| 169 | + dsl::Aggregation::Min(min_agg) => { |
| 170 | + let field = strip_keyword_suffix(&min_agg.min.field.clone()?).to_owned(); |
| 171 | + let aggregation_type = AggregationType::Min; |
| 172 | + Some((field, aggregation_type)) |
| 173 | + } |
| 174 | + dsl::Aggregation::Max(max_agg) => { |
| 175 | + let field = strip_keyword_suffix(&max_agg.max.field.clone()?).to_owned(); |
| 176 | + let aggregation_type = AggregationType::Max; |
| 177 | + Some((field, aggregation_type)) |
| 178 | + } |
| 179 | + dsl::Aggregation::Percentiles(percentiles_agg) => { |
| 180 | + let field = percentiles_agg.percentiles.field.clone(); |
| 181 | + let percents = percentiles_agg |
| 182 | + .percentiles |
| 183 | + .percents |
| 184 | + .clone() |
| 185 | + .unwrap_or_default(); |
| 186 | + let aggregation_type = AggregationType::Percentiles(percents); |
| 187 | + Some((field, aggregation_type)) |
| 188 | + } |
| 189 | + _ => None, // Return None for unsupported aggregation types |
| 190 | + } |
| 191 | +} |
| 192 | + |
| 193 | +fn map_term_to_json_value(term: &dsl::Term) -> Option<TermValue> { |
| 194 | + // Placeholder for extracting field and value from a term query |
| 195 | + match term { |
| 196 | + dsl::Term::String(value) => { |
| 197 | + let value_str = value.to_string(); // Convert the term value to a string representation |
| 198 | + Some(TermValue::String(value_str)) |
| 199 | + } |
| 200 | + dsl::Term::Float32(value) => Some(TermValue::Float(*value as f64)), |
| 201 | + dsl::Term::Float64(value) => Some(TermValue::Float(*value)), |
| 202 | + dsl::Term::PositiveNumber(value) => Some(TermValue::UnsignedInt(*value)), |
| 203 | + dsl::Term::NegativeNumber(value) => Some(TermValue::Int(*value)), |
| 204 | + dsl::Term::Boolean(value) => Some(TermValue::Boolean(*value)), |
| 205 | + } |
| 206 | +} |
| 207 | + |
| 208 | +#[cfg(test)] |
| 209 | +mod tests { |
| 210 | + use super::*; |
| 211 | + |
| 212 | + #[test] |
| 213 | + fn parse_query_to_ast_parses_valid_search_request() { |
| 214 | + let query = r#" |
| 215 | + { |
| 216 | + "query": { |
| 217 | + "bool": { |
| 218 | + "filter": [ |
| 219 | + { "term": { "service.keyword": { "value": "frontend" } } } |
| 220 | + ] |
| 221 | + } |
| 222 | + }, |
| 223 | + "aggs": { |
| 224 | + "avg_latency": { "avg": { "field": "latency_ms" } } |
| 225 | + } |
| 226 | + } |
| 227 | + "#; |
| 228 | + |
| 229 | + let ast = parse_query_to_ast(query); |
| 230 | + assert!(ast.is_some()); |
| 231 | + } |
| 232 | + |
| 233 | + #[test] |
| 234 | + fn parse_query_to_ast_returns_none_for_invalid_json() { |
| 235 | + let query = r#"{ "query": { "bool": { "filter": [ } }"#; |
| 236 | + assert!(parse_query_to_ast(query).is_none()); |
| 237 | + } |
| 238 | + |
| 239 | + #[test] |
| 240 | + fn walk_bool_query_and_extract_info_extracts_term_and_range_predicates() { |
| 241 | + let bool_query = dsl::Query::bool() |
| 242 | + .filter(dsl::Query::term("service.keyword", "frontend")) |
| 243 | + .filter(dsl::Query::term("is_canary", true)) |
| 244 | + .filter(dsl::Query::range("@timestamp").gte("now-30s").lte("now")); |
| 245 | + |
| 246 | + let predicates = walk_bool_query_and_extract_info(&bool_query); |
| 247 | + assert_eq!(predicates.len(), 3); |
| 248 | + assert_eq!( |
| 249 | + predicates[0], |
| 250 | + Predicate::Term { |
| 251 | + field: "service".to_string(), |
| 252 | + value: TermValue::String("frontend".to_string()), |
| 253 | + } |
| 254 | + ); |
| 255 | + assert_eq!( |
| 256 | + predicates[1], |
| 257 | + Predicate::Term { |
| 258 | + field: "is_canary".to_string(), |
| 259 | + value: TermValue::Boolean(true), |
| 260 | + } |
| 261 | + ); |
| 262 | + assert_eq!( |
| 263 | + predicates[2], |
| 264 | + Predicate::Range { |
| 265 | + field: "@timestamp".to_string(), |
| 266 | + gte: Some(TermValue::String("now-30s".to_string())), |
| 267 | + lte: Some(TermValue::String("now".to_string())), |
| 268 | + } |
| 269 | + ); |
| 270 | + } |
| 271 | + |
| 272 | + #[test] |
| 273 | + fn walk_aggregations_and_extract_info_extracts_terms_group_by_and_metric() { |
| 274 | + let query = r#" |
| 275 | + { |
| 276 | + "aggs": { |
| 277 | + "by_service": { |
| 278 | + "terms": { "field": "service.keyword" }, |
| 279 | + "aggs": { |
| 280 | + "avg_latency": { "avg": { "field": "latency_ms" } } |
| 281 | + } |
| 282 | + } |
| 283 | + } |
| 284 | + } |
| 285 | + "#; |
| 286 | + let ast = parse_query_to_ast(query).expect("query should parse"); |
| 287 | + |
| 288 | + let (target_field, agg_type, group_by) = |
| 289 | + walk_aggregations_and_extract_info(&ast.aggs).expect("aggregation info should parse"); |
| 290 | + assert_eq!(target_field, "latency_ms"); |
| 291 | + assert_eq!(agg_type, AggregationType::Avg); |
| 292 | + assert_eq!( |
| 293 | + group_by, |
| 294 | + Some(GroupBySpec::Fields(vec!["service".to_string()])) |
| 295 | + ); |
| 296 | + } |
| 297 | + |
| 298 | + #[test] |
| 299 | + fn walk_aggregations_and_extract_info_extracts_multi_terms_and_percentiles() { |
| 300 | + let query = r#" |
| 301 | + { |
| 302 | + "aggs": { |
| 303 | + "by_labels": { |
| 304 | + "multi_terms": { |
| 305 | + "terms": [ |
| 306 | + { "field": "service.keyword" }, |
| 307 | + { "field": "env.keyword" } |
| 308 | + ] |
| 309 | + }, |
| 310 | + "aggs": { |
| 311 | + "latency_percentiles": { |
| 312 | + "percentiles": { |
| 313 | + "field": "latency_ms", |
| 314 | + "percents": [50.0, 95.0] |
| 315 | + } |
| 316 | + } |
| 317 | + } |
| 318 | + } |
| 319 | + } |
| 320 | + } |
| 321 | + "#; |
| 322 | + let ast = parse_query_to_ast(query).expect("query should parse"); |
| 323 | + |
| 324 | + let (target_field, agg_type, group_by) = |
| 325 | + walk_aggregations_and_extract_info(&ast.aggs).expect("aggregation info should parse"); |
| 326 | + assert_eq!(target_field, "latency_ms"); |
| 327 | + assert_eq!(agg_type, AggregationType::Percentiles(vec![50.0, 95.0])); |
| 328 | + assert_eq!( |
| 329 | + group_by, |
| 330 | + Some(GroupBySpec::Fields(vec![ |
| 331 | + "service".to_string(), |
| 332 | + "env".to_string() |
| 333 | + ])) |
| 334 | + ); |
| 335 | + } |
| 336 | + |
| 337 | + #[test] |
| 338 | + fn walk_ast_and_extract_info_builds_elastic_dsl_query() { |
| 339 | + let ast = dsl::Search::new() |
| 340 | + .query( |
| 341 | + dsl::Query::bool() |
| 342 | + .filter(dsl::Query::term("service.keyword", "frontend")) |
| 343 | + .filter(dsl::Query::range("@timestamp").gte("now-30s").lte("now")), |
| 344 | + ) |
| 345 | + .aggregate( |
| 346 | + "by_service", |
| 347 | + dsl::Aggregation::terms("service.keyword") |
| 348 | + .aggregate("max_latency", dsl::Aggregation::max("latency_ms")), |
| 349 | + ); |
| 350 | + let info = walk_ast_and_extract_info(&ast).expect("info should parse"); |
| 351 | + |
| 352 | + assert_eq!(info.target_field, "latency_ms"); |
| 353 | + assert_eq!(info.aggregation, AggregationType::Max); |
| 354 | + assert_eq!(info.predicates.len(), 2); |
| 355 | + assert_eq!( |
| 356 | + info.group_by_buckets, |
| 357 | + Some(GroupBySpec::Fields(vec!["service".to_string()])) |
| 358 | + ); |
| 359 | + } |
| 360 | +} |
0 commit comments