elasticctl_api/search/
dsl.rs1use elasticctl_core::{Error, ErrorKind, Result, Transport};
4use serde_json::{Value, json};
5
6#[derive(Debug, Clone, PartialEq)]
7pub struct DslHit {
8 pub source: Value,
9 pub sort: Option<Vec<Value>>,
10 pub id: Option<String>,
11 pub index: Option<String>,
12 pub score: Option<f64>,
13}
14
15#[derive(Debug, Clone, PartialEq)]
16pub struct DslPage {
17 pub hits: Vec<DslHit>,
18 pub total: Option<u64>,
19}
20
21pub fn decode(value: &Value) -> Result<DslPage> {
22 let hits = value
23 .pointer("/hits/hits")
24 .and_then(Value::as_array)
25 .ok_or_else(|| {
26 Error::new(
27 ErrorKind::Http,
28 "decoding search response field `hits.hits`",
29 )
30 })?;
31 let out = hits
32 .iter()
33 .map(|h| {
34 Ok(DslHit {
35 source: h.get("_source").cloned().unwrap_or(Value::Null),
36 sort: h.get("sort").and_then(Value::as_array).cloned(),
37 id: h.get("_id").and_then(Value::as_str).map(str::to_owned),
38 index: h.get("_index").and_then(Value::as_str).map(str::to_owned),
39 score: h.get("_score").and_then(Value::as_f64),
40 })
41 })
42 .collect::<Result<Vec<_>>>()?;
43 let total = value.pointer("/hits/total/value").and_then(Value::as_u64);
44 Ok(DslPage { hits: out, total })
45}
46
47pub async fn run_sync(t: &Transport, index: &str, body: &Value) -> Result<DslPage> {
50 let response = t
51 .post_absolute_es(&format!("/{index}/_search"), body)
52 .await?;
53 decode(&response)
54}
55
56fn total_sort(sort: &Value) -> Value {
60 let mut entries = match sort {
61 Value::Array(items) => items.clone(),
62 other => vec![other.clone()],
63 };
64 let has_tiebreaker = entries.iter().any(|entry| {
65 entry
66 .as_object()
67 .is_some_and(|obj| obj.contains_key("_shard_doc"))
68 });
69 if !has_tiebreaker {
70 entries.push(json!({"_shard_doc": "asc"}));
71 }
72 Value::Array(entries)
73}
74
75pub async fn run_stream(
80 t: &Transport,
81 index: &str,
82 query: &Value,
83 sort: &Value,
84 limit: Option<usize>,
85) -> Result<Vec<DslHit>> {
86 let open: Value = t
87 .post_absolute_es(&format!("/{index}/_pit?keep_alive=1m"), &json!({}))
88 .await?;
89 let pit_id = open
90 .get("id")
91 .and_then(Value::as_str)
92 .ok_or_else(|| Error::new(ErrorKind::Http, "decoding _pit open response field `id`"))?
93 .to_string();
94
95 let sort = total_sort(sort);
96 let result = page_loop(t, &pit_id, query, &sort, limit).await;
97
98 let _ = t
103 .delete_absolute_es_json("/_pit", &json!({ "id": pit_id }))
104 .await;
105 result
106}
107
108async fn page_loop(
109 t: &Transport,
110 pit_id: &str,
111 query: &Value,
112 sort: &Value,
113 limit: Option<usize>,
114) -> Result<Vec<DslHit>> {
115 let mut all = Vec::new();
116 let mut search_after: Option<Vec<Value>> = None;
117 let size = limit.map_or(1000, |n| n.min(1000));
118 loop {
119 let mut body = json!({
120 "size": size,
121 "sort": sort,
122 "pit": { "id": pit_id, "keep_alive": "1m" },
123 "query": query
124 });
125 if let Some(sa) = &search_after {
126 body["search_after"] = json!(sa);
127 }
128 let page = decode(&t.post_absolute_es("/_search", &body).await?)?;
129 if page.hits.is_empty() {
130 return Ok(all);
131 }
132 let last_sort = page.hits.last().and_then(|h| h.sort.clone());
133 all.extend(page.hits);
134 if let Some(limit) = limit
135 && all.len() >= limit
136 {
137 all.truncate(limit);
138 return Ok(all);
139 }
140 match last_sort {
141 Some(sa) => search_after = Some(sa),
142 None => return Ok(all),
143 }
144 }
145}