rs_es/operations/
mapping.rs

1/*
2 * Copyright 2016-2019 Ben Ashford
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! Implementation of ElasticSearch Mapping operation
18
19//!
20//! Please note: this will grow and become a full implementation of the ElasticSearch
21//! [Indices API](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices.html)
22//! so subtle (potentially breaking) changes will be made to the API when that happens
23
24use std::collections::HashMap;
25use std::hash::Hash;
26
27use reqwest::StatusCode;
28
29use serde::Serialize;
30use serde_json::{Map, Value};
31
32use crate::{error::EsError, operations::GenericResult, Client, EsResponse};
33
34pub type DocType<'a> = HashMap<&'a str, HashMap<&'a str, &'a str>>;
35pub type Mapping<'a> = HashMap<&'a str, DocType<'a>>;
36
37#[derive(Debug, Serialize)]
38pub struct Settings {
39    pub number_of_shards: u32,
40    pub analysis: Analysis,
41}
42
43#[derive(Debug, Serialize, Default)]
44pub struct Analysis {
45    pub filter: Map<String, Value>,
46    pub analyzer: Map<String, Value>,
47    pub tokenizer: Map<String, Value>,
48    pub char_filter: Map<String, Value>,
49}
50
51/// An indexing operation
52#[derive(Debug)]
53pub struct MappingOperation<'a, 'b> {
54    /// The HTTP client that this operation will use
55    client: &'a mut Client,
56
57    /// The index that will be created and eventually mapped
58    index: &'b str,
59
60    /// A map containing the doc types and their mapping
61    mapping: Option<&'b Mapping<'b>>,
62
63    /// A struct reflecting the settings that enable the
64    /// customization of analyzers
65    settings: Option<&'b Settings>,
66}
67
68impl<'a, 'b> MappingOperation<'a, 'b> {
69    pub fn new(client: &'a mut Client, index: &'b str) -> MappingOperation<'a, 'b> {
70        MappingOperation {
71            client,
72            index,
73            mapping: None,
74            settings: None,
75        }
76    }
77
78    /// Set the actual mapping
79    pub fn with_mapping(&'b mut self, mapping: &'b Mapping) -> &'b mut Self {
80        self.mapping = Some(mapping);
81        self
82    }
83
84    /// Set the settings
85    pub fn with_settings(&'b mut self, settings: &'b Settings) -> &'b mut Self {
86        self.settings = Some(settings);
87        self
88    }
89
90    /// If settings have been provided, the index will be created with them. If the index already
91    /// exists, an `Err(EsError)` will be returned.
92    /// If mapping have been set too, the properties will be applied. The index will be unavailable
93    /// during this process.
94    /// Nothing will be done if either mapping and settings are not present.
95    pub fn send(&'b mut self) -> Result<MappingResult, EsError> {
96        // Return earlier if there is nothing to do
97        if self.mapping.is_none() && self.settings.is_none() {
98            return Ok(MappingResult);
99        }
100
101        if self.settings.is_some() {
102            let body = hashmap("settings", self.settings.unwrap());
103            let url = self.index.to_owned();
104            let _ = self.client.put_body_op(&url, &body)?;
105
106            let _ = self.client.wait_for_status("yellow", "5s");
107        }
108
109        if self.mapping.is_some() {
110            let _ = self.client.close_index(self.index);
111
112            for (entity, properties) in self.mapping.unwrap().iter() {
113                let body = hashmap("properties", properties);
114                let url = format!("{}/_mapping/{}", self.index, entity);
115                let _ = self.client.put_body_op(&url, &body)?;
116            }
117
118            let _ = self.client.open_index(self.index);
119        }
120
121        Ok(MappingResult)
122    }
123}
124
125impl Client {
126    /// Open the index, making it available.
127    pub fn open_index<'a>(&'a mut self, index: &'a str) -> Result<GenericResult, EsError> {
128        let url = format!("{}/_open", index);
129        let response = self.post_op(&url)?;
130
131        match response.status_code() {
132            StatusCode::OK => Ok(response.read_response()?),
133            status_code => Err(EsError::EsError(format!(
134                "Unexpected status: {}",
135                status_code
136            ))),
137        }
138    }
139
140    /// Close the index, making it unavailable and modifiable.
141    pub fn close_index<'a>(&'a mut self, index: &'a str) -> Result<GenericResult, EsError> {
142        let url = format!("{}/_close", index);
143        let response = self.post_op(&url)?;
144
145        match response.status_code() {
146            StatusCode::OK => Ok(response.read_response()?),
147            status_code => Err(EsError::EsError(format!(
148                "Unexpected status: {}",
149                status_code
150            ))),
151        }
152    }
153
154    /// TODO: Return proper health data from
155    /// https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-health.html
156    pub fn wait_for_status<'a>(
157        &'a mut self,
158        status: &'a str,
159        timeout: &'a str,
160    ) -> Result<(), EsError> {
161        let url = format!(
162            "_cluster/health?wait_for_status={}&timeout={}",
163            status, timeout
164        );
165        let response = self.get_op(&url)?;
166
167        match response.status_code() {
168            StatusCode::OK => Ok(()),
169            status_code => Err(EsError::EsError(format!(
170                "Unexpected status: {}",
171                status_code
172            ))),
173        }
174    }
175}
176
177/// The result of a mapping operation
178#[derive(Debug)]
179pub struct MappingResult;
180
181#[cfg(test)]
182pub mod tests {
183    use super::*;
184
185    #[derive(Debug, Serialize)]
186    pub struct Author {
187        pub name: String,
188    }
189
190    #[test]
191    fn test_mapping() {
192        let index_name = "tests_test_mapping";
193        let mut client = crate::tests::make_client();
194
195        // TODO - this fails in many cases (specifically on TravisCI), but we ignore the
196        // failures anyway
197        let _ = client.delete_index(index_name);
198
199        let mapping = hashmap2(
200            "post",
201            hashmap2(
202                "created_at",
203                hashmap2("type", "date", "format", "date_time"),
204                "title",
205                hashmap2("type", "string", "index", "not_analyzed"),
206            ),
207            "author",
208            hashmap("name", hashmap("type", "string")),
209        );
210
211        let settings = Settings {
212            number_of_shards: 1,
213
214            analysis: Analysis {
215                filter: serde_json::json! ({
216                    "autocomplete_filter": {
217                        "type": "edge_ngram",
218                        "min_gram": 1,
219                        "max_gram": 2,
220                    }
221                })
222                .as_object()
223                .expect("by construction 'autocomplete_filter' should be a map")
224                .clone(),
225                analyzer: serde_json::json! ({
226                    "autocomplete": {
227                        "type": "custom",
228                        "tokenizer": "standard",
229                        "filter": [ "lowercase", "autocomplete_filter"]
230                    }
231                })
232                .as_object()
233                .expect("by construction 'autocomplete' should be a map")
234                .clone(),
235                char_filter: serde_json::json! ({
236                    "char_filter": {
237                        "type": "pattern_replace",
238                        "pattern": ",",
239                        "replacement": " "
240                    }
241                })
242                .as_object()
243                .expect("by construction 'char_filter' should be a map")
244                .clone(),
245                tokenizer: serde_json::json! ({
246                })
247                .as_object()
248                .expect("by construction 'empty tokenizer' should be a map")
249                .clone(),
250            },
251        };
252
253        // TODO add appropriate functions to the `Client` struct
254        let result = MappingOperation::new(&mut client, index_name)
255            .with_mapping(&mapping)
256            .with_settings(&settings)
257            .send();
258        assert!(result.is_ok());
259
260        {
261            let result_wrapped = client
262                .index(index_name, "post")
263                .with_doc(&Author {
264                    name: "Homu".to_owned(),
265                })
266                .send();
267
268            assert!(result_wrapped.is_ok());
269
270            let result = result_wrapped.unwrap();
271            assert!(result.created);
272        }
273    }
274}
275
276fn hashmap<K, V>(k: K, v: V) -> HashMap<K, V>
277where
278    K: Eq + Hash,
279{
280    let mut m = HashMap::with_capacity(1);
281    m.insert(k, v);
282    m
283}
284
285#[allow(dead_code)]
286fn hashmap2<K, V>(k1: K, v1: V, k2: K, v2: V) -> HashMap<K, V>
287where
288    K: Eq + Hash,
289{
290    let mut m = HashMap::with_capacity(2);
291    m.insert(k1, v1);
292    m.insert(k2, v2);
293    m
294}