Skip to main content

elasticctl_api/search/
mod.rs

1//! Ad hoc data search: ES|QL and Query DSL.
2
3pub mod dataview;
4pub mod dsl;
5pub mod esql;
6
7use elasticctl_core::{Result, Transport};
8
9pub struct SearchRequest {
10    pub index: Option<String>,
11    pub data_view: Option<String>,
12    pub limit: Option<usize>,
13}
14
15pub fn has_source(query: &str) -> bool {
16    matches!(
17        query
18            .split_whitespace()
19            .next()
20            .unwrap_or_default()
21            .to_ascii_lowercase()
22            .as_str(),
23        "from" | "row" | "show" | "metrics"
24    )
25}
26
27pub fn prepend_from(query: &str, pattern: &str) -> String {
28    if has_source(query) {
29        query.to_string()
30    } else {
31        let q = query.trim_start();
32        let sep = if q.starts_with('|') { "" } else { "| " };
33        format!("FROM {pattern} {sep}{q}")
34    }
35}
36
37/// Rewrite an ES|QL query's source to `pattern`, used when `--index` or
38/// `--data-view` is given. A leading `FROM <source>` becomes `FROM <pattern>`
39/// with any `| …` pipeline kept; `ROW`/`SHOW`/`METRICS` pass through untouched;
40/// a query with no source command gets `FROM <pattern>` prepended.
41pub fn rewrite_from(query: &str, pattern: &str) -> String {
42    let trimmed = query.trim_start();
43    let leading = &query[..query.len() - trimmed.len()];
44    let word_end = trimmed.find(char::is_whitespace).unwrap_or(trimmed.len());
45    let word = &trimmed[..word_end];
46    if word.eq_ignore_ascii_case("from") {
47        let after = &trimmed[word_end..];
48        let source_end = after.find('|').unwrap_or(after.len());
49        let pipeline = &after[source_end..];
50        if pipeline.is_empty() {
51            format!("{leading}FROM {pattern}")
52        } else {
53            format!("{leading}FROM {pattern} {pipeline}")
54        }
55    } else if word.eq_ignore_ascii_case("row")
56        || word.eq_ignore_ascii_case("show")
57        || word.eq_ignore_ascii_case("metrics")
58    {
59        query.to_string()
60    } else {
61        prepend_from(query, pattern)
62    }
63}
64
65/// Resolve an ES|QL query's source: `--index` wins, then `--data-view`, then
66/// the query's own source, then the space's default alerts index.
67pub async fn resolve_esql_query(t: &Transport, query: &str, req: &SearchRequest) -> Result<String> {
68    match (&req.index, &req.data_view) {
69        (Some(i), _) => Ok(rewrite_from(query, i)),
70        (_, Some(dv)) => Ok(rewrite_from(query, &dataview::resolve(t, dv).await?)),
71        _ => {
72            if has_source(query) {
73                Ok(query.to_string())
74            } else {
75                Ok(prepend_from(
76                    query,
77                    &dataview::default_alerts_index(t).await?,
78                ))
79            }
80        }
81    }
82}
83
84/// Resolve a DSL search's index: `--index` wins, then `--data-view`, then the
85/// space's default alerts index.
86pub async fn resolve_dsl_index(t: &Transport, req: &SearchRequest) -> Result<String> {
87    match (&req.index, &req.data_view) {
88        (Some(i), _) => Ok(i.clone()),
89        (_, Some(dv)) => dataview::resolve(t, dv).await,
90        _ => dataview::default_alerts_index(t).await,
91    }
92}