├── .gitignore ├── docker-compose.yml ├── Cargo.toml ├── src ├── query │ ├── common.rs │ ├── joining.rs │ ├── specialized.rs │ ├── compound.rs │ ├── functions.rs │ ├── term.rs │ ├── geo.rs │ └── mod.rs ├── operations │ ├── refresh.rs │ ├── version.rs │ ├── delete_index.rs │ ├── mod.rs │ ├── analyze.rs │ ├── delete.rs │ ├── search │ │ ├── aggregations │ │ │ ├── common.rs │ │ │ ├── mod.rs │ │ │ └── metrics.rs │ │ ├── highlight.rs │ │ └── count.rs │ ├── common.rs │ ├── get.rs │ ├── index.rs │ ├── mapping.rs │ └── bulk.rs ├── util.rs ├── error.rs ├── json.rs ├── lib.rs └── units.rs ├── .travis.yml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | es: 2 | image: elasticsearch:2.0 3 | ports: 4 | - "9200:9200" -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rs-es" 3 | version = "0.12.3" 4 | authors = ["Ben Ashford"] 5 | license = "Apache-2.0" 6 | repository = "https://github.com/benashford/rs-es" 7 | description = "Client for the ElasticSearch REST API" 8 | readme = "README.md" 9 | keywords = ["elasticsearch", "elastic"] 10 | exclude = [".gitignore", ".travis.yml", "docker-compose.yml"] 11 | edition = "2018" 12 | 13 | [features] 14 | default = [] 15 | es5 = [] 16 | geo = ["geojson"] 17 | 18 | [lib] 19 | name = "rs_es" 20 | 21 | [dependencies] 22 | reqwest = "0.9" 23 | log = "0.4" 24 | serde_json = "1.0" 25 | serde = {version = "1", features = ["derive"]} 26 | geojson = { version="0.16", optional=true} 27 | 28 | [dev-dependencies] 29 | env_logger = "0.6" 30 | regex = "1.2" 31 | doc-comment = "0.3" 32 | -------------------------------------------------------------------------------- /src/query/common.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 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 | //! Common macros, utilities, etc. for the query crate 18 | 19 | use crate::json::FieldBased; 20 | 21 | // Helper macros 22 | 23 | /// Build the `build` function for each builder struct 24 | macro_rules! build { 25 | ($t:ident) => ( 26 | pub fn build(self) -> Query { 27 | Query::$t(Box::new(self)) 28 | } 29 | ) 30 | } 31 | 32 | pub type FieldBasedQuery = FieldBased; 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | sudo: false 4 | 5 | addons: 6 | apt: 7 | packages: 8 | - libcurl4-openssl-dev 9 | - libelf-dev 10 | - libdw-dev 11 | 12 | rust: 13 | - stable 14 | - beta 15 | - nightly 16 | 17 | before_install: 18 | - echo "Before install, check Java version..." 19 | - sudo apt-get remove openjdk* 20 | - sudo apt-get install openjdk-8-jre 21 | - which java 22 | - echo $JAVA_HOME 23 | - curl -O ${ES_DOWNLOAD_URL} && unzip elasticsearch-${ES_VERSION}.zip 24 | - elasticsearch-${ES_VERSION}/bin/elasticsearch > /tmp/es.log & 25 | 26 | # As recommended here: http://docs.travis-ci.com/user/database-setup/#ElasticSearch 27 | before_script: 28 | - sleep 10 29 | 30 | script: 31 | - cargo build ${FEATURES} 32 | - cargo test ${FEATURES} 33 | 34 | env: 35 | global: 36 | - RUST_BACKTRACE=1 37 | - JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/jre 38 | matrix: 39 | - FEATURES="--features es5" ES_VERSION=5.6.14 ES_DOWNLOAD_URL=https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${ES_VERSION}.zip 40 | - FEATURES="--features es5,geo" ES_VERSION=5.6.14 ES_DOWNLOAD_URL=https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${ES_VERSION}.zip 41 | - FEATURES="" ES_VERSION=2.0.2 ES_DOWNLOAD_URL=https://download.elasticsearch.org/elasticsearch/release/org/elasticsearch/distribution/zip/elasticsearch/2.0.2/elasticsearch-2.0.2.zip 42 | - FEATURES="--features geo" ES_VERSION=2.0.2 ES_DOWNLOAD_URL=https://download.elasticsearch.org/elasticsearch/release/org/elasticsearch/distribution/zip/elasticsearch/2.0.2/elasticsearch-2.0.2.zip -------------------------------------------------------------------------------- /src/operations/refresh.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 | //! Refresh an Index 18 | 19 | use reqwest::StatusCode; 20 | 21 | use serde::Deserialize; 22 | 23 | use crate::{error::EsError, Client, EsResponse}; 24 | 25 | use super::{format_multi, ShardCountResult}; 26 | 27 | #[derive(Debug)] 28 | pub struct RefreshOperation<'a, 'b> { 29 | /// The HTTP client 30 | client: &'a mut Client, 31 | 32 | /// The indexes being refreshed 33 | indexes: &'b [&'b str], 34 | } 35 | 36 | impl<'a, 'b> RefreshOperation<'a, 'b> { 37 | pub fn new(client: &'a mut Client) -> RefreshOperation { 38 | RefreshOperation { 39 | client, 40 | indexes: &[], 41 | } 42 | } 43 | 44 | pub fn with_indexes(&'b mut self, indexes: &'b [&'b str]) -> &'b mut Self { 45 | self.indexes = indexes; 46 | self 47 | } 48 | 49 | pub fn send(&mut self) -> Result { 50 | let url = format!("/{}/_refresh", format_multi(&self.indexes)); 51 | let response = self.client.post_op(&url)?; 52 | match response.status_code() { 53 | StatusCode::OK => Ok(response.read_response()?), 54 | status_code => Err(EsError::EsError(format!( 55 | "Unexpected status: {}", 56 | status_code 57 | ))), 58 | } 59 | } 60 | } 61 | 62 | impl Client { 63 | /// Refresh 64 | /// 65 | /// See: https://www.elastic.co/guide/en/elasticsearch/reference/1.x/indices-refresh.html 66 | pub fn refresh(&mut self) -> RefreshOperation { 67 | RefreshOperation::new(self) 68 | } 69 | } 70 | 71 | /// Result of a refresh request 72 | #[derive(Deserialize)] 73 | pub struct RefreshResult { 74 | #[serde(rename = "_shards")] 75 | pub shards: ShardCountResult, 76 | } 77 | -------------------------------------------------------------------------------- /src/operations/version.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 | //! Fetch ElasticSearch version information 18 | 19 | use serde::Deserialize; 20 | 21 | use crate::{error::EsError, Client, EsResponse}; 22 | 23 | #[derive(Debug)] 24 | pub struct VersionOperation<'a> { 25 | client: &'a mut Client, 26 | } 27 | 28 | impl<'a> VersionOperation<'a> { 29 | pub fn new(client: &'a mut Client) -> Self { 30 | VersionOperation { client } 31 | } 32 | 33 | pub fn send(&mut self) -> Result { 34 | let response = self.client.get_op("/")?; 35 | Ok(response.read_response()?) 36 | } 37 | } 38 | 39 | impl Client { 40 | /// Calls the base ES path, returning the version number 41 | pub fn version(&mut self) -> VersionOperation { 42 | VersionOperation::new(self) 43 | } 44 | } 45 | 46 | #[derive(Debug, Deserialize)] 47 | pub struct Version { 48 | pub number: String, 49 | pub build_hash: String, 50 | #[cfg(not(feature = "es5"))] 51 | pub build_timestamp: String, 52 | #[cfg(feature = "es5")] 53 | pub build_date: String, 54 | pub build_snapshot: bool, 55 | pub lucene_version: String, 56 | } 57 | 58 | #[derive(Debug, Deserialize)] 59 | pub struct VersionResult { 60 | pub name: String, 61 | pub cluster_name: String, 62 | #[cfg(feature = "es5")] 63 | pub cluster_uuid: String, 64 | pub version: Version, 65 | pub tagline: String, 66 | } 67 | 68 | #[cfg(test)] 69 | pub mod tests { 70 | use crate::tests::make_client; 71 | use regex::Regex; 72 | 73 | #[test] 74 | fn it_works() { 75 | let mut client = make_client(); 76 | let result = client.version().send().unwrap(); 77 | 78 | let expected_regex = Regex::new(r"^\d\.\d\.\d+$").unwrap(); 79 | assert_eq!(expected_regex.is_match(&result.version.number), true); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/operations/delete_index.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 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 Delete Index operation 18 | 19 | use reqwest::StatusCode; 20 | 21 | use crate::{error::EsError, Client, EsResponse}; 22 | 23 | use super::GenericResult; 24 | 25 | impl Client { 26 | /// Delete given index 27 | /// 28 | /// TODO: ensure all options are supported, replace with a `DeleteIndexOperation` to 29 | /// follow the pattern defined elsewhere. 30 | /// 31 | /// See: https://www.elastic.co/guide/en/elasticsearch/reference/2.x/indices-delete-index.html 32 | pub fn delete_index<'a>(&'a mut self, index: &'a str) -> Result { 33 | let url = format!("/{}/", index); 34 | let response = self.delete_op(&url)?; 35 | 36 | match response.status_code() { 37 | StatusCode::OK => Ok(response.read_response()?), 38 | status_code => Err(EsError::EsError(format!( 39 | "Unexpected status: {}", 40 | status_code 41 | ))), 42 | } 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | pub mod tests { 48 | use crate::tests::{clean_db, make_client, TestDocument}; 49 | 50 | #[test] 51 | fn test_delete_index() { 52 | let index_name = "test_delete_index"; 53 | let mut client = make_client(); 54 | 55 | clean_db(&mut client, index_name); 56 | { 57 | let result = client 58 | .index(index_name, "test_type") 59 | .with_doc(&TestDocument::new().with_int_field(1)) 60 | .send(); 61 | assert!(result.is_ok()); 62 | } 63 | { 64 | let result = client.delete_index(index_name); 65 | log::info!("DELETE INDEX RESULT: {:?}", result); 66 | 67 | assert!(result.is_ok()); 68 | 69 | let result_wrapped = result.unwrap(); 70 | assert!(result_wrapped.acknowledged); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 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 | //! Miscellaneous code used in numerous places 18 | 19 | use std::iter::Iterator; 20 | 21 | /// A custom String-join trait as the stdlib one is currently marked as unstable. 22 | pub trait StrJoin { 23 | /// Join an iterator of things that can be referenced as strings into a 24 | /// single owned-string by the given joining string 25 | /// 26 | /// # Example 27 | /// 28 | /// ``` 29 | /// use rs_es::util::StrJoin; 30 | /// 31 | /// let data = vec!["a", "b", "c", "d"]; 32 | /// assert_eq!("a-b-c-d", data.iter().join("-")); 33 | /// ``` 34 | /// 35 | /// This will print: `a-b-c-d` 36 | /// 37 | fn join(self, join: &str) -> String; 38 | } 39 | 40 | impl StrJoin for I 41 | where 42 | S: AsRef, 43 | I: Iterator, 44 | { 45 | fn join(self, join: &str) -> String { 46 | let mut s = String::new(); 47 | for f in self { 48 | s.push_str(f.as_ref()); 49 | s.push_str(join); 50 | } 51 | s.pop(); 52 | s 53 | } 54 | } 55 | 56 | /// Useful macro for adding a function to supply a value to an optional field 57 | macro_rules! add_field { 58 | ($n:ident, $f:ident, $t:ty) => ( 59 | pub fn $n>(mut self, val: T) -> Self { 60 | self.$f = Some(val.into()); 61 | self 62 | } 63 | ); 64 | } 65 | 66 | /// Useful macros for implementing `From` traits 67 | /// 68 | /// TODO: this may only be useful for Query DSL, in which case should be moved 69 | /// to that module 70 | macro_rules! from_exp { 71 | ($ft:ty, $dt:ident, $pi:ident, $ex:expr) => { 72 | impl From<$ft> for $dt { 73 | fn from($pi: $ft) -> $dt { 74 | $ex 75 | } 76 | } 77 | }; 78 | } 79 | 80 | macro_rules! from { 81 | ($ft:ty, $dt:ident, $ev:ident, $pi:ident) => { 82 | from_exp!($ft, $dt, $pi, $dt::$ev($pi)); 83 | }; 84 | ($ft:ty, $dt:ident, $ev:ident) => { 85 | from!($ft, $dt, $ev, from); 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /src/operations/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 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 | //! Implementations of specific ElasticSearch operations 18 | //! 19 | //! The various methods on [`Client`](../struct.Client.html) are entry points to 20 | //! ElasticSearch's set of operations. This module, and it's child modules are 21 | //! the implementation of those operations. 22 | 23 | use std::borrow::Cow; 24 | 25 | use serde::{Serialize, Deserialize}; 26 | 27 | use crate::util::StrJoin; 28 | 29 | // Specific operations 30 | #[macro_use] 31 | pub mod common; 32 | 33 | pub mod analyze; 34 | pub mod bulk; 35 | pub mod delete; 36 | pub mod delete_index; 37 | pub mod get; 38 | pub mod index; 39 | pub mod mapping; 40 | pub mod refresh; 41 | pub mod search; 42 | pub mod version; 43 | 44 | // Common utility functions 45 | 46 | /// A repeating convention in the ElasticSearch REST API is parameters that can 47 | /// take multiple values 48 | fn format_multi<'a>(parts: &[&'a str]) -> Cow<'a, str> { 49 | match parts.len() { 50 | 0 => Cow::Borrowed("_all"), 51 | 1 => Cow::Borrowed(parts[0]), 52 | _ => Cow::Owned(parts.iter().join(",")), 53 | } 54 | } 55 | 56 | /// Multiple operations require indexes and types to be specified, there are 57 | /// rules for combining the two however. E.g. all indexes is specified with 58 | /// `_all`, but all types are specified by omitting type entirely. 59 | fn format_indexes_and_types<'a>(indexes: &[&'a str], types: &[&str]) -> Cow<'a, str> { 60 | if types.is_empty() { 61 | format_multi(indexes) 62 | } else { 63 | Cow::Owned(format!("{}/{}", format_multi(indexes), format_multi(types))) 64 | } 65 | } 66 | 67 | // Results 68 | 69 | /// Shared struct for operations that include counts of success/failed shards. 70 | /// This is returned within various other result structs. 71 | #[derive(Debug, Deserialize, Serialize)] 72 | pub struct ShardCountResult { 73 | pub total: u64, 74 | pub successful: u64, 75 | pub failed: u64, 76 | } 77 | 78 | #[derive(Debug, Deserialize)] 79 | pub struct GenericResult { 80 | pub acknowledged: bool, 81 | } 82 | -------------------------------------------------------------------------------- /src/operations/analyze.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Ben Ashford 3 | * Copyright 2015 Astro 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | //! Implementation of ElasticSearch Analyze operation 19 | 20 | use serde::Deserialize; 21 | 22 | use crate::{ 23 | error::EsError, 24 | {Client, EsResponse}, 25 | }; 26 | 27 | #[derive(Debug)] 28 | pub struct AnalyzeOperation<'a, 'b> { 29 | /// The HTTP client that this operation will use 30 | client: &'a mut Client, 31 | 32 | body: &'b str, 33 | index: Option<&'b str>, 34 | analyzer: Option<&'b str>, 35 | } 36 | 37 | impl<'a, 'b> AnalyzeOperation<'a, 'b> { 38 | pub fn new(client: &'a mut Client, body: &'b str) -> AnalyzeOperation<'a, 'b> { 39 | AnalyzeOperation { 40 | client, 41 | body, 42 | index: None, 43 | analyzer: None, 44 | } 45 | } 46 | 47 | pub fn with_index(&mut self, index: &'b str) -> &mut Self { 48 | self.index = Some(index); 49 | self 50 | } 51 | 52 | pub fn with_analyzer(&mut self, analyzer: &'b str) -> &mut Self { 53 | self.analyzer = Some(analyzer); 54 | self 55 | } 56 | 57 | pub fn send(&mut self) -> Result { 58 | let mut url = match self.index { 59 | None => "/_analyze".to_owned(), 60 | Some(index) => format!("{}/_analyze", index), 61 | }; 62 | match self.analyzer { 63 | None => (), 64 | Some(analyzer) => url.push_str(&format!("?analyzer={}", analyzer)), 65 | } 66 | let response = self.client.do_es_op(&url, |url| { 67 | self.client.http_client.post(url).body(self.body.to_owned()) 68 | })?; 69 | 70 | Ok(response.read_response()?) 71 | } 72 | } 73 | 74 | impl Client { 75 | /// Analyze 76 | /// 77 | /// See: https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html 78 | pub fn analyze<'a>(&'a mut self, body: &'a str) -> AnalyzeOperation { 79 | AnalyzeOperation::new(self, body) 80 | } 81 | } 82 | 83 | /// The result of an analyze operation 84 | #[derive(Debug, Deserialize)] 85 | pub struct AnalyzeResult { 86 | pub tokens: Vec, 87 | } 88 | 89 | #[derive(Debug, Deserialize)] 90 | pub struct Token { 91 | pub token: String, 92 | #[serde(rename = "type")] 93 | pub token_type: String, 94 | pub position: u64, 95 | pub start_offset: u64, 96 | pub end_offset: u64, 97 | } 98 | -------------------------------------------------------------------------------- /src/query/joining.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2018 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 | //! Joining queries 18 | 19 | use serde::Serialize; 20 | 21 | use crate::json::ShouldSkip; 22 | 23 | use super::{Query, ScoreMode}; 24 | 25 | /// Nested query 26 | #[derive(Debug, Default, Serialize)] 27 | pub struct NestedQuery { 28 | path: String, 29 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 30 | score_mode: Option, 31 | query: Query, 32 | } 33 | 34 | impl Query { 35 | pub fn build_nested(path: A, query: B) -> NestedQuery 36 | where 37 | A: Into, 38 | B: Into, 39 | { 40 | NestedQuery { 41 | path: path.into(), 42 | query: query.into(), 43 | ..Default::default() 44 | } 45 | } 46 | } 47 | 48 | impl NestedQuery { 49 | add_field!(with_score_mode, score_mode, ScoreMode); 50 | 51 | build!(Nested); 52 | } 53 | 54 | /// Has Child query 55 | #[derive(Debug, Default, Serialize)] 56 | pub struct HasChildQuery { 57 | doc_type: String, 58 | query: Query, 59 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 60 | score_mode: Option, 61 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 62 | min_children: Option, 63 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 64 | max_children: Option, 65 | } 66 | 67 | /// Has Parent query 68 | #[derive(Debug, Default, Serialize)] 69 | pub struct HasParentQuery { 70 | parent_type: String, 71 | query: Query, 72 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 73 | score_mode: Option, 74 | } 75 | 76 | impl Query { 77 | pub fn build_has_child(doc_type: A, query: B) -> HasChildQuery 78 | where 79 | A: Into, 80 | B: Into, 81 | { 82 | HasChildQuery { 83 | doc_type: doc_type.into(), 84 | query: query.into(), 85 | ..Default::default() 86 | } 87 | } 88 | 89 | pub fn build_has_parent(parent_type: A, query: B) -> HasParentQuery 90 | where 91 | A: Into, 92 | B: Into, 93 | { 94 | HasParentQuery { 95 | parent_type: parent_type.into(), 96 | query: query.into(), 97 | ..Default::default() 98 | } 99 | } 100 | } 101 | 102 | impl HasChildQuery { 103 | add_field!(with_score_mode, score_mode, ScoreMode); 104 | add_field!(with_min_children, min_children, u64); 105 | add_field!(with_max_children, max_children, u64); 106 | 107 | build!(HasChild); 108 | } 109 | 110 | impl HasParentQuery { 111 | add_field!(with_score_mode, score_mode, ScoreMode); 112 | 113 | build!(HasParent); 114 | } 115 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 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 | //! Errors and error conversion code for the `rs_es` crate 18 | 19 | use std::error::Error; 20 | use std::fmt; 21 | use std::io::{self, Read}; 22 | 23 | use serde_json; 24 | 25 | // Error handling 26 | 27 | /// Error that can occur include IO and parsing errors, as well as specific 28 | /// errors from the ElasticSearch server and logic errors from this library 29 | #[derive(Debug)] 30 | pub enum EsError { 31 | /// An internal error from this library 32 | EsError(String), 33 | 34 | /// An error reported in a JSON response from the ElasticSearch server 35 | EsServerError(String), 36 | 37 | /// Miscellaneous error from the HTTP library 38 | HttpError(reqwest::Error), 39 | 40 | /// Miscellaneous IO error 41 | IoError(io::Error), 42 | 43 | /// JSON error 44 | JsonError(serde_json::error::Error), 45 | } 46 | 47 | impl From for EsError { 48 | fn from(err: io::Error) -> EsError { 49 | EsError::IoError(err) 50 | } 51 | } 52 | 53 | impl From for EsError { 54 | fn from(err: reqwest::Error) -> EsError { 55 | EsError::HttpError(err) 56 | } 57 | } 58 | 59 | impl From for EsError { 60 | fn from(err: serde_json::error::Error) -> EsError { 61 | EsError::JsonError(err) 62 | } 63 | } 64 | 65 | impl<'a> From<&'a mut reqwest::Response> for EsError { 66 | fn from(err: &'a mut reqwest::Response) -> EsError { 67 | let mut body = String::new(); 68 | match err.read_to_string(&mut body) { 69 | Ok(_) => (), 70 | Err(_) => { 71 | return EsError::EsServerError(format!( 72 | "{} - cannot read response - {:?}", 73 | err.status(), 74 | err 75 | )); 76 | } 77 | } 78 | EsError::EsServerError(format!("{} - {}", err.status(), body)) 79 | } 80 | } 81 | 82 | impl Error for EsError { 83 | fn description(&self) -> &str { 84 | match *self { 85 | EsError::EsError(ref err) => err, 86 | EsError::EsServerError(ref err) => err, 87 | EsError::HttpError(ref err) => err.description(), 88 | EsError::IoError(ref err) => err.description(), 89 | EsError::JsonError(ref err) => err.description(), 90 | } 91 | } 92 | 93 | fn cause(&self) -> Option<&dyn Error> { 94 | match *self { 95 | EsError::EsError(_) => None, 96 | EsError::EsServerError(_) => None, 97 | EsError::HttpError(ref err) => Some(err as &dyn Error), 98 | EsError::IoError(ref err) => Some(err as &dyn Error), 99 | EsError::JsonError(ref err) => Some(err as &dyn Error), 100 | } 101 | } 102 | } 103 | 104 | impl fmt::Display for EsError { 105 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 106 | match *self { 107 | EsError::EsError(ref s) => fmt::Display::fmt(s, f), 108 | EsError::EsServerError(ref s) => fmt::Display::fmt(s, f), 109 | EsError::HttpError(ref err) => fmt::Display::fmt(err, f), 110 | EsError::IoError(ref err) => fmt::Display::fmt(err, f), 111 | EsError::JsonError(ref err) => fmt::Display::fmt(err, f), 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/operations/delete.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-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 delete operations, both Delete-By-Query and Delete-By-Id 18 | 19 | use reqwest::StatusCode; 20 | 21 | use serde::Deserialize; 22 | 23 | use crate::{error::EsError, Client, EsResponse}; 24 | 25 | use super::common::{OptionVal, Options}; 26 | 27 | #[derive(Debug)] 28 | pub struct DeleteOperation<'a, 'b> { 29 | /// The HTTP client 30 | client: &'a mut Client, 31 | 32 | /// The index 33 | index: &'b str, 34 | 35 | /// The type 36 | doc_type: &'b str, 37 | 38 | /// The ID 39 | id: &'b str, 40 | 41 | /// Optional options 42 | options: Options<'b>, 43 | } 44 | 45 | impl<'a, 'b> DeleteOperation<'a, 'b> { 46 | pub fn new( 47 | client: &'a mut Client, 48 | index: &'b str, 49 | doc_type: &'b str, 50 | id: &'b str, 51 | ) -> DeleteOperation<'a, 'b> { 52 | DeleteOperation { 53 | client, 54 | index, 55 | doc_type, 56 | id, 57 | options: Options::default(), 58 | } 59 | } 60 | 61 | add_option!(with_version, "version"); 62 | add_option!(with_version_type, "version_type"); 63 | add_option!(with_routing, "routing"); 64 | add_option!(with_parent, "parent"); 65 | add_option!(with_consistency, "consistency"); 66 | add_option!(with_refresh, "refresh"); 67 | add_option!(with_timeout, "timeout"); 68 | 69 | pub fn send(&'a mut self) -> Result { 70 | let url = format!( 71 | "/{}/{}/{}{}", 72 | self.index, self.doc_type, self.id, self.options 73 | ); 74 | let response = self.client.delete_op(&url)?; 75 | match response.status_code() { 76 | StatusCode::OK => Ok(response.read_response()?), 77 | status_code => Err(EsError::EsError(format!( 78 | "Unexpected status: {}", 79 | status_code 80 | ))), 81 | } 82 | } 83 | } 84 | 85 | impl Client { 86 | /// Delete by ID 87 | /// 88 | /// See: https://www.elastic.co/guide/en/elasticsearch/reference/1.x/docs-delete.html 89 | pub fn delete<'a>( 90 | &'a mut self, 91 | index: &'a str, 92 | doc_type: &'a str, 93 | id: &'a str, 94 | ) -> DeleteOperation { 95 | DeleteOperation::new(self, index, doc_type, id) 96 | } 97 | } 98 | 99 | /// Result of a DELETE operation 100 | #[derive(Debug, Deserialize)] 101 | pub struct DeleteResult { 102 | pub found: bool, 103 | #[serde(rename = "_index")] 104 | pub index: String, 105 | #[serde(rename = "_type")] 106 | pub doc_type: String, 107 | #[serde(rename = "_id")] 108 | pub id: String, 109 | #[serde(rename = "_version")] 110 | pub version: u64, 111 | } 112 | 113 | #[cfg(test)] 114 | pub mod tests { 115 | use crate::tests::{clean_db, make_client, TestDocument}; 116 | 117 | #[test] 118 | fn test_delete() { 119 | let index_name = "test_delete"; 120 | let mut client = make_client(); 121 | 122 | clean_db(&mut client, index_name); 123 | let id = { 124 | let doc = TestDocument::new().with_int_field(4); 125 | let result = client 126 | .index(index_name, "test_type") 127 | .with_doc(&doc) 128 | .send() 129 | .unwrap(); 130 | result.id 131 | }; 132 | 133 | let delete_result = client.delete(index_name, "test_type", &id).send().unwrap(); 134 | assert_eq!(id, delete_result.id); 135 | assert_eq!(true, delete_result.found); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/operations/search/aggregations/common.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2018 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 | //! Features that are common to all aggregations 18 | 19 | use std::collections::HashMap; 20 | 21 | use serde::ser::{Serialize, SerializeMap, Serializer}; 22 | 23 | use crate::json::{serialize_map_optional_kv, MergeSerialize}; 24 | use crate::units::JsonVal; 25 | 26 | macro_rules! agg { 27 | ($b:ident) => { 28 | impl<'a> $b<'a> { 29 | pub fn field(field: &'a str) -> Self { 30 | $b(Agg { 31 | field: Some(field), 32 | ..Default::default() 33 | }) 34 | } 35 | 36 | pub fn script>>(script: S) -> Self { 37 | $b(Agg { 38 | script: script.into(), 39 | ..Default::default() 40 | }) 41 | } 42 | 43 | pub fn with_script>>(mut self, script: S) -> Self { 44 | self.0.script = script.into(); 45 | self 46 | } 47 | 48 | pub fn with_missing>(mut self, missing: J) -> Self { 49 | self.0.missing = Some(missing.into()); 50 | self 51 | } 52 | } 53 | 54 | impl<'a> Serialize for $b<'a> { 55 | fn serialize(&self, serializer: S) -> Result 56 | where 57 | S: Serializer, 58 | { 59 | self.0.serialize(serializer) 60 | } 61 | } 62 | }; 63 | } 64 | 65 | /// Scripts used in aggregations 66 | #[derive(Debug, Default)] 67 | pub struct Script<'a> { 68 | pub inline: Option<&'a str>, 69 | pub file: Option<&'a str>, 70 | pub id: Option<&'a str>, 71 | pub params: Option>, 72 | } 73 | 74 | /// Base of all Metrics aggregations 75 | #[derive(Debug, Default)] 76 | pub struct Agg<'a, E> 77 | where 78 | E: MergeSerialize, 79 | { 80 | pub field: Option<&'a str>, 81 | pub script: Script<'a>, 82 | pub missing: Option, 83 | pub extra: E, 84 | } 85 | 86 | macro_rules! add_extra_option { 87 | ($n:ident, $e:ident, $t:ty) => { 88 | pub fn $n>(mut self, val: T) -> Self { 89 | self.0.extra.$e = Some(val.into()); 90 | self 91 | } 92 | } 93 | } 94 | 95 | impl<'a, E> Serialize for Agg<'a, E> 96 | where 97 | E: MergeSerialize, 98 | { 99 | fn serialize(&self, serializer: S) -> Result 100 | where 101 | S: Serializer, 102 | { 103 | let mut map = serializer.serialize_map(None)?; 104 | 105 | serialize_map_optional_kv(&mut map, "field", &self.field)?; 106 | serialize_map_optional_kv(&mut map, "inline", &self.script.inline)?; 107 | serialize_map_optional_kv(&mut map, "file", &self.script.file)?; 108 | serialize_map_optional_kv(&mut map, "id", &self.script.id)?; 109 | serialize_map_optional_kv(&mut map, "params", &self.script.params)?; 110 | serialize_map_optional_kv(&mut map, "missing", &self.missing)?; 111 | self.extra.merge_serialize(&mut map)?; 112 | 113 | map.end() 114 | } 115 | } 116 | 117 | // Useful for results 118 | 119 | /// Macro to implement the various as... functions that return the details of an 120 | /// aggregation for that particular type 121 | macro_rules! agg_as { 122 | ($n:ident,$st:ident,$tp:ident,$t:ident,$rt:ty) => { 123 | pub fn $n(&self) -> Result<&$rt, EsError> { 124 | match self { 125 | AggregationResult::$st(ref res) => { 126 | match res { 127 | $tp::$t(ref res) => Ok(res), 128 | _ => Err(EsError::EsError(format!("Wrong type: {:?}", self))) 129 | } 130 | }, 131 | _ => Err(EsError::EsError(format!("Wrong type: {:?}", self))) 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/query/specialized.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2018 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 | //! Specialised queries 18 | 19 | use serde::Serialize; 20 | use serde_json::Value; 21 | 22 | use crate::json::ShouldSkip; 23 | 24 | use super::{MinimumShouldMatch, Query}; 25 | 26 | /// More like this query 27 | #[derive(Debug, Default, Serialize)] 28 | pub struct MoreLikeThisQuery { 29 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 30 | fields: Option>, 31 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 32 | like_text: Option, 33 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 34 | ids: Option>, 35 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 36 | docs: Option>, 37 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 38 | max_query_terms: Option, 39 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 40 | min_term_freq: Option, 41 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 42 | min_doc_freq: Option, 43 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 44 | max_doc_freq: Option, 45 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 46 | min_word_length: Option, 47 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 48 | max_word_length: Option, 49 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 50 | stop_words: Option>, 51 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 52 | analyzer: Option, 53 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 54 | minimum_should_match: Option, 55 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 56 | boost_terms: Option, 57 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 58 | include: Option, 59 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 60 | boost: Option, 61 | } 62 | 63 | impl Query { 64 | pub fn build_more_like_this() -> MoreLikeThisQuery { 65 | Default::default() 66 | } 67 | } 68 | 69 | impl MoreLikeThisQuery { 70 | add_field!(with_fields, fields, Vec); 71 | add_field!(with_like_text, like_text, String); 72 | add_field!(with_ids, ids, Vec); 73 | add_field!(with_docs, docs, Vec); 74 | add_field!(with_max_query_terms, max_query_terms, u64); 75 | add_field!(with_min_term_freq, min_term_freq, u64); 76 | add_field!(with_min_doc_freq, min_doc_freq, u64); 77 | add_field!(with_max_doc_freq, max_doc_freq, u64); 78 | add_field!(with_min_word_length, min_word_length, u64); 79 | add_field!(with_max_word_length, max_word_length, u64); 80 | add_field!(with_stop_words, stop_words, Vec); 81 | add_field!(with_analyzer, analyzer, String); 82 | add_field!( 83 | with_minimum_should_match, 84 | minimum_should_match, 85 | MinimumShouldMatch 86 | ); 87 | add_field!(with_boost_terms, boost_terms, f64); 88 | add_field!(with_include, include, bool); 89 | add_field!(with_boost, boost, f64); 90 | 91 | build!(MoreLikeThis); 92 | } 93 | 94 | // A document can be provided as an example 95 | #[derive(Debug, Serialize)] 96 | pub struct Doc { 97 | #[serde(rename = "_index")] 98 | index: String, 99 | #[serde(rename = "_type")] 100 | doc_type: String, 101 | // TODO - consider generifying this option 102 | #[serde(skip_serializing_if = "ShouldSkip::should_skip", rename = "doc")] 103 | doc: Option, 104 | #[serde(skip_serializing_if = "ShouldSkip::should_skip", rename = "_id")] 105 | id: Option, 106 | } 107 | 108 | impl Doc { 109 | pub fn from_doc(index: A, doc_type: B, doc: Value) -> Doc 110 | where 111 | A: Into, 112 | B: Into, 113 | { 114 | Doc { 115 | index: index.into(), 116 | doc_type: doc_type.into(), 117 | doc: Some(doc), 118 | id: None, 119 | } 120 | } 121 | 122 | pub fn id(index: A, doc_type: B, id: C) -> Doc 123 | where 124 | A: Into, 125 | B: Into, 126 | C: Into, 127 | { 128 | Doc { 129 | index: index.into(), 130 | doc_type: doc_type.into(), 131 | doc: None, 132 | id: Some(id.into()), 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/operations/common.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 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 | //! Features common to all operations 18 | 19 | use std::fmt; 20 | 21 | use serde::ser::{Serialize, Serializer}; 22 | 23 | use crate::util::StrJoin; 24 | 25 | /// A newtype for the value of a URI option, this is to allow conversion traits 26 | /// to be implemented for it 27 | #[derive(Debug)] 28 | pub struct OptionVal(pub String); 29 | 30 | /// Conversion from `&str` to `OptionVal` 31 | impl<'a> From<&'a str> for OptionVal { 32 | fn from(from: &'a str) -> OptionVal { 33 | OptionVal(from.to_owned()) 34 | } 35 | } 36 | 37 | // Basic types have conversions to `OptionVal` 38 | from_exp!(String, OptionVal, from, OptionVal(from)); 39 | from_exp!(i32, OptionVal, from, OptionVal(from.to_string())); 40 | from_exp!(i64, OptionVal, from, OptionVal(from.to_string())); 41 | from_exp!(u32, OptionVal, from, OptionVal(from.to_string())); 42 | from_exp!(u64, OptionVal, from, OptionVal(from.to_string())); 43 | from_exp!(bool, OptionVal, from, OptionVal(from.to_string())); 44 | 45 | /// Every ES operation has a set of options 46 | #[derive(Default, Debug)] 47 | pub struct Options<'a>(pub Vec<(&'a str, OptionVal)>); 48 | 49 | impl<'a> Options<'a> { 50 | pub fn new() -> Options<'a> { 51 | Options(Vec::new()) 52 | } 53 | 54 | pub fn is_empty(&self) -> bool { 55 | self.0.is_empty() 56 | } 57 | 58 | /// Add a value 59 | /// 60 | /// ``` 61 | /// use rs_es::operations::common::Options; 62 | /// let mut options = Options::new(); 63 | /// options.push("a", 1); 64 | /// options.push("b", "2"); 65 | /// ``` 66 | pub fn push>(&mut self, key: &'a str, val: O) { 67 | self.0.push((key, val.into())); 68 | } 69 | } 70 | 71 | impl<'a> fmt::Display for Options<'a> { 72 | fn fmt(&self, formatter: &mut fmt::Formatter) -> Result<(), fmt::Error> { 73 | if !self.is_empty() { 74 | formatter.write_str("?")?; 75 | formatter.write_str( 76 | &self 77 | .0 78 | .iter() 79 | .map(|&(ref k, ref v)| format!("{}={}", k, v.0)) 80 | .join("&"), 81 | )?; 82 | } 83 | Ok(()) 84 | } 85 | } 86 | 87 | /// Adds a function to an operation to add specific query-string options to that 88 | /// operations builder interface. 89 | macro_rules! add_option { 90 | ($n:ident, $e:expr) => ( 91 | pub fn $n>(&'a mut self, val: T) -> &'a mut Self { 92 | self.options.push($e, val); 93 | self 94 | } 95 | ) 96 | } 97 | 98 | /// The [`version_type` field](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-versioning) 99 | #[derive(Debug)] 100 | pub enum VersionType { 101 | Internal, 102 | External, 103 | ExternalGt, 104 | ExternalGte, 105 | Force, 106 | } 107 | 108 | impl Serialize for VersionType { 109 | fn serialize(&self, serializer: S) -> Result 110 | where 111 | S: Serializer, 112 | { 113 | self.to_string().serialize(serializer) 114 | } 115 | } 116 | 117 | impl ToString for VersionType { 118 | fn to_string(&self) -> String { 119 | match *self { 120 | VersionType::Internal => "internal", 121 | VersionType::External => "external", 122 | VersionType::ExternalGt => "external_gt", 123 | VersionType::ExternalGte => "external_gte", 124 | VersionType::Force => "force", 125 | } 126 | .to_owned() 127 | } 128 | } 129 | 130 | from_exp!(VersionType, OptionVal, from, OptionVal(from.to_string())); 131 | 132 | /// The consistency query parameter 133 | #[derive(Debug)] 134 | pub enum Consistency { 135 | One, 136 | Quorum, 137 | All, 138 | } 139 | 140 | impl From for OptionVal { 141 | fn from(from: Consistency) -> OptionVal { 142 | OptionVal( 143 | match from { 144 | Consistency::One => "one", 145 | Consistency::Quorum => "quorum", 146 | Consistency::All => "all", 147 | } 148 | .to_owned(), 149 | ) 150 | } 151 | } 152 | 153 | /// Values for `default_operator` query parameters 154 | #[derive(Debug)] 155 | pub enum DefaultOperator { 156 | And, 157 | Or, 158 | } 159 | 160 | impl From for OptionVal { 161 | fn from(from: DefaultOperator) -> OptionVal { 162 | OptionVal( 163 | match from { 164 | DefaultOperator::And => "and", 165 | DefaultOperator::Or => "or", 166 | } 167 | .to_owned(), 168 | ) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/operations/get.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-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 the Get API 18 | 19 | use serde::de::DeserializeOwned; 20 | 21 | use serde::Deserialize; 22 | 23 | use super::common::{OptionVal, Options}; 24 | 25 | use crate::{error::EsError, util::StrJoin, Client, EsResponse}; 26 | 27 | /// Values for the `preference` query parameter 28 | pub enum Preference { 29 | Primary, 30 | Local, 31 | } 32 | 33 | impl From for OptionVal { 34 | fn from(from: Preference) -> OptionVal { 35 | OptionVal( 36 | match from { 37 | Preference::Primary => "_primary", 38 | Preference::Local => "_local", 39 | } 40 | .to_owned(), 41 | ) 42 | } 43 | } 44 | 45 | /// An ES GET operation, to get a document by ID 46 | #[derive(Debug)] 47 | pub struct GetOperation<'a, 'b> { 48 | /// The HTTP connection 49 | client: &'a mut Client, 50 | 51 | /// The index to load the document. 52 | index: &'b str, 53 | 54 | /// Optional type 55 | doc_type: Option<&'b str>, 56 | 57 | /// The ID of the document. 58 | id: &'b str, 59 | 60 | /// Optional options 61 | options: Options<'b>, 62 | } 63 | 64 | impl<'a, 'b> GetOperation<'a, 'b> { 65 | pub fn new(client: &'a mut Client, index: &'b str, id: &'b str) -> Self { 66 | GetOperation { 67 | client, 68 | index, 69 | doc_type: None, 70 | id, 71 | options: Options::default(), 72 | } 73 | } 74 | 75 | pub fn with_all_types(&'b mut self) -> &'b mut Self { 76 | self.doc_type = Some("_all"); 77 | self 78 | } 79 | 80 | pub fn with_doc_type(&'b mut self, doc_type: &'b str) -> &'b mut Self { 81 | self.doc_type = Some(doc_type); 82 | self 83 | } 84 | 85 | pub fn with_fields(&'b mut self, fields: &[&'b str]) -> &'b mut Self { 86 | self.options.push("fields", fields.iter().join(",")); 87 | self 88 | } 89 | 90 | add_option!(with_realtime, "realtime"); 91 | add_option!(with_source, "_source"); 92 | add_option!(with_routing, "routing"); 93 | add_option!(with_preference, "preference"); 94 | add_option!(with_refresh, "refresh"); 95 | add_option!(with_version, "version"); 96 | add_option!(with_version_type, "version_type"); 97 | 98 | pub fn send(&'b mut self) -> Result, EsError> 99 | where 100 | T: DeserializeOwned, 101 | { 102 | let url = format!( 103 | "/{}/{}/{}{}", 104 | self.index, 105 | self.doc_type.expect("No doc_type specified"), 106 | self.id, 107 | self.options 108 | ); 109 | // We're ignoring status_code as all valid codes should return a value, 110 | // so anything else is an error. 111 | let response = self.client.get_op(&url)?; 112 | Ok(response.read_response()?) 113 | } 114 | } 115 | 116 | impl Client { 117 | /// Implementation of the ES GET API 118 | /// 119 | /// See: https://www.elastic.co/guide/en/elasticsearch/reference/1.x/docs-get.html 120 | pub fn get<'a>(&'a mut self, index: &'a str, id: &'a str) -> GetOperation { 121 | GetOperation::new(self, index, id) 122 | } 123 | } 124 | 125 | /// The result of a GET request 126 | #[derive(Debug, Deserialize)] 127 | pub struct GetResult { 128 | #[serde(rename = "_index")] 129 | pub index: String, 130 | #[serde(rename = "_type")] 131 | pub doc_type: String, 132 | #[serde(rename = "_id")] 133 | pub id: String, 134 | #[serde(rename = "_version")] 135 | pub version: Option, 136 | pub found: bool, 137 | #[serde(rename = "_source")] 138 | pub source: Option, 139 | } 140 | 141 | #[cfg(test)] 142 | pub mod tests { 143 | use crate::tests::{clean_db, make_client, TestDocument}; 144 | 145 | #[test] 146 | fn test_get() { 147 | let index_name = "test_get"; 148 | let mut client = make_client(); 149 | clean_db(&mut client, index_name); 150 | { 151 | let doc = TestDocument::new().with_int_field(3).with_bool_field(false); 152 | client 153 | .index(index_name, "test_type") 154 | .with_id("TEST_GETTING") 155 | .with_doc(&doc) 156 | .send() 157 | .unwrap(); 158 | } 159 | { 160 | let mut getter = client.get(index_name, "TEST_GETTING"); 161 | let result_wrapped = getter.with_doc_type("test_type").send(); 162 | let result = result_wrapped.unwrap(); 163 | assert_eq!(result.id, "TEST_GETTING"); 164 | 165 | let source: TestDocument = result.source.expect("Source document"); 166 | assert_eq!(source.str_field, "I am a test"); 167 | assert_eq!(source.int_field, 3); 168 | assert_eq!(source.bool_field, false); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/json.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 | //! Helper for common requirements when producing/parsing JSON 18 | 19 | use serde::ser::{Serialize, SerializeMap, Serializer}; 20 | 21 | /// To tell Serde to skip various fields 22 | pub trait ShouldSkip { 23 | fn should_skip(&self) -> bool; 24 | } 25 | 26 | /// To indicate whether an optional field should be skipped if None 27 | impl ShouldSkip for Option { 28 | fn should_skip(&self) -> bool { 29 | self.is_none() 30 | } 31 | } 32 | 33 | /// Useful serialization functions 34 | pub fn serialize_map_optional_kv( 35 | map_ser: &mut S, 36 | key: K, 37 | value: &Option, 38 | ) -> Result<(), S::Error> 39 | where 40 | S: SerializeMap, 41 | K: Serialize, 42 | V: Serialize, 43 | { 44 | if let Some(ref x) = value { 45 | map_ser.serialize_entry(&key, x)?; 46 | } 47 | Ok(()) 48 | } 49 | 50 | /// No outer options 51 | /// 52 | /// Literally serializes to nothing 53 | #[derive(Debug, Default)] 54 | pub struct NoOuter; 55 | 56 | impl MergeSerialize for NoOuter { 57 | fn merge_serialize(&self, _: &mut S) -> Result<(), S::Error> 58 | where 59 | S: SerializeMap, 60 | { 61 | // No-op 62 | Ok(()) 63 | } 64 | } 65 | 66 | /// A recurring theme in ElasticSearch is for JSON to be `{"variable": {..map of options..}` 67 | #[derive(Debug)] 68 | pub struct FieldBased { 69 | pub field: F, 70 | pub inner: I, 71 | pub outer: O, 72 | } 73 | 74 | impl FieldBased { 75 | pub fn new(field: F, inner: I, outer: O) -> Self { 76 | FieldBased { 77 | field, 78 | inner, 79 | outer, 80 | } 81 | } 82 | } 83 | 84 | impl Serialize for FieldBased 85 | where 86 | F: Serialize, 87 | I: Serialize, 88 | O: MergeSerialize, 89 | { 90 | fn serialize(&self, serializer: S) -> Result 91 | where 92 | S: Serializer, 93 | { 94 | let mut map = serializer.serialize_map(None)?; 95 | 96 | map.serialize_entry(&self.field, &self.inner)?; 97 | self.outer.merge_serialize(&mut map)?; 98 | 99 | map.end() 100 | } 101 | } 102 | 103 | /// MergeSerialize, implemented by structs that want to add to an existing struct 104 | pub trait MergeSerialize { 105 | fn merge_serialize(&self, serializer: &mut S) -> Result<(), S::Error> 106 | where 107 | S: SerializeMap; 108 | } 109 | 110 | /// Macro to allow access to the inner object, assumes FieldBased is wrapped in a newtype 111 | macro_rules! add_inner_field { 112 | ($n:ident, $f:ident, $t:ty) => ( 113 | pub fn $n>(mut self, val: T) -> Self { 114 | self.0.inner.$f = Some(val.into()); 115 | self 116 | } 117 | ); 118 | } 119 | 120 | macro_rules! add_outer_field { 121 | ($n:ident, $e:ident, $t:ty) => ( 122 | pub fn $n>(mut self, val: T) -> Self { 123 | self.0.outer.$e = Some(val.into()); 124 | self 125 | } 126 | ) 127 | } 128 | 129 | #[cfg(test)] 130 | pub mod tests { 131 | use serde_json; 132 | 133 | use serde::ser::SerializeMap; 134 | use serde::Serialize; 135 | 136 | use super::{FieldBased, MergeSerialize, NoOuter}; 137 | 138 | #[derive(Serialize)] 139 | struct TestOptions { 140 | opt_a: i64, 141 | opt_b: f64, 142 | } 143 | 144 | impl MergeSerialize for TestOptions { 145 | fn merge_serialize(&self, serializer: &mut S) -> Result<(), S::Error> 146 | where 147 | S: SerializeMap, 148 | { 149 | serializer.serialize_entry("opt_a", &self.opt_a)?; 150 | serializer.serialize_entry("opt_b", &self.opt_b) 151 | } 152 | } 153 | 154 | #[derive(Serialize)] 155 | struct TestStruct(FieldBased); 156 | 157 | impl TestStruct { 158 | fn new(key: String, options: TestOptions) -> Self { 159 | TestStruct(FieldBased::new(key, options, NoOuter)) 160 | } 161 | } 162 | 163 | #[derive(Serialize)] 164 | struct TestWithOuter(FieldBased); 165 | 166 | impl TestWithOuter { 167 | fn new(key: String, options: TestOptions, outer: TestOptions) -> Self { 168 | TestWithOuter(FieldBased::new(key, options, outer)) 169 | } 170 | } 171 | 172 | #[test] 173 | fn test_simple_field_based() { 174 | let t = TestStruct::new( 175 | "key".to_owned(), 176 | TestOptions { 177 | opt_a: 4i64, 178 | opt_b: 3.5f64, 179 | }, 180 | ); 181 | let s = serde_json::to_string(&t).unwrap(); 182 | assert_eq!("{\"key\":{\"opt_a\":4,\"opt_b\":3.5}}", s); 183 | } 184 | 185 | #[test] 186 | fn test_outer_field_based() { 187 | let t = TestWithOuter::new( 188 | "key".to_owned(), 189 | TestOptions { 190 | opt_a: 8i64, 191 | opt_b: 2.5f64, 192 | }, 193 | TestOptions { 194 | opt_a: 9i64, 195 | opt_b: 1.5f64, 196 | }, 197 | ); 198 | let s = serde_json::to_string(&t).unwrap(); 199 | assert_eq!( 200 | "{\"key\":{\"opt_a\":8,\"opt_b\":2.5},\"opt_a\":9,\"opt_b\":1.5}", 201 | s 202 | ); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/operations/index.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-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 Index operation 18 | 19 | use serde::ser::Serialize; 20 | 21 | use serde::Deserialize; 22 | 23 | use crate::{error::EsError, Client, EsResponse}; 24 | 25 | use super::common::{OptionVal, Options}; 26 | 27 | /// Values for the op_type option 28 | pub enum OpType { 29 | Create, 30 | } 31 | 32 | impl From for OptionVal { 33 | fn from(from: OpType) -> OptionVal { 34 | match from { 35 | OpType::Create => OptionVal("create".to_owned()), 36 | } 37 | } 38 | } 39 | 40 | /// An indexing operation 41 | #[derive(Debug)] 42 | pub struct IndexOperation<'a, 'b, E: Serialize + 'b> { 43 | /// The HTTP client that this operation will use 44 | client: &'a mut Client, 45 | 46 | /// The index into which the document will be added 47 | index: &'b str, 48 | 49 | /// The type of the document 50 | doc_type: &'b str, 51 | 52 | /// Optional the ID of the document. 53 | id: Option<&'b str>, 54 | 55 | /// The optional options 56 | options: Options<'b>, 57 | 58 | /// The document to be indexed 59 | document: Option<&'b E>, 60 | } 61 | 62 | impl<'a, 'b, E: Serialize + 'b> IndexOperation<'a, 'b, E> { 63 | pub fn new( 64 | client: &'a mut Client, 65 | index: &'b str, 66 | doc_type: &'b str, 67 | ) -> IndexOperation<'a, 'b, E> { 68 | IndexOperation { 69 | client, 70 | index, 71 | doc_type, 72 | id: None, 73 | options: Options::default(), 74 | document: None, 75 | } 76 | } 77 | 78 | pub fn with_doc(&'b mut self, doc: &'b E) -> &'b mut Self { 79 | self.document = Some(doc); 80 | self 81 | } 82 | 83 | pub fn with_id(&'b mut self, id: &'b str) -> &'b mut Self { 84 | self.id = Some(id); 85 | self 86 | } 87 | 88 | add_option!(with_ttl, "ttl"); 89 | add_option!(with_version, "version"); 90 | add_option!(with_version_type, "version_type"); 91 | add_option!(with_op_type, "op_type"); 92 | add_option!(with_routing, "routing"); 93 | add_option!(with_parent, "parent"); 94 | add_option!(with_timestamp, "timestamp"); 95 | add_option!(with_refresh, "refresh"); 96 | add_option!(with_timeout, "timeout"); 97 | 98 | pub fn send(&'b mut self) -> Result { 99 | // Ignoring status_code as everything should return an IndexResult or 100 | // already be an error 101 | let response = (match self.id { 102 | Some(ref id) => { 103 | let url = format!("/{}/{}/{}{}", self.index, self.doc_type, id, self.options); 104 | match self.document { 105 | Some(ref doc) => self.client.put_body_op(&url, doc), 106 | None => self.client.put_op(&url), 107 | } 108 | } 109 | None => { 110 | let url = format!("/{}/{}{}", self.index, self.doc_type, self.options); 111 | match self.document { 112 | Some(ref doc) => self.client.post_body_op(&url, doc), 113 | None => self.client.post_op(&url), 114 | } 115 | } 116 | })?; 117 | Ok(response.read_response()?) 118 | } 119 | } 120 | 121 | impl Client { 122 | /// An index operation to index a document in the specified index. 123 | /// 124 | /// See: https://www.elastic.co/guide/en/elasticsearch/reference/1.x/docs-index_.html 125 | pub fn index<'a, 'b, E: Serialize>( 126 | &'a mut self, 127 | index: &'b str, 128 | doc_type: &'b str, 129 | ) -> IndexOperation<'a, 'b, E> { 130 | IndexOperation::new(self, index, doc_type) 131 | } 132 | } 133 | 134 | /// The result of an index operation 135 | #[derive(Debug, Deserialize)] 136 | pub struct IndexResult { 137 | #[serde(rename = "_index")] 138 | pub index: String, 139 | #[serde(rename = "_type")] 140 | pub doc_type: String, 141 | #[serde(rename = "_id")] 142 | pub id: String, 143 | #[serde(rename = "_version")] 144 | pub version: u64, 145 | pub created: bool, 146 | } 147 | 148 | #[cfg(test)] 149 | pub mod tests { 150 | use crate::tests::{clean_db, make_client, TestDocument}; 151 | 152 | use crate::units::Duration; 153 | 154 | use super::OpType; 155 | 156 | #[test] 157 | fn test_indexing() { 158 | let index_name = "test_indexing"; 159 | let mut client = make_client(); 160 | clean_db(&mut client, index_name); 161 | { 162 | let result_wrapped = client 163 | .index(index_name, "test_type") 164 | .with_doc(&TestDocument::new().with_int_field(1)) 165 | .with_ttl(&Duration::milliseconds(927_500)) 166 | .send(); 167 | println!("TEST RESULT: {:?}", result_wrapped); 168 | let result = result_wrapped.unwrap(); 169 | assert_eq!(result.created, true); 170 | assert_eq!(result.index, index_name); 171 | assert_eq!(result.doc_type, "test_type"); 172 | assert!(!result.id.is_empty()); 173 | assert_eq!(result.version, 1); 174 | } 175 | { 176 | let result_wrapped = client 177 | .index(index_name, "test_type") 178 | .with_doc(&TestDocument::new().with_int_field(2)) 179 | .with_id("TEST_INDEXING_2") 180 | .with_op_type(OpType::Create) 181 | .send(); 182 | let result = result_wrapped.unwrap(); 183 | 184 | assert_eq!(result.created, true); 185 | assert_eq!(result.index, index_name); 186 | assert_eq!(result.doc_type, "test_type"); 187 | assert_eq!(result.id, "TEST_INDEXING_2"); 188 | assert!(result.version >= 1); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/operations/search/aggregations/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 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 [aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html) 18 | 19 | #[macro_use] 20 | mod common; 21 | 22 | pub mod bucket; 23 | pub mod metrics; 24 | 25 | use std::collections::HashMap; 26 | 27 | use serde::ser::{SerializeMap, Serializer}; 28 | use serde::{Serialize, Deserialize}; 29 | use serde_json::{Map, Value}; 30 | 31 | use crate::error::EsError; 32 | 33 | use self::{bucket::BucketAggregationResult, metrics::MetricsAggregationResult}; 34 | 35 | /// Aggregations are either metrics or bucket-based aggregations 36 | #[derive(Debug)] 37 | pub enum Aggregation<'a> { 38 | /// A metric aggregation (e.g. min) 39 | Metrics(metrics::MetricsAggregation<'a>), 40 | 41 | /// A bucket aggregation, groups data into buckets and optionally applies 42 | /// sub-aggregations 43 | Bucket(bucket::BucketAggregation<'a>, Option>), 44 | } 45 | 46 | impl<'a> Serialize for Aggregation<'a> { 47 | fn serialize(&self, serializer: S) -> Result 48 | where 49 | S: Serializer, 50 | { 51 | use self::Aggregation::*; 52 | let mut map = (serializer.serialize_map(Some(match self { 53 | Metrics(_) => 1, 54 | Bucket(_, ref opt_aggs) => match opt_aggs { 55 | Some(_) => 2, 56 | None => 1, 57 | }, 58 | })))?; 59 | match self { 60 | Metrics(ref metric_agg) => { 61 | let agg_name = metric_agg.details(); 62 | map.serialize_entry(agg_name, metric_agg)?; 63 | } 64 | Bucket(ref bucket_agg, ref opt_aggs) => { 65 | let agg_name = bucket_agg.details(); 66 | map.serialize_entry(agg_name, bucket_agg)?; 67 | match opt_aggs { 68 | Some(ref other_aggs) => { 69 | map.serialize_entry("aggregations", other_aggs)?; 70 | } 71 | None => (), 72 | } 73 | } 74 | } 75 | map.end() 76 | } 77 | } 78 | 79 | /// The set of aggregations 80 | /// 81 | /// There are many ways of creating aggregations, either standalone or via a 82 | /// conversion trait 83 | #[derive(Debug, Default, Serialize)] 84 | pub struct Aggregations<'a>(HashMap<&'a str, Aggregation<'a>>); 85 | 86 | impl<'a> Aggregations<'a> { 87 | /// Create an empty-set of aggregations, individual aggregations should be 88 | /// added via the `add` method 89 | /// 90 | /// # Examples 91 | /// 92 | /// ``` 93 | /// use rs_es::operations::search::aggregations::Aggregations; 94 | /// use rs_es::operations::search::aggregations::metrics::Min; 95 | /// 96 | /// let mut aggs = Aggregations::new(); 97 | /// aggs.add("agg_name", Min::field("field_name")); 98 | /// ``` 99 | pub fn new() -> Aggregations<'a> { 100 | Aggregations(HashMap::new()) 101 | } 102 | 103 | /// Add an aggregation to the set of aggregations 104 | pub fn add>>(&mut self, key: &'a str, val: A) { 105 | self.0.insert(key, val.into()); 106 | } 107 | } 108 | 109 | impl<'b> From)>> for Aggregations<'b> { 110 | fn from(from: Vec<(&'b str, Aggregation<'b>)>) -> Aggregations<'b> { 111 | let mut aggs = Aggregations::new(); 112 | for (name, agg) in from { 113 | aggs.add(name, agg); 114 | } 115 | aggs 116 | } 117 | } 118 | 119 | impl<'a, A: Into>> From<(&'a str, A)> for Aggregations<'a> { 120 | fn from(from: (&'a str, A)) -> Aggregations<'a> { 121 | let mut aggs = Aggregations::new(); 122 | aggs.add(from.0, from.1.into()); 123 | aggs 124 | } 125 | } 126 | 127 | /// The result of one specific aggregation 128 | /// 129 | /// The data returned varies depending on aggregation type 130 | #[derive(Debug, Serialize, Deserialize)] 131 | pub enum AggregationResult { 132 | /// Results of metrics aggregations 133 | Metrics(MetricsAggregationResult), 134 | 135 | /// Result of a bucket aggregation 136 | Bucket(BucketAggregationResult), 137 | } 138 | 139 | #[derive(Debug, Serialize, Deserialize)] 140 | pub struct AggregationsResult(HashMap); 141 | 142 | /// Loads a Json object of aggregation results into an `AggregationsResult`. 143 | fn object_to_result( 144 | aggs: &Aggregations, 145 | object: &Map, 146 | ) -> Result { 147 | use self::Aggregation::*; 148 | 149 | let mut ar_map = HashMap::new(); 150 | for (&key, val) in aggs.0.iter() { 151 | let owned_key = key.to_owned(); 152 | let json = match object.get(&owned_key) { 153 | Some(json) => json, 154 | None => return Err(EsError::EsError(format!("No key: {}", &owned_key))), 155 | }; 156 | ar_map.insert( 157 | owned_key, 158 | match val { 159 | Metrics(ref ma) => { 160 | AggregationResult::Metrics(MetricsAggregationResult::from(ma, json)?) 161 | } 162 | Aggregation::Bucket(ref ba, ref aggs) => { 163 | AggregationResult::Bucket(BucketAggregationResult::from(ba, json, aggs)?) 164 | } 165 | }, 166 | ); 167 | } 168 | 169 | log::info!("Processed aggs - From: {:?}. To: {:?}", object, ar_map); 170 | 171 | Ok(AggregationsResult(ar_map)) 172 | } 173 | 174 | impl AggregationsResult { 175 | pub fn get<'a>(&'a self, key: &str) -> Result<&'a AggregationResult, EsError> { 176 | match self.0.get(key) { 177 | Some(ref agg_res) => Ok(agg_res), 178 | None => Err(EsError::EsError(format!("No agg for key: {}", key))), 179 | } 180 | } 181 | 182 | pub fn from(aggs: &Aggregations, json: &Value) -> Result { 183 | let object = match json.as_object() { 184 | Some(o) => o, 185 | None => return Err(EsError::EsError("Aggregations is not an object".to_owned())), 186 | }; 187 | object_to_result(aggs, object) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/operations/search/highlight.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 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 [highlight](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html) 18 | use crate::json::ShouldSkip; 19 | 20 | use std::collections::HashMap; 21 | 22 | use serde::ser::{Serializer}; 23 | use serde::Serialize; 24 | 25 | #[derive(Debug, Clone)] 26 | pub enum Encoders { 27 | Default, 28 | HTML, 29 | } 30 | 31 | impl Serialize for Encoders { 32 | fn serialize(&self, serializer: S) -> Result 33 | where 34 | S: Serializer, 35 | { 36 | match self { 37 | Encoders::Default => "default", 38 | Encoders::HTML => "html", 39 | } 40 | .serialize(serializer) 41 | } 42 | } 43 | 44 | #[derive(Debug, Clone)] 45 | pub enum SettingTypes { 46 | Plain, 47 | FVH, 48 | Postings, 49 | } 50 | 51 | impl Serialize for SettingTypes { 52 | fn serialize(&self, serializer: S) -> Result 53 | where 54 | S: Serializer, 55 | { 56 | match self { 57 | SettingTypes::Plain => "plain", 58 | SettingTypes::FVH => "fvh", 59 | SettingTypes::Postings => "postings", 60 | } 61 | .serialize(serializer) 62 | } 63 | } 64 | 65 | #[derive(Debug, Clone)] 66 | pub enum IndexOptions { 67 | Offsets, 68 | } 69 | 70 | impl Serialize for IndexOptions { 71 | fn serialize(&self, serializer: S) -> Result 72 | where 73 | S: Serializer, 74 | { 75 | match self { 76 | IndexOptions::Offsets => "offsets", 77 | } 78 | .serialize(serializer) 79 | } 80 | } 81 | 82 | #[derive(Debug, Clone)] 83 | pub enum TermVector { 84 | WithPositionsOffsets, 85 | BoundaryChars, 86 | BoundaryMaxScan, 87 | } 88 | 89 | impl Serialize for TermVector { 90 | fn serialize(&self, serializer: S) -> Result 91 | where 92 | S: Serializer, 93 | { 94 | match self { 95 | TermVector::WithPositionsOffsets => "with_positions_offsets", 96 | TermVector::BoundaryChars => "boundary_chars", 97 | TermVector::BoundaryMaxScan => "boundary_max_scan", 98 | } 99 | .serialize(serializer) 100 | } 101 | } 102 | 103 | #[derive(Debug, Serialize, Clone)] 104 | pub struct Setting { 105 | #[serde(rename = "type")] 106 | pub setting_type: Option, 107 | #[cfg(not(feature = "es5"))] 108 | pub index_options: Option, 109 | #[cfg(not(feature = "es5"))] 110 | pub term_vector: Option, 111 | pub force_source: bool, 112 | pub fragment_size: u32, 113 | pub number_of_fragments: u32, 114 | pub no_match_size: u32, 115 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 116 | pub matched_fields: Option>, 117 | } 118 | 119 | impl Default for Setting { 120 | fn default() -> Self { 121 | Setting { 122 | setting_type: None, 123 | force_source: false, 124 | fragment_size: 150, 125 | number_of_fragments: 5, 126 | no_match_size: 0, 127 | matched_fields: None, 128 | 129 | #[cfg(not(feature = "es5"))] 130 | index_options: None, 131 | 132 | #[cfg(not(feature = "es5"))] 133 | term_vector: None, 134 | } 135 | } 136 | } 137 | 138 | impl Setting { 139 | pub fn new() -> Self { 140 | Default::default() 141 | } 142 | 143 | pub fn with_type(&mut self, setting_type: SettingTypes) -> &mut Setting { 144 | self.setting_type = Some(setting_type); 145 | self 146 | } 147 | 148 | #[cfg(not(feature = "es5"))] 149 | pub fn with_index_options(&mut self, index_options: IndexOptions) -> &mut Setting { 150 | self.index_options = Some(index_options); 151 | self 152 | } 153 | 154 | #[cfg(not(feature = "es5"))] 155 | pub fn with_term_vector(&mut self, term_vector: TermVector) -> &mut Setting { 156 | self.term_vector = Some(term_vector); 157 | self 158 | } 159 | 160 | pub fn with_force_source(&mut self, force_source: bool) -> &mut Setting { 161 | self.force_source = force_source; 162 | self 163 | } 164 | 165 | pub fn with_fragment_size(&mut self, fragment_size: u32) -> &mut Setting { 166 | self.fragment_size = fragment_size; 167 | self 168 | } 169 | 170 | pub fn with_number_of_fragments(&mut self, number_of_fragments: u32) -> &mut Setting { 171 | self.number_of_fragments = number_of_fragments; 172 | self 173 | } 174 | 175 | pub fn with_no_match_size(&mut self, no_match_size: u32) -> &mut Setting { 176 | self.no_match_size = no_match_size; 177 | self 178 | } 179 | 180 | pub fn with_matched_fields(&mut self, matched_fields: Vec) -> &mut Setting { 181 | self.matched_fields = Some(matched_fields); 182 | self 183 | } 184 | } 185 | 186 | #[derive(Debug, Default, Serialize, Clone)] 187 | pub struct Highlight { 188 | pub fields: HashMap, 189 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 190 | pub pre_tags: Option>, 191 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 192 | pub post_tags: Option>, 193 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 194 | pub encoder: Option, 195 | } 196 | 197 | impl Highlight { 198 | /// Create an Highlight entity without any field or setting 199 | /// specified as they are supposed to be added via the `add` 200 | /// method. 201 | /// 202 | /// # Examples 203 | /// 204 | /// ``` 205 | /// use rs_es::operations::search::highlight::{Highlight, Setting, SettingTypes, Encoders}; 206 | /// 207 | /// let mut highlight = Highlight::new().with_encoder(Encoders::HTML).to_owned(); 208 | /// let setting = Setting::new().with_type(SettingTypes::Plain).to_owned(); 209 | /// highlight.add_setting("first_name".to_owned(), setting); 210 | /// ``` 211 | pub fn new() -> Highlight { 212 | Highlight { 213 | fields: HashMap::new(), 214 | pre_tags: None, 215 | post_tags: None, 216 | encoder: None, 217 | } 218 | } 219 | 220 | pub fn with_encoder(&mut self, encoder: Encoders) -> &mut Highlight { 221 | self.encoder = Some(encoder); 222 | self 223 | } 224 | 225 | pub fn with_pre_tags(&mut self, pre_tags: Vec) -> &mut Highlight { 226 | self.pre_tags = Some(pre_tags); 227 | self 228 | } 229 | 230 | pub fn with_post_tags(&mut self, post_tags: Vec) -> &mut Highlight { 231 | self.post_tags = Some(post_tags); 232 | self 233 | } 234 | 235 | /// Add a field to highlight to the set 236 | pub fn add_setting(&mut self, name: String, setting: Setting) { 237 | self.fields.insert(name, setting); 238 | } 239 | } 240 | 241 | /// The fields containing found terms 242 | pub type HighlightResult = HashMap>; 243 | -------------------------------------------------------------------------------- /src/query/compound.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2018 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 | //! Compound queries 18 | 19 | use serde::{Serialize, Serializer}; 20 | 21 | use crate::{json::ShouldSkip, units::OneOrMany}; 22 | 23 | use super::{functions::Function, MinimumShouldMatch, Query, ScoreMode}; 24 | 25 | /// BoostMode 26 | #[derive(Debug, Copy, Clone)] 27 | pub enum BoostMode { 28 | Multiply, 29 | Replace, 30 | Sum, 31 | Avg, 32 | Max, 33 | Min, 34 | } 35 | 36 | impl Serialize for BoostMode { 37 | fn serialize(&self, serializer: S) -> Result 38 | where 39 | S: Serializer, 40 | { 41 | match self { 42 | BoostMode::Multiply => "multiply", 43 | BoostMode::Replace => "replace", 44 | BoostMode::Sum => "sum", 45 | BoostMode::Avg => "avg", 46 | BoostMode::Max => "max", 47 | BoostMode::Min => "min", 48 | } 49 | .serialize(serializer) 50 | } 51 | } 52 | 53 | /// Constant score query 54 | #[derive(Debug, Default, Serialize)] 55 | pub struct ConstantScoreQuery { 56 | query: Query, 57 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 58 | boost: Option, 59 | } 60 | 61 | impl Query { 62 | pub fn build_constant_score(query: A) -> ConstantScoreQuery 63 | where 64 | A: Into, 65 | { 66 | ConstantScoreQuery { 67 | query: query.into(), 68 | ..Default::default() 69 | } 70 | } 71 | } 72 | 73 | impl ConstantScoreQuery { 74 | add_field!(with_boost, boost, f64); 75 | 76 | build!(ConstantScore); 77 | } 78 | 79 | /// Bool query 80 | #[derive(Debug, Default, Serialize)] 81 | pub struct BoolQuery { 82 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 83 | must: Option>, 84 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 85 | filter: Option, 86 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 87 | should: Option>, 88 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 89 | must_not: Option>, 90 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 91 | minimum_should_match: Option, 92 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 93 | boost: Option, 94 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 95 | disable_coord: Option, 96 | } 97 | 98 | impl Query { 99 | pub fn build_bool() -> BoolQuery { 100 | Default::default() 101 | } 102 | } 103 | 104 | impl BoolQuery { 105 | add_field!(with_must, must, OneOrMany); 106 | add_field!(with_filter, filter, Query); 107 | add_field!(with_should, should, OneOrMany); 108 | add_field!(with_must_not, must_not, OneOrMany); 109 | add_field!( 110 | with_minimum_should_match, 111 | minimum_should_match, 112 | MinimumShouldMatch 113 | ); 114 | add_field!(with_boost, boost, f64); 115 | add_field!(with_disable_coord, disable_coord, bool); 116 | 117 | build!(Bool); 118 | } 119 | 120 | /// DisMax query 121 | #[derive(Debug, Default, Serialize)] 122 | pub struct DisMaxQuery { 123 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 124 | tie_breaker: Option, 125 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 126 | boost: Option, 127 | queries: Vec, 128 | } 129 | 130 | impl Query { 131 | pub fn build_dis_max(queries: A) -> DisMaxQuery 132 | where 133 | A: Into>, 134 | { 135 | DisMaxQuery { 136 | queries: queries.into(), 137 | ..Default::default() 138 | } 139 | } 140 | } 141 | 142 | impl DisMaxQuery { 143 | add_field!(with_tie_breaker, tie_breaker, f64); 144 | add_field!(with_boost, boost, f64); 145 | 146 | build!(DisMax); 147 | } 148 | 149 | /// Function Score query 150 | #[derive(Debug, Default, Serialize)] 151 | pub struct FunctionScoreQuery { 152 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 153 | query: Option, 154 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 155 | boost: Option, 156 | functions: Vec, 157 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 158 | max_boost: Option, 159 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 160 | score_mode: Option, 161 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 162 | boost_mode: Option, 163 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 164 | min_score: Option, 165 | } 166 | 167 | impl Query { 168 | pub fn build_function_score() -> FunctionScoreQuery { 169 | Default::default() 170 | } 171 | } 172 | 173 | impl FunctionScoreQuery { 174 | add_field!(with_query, query, Query); 175 | add_field!(with_boost, boost, f64); 176 | add_field!(with_max_boost, max_boost, f64); 177 | add_field!(with_score_mode, score_mode, ScoreMode); 178 | add_field!(with_boost_mode, boost_mode, BoostMode); 179 | add_field!(with_min_score, min_score, f64); 180 | 181 | pub fn with_functions>>(mut self, functions: A) -> Self { 182 | self.functions = functions.into(); 183 | self 184 | } 185 | 186 | pub fn with_function>(mut self, function: A) -> Self { 187 | self.functions = vec![function.into()]; 188 | self 189 | } 190 | 191 | build!(FunctionScore); 192 | } 193 | 194 | /// Boosting query 195 | #[derive(Debug, Default, Serialize)] 196 | pub struct BoostingQuery { 197 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 198 | positive: Option, 199 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 200 | negative: Option, 201 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 202 | negative_boost: Option, 203 | } 204 | 205 | impl Query { 206 | pub fn build_boosting() -> BoostingQuery { 207 | Default::default() 208 | } 209 | } 210 | 211 | impl BoostingQuery { 212 | add_field!(with_positive, positive, Query); 213 | add_field!(with_negative, negative, Query); 214 | add_field!(with_negative_boost, negative_boost, f64); 215 | 216 | build!(Boosting); 217 | } 218 | 219 | /// Indices query 220 | #[derive(Debug, Default, Serialize)] 221 | pub struct IndicesQuery { 222 | indices: OneOrMany, 223 | query: Query, 224 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 225 | no_match_query: Option, 226 | } 227 | 228 | impl Query { 229 | pub fn build_indices(indices: A, query: B) -> IndicesQuery 230 | where 231 | A: Into>, 232 | B: Into, 233 | { 234 | IndicesQuery { 235 | indices: indices.into(), 236 | query: query.into(), 237 | ..Default::default() 238 | } 239 | } 240 | } 241 | 242 | impl IndicesQuery { 243 | add_field!(with_no_match_query, no_match_query, NoMatchQuery); 244 | 245 | build!(Indices); 246 | } 247 | 248 | /// Options for the `no_match_query` option of IndicesQuery 249 | #[derive(Debug)] 250 | pub enum NoMatchQuery { 251 | None, 252 | All, 253 | Query(Query), 254 | } 255 | 256 | from_exp!(Query, NoMatchQuery, from, NoMatchQuery::Query(from)); 257 | 258 | impl Serialize for NoMatchQuery { 259 | fn serialize(&self, serializer: S) -> Result 260 | where 261 | S: Serializer, 262 | { 263 | use self::NoMatchQuery::*; 264 | match self { 265 | None => "none".serialize(serializer), 266 | All => "all".serialize(serializer), 267 | Query(ref q) => q.serialize(serializer), 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/operations/search/count.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-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 | //! Implementations of the Count API 18 | 19 | use reqwest::StatusCode; 20 | 21 | use serde::{Deserialize, Serialize}; 22 | 23 | use crate::{ 24 | error::EsError, 25 | json::ShouldSkip, 26 | operations::{ 27 | common::{OptionVal, Options}, 28 | format_indexes_and_types, ShardCountResult, 29 | }, 30 | query::Query, 31 | Client, EsResponse, 32 | }; 33 | 34 | /// Representing a count operation 35 | #[derive(Debug)] 36 | pub struct CountURIOperation<'a, 'b> { 37 | client: &'a mut Client, 38 | indexes: &'b [&'b str], 39 | doc_types: &'b [&'b str], 40 | options: Options<'b>, 41 | } 42 | 43 | impl<'a, 'b> CountURIOperation<'a, 'b> { 44 | pub fn new(client: &'a mut Client) -> CountURIOperation<'a, 'b> { 45 | CountURIOperation { 46 | client, 47 | indexes: &[], 48 | doc_types: &[], 49 | options: Options::default(), 50 | } 51 | } 52 | 53 | pub fn with_indexes(&'b mut self, indexes: &'b [&'b str]) -> &'b mut Self { 54 | self.indexes = indexes; 55 | self 56 | } 57 | 58 | pub fn with_types(&'b mut self, doc_types: &'b [&'b str]) -> &'b mut Self { 59 | self.doc_types = doc_types; 60 | self 61 | } 62 | 63 | pub fn with_query>(&'b mut self, qs: S) -> &'b mut Self { 64 | self.options.push("q", qs.into()); 65 | self 66 | } 67 | 68 | add_option!(with_df, "df"); 69 | add_option!(with_analyzer, "analyzer"); 70 | add_option!(with_default_operator, "default_operator"); 71 | add_option!(with_lenient, "lenient"); 72 | add_option!(with_analyze_wildcard, "analyze_wildcard"); 73 | add_option!(with_terminate_after, "terminate_after"); 74 | 75 | pub fn send(&'b mut self) -> Result { 76 | let url = format!( 77 | "/{}/_count{}", 78 | format_indexes_and_types(&self.indexes, &self.doc_types), 79 | self.options 80 | ); 81 | log::info!("Counting with: {}", url); 82 | let response = self.client.get_op(&url)?; 83 | match response.status_code() { 84 | StatusCode::OK => Ok(response.read_response()?), 85 | status_code => Err(EsError::EsError(format!( 86 | "Unexpected status: {}", 87 | status_code 88 | ))), 89 | } 90 | } 91 | } 92 | 93 | #[derive(Debug, Default, Serialize)] 94 | struct CountQueryOperationBody<'b> { 95 | /// The query 96 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 97 | query: Option<&'b Query>, 98 | } 99 | 100 | #[derive(Debug)] 101 | pub struct CountQueryOperation<'a, 'b> { 102 | /// The HTTP client 103 | client: &'a mut Client, 104 | 105 | /// The indexes to which this query applies 106 | indexes: &'b [&'b str], 107 | 108 | /// The types to which the query applies 109 | doc_types: &'b [&'b str], 110 | 111 | /// Optionals 112 | options: Options<'b>, 113 | 114 | /// The query body 115 | body: CountQueryOperationBody<'b>, 116 | } 117 | 118 | impl<'a, 'b> CountQueryOperation<'a, 'b> { 119 | pub fn new(client: &'a mut Client) -> Self { 120 | CountQueryOperation { 121 | client, 122 | indexes: &[], 123 | doc_types: &[], 124 | options: Options::new(), 125 | body: Default::default(), 126 | } 127 | } 128 | 129 | pub fn with_indexes(&'b mut self, indexes: &'b [&'b str]) -> &'b mut Self { 130 | self.indexes = indexes; 131 | self 132 | } 133 | 134 | pub fn with_types(&'b mut self, doc_types: &'b [&'b str]) -> &'b mut Self { 135 | self.doc_types = doc_types; 136 | self 137 | } 138 | 139 | pub fn with_query(&'b mut self, query: &'b Query) -> &'b mut Self { 140 | self.body.query = Some(query); 141 | self 142 | } 143 | 144 | add_option!(with_df, "df"); 145 | add_option!(with_analyzer, "analyzer"); 146 | add_option!(with_default_operator, "default_operator"); 147 | add_option!(with_lenient, "lenient"); 148 | add_option!(with_analyze_wildcard, "analyze_wildcard"); 149 | add_option!(with_terminate_after, "terminate_after"); 150 | 151 | /// Performs the count with the specified query and options 152 | pub fn send(&'b mut self) -> Result { 153 | let url = format!( 154 | "/{}/_count{}", 155 | format_indexes_and_types(&self.indexes, &self.doc_types), 156 | self.options 157 | ); 158 | let response = self.client.post_body_op(&url, &self.body)?; 159 | match response.status_code() { 160 | StatusCode::OK => Ok(response.read_response()?), 161 | status_code => Err(EsError::EsError(format!( 162 | "Unexpected status: {}", 163 | status_code 164 | ))), 165 | } 166 | } 167 | } 168 | 169 | impl Client { 170 | /// Count via the query parameter 171 | /// 172 | /// See: https://www.elastic.co/guide/en/elasticsearch/reference/1.x/search-uri-request.html 173 | pub fn count_uri(&mut self) -> CountURIOperation { 174 | CountURIOperation::new(self) 175 | } 176 | 177 | /// Count via the query DSL 178 | /// 179 | /// See: https://www.elastic.co/guide/en/elasticsearch/reference/1.x/search-request-body.html 180 | pub fn count_query(&mut self) -> CountQueryOperation { 181 | CountQueryOperation::new(self) 182 | } 183 | } 184 | 185 | #[derive(Debug, Deserialize)] 186 | pub struct CountResult { 187 | pub count: u64, 188 | 189 | #[serde(rename = "_shards")] 190 | pub shards: ShardCountResult, 191 | } 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use crate::tests::setup_test_data; 196 | use crate::tests::{clean_db, make_client}; 197 | 198 | use crate::query::Query; 199 | 200 | use super::CountResult; 201 | 202 | #[test] 203 | fn test_count_uri() { 204 | let index_name = "test_count_uri"; 205 | let mut client = make_client(); 206 | 207 | clean_db(&mut client, index_name); 208 | setup_test_data(&mut client, index_name); 209 | 210 | let all_results: CountResult = client 211 | .count_uri() 212 | .with_indexes(&[index_name]) 213 | .send() 214 | .unwrap(); 215 | assert_eq!(3, all_results.count); 216 | 217 | let doc_1: CountResult = client 218 | .count_uri() 219 | .with_indexes(&[index_name]) 220 | .with_query("str_field:1ABC") 221 | .send() 222 | .unwrap(); 223 | assert_eq!(1, doc_1.count); 224 | 225 | let not_found_doc: CountResult = client 226 | .count_uri() 227 | .with_indexes(&[index_name]) 228 | .with_query("str_field:lolol") 229 | .send() 230 | .unwrap(); 231 | assert_eq!(0, not_found_doc.count); 232 | } 233 | 234 | #[test] 235 | fn test_count_query() { 236 | let index_name = "test_count_query"; 237 | let mut client = make_client(); 238 | 239 | clean_db(&mut client, index_name); 240 | setup_test_data(&mut client, index_name); 241 | 242 | let all_results: CountResult = client 243 | .count_query() 244 | .with_indexes(&[index_name]) 245 | .with_query(&Query::build_match_all().build()) 246 | .send() 247 | .unwrap(); 248 | assert_eq!(3, all_results.count); 249 | 250 | let doc_1: CountResult = client 251 | .count_query() 252 | .with_indexes(&[index_name]) 253 | .with_query( 254 | &Query::build_range("int_field") 255 | .with_gte(2) 256 | .with_lte(3) 257 | .build(), 258 | ) 259 | .send() 260 | .unwrap(); 261 | assert_eq!(2, doc_1.count); 262 | 263 | let not_found_doc: CountResult = client 264 | .count_query() 265 | .with_indexes(&[index_name]) 266 | .with_query(&Query::build_range("int_field").with_gte(99).build()) 267 | .send() 268 | .unwrap(); 269 | assert_eq!(0, not_found_doc.count); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-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 | //! A client for ElasticSearch's REST API 18 | //! 19 | //! The `Client` itself is used as the central access point, from which numerous 20 | //! operations are defined implementing each of the specific ElasticSearch APIs. 21 | //! 22 | //! Warning: at the time of writing the majority of such APIs are currently 23 | //! unimplemented. 24 | 25 | #[cfg(test)] 26 | #[macro_use] 27 | extern crate doc_comment; 28 | 29 | #[cfg(test)] 30 | doctest!("../README.md"); 31 | 32 | #[macro_use] 33 | pub mod util; 34 | 35 | #[macro_use] 36 | pub mod json; 37 | 38 | pub mod error; 39 | pub mod operations; 40 | pub mod query; 41 | pub mod units; 42 | 43 | use std::time; 44 | 45 | use reqwest::{header::CONTENT_TYPE, RequestBuilder, StatusCode, Url}; 46 | 47 | use serde::{de::DeserializeOwned, ser::Serialize}; 48 | 49 | use crate::error::EsError; 50 | 51 | pub trait EsResponse { 52 | fn status_code(&self) -> StatusCode; 53 | fn read_response(self) -> Result 54 | where 55 | R: DeserializeOwned; 56 | } 57 | 58 | impl EsResponse for reqwest::Response { 59 | fn status_code(&self) -> StatusCode { 60 | self.status() 61 | } 62 | 63 | fn read_response(self) -> Result 64 | where 65 | R: DeserializeOwned, 66 | { 67 | Ok(serde_json::from_reader(self)?) 68 | } 69 | } 70 | 71 | // The client 72 | 73 | /// Process the result of an HTTP request, returning the status code and the 74 | /// `Json` result (if the result had a body) or an `EsError` if there were any 75 | /// errors 76 | /// 77 | /// This function is exposed to allow extensions to certain operations, it is 78 | /// not expected to be used by consumers of the library 79 | fn do_req(resp: reqwest::Response) -> Result { 80 | let mut resp = resp; 81 | let status = resp.status(); 82 | match status { 83 | StatusCode::OK | StatusCode::CREATED | StatusCode::NOT_FOUND => Ok(resp), 84 | _ => Err(EsError::from(&mut resp)), 85 | } 86 | } 87 | 88 | /// The core of the ElasticSearch client, owns a HTTP connection. 89 | /// 90 | /// Each instance of `Client` is reusable, but only one thread can use each one 91 | /// at once. This will be enforced by the borrow-checker as most methods are 92 | /// defined on `&mut self`. 93 | /// 94 | /// To create a `Client`, the URL needs to be specified. 95 | /// 96 | /// Each ElasticSearch API operation is defined as a method on `Client`. Any 97 | /// compulsory parameters must be given as arguments to this method. It returns 98 | /// an operation builder that can be used to add any optional parameters. 99 | /// 100 | /// Finally `send` is called to submit the operation: 101 | /// 102 | /// # Examples 103 | /// 104 | /// ``` 105 | /// use rs_es::Client; 106 | /// 107 | /// let mut client = Client::init("http://localhost:9200"); 108 | /// ``` 109 | /// 110 | /// See the specific operations and their builder objects for details. 111 | #[derive(Debug, Clone)] 112 | pub struct Client { 113 | base_url: Url, 114 | http_client: reqwest::Client, 115 | } 116 | 117 | impl Client { 118 | fn do_es_op( 119 | &self, 120 | url: &str, 121 | action: impl FnOnce(Url) -> RequestBuilder, 122 | ) -> Result { 123 | let url = self.full_url(url); 124 | let username = self.base_url.username(); 125 | let mut method = action(url); 126 | if !username.is_empty() { 127 | method = method.basic_auth(username, self.base_url.password()); 128 | } 129 | let result = method.header(CONTENT_TYPE, "application/json").send()?; 130 | do_req(result) 131 | } 132 | } 133 | 134 | /// Create a HTTP function for the given method (GET/PUT/POST/DELETE) 135 | macro_rules! es_op { 136 | ($n:ident,$cn:ident) => { 137 | fn $n(&self, url: &str) -> Result { 138 | log::info!("Doing {} on {}", stringify!($n), url); 139 | self.do_es_op(url, |url| self.http_client.$cn(url.clone())) 140 | } 141 | } 142 | } 143 | 144 | /// Create a HTTP function with a request body for the given method 145 | /// (GET/PUT/POST/DELETE) 146 | /// 147 | macro_rules! es_body_op { 148 | ($n:ident,$cn:ident) => { 149 | fn $n(&mut self, url: &str, body: &E) -> Result 150 | where E: Serialize { 151 | 152 | log::info!("Doing {} on {}", stringify!($n), url); 153 | let json_string = serde_json::to_string(body)?; 154 | log::debug!("With body: {}", &json_string); 155 | 156 | self.do_es_op(url, |url| { 157 | self.http_client.$cn(url.clone()).body(json_string) 158 | }) 159 | } 160 | } 161 | } 162 | 163 | impl Client { 164 | /// Create a new client 165 | pub fn init(url_s: &str) -> Result { 166 | let url = Url::parse(url_s)?; 167 | 168 | Ok(Client { 169 | http_client: reqwest::Client::new(), 170 | base_url: url, 171 | }) 172 | } 173 | 174 | // TODO - this should be replaced with a builder object, especially if more options are going 175 | // to be allowed 176 | pub fn init_with_timeout( 177 | url_s: &str, 178 | timeout: Option, 179 | ) -> Result { 180 | let url = Url::parse(url_s)?; 181 | 182 | Ok(Client { 183 | http_client: reqwest::Client::builder() 184 | .timeout(timeout) 185 | .build() 186 | .expect("Failed to build client"), 187 | base_url: url, 188 | }) 189 | } 190 | 191 | /// Take a nearly complete ElasticSearch URL, and stick 192 | /// the URL on the front. 193 | pub fn full_url(&self, suffix: &str) -> Url { 194 | self.base_url.join(suffix).expect("Invalid URL created") 195 | } 196 | 197 | es_op!(get_op, get); 198 | 199 | es_op!(post_op, post); 200 | es_body_op!(post_body_op, post); 201 | es_op!(put_op, put); 202 | es_body_op!(put_body_op, put); 203 | es_op!(delete_op, delete); 204 | } 205 | 206 | #[cfg(test)] 207 | pub mod tests { 208 | use std::env; 209 | 210 | use serde::{Deserialize, Serialize}; 211 | 212 | use super::{error::EsError, Client}; 213 | 214 | // test setup 215 | 216 | pub fn make_client() -> Client { 217 | let hostname = match env::var("ES_HOST") { 218 | Ok(val) => val, 219 | Err(_) => "http://localhost:9200".to_owned(), 220 | }; 221 | Client::init(&hostname).unwrap() 222 | } 223 | 224 | #[derive(Debug, Serialize, Deserialize)] 225 | pub struct TestDocument { 226 | pub str_field: String, 227 | pub int_field: i64, 228 | pub bool_field: bool, 229 | } 230 | 231 | #[allow(clippy::new_without_default)] 232 | impl TestDocument { 233 | pub fn new() -> TestDocument { 234 | TestDocument { 235 | str_field: "I am a test".to_owned(), 236 | int_field: 1, 237 | bool_field: true, 238 | } 239 | } 240 | 241 | pub fn with_str_field(mut self, s: &str) -> TestDocument { 242 | self.str_field = s.to_owned(); 243 | self 244 | } 245 | 246 | pub fn with_int_field(mut self, i: i64) -> TestDocument { 247 | self.int_field = i; 248 | self 249 | } 250 | 251 | pub fn with_bool_field(mut self, b: bool) -> TestDocument { 252 | self.bool_field = b; 253 | self 254 | } 255 | } 256 | 257 | pub fn setup_test_data(client: &mut Client, index_name: &str) { 258 | // TODO - this should use the Bulk API 259 | let documents = vec![ 260 | TestDocument::new() 261 | .with_str_field("Document A123") 262 | .with_int_field(1), 263 | TestDocument::new() 264 | .with_str_field("Document B456") 265 | .with_int_field(2), 266 | TestDocument::new() 267 | .with_str_field("Document 1ABC") 268 | .with_int_field(3), 269 | ]; 270 | for doc in documents.iter() { 271 | client 272 | .index(index_name, "test_type") 273 | .with_doc(doc) 274 | .send() 275 | .unwrap(); 276 | } 277 | client.refresh().with_indexes(&[index_name]).send().unwrap(); 278 | } 279 | 280 | pub fn clean_db(client: &mut Client, test_idx: &str) { 281 | match client.delete_index(test_idx) { 282 | // Ignore indices which don't exist yet 283 | Err(EsError::EsError(ref msg)) if msg == "Unexpected status: 404 Not Found" => {} 284 | Ok(_) => {} 285 | e => { 286 | e.unwrap_or_else(|_| panic!("Failed to clean db for index {:?}", test_idx)); 287 | } 288 | }; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/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 | 24 | use std::collections::HashMap; 25 | use std::hash::Hash; 26 | 27 | use reqwest::StatusCode; 28 | 29 | use serde::Serialize; 30 | use serde_json::{Map, Value}; 31 | 32 | use crate::{error::EsError, operations::GenericResult, Client, EsResponse}; 33 | 34 | pub type DocType<'a> = HashMap<&'a str, HashMap<&'a str, &'a str>>; 35 | pub type Mapping<'a> = HashMap<&'a str, DocType<'a>>; 36 | 37 | #[derive(Debug, Serialize)] 38 | pub struct Settings { 39 | pub number_of_shards: u32, 40 | pub analysis: Analysis, 41 | } 42 | 43 | #[derive(Debug, Serialize, Default)] 44 | pub struct Analysis { 45 | pub filter: Map, 46 | pub analyzer: Map, 47 | pub tokenizer: Map, 48 | pub char_filter: Map, 49 | } 50 | 51 | /// An indexing operation 52 | #[derive(Debug)] 53 | pub 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 | 68 | impl<'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 { 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 | 125 | impl Client { 126 | /// Open the index, making it available. 127 | pub fn open_index<'a>(&'a mut self, index: &'a str) -> Result { 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 { 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)] 179 | pub struct MappingResult; 180 | 181 | #[cfg(test)] 182 | pub 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 | 276 | fn hashmap(k: K, v: V) -> HashMap 277 | where 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)] 286 | fn hashmap2(k1: K, v1: V, k2: K, v2: V) -> HashMap 287 | where 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 | } 295 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | -------------------------------------------------------------------------------- /src/query/functions.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-2018 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 | //! Specific options for the Function option of various queries 18 | 19 | use std::collections::HashMap; 20 | 21 | use serde::{Serialize, Serializer}; 22 | 23 | use crate::{ 24 | json::{FieldBased, NoOuter, ShouldSkip}, 25 | units::{Distance, Duration, JsonVal, Location}, 26 | }; 27 | 28 | /// Function 29 | #[derive(Debug, Serialize)] 30 | pub enum Function { 31 | #[serde(rename = "script_score")] 32 | ScriptScore(ScriptScore), 33 | #[serde(rename = "weight")] 34 | Weight(Weight), 35 | #[serde(rename = "random_score")] 36 | RandomScore(RandomScore), 37 | #[serde(rename = "field_value_factor")] 38 | FieldValueFactor(FieldValueFactor), 39 | #[serde(rename = "linear")] 40 | Linear(Decay), 41 | #[serde(rename = "exp")] 42 | Exp(Decay), 43 | #[serde(rename = "gauss")] 44 | Gauss(Decay), 45 | } 46 | 47 | /// ScriptScore function 48 | #[derive(Debug, Default, Serialize)] 49 | pub struct ScriptScore { 50 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 51 | lang: Option, 52 | params: HashMap, 53 | inline: String, 54 | } 55 | 56 | impl Function { 57 | pub fn build_script_score(script: A) -> ScriptScore 58 | where 59 | A: Into, 60 | { 61 | ScriptScore { 62 | inline: script.into(), 63 | ..Default::default() 64 | } 65 | } 66 | } 67 | 68 | impl ScriptScore { 69 | add_field!(with_lang, lang, String); 70 | 71 | pub fn with_params(mut self, params: A) -> Self 72 | where 73 | A: IntoIterator, 74 | { 75 | self.params.extend(params); 76 | self 77 | } 78 | 79 | pub fn add_param(mut self, key: A, value: B) -> Self 80 | where 81 | A: Into, 82 | B: Into, 83 | { 84 | self.params.insert(key.into(), value.into()); 85 | self 86 | } 87 | 88 | pub fn build(self) -> Function { 89 | Function::ScriptScore(self) 90 | } 91 | } 92 | 93 | /// Weight function 94 | #[derive(Debug, Default, Serialize)] 95 | pub struct Weight(f64); 96 | 97 | impl Function { 98 | pub fn build_weight(weight: A) -> Weight 99 | where 100 | A: Into, 101 | { 102 | Weight(weight.into()) 103 | } 104 | } 105 | 106 | impl Weight { 107 | pub fn build(self) -> Function { 108 | Function::Weight(self) 109 | } 110 | } 111 | 112 | /// Random score function 113 | #[derive(Debug, Default, Serialize)] 114 | pub struct RandomScore(i64); 115 | 116 | impl Function { 117 | pub fn build_random_score(seed: A) -> RandomScore 118 | where 119 | A: Into, 120 | { 121 | RandomScore(seed.into()) 122 | } 123 | } 124 | 125 | impl RandomScore { 126 | pub fn build(self) -> Function { 127 | Function::RandomScore(self) 128 | } 129 | } 130 | 131 | /// Field value factor function 132 | #[derive(Debug, Default, Serialize)] 133 | pub struct FieldValueFactor { 134 | field: String, 135 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 136 | factor: Option, 137 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 138 | modifier: Option, 139 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 140 | missing: Option, 141 | } 142 | 143 | impl Function { 144 | pub fn build_field_value_factor(field: A) -> FieldValueFactor 145 | where 146 | A: Into, 147 | { 148 | FieldValueFactor { 149 | field: field.into(), 150 | ..Default::default() 151 | } 152 | } 153 | } 154 | 155 | impl FieldValueFactor { 156 | add_field!(with_factor, factor, f64); 157 | add_field!(with_modifier, modifier, Modifier); 158 | add_field!(with_missing, missing, JsonVal); 159 | 160 | pub fn build(self) -> Function { 161 | Function::FieldValueFactor(self) 162 | } 163 | } 164 | 165 | /// Modifier for the FieldValueFactor function 166 | #[derive(Debug)] 167 | pub enum Modifier { 168 | None, 169 | Log, 170 | Log1p, 171 | Log2p, 172 | Ln, 173 | Ln1p, 174 | Ln2p, 175 | Square, 176 | Sqrt, 177 | Reciprocal, 178 | } 179 | 180 | impl Serialize for Modifier { 181 | fn serialize(&self, serializer: S) -> Result 182 | where 183 | S: Serializer, 184 | { 185 | match self { 186 | Modifier::None => "none".serialize(serializer), 187 | Modifier::Log => "log".serialize(serializer), 188 | Modifier::Log1p => "log1p".serialize(serializer), 189 | Modifier::Log2p => "log2p".serialize(serializer), 190 | Modifier::Ln => "ln".serialize(serializer), 191 | Modifier::Ln1p => "ln1p".serialize(serializer), 192 | Modifier::Ln2p => "ln2p".serialize(serializer), 193 | Modifier::Square => "square".serialize(serializer), 194 | Modifier::Sqrt => "sqrt".serialize(serializer), 195 | Modifier::Reciprocal => "reciprocal".serialize(serializer), 196 | } 197 | } 198 | } 199 | 200 | #[derive(Debug, Default, Serialize)] 201 | pub struct DecayOptions { 202 | origin: Origin, 203 | scale: Scale, 204 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 205 | offset: Option, 206 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 207 | decay: Option, 208 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 209 | multi_value_mode: Option, 210 | } 211 | 212 | impl DecayOptions { 213 | pub fn new(origin: A, scale: B) -> DecayOptions 214 | where 215 | A: Into, 216 | B: Into, 217 | { 218 | DecayOptions { 219 | origin: origin.into(), 220 | scale: scale.into(), 221 | offset: None, 222 | decay: None, 223 | multi_value_mode: None, 224 | } 225 | } 226 | 227 | add_field!(with_offset, offset, Scale); 228 | add_field!(with_decay, decay, f64); 229 | add_field!(with_multi_value_mode, multi_value_mode, MultiValueMode); 230 | 231 | pub fn with_scale(mut self, val: Scale) -> Self { 232 | self.scale = val; 233 | self 234 | } 235 | 236 | pub fn with_origin(mut self, val: Origin) -> Self { 237 | self.origin = val; 238 | self 239 | } 240 | 241 | pub fn build>(self, field: A) -> Decay { 242 | Decay(FieldBased::new( 243 | field.into(), 244 | self, 245 | NoOuter, 246 | )) 247 | } 248 | } 249 | 250 | /// Decay functions 251 | #[derive(Debug, Serialize)] 252 | pub struct Decay(FieldBased); 253 | 254 | impl Function { 255 | pub fn build_decay(field: A, origin: B, scale: C) -> Decay 256 | where 257 | A: Into, 258 | B: Into, 259 | C: Into, 260 | { 261 | Decay(FieldBased::new( 262 | field.into(), 263 | DecayOptions { 264 | origin: origin.into(), 265 | scale: scale.into(), 266 | ..Default::default() 267 | }, 268 | NoOuter, 269 | )) 270 | } 271 | 272 | pub fn build_decay_from_options>(field: A, options: DecayOptions) -> Decay { 273 | options.build(field) 274 | } 275 | } 276 | 277 | impl Decay { 278 | pub fn build_linear(self) -> Function { 279 | Function::Linear(self) 280 | } 281 | 282 | pub fn build_exp(self) -> Function { 283 | Function::Exp(self) 284 | } 285 | 286 | pub fn build_gauss(self) -> Function { 287 | Function::Gauss(self) 288 | } 289 | } 290 | 291 | // options used by decay functions 292 | 293 | /// Origin for decay function 294 | #[derive(Debug)] 295 | pub enum Origin { 296 | I64(i64), 297 | U64(u64), 298 | F64(f64), 299 | Location(Location), 300 | Date(String), 301 | } 302 | 303 | impl Default for Origin { 304 | fn default() -> Origin { 305 | Origin::I64(0) 306 | } 307 | } 308 | 309 | from!(i64, Origin, I64); 310 | from!(u64, Origin, U64); 311 | from!(f64, Origin, F64); 312 | from!(Location, Origin, Location); 313 | from!(String, Origin, Date); 314 | 315 | impl Serialize for Origin { 316 | fn serialize(&self, serializer: S) -> Result 317 | where 318 | S: Serializer, 319 | { 320 | match self { 321 | Origin::I64(orig) => orig.serialize(serializer), 322 | Origin::U64(orig) => orig.serialize(serializer), 323 | Origin::F64(orig) => orig.serialize(serializer), 324 | Origin::Location(ref orig) => orig.serialize(serializer), 325 | Origin::Date(ref orig) => orig.serialize(serializer), 326 | } 327 | } 328 | } 329 | 330 | /// Scale used by decay function 331 | #[derive(Debug)] 332 | pub enum Scale { 333 | I64(i64), 334 | U64(u64), 335 | F64(f64), 336 | Distance(Distance), 337 | Duration(Duration), 338 | } 339 | 340 | impl Default for Scale { 341 | fn default() -> Self { 342 | Scale::I64(0) 343 | } 344 | } 345 | 346 | from!(i64, Scale, I64); 347 | from!(u64, Scale, U64); 348 | from!(f64, Scale, F64); 349 | from!(Distance, Scale, Distance); 350 | from!(Duration, Scale, Duration); 351 | 352 | impl Serialize for Scale { 353 | fn serialize(&self, serializer: S) -> Result 354 | where 355 | S: Serializer, 356 | { 357 | match self { 358 | Scale::I64(s) => s.serialize(serializer), 359 | Scale::U64(s) => s.serialize(serializer), 360 | Scale::F64(s) => s.serialize(serializer), 361 | Scale::Distance(ref s) => s.serialize(serializer), 362 | Scale::Duration(ref s) => s.serialize(serializer), 363 | } 364 | } 365 | } 366 | 367 | /// Values for multi_value_mode 368 | #[derive(Debug)] 369 | pub enum MultiValueMode { 370 | Min, 371 | Max, 372 | Avg, 373 | Sum, 374 | } 375 | 376 | impl Serialize for MultiValueMode { 377 | fn serialize(&self, serializer: S) -> Result 378 | where 379 | S: Serializer, 380 | { 381 | use self::MultiValueMode::*; 382 | match self { 383 | Min => "min", 384 | Max => "max", 385 | Avg => "avg", 386 | Sum => "sum", 387 | } 388 | .serialize(serializer) 389 | } 390 | } 391 | 392 | #[cfg(test)] 393 | pub mod tests { 394 | use serde_json; 395 | 396 | #[test] 397 | fn test_decay_query() { 398 | use crate::units::*; 399 | let gauss_decay_query = super::Function::build_decay( 400 | "my_field", 401 | Location::LatLon(42., 24.), 402 | Distance::new(3., DistanceUnit::Kilometer), 403 | ) 404 | .build_gauss(); 405 | 406 | assert_eq!( 407 | r#"{"gauss":{"my_field":{"origin":{"lat":42.0,"lon":24.0},"scale":"3km"}}}"#, 408 | serde_json::to_string(&gauss_decay_query).unwrap() 409 | ); 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /src/operations/bulk.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-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 the Bulk API 18 | 19 | use std::fmt; 20 | 21 | use reqwest::StatusCode; 22 | 23 | use serde::{ 24 | de::{Error, MapAccess, Visitor}, 25 | Deserialize, Deserializer, Serialize, Serializer, 26 | }; 27 | 28 | use serde_json; 29 | 30 | use crate::{ 31 | error::EsError, 32 | json::{FieldBased, NoOuter, ShouldSkip}, 33 | units::Duration, 34 | Client, EsResponse, 35 | }; 36 | 37 | use super::{ 38 | common::{OptionVal, Options, VersionType}, 39 | ShardCountResult, 40 | }; 41 | 42 | #[derive(Debug)] 43 | pub enum ActionType { 44 | Index, 45 | Create, 46 | Delete, 47 | /// WARNING - currently un-implemented 48 | Update, 49 | } 50 | 51 | impl Serialize for ActionType { 52 | fn serialize(&self, serializer: S) -> Result 53 | where 54 | S: Serializer, 55 | { 56 | self.to_string().serialize(serializer) 57 | } 58 | } 59 | 60 | impl ToString for ActionType { 61 | fn to_string(&self) -> String { 62 | match *self { 63 | ActionType::Index => "index", 64 | ActionType::Create => "create", 65 | ActionType::Delete => "delete", 66 | ActionType::Update => "update", 67 | } 68 | .to_owned() 69 | } 70 | } 71 | 72 | #[derive(Debug, Default, Serialize)] 73 | pub struct ActionOptions { 74 | #[serde(rename = "_index", skip_serializing_if = "ShouldSkip::should_skip")] 75 | index: Option, 76 | #[serde(rename = "_type", skip_serializing_if = "ShouldSkip::should_skip")] 77 | doc_type: Option, 78 | #[serde(rename = "_id", skip_serializing_if = "ShouldSkip::should_skip")] 79 | id: Option, 80 | #[serde(rename = "_version", skip_serializing_if = "ShouldSkip::should_skip")] 81 | version: Option, 82 | #[serde( 83 | rename = "_version_type", 84 | skip_serializing_if = "ShouldSkip::should_skip" 85 | )] 86 | version_type: Option, 87 | #[serde(rename = "_routing", skip_serializing_if = "ShouldSkip::should_skip")] 88 | routing: Option, 89 | #[serde(rename = "_parent", skip_serializing_if = "ShouldSkip::should_skip")] 90 | parent: Option, 91 | #[serde(rename = "_timestamp", skip_serializing_if = "ShouldSkip::should_skip")] 92 | timestamp: Option, 93 | #[serde(rename = "_ttl", skip_serializing_if = "ShouldSkip::should_skip")] 94 | ttl: Option, 95 | #[serde( 96 | rename = "_retry_on_conflict", 97 | skip_serializing_if = "ShouldSkip::should_skip" 98 | )] 99 | retry_on_conflict: Option, 100 | } 101 | 102 | #[derive(Debug, Serialize)] 103 | pub struct Action(FieldBased, Option); 104 | 105 | impl Action 106 | where 107 | S: Serialize, 108 | { 109 | /// An index action. 110 | /// 111 | /// Takes the document to be indexed, other parameters can be set as 112 | /// optional on the `Action` struct returned. 113 | pub fn index(document: S) -> Self { 114 | Action( 115 | FieldBased::new(ActionType::Index, Default::default(), NoOuter), 116 | Some(document), 117 | ) 118 | } 119 | 120 | /// Create action 121 | pub fn create(document: S) -> Self { 122 | Action( 123 | FieldBased::new(ActionType::Create, Default::default(), NoOuter), 124 | Some(document), 125 | ) 126 | } 127 | 128 | /// Add the serialized version of this action to the bulk `String`. 129 | fn add(&self, actstr: &mut String) -> Result<(), EsError> { 130 | let command_str = serde_json::to_string(&self.0)?; 131 | 132 | actstr.push_str(&command_str); 133 | actstr.push_str("\n"); 134 | 135 | if let Some(ref source) = self.1 { 136 | let payload_str = serde_json::to_string(source)?; 137 | actstr.push_str(&payload_str); 138 | actstr.push_str("\n"); 139 | } 140 | Ok(()) 141 | } 142 | } 143 | 144 | impl Action { 145 | /// Delete a document based on ID. 146 | /// 147 | /// # Example 148 | /// 149 | /// ``` 150 | /// use rs_es::operations::bulk::Action; 151 | /// 152 | /// let delete_action:Action<()> = Action::delete("doc_id"); 153 | /// let delete_with_index:Action<()> = Action::delete("doc_id").with_index("index_name"); 154 | /// ``` 155 | pub fn delete>(id: A) -> Self { 156 | Action( 157 | FieldBased::new( 158 | ActionType::Delete, 159 | ActionOptions { 160 | id: Some(id.into()), 161 | ..Default::default() 162 | }, 163 | NoOuter, 164 | ), 165 | None, 166 | ) 167 | } 168 | 169 | // TODO - implement update 170 | 171 | add_inner_field!(with_index, index, String); 172 | add_inner_field!(with_doc_type, doc_type, String); 173 | add_inner_field!(with_id, id, String); 174 | add_inner_field!(with_version, version, u64); 175 | add_inner_field!(with_version_type, version_type, VersionType); 176 | add_inner_field!(with_routing, routing, String); 177 | add_inner_field!(with_parent, parent, String); 178 | add_inner_field!(with_timestamp, timestamp, String); 179 | add_inner_field!(with_ttl, ttl, Duration); 180 | add_inner_field!(with_retry_on_conflict, retry_on_conflict, u64); 181 | } 182 | 183 | #[derive(Debug)] 184 | pub struct BulkOperation<'a, 'b, S: 'b> { 185 | client: &'a mut Client, 186 | index: Option<&'b str>, 187 | doc_type: Option<&'b str>, 188 | actions: &'b [Action], 189 | options: Options<'b>, 190 | } 191 | 192 | impl<'a, 'b, S> BulkOperation<'a, 'b, S> 193 | where 194 | S: Serialize, 195 | { 196 | pub fn new(client: &'a mut Client, actions: &'b [Action]) -> Self { 197 | BulkOperation { 198 | client, 199 | index: None, 200 | doc_type: None, 201 | actions, 202 | options: Options::default(), 203 | } 204 | } 205 | 206 | pub fn with_index(&'b mut self, index: &'b str) -> &'b mut Self { 207 | self.index = Some(index); 208 | self 209 | } 210 | 211 | pub fn with_doc_type(&'b mut self, doc_type: &'b str) -> &'b mut Self { 212 | self.doc_type = Some(doc_type); 213 | self 214 | } 215 | 216 | add_option!(with_consistency, "consistency"); 217 | add_option!(with_refresh, "refresh"); 218 | 219 | fn format_url(&self) -> String { 220 | let mut url = String::new(); 221 | url.push_str("/"); 222 | if let Some(index) = self.index { 223 | url.push_str(index); 224 | url.push_str("/"); 225 | } 226 | if let Some(doc_type) = self.doc_type { 227 | url.push_str(doc_type); 228 | url.push_str("/"); 229 | } 230 | url.push_str("_bulk"); 231 | url.push_str(&self.options.to_string()); 232 | url 233 | } 234 | 235 | fn format_actions(&self) -> String { 236 | let mut actstr = String::new(); 237 | for action in self.actions { 238 | action.add(&mut actstr).unwrap(); 239 | } 240 | actstr 241 | } 242 | 243 | pub fn send(&self) -> Result { 244 | // 245 | // This function does not use the standard GET/POST/DELETE functions of 246 | // the client, as they serve the happy path of JSON-in/JSON-out, this 247 | // function does send send JSON in. 248 | // 249 | // Various parts of the client are reused where it makes sense. 250 | // 251 | let response = self.client.do_es_op(&self.format_url(), |url| { 252 | self.client 253 | .http_client 254 | .post(url) 255 | .body(self.format_actions()) 256 | })?; 257 | 258 | match response.status_code() { 259 | StatusCode::OK => Ok(response.read_response()?), 260 | status_code => Err(EsError::EsError(format!( 261 | "Unexpected status: {}", 262 | status_code 263 | ))), 264 | } 265 | } 266 | } 267 | 268 | impl Client { 269 | /// Bulk 270 | /// 271 | /// See: https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html 272 | pub fn bulk<'a, 'b, S>(&'a mut self, actions: &'b [Action]) -> BulkOperation<'a, 'b, S> 273 | where 274 | S: Serialize, 275 | { 276 | BulkOperation::new(self, actions) 277 | } 278 | } 279 | 280 | /// The result of specific actions 281 | #[derive(Debug)] 282 | pub struct ActionResult { 283 | pub action: ActionType, 284 | pub inner: ActionResultInner, 285 | } 286 | 287 | impl<'de> Deserialize<'de> for ActionResult { 288 | fn deserialize(deserializer: D) -> Result 289 | where 290 | D: Deserializer<'de>, 291 | { 292 | struct ActionResultVisitor; 293 | 294 | impl<'vde> Visitor<'vde> for ActionResultVisitor { 295 | type Value = ActionResult; 296 | 297 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 298 | formatter.write_str("an ActionResult") 299 | } 300 | 301 | fn visit_map(self, mut visitor: V) -> Result 302 | where 303 | V: MapAccess<'vde>, 304 | { 305 | let visited: Option<(String, ActionResultInner)> = visitor.next_entry()?; 306 | let (key, value) = match visited { 307 | Some((key, value)) => (key, value), 308 | None => return Err(V::Error::custom("expecting at least one field")), 309 | }; 310 | 311 | let result = ActionResult { 312 | action: match key.as_ref() { 313 | "index" => ActionType::Index, 314 | "create" => ActionType::Create, 315 | "delete" => ActionType::Delete, 316 | "update" => ActionType::Update, 317 | _ => return Err(V::Error::custom(format!("Unrecognised key: {}", key))), 318 | }, 319 | inner: value, 320 | }; 321 | 322 | Ok(result) 323 | } 324 | } 325 | 326 | deserializer.deserialize_any(ActionResultVisitor) 327 | } 328 | } 329 | 330 | #[derive(Debug, serde::Deserialize)] 331 | pub struct ActionResultInner { 332 | #[serde(rename = "_index")] 333 | pub index: String, 334 | #[serde(rename = "_type")] 335 | pub doc_type: String, 336 | #[serde(rename = "_version")] 337 | pub version: u64, 338 | pub status: u64, 339 | #[serde(rename = "_shards")] 340 | pub shards: ShardCountResult, 341 | pub found: Option, 342 | } 343 | 344 | /// The result of a bulk operation 345 | #[derive(Debug, serde::Deserialize)] 346 | pub struct BulkResult { 347 | pub errors: bool, 348 | pub items: Vec, 349 | pub took: u64, 350 | } 351 | 352 | #[cfg(test)] 353 | pub mod tests { 354 | use crate::tests::{clean_db, make_client, TestDocument}; 355 | 356 | use super::Action; 357 | 358 | #[test] 359 | fn test_bulk() { 360 | let index_name = "test_bulk"; 361 | let mut client = make_client(); 362 | 363 | clean_db(&mut client, index_name); 364 | 365 | let actions: Vec> = (1..10) 366 | .map(|i| { 367 | let doc = TestDocument::new() 368 | .with_str_field("bulk_doc") 369 | .with_int_field(i); 370 | Action::index(doc) 371 | }) 372 | .collect(); 373 | 374 | let result = client 375 | .bulk(&actions) 376 | .with_index(index_name) 377 | .with_doc_type("bulk_type") 378 | .send() 379 | .unwrap(); 380 | 381 | assert_eq!(false, result.errors); 382 | assert_eq!(9, result.items.len()); 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/query/term.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 | //! Specific Term level queries 18 | 19 | use serde::{Serialize, Serializer}; 20 | 21 | use crate::{ 22 | json::{NoOuter, ShouldSkip}, 23 | units::{JsonPotential, JsonVal, OneOrMany}, 24 | }; 25 | 26 | use super::{common::FieldBasedQuery, Flags, Fuzziness, Query}; 27 | 28 | /// Values of the rewrite option used by multi-term queries 29 | #[derive(Debug)] 30 | pub enum Rewrite { 31 | ConstantScoreAuto, 32 | ScoringBoolean, 33 | ConstantScoreBoolean, 34 | ConstantScoreFilter, 35 | TopTerms(i64), 36 | TopTermsBoost(i64), 37 | TopTermsBlendedFreqs(i64), 38 | } 39 | 40 | impl Serialize for Rewrite { 41 | fn serialize(&self, serializer: S) -> Result 42 | where 43 | S: Serializer, 44 | { 45 | use self::Rewrite::*; 46 | match self { 47 | ConstantScoreAuto => "constant_score_auto".serialize(serializer), 48 | ScoringBoolean => "scoring_boolean".serialize(serializer), 49 | ConstantScoreBoolean => "constant_score_boolean".serialize(serializer), 50 | ConstantScoreFilter => "constant_score_filter".serialize(serializer), 51 | TopTerms(n) => format!("top_terms_{}", n).serialize(serializer), 52 | TopTermsBoost(n) => format!("top_terms_boost_{}", n).serialize(serializer), 53 | TopTermsBlendedFreqs(n) => { 54 | format!("top_terms_blended_freqs_{}", n).serialize(serializer) 55 | } 56 | } 57 | } 58 | } 59 | 60 | /// Term query 61 | #[derive(Debug, Default, Serialize)] 62 | pub struct TermQueryInner { 63 | value: JsonVal, 64 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 65 | boost: Option, 66 | } 67 | 68 | impl TermQueryInner { 69 | fn new(value: JsonVal) -> Self { 70 | TermQueryInner { 71 | value, 72 | ..Default::default() 73 | } 74 | } 75 | } 76 | 77 | #[derive(Debug, Serialize)] 78 | pub struct TermQuery(FieldBasedQuery); 79 | 80 | impl Query { 81 | pub fn build_term(field: A, value: B) -> TermQuery 82 | where 83 | A: Into, 84 | B: Into, 85 | { 86 | TermQuery(FieldBasedQuery::new( 87 | field.into(), 88 | TermQueryInner::new(value.into()), 89 | NoOuter, 90 | )) 91 | } 92 | } 93 | 94 | impl TermQuery { 95 | add_inner_field!(with_boost, boost, f64); 96 | 97 | build!(Term); 98 | } 99 | 100 | // Terms query 101 | /// Terms Query Lookup 102 | #[derive(Debug, Default, Serialize)] 103 | pub struct TermsQueryLookup { 104 | id: JsonVal, 105 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 106 | index: Option, 107 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 108 | doc_type: Option, 109 | path: String, 110 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 111 | routing: Option, 112 | } 113 | 114 | impl<'a> TermsQueryLookup { 115 | pub fn new(id: A, path: B) -> TermsQueryLookup 116 | where 117 | A: Into, 118 | B: Into, 119 | { 120 | TermsQueryLookup { 121 | id: id.into(), 122 | path: path.into(), 123 | ..Default::default() 124 | } 125 | } 126 | 127 | add_field!(with_index, index, String); 128 | add_field!(with_type, doc_type, String); 129 | add_field!(with_routing, routing, String); 130 | } 131 | 132 | /// TermsQueryIn 133 | #[derive(Debug)] 134 | pub enum TermsQueryIn { 135 | /// A `Vec` of values 136 | Values(Vec), 137 | 138 | /// An indirect reference to another document 139 | Lookup(TermsQueryLookup), 140 | } 141 | 142 | // TODO - if this looks useful it can be extracted into a macro 143 | impl Serialize for TermsQueryIn { 144 | fn serialize(&self, serializer: S) -> Result 145 | where 146 | S: Serializer, 147 | { 148 | match self { 149 | TermsQueryIn::Values(ref q) => q.serialize(serializer), 150 | TermsQueryIn::Lookup(ref q) => q.serialize(serializer), 151 | } 152 | } 153 | } 154 | 155 | impl Default for TermsQueryIn { 156 | fn default() -> Self { 157 | TermsQueryIn::Values(Default::default()) 158 | } 159 | } 160 | 161 | impl From for TermsQueryIn { 162 | fn from(from: TermsQueryLookup) -> TermsQueryIn { 163 | TermsQueryIn::Lookup(from) 164 | } 165 | } 166 | 167 | impl From> for TermsQueryIn { 168 | fn from(from: Vec) -> TermsQueryIn { 169 | TermsQueryIn::Values(from) 170 | } 171 | } 172 | 173 | impl<'a, A> From<&'a [A]> for TermsQueryIn 174 | where 175 | A: JsonPotential, 176 | { 177 | fn from(from: &'a [A]) -> Self { 178 | TermsQueryIn::Values(from.iter().map(JsonPotential::to_json_val).collect()) 179 | } 180 | } 181 | 182 | impl From> for TermsQueryIn 183 | where 184 | A: JsonPotential, 185 | { 186 | fn from(from: Vec) -> TermsQueryIn { 187 | (&from[..]).into() 188 | } 189 | } 190 | 191 | /// Terms Query 192 | #[derive(Debug, Serialize)] 193 | pub struct TermsQuery(FieldBasedQuery); 194 | 195 | impl Query { 196 | pub fn build_terms(field: A) -> TermsQuery 197 | where 198 | A: Into, 199 | { 200 | TermsQuery(FieldBasedQuery::new( 201 | field.into(), 202 | Default::default(), 203 | NoOuter, 204 | )) 205 | } 206 | } 207 | 208 | impl TermsQuery { 209 | pub fn with_values(mut self, values: T) -> Self 210 | where 211 | T: Into, 212 | { 213 | self.0.inner = values.into(); 214 | self 215 | } 216 | 217 | build!(Terms); 218 | } 219 | 220 | /// Range query 221 | /// TODO: Check all possible combinations: gt, gte, lte, lt, from, to, include_upper, include_lower 222 | /// and share with other range queries 223 | #[derive(Debug, Default, Serialize)] 224 | pub struct RangeQueryInner { 225 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 226 | gte: Option, 227 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 228 | gt: Option, 229 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 230 | lte: Option, 231 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 232 | lt: Option, 233 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 234 | boost: Option, 235 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 236 | time_zone: Option, 237 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 238 | format: Option, 239 | } 240 | 241 | #[derive(Debug, Serialize)] 242 | pub struct RangeQuery(FieldBasedQuery); 243 | 244 | impl Query { 245 | pub fn build_range(field: A) -> RangeQuery 246 | where 247 | A: Into, 248 | { 249 | RangeQuery(FieldBasedQuery::new( 250 | field.into(), 251 | Default::default(), 252 | NoOuter, 253 | )) 254 | } 255 | } 256 | 257 | impl RangeQuery { 258 | add_inner_field!(with_gte, gte, JsonVal); 259 | add_inner_field!(with_gt, gt, JsonVal); 260 | add_inner_field!(with_lte, lte, JsonVal); 261 | add_inner_field!(with_lt, lt, JsonVal); 262 | add_inner_field!(with_boost, boost, f64); 263 | add_inner_field!(with_time_zone, time_zone, String); 264 | add_inner_field!(with_format, format, String); 265 | 266 | build!(Range); 267 | } 268 | 269 | /// Exists query 270 | #[derive(Debug, Serialize)] 271 | pub struct ExistsQuery { 272 | field: String, 273 | } 274 | 275 | impl Query { 276 | pub fn build_exists(field: A) -> ExistsQuery 277 | where 278 | A: Into, 279 | { 280 | ExistsQuery { 281 | field: field.into(), 282 | } 283 | } 284 | } 285 | 286 | impl ExistsQuery { 287 | build!(Exists); 288 | } 289 | 290 | /// Prefix query 291 | #[derive(Debug, Serialize)] 292 | pub struct PrefixQuery(FieldBasedQuery); 293 | 294 | #[derive(Debug, Default, Serialize)] 295 | pub struct PrefixQueryInner { 296 | value: String, 297 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 298 | boost: Option, 299 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 300 | rewrite: Option, 301 | } 302 | 303 | impl Query { 304 | pub fn build_prefix(field: A, value: B) -> PrefixQuery 305 | where 306 | A: Into, 307 | B: Into, 308 | { 309 | PrefixQuery(FieldBasedQuery::new( 310 | field.into(), 311 | PrefixQueryInner { 312 | value: value.into(), 313 | ..Default::default() 314 | }, 315 | NoOuter, 316 | )) 317 | } 318 | } 319 | 320 | impl PrefixQuery { 321 | add_inner_field!(with_boost, boost, f64); 322 | add_inner_field!(with_rewrite, rewrite, Rewrite); 323 | 324 | build!(Prefix); 325 | } 326 | 327 | /// Wildcard query 328 | #[derive(Debug, Serialize)] 329 | pub struct WildcardQuery(FieldBasedQuery); 330 | 331 | #[derive(Debug, Default, Serialize)] 332 | pub struct WildcardQueryInner { 333 | value: String, 334 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 335 | boost: Option, 336 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 337 | rewrite: Option, 338 | } 339 | 340 | impl Query { 341 | pub fn build_wildcard(field: A, value: B) -> WildcardQuery 342 | where 343 | A: Into, 344 | B: Into, 345 | { 346 | WildcardQuery(FieldBasedQuery::new( 347 | field.into(), 348 | WildcardQueryInner { 349 | value: value.into(), 350 | ..Default::default() 351 | }, 352 | NoOuter, 353 | )) 354 | } 355 | } 356 | 357 | impl WildcardQuery { 358 | add_inner_field!(with_boost, boost, f64); 359 | add_inner_field!(with_rewrite, rewrite, Rewrite); 360 | 361 | build!(Wildcard); 362 | } 363 | 364 | // Regexp query 365 | /// Flags for the Regexp query 366 | #[derive(Debug)] 367 | pub enum RegexpQueryFlags { 368 | All, 369 | Anystring, 370 | Complement, 371 | Empty, 372 | Intersection, 373 | Interval, 374 | None, 375 | } 376 | 377 | impl AsRef for RegexpQueryFlags { 378 | fn as_ref(&self) -> &str { 379 | match self { 380 | RegexpQueryFlags::All => "ALL", 381 | RegexpQueryFlags::Anystring => "ANYSTRING", 382 | RegexpQueryFlags::Complement => "COMPLEMENT", 383 | RegexpQueryFlags::Empty => "EMPTY", 384 | RegexpQueryFlags::Intersection => "INTERSECTION", 385 | RegexpQueryFlags::Interval => "INTERVAL", 386 | RegexpQueryFlags::None => "NONE", 387 | } 388 | } 389 | } 390 | 391 | /// Regexp query 392 | #[derive(Debug, Serialize)] 393 | pub struct RegexpQuery(FieldBasedQuery); 394 | 395 | #[derive(Debug, Default, Serialize)] 396 | pub struct RegexpQueryInner { 397 | value: String, 398 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 399 | boost: Option, 400 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 401 | flags: Option>, 402 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 403 | max_determined_states: Option, 404 | } 405 | 406 | impl Query { 407 | pub fn build_query(field: A, value: B) -> RegexpQuery 408 | where 409 | A: Into, 410 | B: Into, 411 | { 412 | RegexpQuery(FieldBasedQuery::new( 413 | field.into(), 414 | RegexpQueryInner { 415 | value: value.into(), 416 | ..Default::default() 417 | }, 418 | NoOuter, 419 | )) 420 | } 421 | } 422 | 423 | impl RegexpQuery { 424 | add_inner_field!(with_boost, boost, f64); 425 | add_inner_field!(with_flags, flags, Flags); 426 | add_inner_field!(with_max_determined_states, max_determined_states, u64); 427 | 428 | build!(Regexp); 429 | } 430 | 431 | /// Fuzzy query 432 | #[derive(Debug, Serialize)] 433 | pub struct FuzzyQuery(FieldBasedQuery); 434 | 435 | #[derive(Debug, Default, Serialize)] 436 | pub struct FuzzyQueryInner { 437 | value: String, 438 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 439 | boost: Option, 440 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 441 | fuzziness: Option, 442 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 443 | prefix_length: Option, 444 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 445 | max_expansions: Option, 446 | } 447 | 448 | impl Query { 449 | pub fn build_fuzzy(field: A, value: B) -> FuzzyQuery 450 | where 451 | A: Into, 452 | B: Into, 453 | { 454 | FuzzyQuery(FieldBasedQuery::new( 455 | field.into(), 456 | FuzzyQueryInner { 457 | value: value.into(), 458 | ..Default::default() 459 | }, 460 | NoOuter, 461 | )) 462 | } 463 | } 464 | 465 | impl FuzzyQuery { 466 | add_inner_field!(with_boost, boost, f64); 467 | add_inner_field!(with_fuzziness, fuzziness, Fuzziness); 468 | add_inner_field!(with_prefix_length, prefix_length, u64); 469 | add_inner_field!(with_max_expansions, max_expansions, u64); 470 | 471 | build!(Fuzzy); 472 | } 473 | 474 | /// Type query 475 | #[derive(Debug, Serialize)] 476 | pub struct TypeQuery { 477 | value: String, 478 | } 479 | 480 | impl Query { 481 | pub fn build_type(value: A) -> TypeQuery 482 | where 483 | A: Into, 484 | { 485 | TypeQuery { 486 | value: value.into(), 487 | } 488 | } 489 | } 490 | 491 | impl TypeQuery { 492 | build!(Type); 493 | } 494 | 495 | /// Ids query 496 | #[derive(Debug, Default, Serialize)] 497 | pub struct IdsQuery { 498 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 499 | doc_type: Option>, 500 | values: Vec, 501 | } 502 | 503 | impl Query { 504 | pub fn build_ids(values: A) -> IdsQuery 505 | where 506 | A: Into>, 507 | { 508 | IdsQuery { 509 | values: values.into(), 510 | ..Default::default() 511 | } 512 | } 513 | } 514 | 515 | impl IdsQuery { 516 | add_field!(with_type, doc_type, OneOrMany); 517 | 518 | build!(Ids); 519 | } 520 | -------------------------------------------------------------------------------- /src/operations/search/aggregations/metrics.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-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 | //! For metrics-based aggregations 18 | 19 | use std::collections::HashMap; 20 | 21 | use serde::ser::{SerializeMap, Serializer}; 22 | use serde::{Deserialize, Serialize}; 23 | use serde_json::{from_value, Value}; 24 | 25 | use crate::error::EsError; 26 | use crate::json::{serialize_map_optional_kv, MergeSerialize, NoOuter, ShouldSkip}; 27 | use crate::units::{GeoBox, JsonVal}; 28 | 29 | use super::common::{Agg, Script}; 30 | use super::{Aggregation, AggregationResult}; 31 | 32 | macro_rules! metrics_agg { 33 | ($b:ident) => { 34 | agg!($b); 35 | 36 | impl<'a> From<$b<'a>> for Aggregation<'a> { 37 | fn from(from: $b<'a>) -> Aggregation<'a> { 38 | Aggregation::Metrics(MetricsAggregation::$b(from)) 39 | } 40 | } 41 | }; 42 | } 43 | 44 | /// Min aggregation 45 | #[derive(Debug)] 46 | pub struct Min<'a>(Agg<'a, NoOuter>); 47 | metrics_agg!(Min); 48 | 49 | #[derive(Debug)] 50 | pub struct Max<'a>(Agg<'a, NoOuter>); 51 | metrics_agg!(Max); 52 | 53 | /// Sum aggregation 54 | #[derive(Debug)] 55 | pub struct Sum<'a>(Agg<'a, NoOuter>); 56 | metrics_agg!(Sum); 57 | 58 | /// Avg aggregation 59 | #[derive(Debug)] 60 | pub struct Avg<'a>(Agg<'a, NoOuter>); 61 | metrics_agg!(Avg); 62 | 63 | /// Stats aggregation 64 | #[derive(Debug)] 65 | pub struct Stats<'a>(Agg<'a, NoOuter>); 66 | metrics_agg!(Stats); 67 | 68 | /// Extended stats aggregation 69 | #[derive(Debug)] 70 | pub struct ExtendedStats<'a>(Agg<'a, NoOuter>); 71 | metrics_agg!(ExtendedStats); 72 | 73 | /// Value count aggregation 74 | #[derive(Debug)] 75 | pub struct ValueCount<'a>(Agg<'a, NoOuter>); 76 | metrics_agg!(ValueCount); 77 | 78 | /// Percentiles aggregation, see: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-aggregation.html 79 | /// 80 | /// # Examples 81 | /// 82 | /// ``` 83 | /// use rs_es::operations::search::aggregations::metrics::Percentiles; 84 | /// 85 | /// let p1 = Percentiles::field("field_name").with_compression(100u64); 86 | /// let p2 = Percentiles::field("field_name").with_percents(vec![10.0, 20.0]); 87 | /// ``` 88 | #[derive(Debug)] 89 | pub struct Percentiles<'a>(Agg<'a, PercentilesExtra>); 90 | metrics_agg!(Percentiles); 91 | 92 | #[derive(Debug, Default)] 93 | pub struct PercentilesExtra { 94 | percents: Option>, 95 | compression: Option, 96 | } 97 | 98 | impl<'a> Percentiles<'a> { 99 | add_extra_option!(with_percents, percents, Vec); 100 | add_extra_option!(with_compression, compression, u64); 101 | } 102 | 103 | impl MergeSerialize for PercentilesExtra { 104 | fn merge_serialize(&self, serializer: &mut S) -> Result<(), S::Error> 105 | where 106 | S: SerializeMap, 107 | { 108 | serialize_map_optional_kv(serializer, "percents", &self.percents)?; 109 | serialize_map_optional_kv(serializer, "compression", &self.compression) 110 | } 111 | } 112 | 113 | /// Percentile Ranks aggregation 114 | #[derive(Debug)] 115 | pub struct PercentileRanks<'a>(Agg<'a, PercentileRanksExtra>); 116 | metrics_agg!(PercentileRanks); 117 | 118 | #[derive(Debug, Default)] 119 | pub struct PercentileRanksExtra { 120 | values: Vec, 121 | } 122 | 123 | impl<'a> PercentileRanks<'a> { 124 | pub fn with_values(mut self, values: A) -> Self 125 | where 126 | A: Into>, 127 | { 128 | self.0.extra.values = values.into(); 129 | self 130 | } 131 | } 132 | 133 | impl MergeSerialize for PercentileRanksExtra { 134 | fn merge_serialize(&self, serializer: &mut S) -> Result<(), S::Error> 135 | where 136 | S: SerializeMap, 137 | { 138 | serializer.serialize_entry("values", &self.values) 139 | } 140 | } 141 | 142 | /// Cardinality aggregation 143 | #[derive(Debug)] 144 | pub struct Cardinality<'a>(Agg<'a, CardinalityExtra>); 145 | metrics_agg!(Cardinality); 146 | 147 | #[derive(Debug, Default)] 148 | pub struct CardinalityExtra { 149 | precision_threshold: Option, 150 | rehash: Option, 151 | } 152 | 153 | impl<'a> Cardinality<'a> { 154 | add_extra_option!(with_precision_threshold, precision_threshold, u64); 155 | add_extra_option!(with_rehash, rehash, bool); 156 | } 157 | 158 | impl MergeSerialize for CardinalityExtra { 159 | fn merge_serialize(&self, serializer: &mut S) -> Result<(), S::Error> 160 | where 161 | S: SerializeMap, 162 | { 163 | serialize_map_optional_kv(serializer, "precision_threshold", &self.precision_threshold)?; 164 | serialize_map_optional_kv(serializer, "rehash", &self.rehash) 165 | } 166 | } 167 | 168 | /// Geo Bounds aggregation 169 | #[derive(Debug, Default, Serialize)] 170 | pub struct GeoBounds<'a> { 171 | field: &'a str, 172 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 173 | wrap_longitude: Option, 174 | } 175 | 176 | impl<'a> GeoBounds<'a> { 177 | pub fn new(field: &'a str) -> Self { 178 | GeoBounds { 179 | field, 180 | ..Default::default() 181 | } 182 | } 183 | 184 | add_field!(with_wrap_longitude, wrap_longitude, bool); 185 | } 186 | 187 | /// Scripted method aggregation 188 | #[derive(Debug, Default, Serialize)] 189 | pub struct ScriptedMetric<'a> { 190 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 191 | init_script: Option<&'a str>, 192 | map_script: &'a str, 193 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 194 | combine_script: Option<&'a str>, 195 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 196 | reduce_script: Option<&'a str>, 197 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 198 | params: Option, // TODO - should this be generified? 199 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 200 | reduce_params: Option, // TODO - should this be generified? 201 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 202 | lang: Option<&'a str>, 203 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 204 | init_script_file: Option<&'a str>, 205 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 206 | init_script_id: Option<&'a str>, 207 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 208 | map_script_file: Option<&'a str>, 209 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 210 | map_script_id: Option<&'a str>, 211 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 212 | combine_script_file: Option<&'a str>, 213 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 214 | combine_script_id: Option<&'a str>, 215 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 216 | reduce_script_file: Option<&'a str>, 217 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 218 | reduce_script_id: Option<&'a str>, 219 | } 220 | 221 | impl<'a> ScriptedMetric<'a> { 222 | pub fn new(map_script: &'a str) -> Self { 223 | ScriptedMetric { 224 | map_script, 225 | ..Default::default() 226 | } 227 | } 228 | 229 | add_field!(with_init_script, init_script, &'a str); 230 | add_field!(with_combine_script, combine_script, &'a str); 231 | add_field!(with_reduce_script, reduce_script, &'a str); 232 | add_field!(with_params, params, Value); 233 | add_field!(with_reduce_params, reduce_params, Value); 234 | add_field!(with_lang, lang, &'a str); 235 | add_field!(with_init_script_file, init_script_file, &'a str); 236 | add_field!(with_init_script_id, init_script_id, &'a str); 237 | add_field!(with_map_script_file, map_script_file, &'a str); 238 | add_field!(with_map_script_id, map_script_id, &'a str); 239 | add_field!(with_combine_script_file, combine_script_file, &'a str); 240 | add_field!(with_combine_script_id, combine_script_id, &'a str); 241 | add_field!(with_reduce_script_file, reduce_script_file, &'a str); 242 | add_field!(with_reduce_script_id, reduce_script_id, &'a str); 243 | } 244 | 245 | /// Individual aggregations and their options 246 | #[derive(Debug)] 247 | pub enum MetricsAggregation<'a> { 248 | Min(Min<'a>), 249 | Max(Max<'a>), 250 | Sum(Sum<'a>), 251 | Avg(Avg<'a>), 252 | Stats(Stats<'a>), 253 | ExtendedStats(ExtendedStats<'a>), 254 | ValueCount(ValueCount<'a>), 255 | Percentiles(Percentiles<'a>), 256 | PercentileRanks(PercentileRanks<'a>), 257 | Cardinality(Cardinality<'a>), 258 | GeoBounds(GeoBounds<'a>), 259 | ScriptedMetric(Box>), 260 | } 261 | 262 | impl<'a> MetricsAggregation<'a> { 263 | pub fn details(&self) -> &'static str { 264 | use self::MetricsAggregation::*; 265 | match self { 266 | Min(_) => "min", 267 | Max(_) => "max", 268 | Sum(_) => "sum", 269 | Avg(_) => "avg", 270 | Stats(_) => "stats", 271 | ExtendedStats(_) => "extended_stats", 272 | ValueCount(_) => "value_count", 273 | Percentiles(_) => "percentiles", 274 | PercentileRanks(_) => "percentile_ranks", 275 | Cardinality(_) => "cardinality", 276 | GeoBounds(_) => "geo_bounds", 277 | ScriptedMetric(_) => "scripted_metric", 278 | } 279 | } 280 | } 281 | 282 | impl<'a> Serialize for MetricsAggregation<'a> { 283 | fn serialize(&self, serializer: S) -> Result 284 | where 285 | S: Serializer, 286 | { 287 | use self::MetricsAggregation::*; 288 | match self { 289 | Min(ref min) => min.serialize(serializer), 290 | Max(ref max) => max.serialize(serializer), 291 | Sum(ref sum) => sum.serialize(serializer), 292 | Avg(ref avg) => avg.serialize(serializer), 293 | Stats(ref stats) => stats.serialize(serializer), 294 | ExtendedStats(ref extended_stats) => extended_stats.serialize(serializer), 295 | ValueCount(ref value_count) => value_count.serialize(serializer), 296 | Percentiles(ref percentiles) => percentiles.serialize(serializer), 297 | PercentileRanks(ref percentile_ranks) => percentile_ranks.serialize(serializer), 298 | Cardinality(ref cardinality) => cardinality.serialize(serializer), 299 | GeoBounds(ref geo_bounds) => geo_bounds.serialize(serializer), 300 | ScriptedMetric(ref scripted_metric) => scripted_metric.serialize(serializer), 301 | } 302 | } 303 | } 304 | 305 | // results 306 | 307 | #[derive(Debug, Serialize, Deserialize)] 308 | pub enum MetricsAggregationResult { 309 | Min(MinResult), 310 | Max(MaxResult), 311 | Sum(SumResult), 312 | Avg(AvgResult), 313 | Stats(StatsResult), 314 | ExtendedStats(ExtendedStatsResult), 315 | ValueCount(ValueCountResult), 316 | Percentiles(PercentilesResult), 317 | PercentileRanks(PercentileRanksResult), 318 | Cardinality(CardinalityResult), 319 | GeoBounds(GeoBoundsResult), 320 | ScriptedMetric(ScriptedMetricResult), 321 | } 322 | 323 | impl MetricsAggregationResult { 324 | pub fn from<'a>(ma: &MetricsAggregation<'a>, json: &Value) -> Result { 325 | use self::MetricsAggregation::*; 326 | // TODO - must be a more efficient way to do this 327 | let json = json.clone(); 328 | Ok(match ma { 329 | Min(_) => MetricsAggregationResult::Min(from_value(json)?), 330 | Max(_) => MetricsAggregationResult::Max(from_value(json)?), 331 | Sum(_) => MetricsAggregationResult::Sum(from_value(json)?), 332 | Avg(_) => MetricsAggregationResult::Avg(from_value(json)?), 333 | Stats(_) => MetricsAggregationResult::Stats(from_value(json)?), 334 | ExtendedStats(_) => MetricsAggregationResult::ExtendedStats(from_value(json)?), 335 | ValueCount(_) => MetricsAggregationResult::ValueCount(from_value(json)?), 336 | Percentiles(_) => MetricsAggregationResult::Percentiles(from_value(json)?), 337 | PercentileRanks(_) => MetricsAggregationResult::PercentileRanks(from_value(json)?), 338 | Cardinality(_) => MetricsAggregationResult::Cardinality(from_value(json)?), 339 | GeoBounds(_) => MetricsAggregationResult::GeoBounds(from_value(json)?), 340 | ScriptedMetric(_) => MetricsAggregationResult::ScriptedMetric(from_value(json)?), 341 | }) 342 | } 343 | } 344 | 345 | macro_rules! metrics_agg_as { 346 | ($n:ident,$t:ident,$rt:ty) => { 347 | agg_as!($n, Metrics, MetricsAggregationResult, $t, $rt); 348 | }; 349 | } 350 | 351 | impl AggregationResult { 352 | metrics_agg_as!(as_min, Min, MinResult); 353 | metrics_agg_as!(as_max, Max, MaxResult); 354 | metrics_agg_as!(as_sum, Sum, SumResult); 355 | metrics_agg_as!(as_avg, Avg, AvgResult); 356 | metrics_agg_as!(as_stats, Stats, StatsResult); 357 | metrics_agg_as!(as_extended_stats, ExtendedStats, ExtendedStatsResult); 358 | metrics_agg_as!(as_value_count, ValueCount, ValueCountResult); 359 | metrics_agg_as!(as_percentiles, Percentiles, PercentilesResult); 360 | metrics_agg_as!(as_percentile_ranks, PercentileRanks, PercentileRanksResult); 361 | metrics_agg_as!(as_cardinality, Cardinality, CardinalityResult); 362 | metrics_agg_as!(as_geo_bounds, GeoBounds, GeoBoundsResult); 363 | metrics_agg_as!(as_scripted_metric, ScriptedMetric, ScriptedMetricResult); 364 | } 365 | 366 | // specific result objects 367 | 368 | /// Min Result 369 | #[derive(Debug, Deserialize, Serialize)] 370 | pub struct MinResult { 371 | pub value: JsonVal, 372 | } 373 | 374 | #[derive(Debug, Deserialize, Serialize)] 375 | pub struct MaxResult { 376 | pub value: JsonVal, 377 | } 378 | 379 | #[derive(Debug, Deserialize, Serialize)] 380 | pub struct SumResult { 381 | pub value: f64, 382 | } 383 | 384 | #[derive(Debug, Deserialize, Serialize)] 385 | pub struct AvgResult { 386 | pub value: f64, 387 | } 388 | 389 | #[derive(Debug, Deserialize, Serialize)] 390 | pub struct StatsResult { 391 | pub count: u64, 392 | pub min: f64, 393 | pub max: f64, 394 | pub avg: f64, 395 | pub sum: f64, 396 | } 397 | 398 | /// Used by the `ExtendedStatsResult` 399 | #[derive(Debug, Deserialize, Serialize)] 400 | pub struct Bounds { 401 | pub upper: f64, 402 | pub lower: f64, 403 | } 404 | 405 | #[derive(Debug, Deserialize, Serialize)] 406 | pub struct ExtendedStatsResult { 407 | pub count: u64, 408 | pub min: f64, 409 | pub max: f64, 410 | pub avg: f64, 411 | pub sum: f64, 412 | pub sum_of_squares: f64, 413 | pub variance: f64, 414 | pub std_deviation: f64, 415 | pub std_deviation_bounds: Bounds, 416 | } 417 | 418 | #[derive(Debug, Deserialize, Serialize)] 419 | pub struct ValueCountResult { 420 | pub value: u64, 421 | } 422 | 423 | #[derive(Debug, Deserialize, Serialize)] 424 | pub struct PercentilesResult { 425 | pub values: HashMap, 426 | } 427 | 428 | #[derive(Debug, Deserialize, Serialize)] 429 | pub struct PercentileRanksResult { 430 | pub values: HashMap, 431 | } 432 | 433 | #[derive(Debug, Deserialize, Serialize)] 434 | pub struct CardinalityResult { 435 | pub value: u64, 436 | } 437 | 438 | #[derive(Debug, Deserialize, Serialize)] 439 | pub struct GeoBoundsResult { 440 | pub bounds: GeoBox, 441 | } 442 | 443 | #[derive(Debug, Deserialize, Serialize)] 444 | pub struct ScriptedMetricResult { 445 | pub value: JsonVal, 446 | } 447 | 448 | #[cfg(test)] 449 | pub mod tests { 450 | use serde_json; 451 | 452 | use super::super::Aggregations; 453 | use super::Min; 454 | 455 | #[test] 456 | fn test_min_aggregation() { 457 | let aggs: Aggregations = ("min_test", Min::field("blah")).into(); 458 | 459 | assert_eq!( 460 | "{\"min_test\":{\"min\":{\"field\":\"blah\"}}}", 461 | serde_json::to_string(&aggs).unwrap() 462 | ); 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /src/query/geo.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 | //! Geo queries 18 | 19 | use serde::ser::{SerializeMap, Serializer}; 20 | use serde::Serialize; 21 | 22 | use crate::{ 23 | json::{serialize_map_optional_kv, MergeSerialize, NoOuter, ShouldSkip}, 24 | units::{Distance, DistanceType, GeoBox, Location}, 25 | }; 26 | 27 | use super::{common::FieldBasedQuery, Query}; 28 | 29 | #[derive(Debug, Serialize)] 30 | pub enum ShapeOption { 31 | #[serde(rename = "shape")] 32 | Shape(Shape), 33 | #[serde(rename = "indexed_shape")] 34 | IndexedShape(IndexedShape), 35 | #[cfg(feature = "geo")] 36 | #[serde(rename = "shape")] 37 | Geojson(geojson::Geometry), 38 | } 39 | 40 | from!(Shape, ShapeOption, Shape); 41 | from!(IndexedShape, ShapeOption, IndexedShape); 42 | 43 | /// GeoShape query 44 | #[derive(Debug, Serialize)] 45 | pub struct GeoShapeQuery(FieldBasedQuery, NoOuter>); 46 | 47 | impl Query { 48 | pub fn build_geo_shape(field: A) -> GeoShapeQuery 49 | where 50 | A: Into, 51 | { 52 | GeoShapeQuery(FieldBasedQuery::new(field.into(), None, NoOuter)) 53 | } 54 | } 55 | 56 | impl GeoShapeQuery { 57 | pub fn with_shape(mut self, shape: A) -> Self 58 | where 59 | A: Into, 60 | { 61 | self.0.inner = Some(ShapeOption::Shape(shape.into())); 62 | self 63 | } 64 | 65 | pub fn with_indexed_shape(mut self, indexed_shape: A) -> Self 66 | where 67 | A: Into, 68 | { 69 | self.0.inner = Some(ShapeOption::IndexedShape(indexed_shape.into())); 70 | self 71 | } 72 | 73 | #[cfg(feature = "geo")] 74 | /// Use a geojson object as shape. 75 | /// Require to enable the `geo` feature. 76 | pub fn with_geojson(mut self, shape: A) -> Self 77 | where 78 | A: Into, 79 | { 80 | self.0.inner = Some(ShapeOption::Geojson(shape.into())); 81 | self 82 | } 83 | 84 | build!(GeoShape); 85 | } 86 | 87 | // Required for GeoShape 88 | #[derive(Debug, Serialize)] 89 | pub struct Shape { 90 | #[serde(rename = "type")] 91 | shape_type: String, 92 | coordinates: Vec<(f64, f64)>, 93 | } 94 | 95 | impl Shape { 96 | pub fn new>(shape_type: A, coordinates: Vec<(f64, f64)>) -> Shape { 97 | Shape { 98 | shape_type: shape_type.into(), 99 | coordinates, 100 | } 101 | } 102 | } 103 | 104 | #[derive(Debug, Serialize)] 105 | pub struct IndexedShape { 106 | id: String, 107 | doc_type: String, 108 | index: String, 109 | path: String, 110 | } 111 | 112 | impl IndexedShape { 113 | pub fn new(id: A, doc_type: B, index: C, path: D) -> IndexedShape 114 | where 115 | A: Into, 116 | B: Into, 117 | C: Into, 118 | D: Into, 119 | { 120 | IndexedShape { 121 | id: id.into(), 122 | doc_type: doc_type.into(), 123 | index: index.into(), 124 | path: path.into(), 125 | } 126 | } 127 | } 128 | 129 | /// Geo Bounding Box Query 130 | #[derive(Debug, Serialize)] 131 | pub struct GeoBoundingBoxQuery(FieldBasedQuery); 132 | 133 | #[derive(Debug, Default, Serialize)] 134 | pub struct GeoBoundingBoxQueryInner { 135 | geo_box: GeoBox, 136 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 137 | coerce: Option, 138 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 139 | ignore_malformed: Option, 140 | #[serde(skip_serializing_if = "ShouldSkip::should_skip", rename = "type")] 141 | filter_type: Option, 142 | } 143 | 144 | impl Query { 145 | pub fn build_geo_bounding_box(field: A, geo_box: B) -> GeoBoundingBoxQuery 146 | where 147 | A: Into, 148 | B: Into, 149 | { 150 | GeoBoundingBoxQuery(FieldBasedQuery::new( 151 | field.into(), 152 | GeoBoundingBoxQueryInner { 153 | geo_box: geo_box.into(), 154 | ..Default::default() 155 | }, 156 | NoOuter, 157 | )) 158 | } 159 | } 160 | 161 | impl GeoBoundingBoxQuery { 162 | add_inner_field!(with_coerce, coerce, bool); 163 | add_inner_field!(with_ignore_malformed, ignore_malformed, bool); 164 | add_inner_field!(with_type, filter_type, Type); 165 | 166 | build!(GeoBoundingBox); 167 | } 168 | 169 | /// Geo Bounding Box filter type 170 | #[derive(Debug)] 171 | pub enum Type { 172 | Indexed, 173 | Memory, 174 | } 175 | 176 | impl Serialize for Type { 177 | fn serialize(&self, serializer: S) -> Result 178 | where 179 | S: Serializer, 180 | { 181 | use self::Type::*; 182 | match self { 183 | Indexed => "indexed", 184 | Memory => "memory", 185 | } 186 | .serialize(serializer) 187 | } 188 | } 189 | 190 | /// Geo Distance query 191 | /// 192 | /// TODO: Specific full unit test for querying with a generated query from here 193 | #[derive(Debug, Serialize)] 194 | pub struct GeoDistanceQuery(FieldBasedQuery); 195 | 196 | #[derive(Debug, Default)] 197 | struct GeoDistanceQueryOuter { 198 | distance: Distance, 199 | distance_type: Option, 200 | optimize_bbox: Option, 201 | coerce: Option, 202 | ignore_malformed: Option, 203 | } 204 | 205 | impl MergeSerialize for GeoDistanceQueryOuter { 206 | fn merge_serialize(&self, serializer: &mut S) -> Result<(), S::Error> 207 | where 208 | S: SerializeMap, 209 | { 210 | serializer.serialize_entry("distance", &self.distance)?; 211 | serialize_map_optional_kv(serializer, "distance_type", &self.distance_type)?; 212 | serialize_map_optional_kv(serializer, "optimize_bbox", &self.optimize_bbox)?; 213 | serialize_map_optional_kv(serializer, "coerce", &self.coerce)?; 214 | serialize_map_optional_kv(serializer, "ignore_malformed", &self.ignore_malformed)?; 215 | Ok(()) 216 | } 217 | } 218 | 219 | impl Query { 220 | pub fn build_geo_distance(field: A, location: B, distance: C) -> GeoDistanceQuery 221 | where 222 | A: Into, 223 | B: Into, 224 | C: Into, 225 | { 226 | let outer = GeoDistanceQueryOuter { 227 | distance: distance.into(), 228 | ..Default::default() 229 | }; 230 | GeoDistanceQuery(FieldBasedQuery::new(field.into(), location.into(), outer)) 231 | } 232 | } 233 | 234 | impl GeoDistanceQuery { 235 | add_outer_field!(with_distance_type, distance_type, DistanceType); 236 | add_outer_field!(with_optimize_bbox, optimize_bbox, OptimizeBbox); 237 | add_outer_field!(with_coerce, coerce, bool); 238 | add_outer_field!(with_ignore_malformed, ignore_malformed, bool); 239 | 240 | build!(GeoDistance); 241 | } 242 | 243 | /// Options for `optimize_bbox` 244 | #[derive(Debug)] 245 | pub enum OptimizeBbox { 246 | Memory, 247 | Indexed, 248 | None, 249 | } 250 | 251 | impl Serialize for OptimizeBbox { 252 | fn serialize(&self, serializer: S) -> Result 253 | where 254 | S: Serializer, 255 | { 256 | use self::OptimizeBbox::*; 257 | match self { 258 | Memory => "memory".serialize(serializer), 259 | Indexed => "indexed".serialize(serializer), 260 | None => "none".serialize(serializer), 261 | } 262 | } 263 | } 264 | 265 | /// Geo Polygon query 266 | #[derive(Debug, Serialize)] 267 | pub struct GeoPolygonQuery(FieldBasedQuery); 268 | 269 | #[derive(Debug, Default, Serialize)] 270 | pub struct GeoPolygonQueryInner { 271 | points: Vec, 272 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 273 | coerce: Option, 274 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 275 | ignore_malformed: Option, 276 | } 277 | 278 | impl Query { 279 | pub fn build_geo_polygon(field: A, points: B) -> GeoPolygonQuery 280 | where 281 | A: Into, 282 | B: Into>, 283 | { 284 | GeoPolygonQuery(FieldBasedQuery::new( 285 | field.into(), 286 | GeoPolygonQueryInner { 287 | points: points.into(), 288 | ..Default::default() 289 | }, 290 | NoOuter, 291 | )) 292 | } 293 | } 294 | 295 | impl GeoPolygonQuery { 296 | add_inner_field!(with_coerce, coerce, bool); 297 | add_inner_field!(with_ignore_malformed, ignore_malformed, bool); 298 | 299 | build!(GeoPolygon); 300 | } 301 | 302 | /// Geohash cell query 303 | #[derive(Debug, Serialize)] 304 | pub struct GeohashCellQuery(FieldBasedQuery); 305 | 306 | #[derive(Debug, Default)] 307 | pub struct GeohashCellQueryOuter { 308 | precision: Option, 309 | neighbors: Option, 310 | } 311 | 312 | impl MergeSerialize for GeohashCellQueryOuter { 313 | fn merge_serialize(&self, serializer: &mut S) -> Result<(), S::Error> 314 | where 315 | S: SerializeMap, 316 | { 317 | serialize_map_optional_kv(serializer, "precision", &self.precision)?; 318 | serialize_map_optional_kv(serializer, "neighbors", &self.neighbors)?; 319 | Ok(()) 320 | } 321 | } 322 | 323 | impl Query { 324 | pub fn build_geohash_cell(field: A, location: B) -> GeohashCellQuery 325 | where 326 | A: Into, 327 | B: Into, 328 | { 329 | GeohashCellQuery(FieldBasedQuery::new( 330 | field.into(), 331 | location.into(), 332 | Default::default(), 333 | )) 334 | } 335 | } 336 | 337 | impl GeohashCellQuery { 338 | add_outer_field!(with_precision, precision, Precision); 339 | add_outer_field!(with_neighbors, neighbors, bool); 340 | 341 | build!(GeohashCell); 342 | } 343 | 344 | #[derive(Debug)] 345 | pub enum Precision { 346 | Geohash(u64), 347 | Distance(Distance), 348 | } 349 | 350 | impl Default for Precision { 351 | fn default() -> Self { 352 | Precision::Distance(Default::default()) 353 | } 354 | } 355 | 356 | from!(u64, Precision, Geohash); 357 | from!(Distance, Precision, Distance); 358 | 359 | impl Serialize for Precision { 360 | fn serialize(&self, serializer: S) -> Result 361 | where 362 | S: Serializer, 363 | { 364 | use self::Precision::*; 365 | match self { 366 | Geohash(precision) => precision.serialize(serializer), 367 | Distance(ref dist) => dist.serialize(serializer), 368 | } 369 | } 370 | } 371 | 372 | #[cfg(test)] 373 | #[cfg(feature = "geo")] 374 | pub mod tests { 375 | use crate::operations::mapping::{Analysis, MappingOperation, Settings}; 376 | use crate::operations::search::SearchResult; 377 | use crate::query::Query; 378 | use crate::tests::{clean_db, make_client}; 379 | use crate::Client; 380 | use serde::{Deserialize, Serialize}; 381 | use std::collections::HashMap; 382 | 383 | #[derive(Debug, Serialize, Deserialize)] 384 | pub struct GeoTestDocument { 385 | pub str_field: String, 386 | pub geojson_field: geojson::Geometry, 387 | } 388 | 389 | impl Default for GeoTestDocument { 390 | fn default() -> GeoTestDocument { 391 | GeoTestDocument { 392 | str_field: "null island".to_owned(), 393 | geojson_field: geojson::Geometry::new(geojson::Value::Point(vec![0.0, 0.0])), 394 | } 395 | } 396 | } 397 | 398 | impl GeoTestDocument { 399 | pub fn with_str_field(mut self, s: &str) -> GeoTestDocument { 400 | self.str_field = s.to_owned(); 401 | self 402 | } 403 | 404 | pub fn with_point(mut self, p: Vec) -> GeoTestDocument { 405 | self.geojson_field = geojson::Geometry::new(geojson::Value::Point(p)); 406 | self 407 | } 408 | } 409 | 410 | pub fn setup_test_data(mut client: &mut Client, index_name: &str) { 411 | let mut mapping = HashMap::new(); 412 | let mut doc = HashMap::new(); 413 | let mut geo_field = HashMap::new(); 414 | let mut str_field = HashMap::new(); 415 | str_field.insert("type", "string"); 416 | geo_field.insert("type", "geo_shape"); 417 | doc.insert("str_field", str_field); 418 | doc.insert("geojson_field", geo_field); 419 | mapping.insert("geo_test_type", doc); 420 | 421 | let settings = Settings { 422 | number_of_shards: 1, 423 | analysis: Analysis { 424 | filter: serde_json::json!({}).as_object().unwrap().clone(), 425 | analyzer: serde_json::json!({}).as_object().unwrap().clone(), 426 | .. Default::default() 427 | }, 428 | }; 429 | 430 | // TODO - this fails in many cases (specifically on TravisCI), but we ignore the 431 | // failures anyway 432 | let _ = client.delete_index(index_name); 433 | 434 | let result = MappingOperation::new(&mut client, index_name) 435 | .with_mapping(&mapping) 436 | .with_settings(&settings) 437 | .send(); 438 | result.unwrap(); 439 | let documents = vec![ 440 | GeoTestDocument::default(), 441 | GeoTestDocument::default() 442 | .with_str_field("p1") 443 | .with_point(vec![1.0, 1.0]), 444 | GeoTestDocument::default() 445 | .with_str_field("p2") 446 | .with_point(vec![5.0, 1.0]), 447 | ]; 448 | for doc in documents.iter() { 449 | client 450 | .index(index_name, "geo_test_type") 451 | .with_doc(doc) 452 | .send() 453 | .unwrap(); 454 | } 455 | client.refresh().with_indexes(&[index_name]).send().unwrap(); 456 | } 457 | 458 | #[test] 459 | fn test_geoshape_search_point() { 460 | let index_name = "test_geoshape_search_point"; 461 | let mut client = make_client(); 462 | 463 | clean_db(&mut client, index_name); 464 | setup_test_data(&mut client, index_name); 465 | 466 | let all_results: SearchResult = client 467 | .search_query() 468 | .with_indexes(&[index_name]) 469 | .with_query( 470 | &Query::build_geo_shape("geojson_field") 471 | .with_geojson(geojson::Geometry::new(geojson::Value::Point(vec![ 472 | 0.0, 0.0, 473 | ]))) 474 | .build(), 475 | ) 476 | .send() 477 | .unwrap(); 478 | assert_eq!(1, all_results.hits.total); 479 | } 480 | 481 | #[test] 482 | fn test_geoshape_search_polygon() { 483 | let index_name = "test_geoshape_search_polygon"; 484 | let mut client = make_client(); 485 | 486 | clean_db(&mut client, index_name); 487 | setup_test_data(&mut client, index_name); 488 | 489 | let all_results: SearchResult = client 490 | .search_query() 491 | .with_indexes(&[index_name]) 492 | .with_query( 493 | &Query::build_geo_shape("geojson_field") 494 | .with_geojson(geojson::Geometry::new(geojson::Value::Polygon(vec![vec![ 495 | vec![1.0, 1.0], 496 | vec![1.0, -1.0], 497 | vec![-1.0, -1.0], 498 | vec![-1.0, 1.0], 499 | vec![1.0, 1.0], 500 | ]]))) 501 | .build(), 502 | ) 503 | .send() 504 | .unwrap(); 505 | assert_eq!(2, all_results.hits.total); 506 | } 507 | } 508 | -------------------------------------------------------------------------------- /src/units.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-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 | //! Various re-occuring types that are used by the ElasticSearch API. 18 | //! 19 | //! E.g. `Duration` 20 | //! 21 | //! This isn't all types. Types that are specific to one API are defined in the 22 | //! appropriate place, e.g. types only used by the Query DSL are in `query.rs` 23 | 24 | use std::collections::{BTreeMap, HashMap}; 25 | use std::fmt; 26 | 27 | use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; 28 | use serde_json::{Number, Value}; 29 | 30 | use crate::{error::EsError, operations::common::OptionVal}; 31 | 32 | /// The units by which duration is measured. 33 | /// 34 | /// TODO - this list is incomplete, see: https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units 35 | /// TODO - ensure deserialization works correctly 36 | #[derive(Debug, Serialize, Deserialize)] 37 | pub enum DurationUnit { 38 | Month, 39 | Week, 40 | Day, 41 | Hour, 42 | Minute, 43 | Second, 44 | Millisecond, 45 | } 46 | 47 | impl ToString for DurationUnit { 48 | fn to_string(&self) -> String { 49 | match *self { 50 | DurationUnit::Month => "M", 51 | DurationUnit::Week => "w", 52 | DurationUnit::Day => "d", 53 | DurationUnit::Hour => "h", 54 | DurationUnit::Minute => "m", 55 | DurationUnit::Second => "s", 56 | DurationUnit::Millisecond => "ms", 57 | } 58 | .to_owned() 59 | } 60 | } 61 | 62 | /// A time-period unit, will be formatted into the ElasticSearch standard format 63 | /// 64 | /// # Examples 65 | /// 66 | /// ``` 67 | /// use rs_es::units::{Duration, DurationUnit}; 68 | /// 69 | /// assert_eq!("100d", Duration::new(100, DurationUnit::Day).to_string()); 70 | /// ``` 71 | /// 72 | /// TODO - implement Deserialize correctly 73 | #[derive(Debug, Deserialize, Serialize)] 74 | pub struct Duration { 75 | amt: i64, 76 | unit: DurationUnit, 77 | } 78 | 79 | impl Duration { 80 | pub fn new(amt: i64, unit: DurationUnit) -> Self { 81 | Duration { amt, unit } 82 | } 83 | 84 | pub fn months(amt: i64) -> Self { 85 | Duration::new(amt, DurationUnit::Month) 86 | } 87 | 88 | pub fn weeks(amt: i64) -> Self { 89 | Duration::new(amt, DurationUnit::Week) 90 | } 91 | 92 | pub fn days(amt: i64) -> Self { 93 | Duration::new(amt, DurationUnit::Day) 94 | } 95 | 96 | pub fn hours(amt: i64) -> Self { 97 | Duration::new(amt, DurationUnit::Hour) 98 | } 99 | 100 | pub fn minutes(amt: i64) -> Self { 101 | Duration::new(amt, DurationUnit::Minute) 102 | } 103 | 104 | pub fn seconds(amt: i64) -> Self { 105 | Duration::new(amt, DurationUnit::Second) 106 | } 107 | 108 | pub fn milliseconds(amt: i64) -> Self { 109 | Duration::new(amt, DurationUnit::Millisecond) 110 | } 111 | } 112 | 113 | impl ToString for Duration { 114 | fn to_string(&self) -> String { 115 | format!("{}{}", self.amt, self.unit.to_string()) 116 | } 117 | } 118 | 119 | impl<'a> From<&'a Duration> for OptionVal { 120 | fn from(from: &'a Duration) -> OptionVal { 121 | OptionVal(from.to_string()) 122 | } 123 | } 124 | 125 | from_exp!(Duration, OptionVal, from, OptionVal(from.to_string())); 126 | 127 | /// Representing a geographic location 128 | #[derive(Debug)] 129 | pub enum Location { 130 | LatLon(f64, f64), 131 | GeoHash(String), 132 | } 133 | 134 | impl Default for Location { 135 | fn default() -> Location { 136 | Location::LatLon(0f64, 0f64) 137 | } 138 | } 139 | 140 | impl<'de> Deserialize<'de> for Location { 141 | fn deserialize(deserializer: D) -> Result 142 | where 143 | D: Deserializer<'de>, 144 | { 145 | // TODO - maybe use a specific struct? 146 | let mut raw_location = HashMap::::deserialize(deserializer)?; 147 | Ok(Location::LatLon( 148 | raw_location.remove("lat").unwrap(), 149 | raw_location.remove("lon").unwrap(), 150 | )) 151 | } 152 | } 153 | 154 | from_exp!((f64, f64), Location, from, Location::LatLon(from.0, from.1)); 155 | from!(String, Location, GeoHash); 156 | 157 | impl Serialize for Location { 158 | fn serialize(&self, serializer: S) -> Result 159 | where 160 | S: Serializer, 161 | { 162 | match self { 163 | Location::LatLon(lat, lon) => { 164 | let mut d = BTreeMap::new(); 165 | d.insert("lat", lat); 166 | d.insert("lon", lon); 167 | d.serialize(serializer) 168 | } 169 | Location::GeoHash(ref geo_hash) => geo_hash.serialize(serializer), 170 | } 171 | } 172 | } 173 | 174 | /// Representing a geographic box 175 | // TODO - this could probably refactored in a way that makes serialization easier 176 | #[derive(Debug)] 177 | pub enum GeoBox { 178 | Corners(Location, Location), 179 | Vertices(f64, f64, f64, f64), 180 | } 181 | 182 | impl Default for GeoBox { 183 | fn default() -> Self { 184 | GeoBox::Vertices(0f64, 0f64, 0f64, 0f64) 185 | } 186 | } 187 | 188 | impl<'de> Deserialize<'de> for GeoBox { 189 | fn deserialize(deserializer: D) -> Result 190 | where 191 | D: Deserializer<'de>, 192 | { 193 | // TODO - maybe use a specific struct? 194 | let mut raw_geo_box = HashMap::::deserialize(deserializer)?; 195 | Ok(GeoBox::Corners( 196 | raw_geo_box.remove("top_left").unwrap(), 197 | raw_geo_box.remove("bottom_right").unwrap(), 198 | )) 199 | } 200 | } 201 | 202 | from_exp!( 203 | (Location, Location), 204 | GeoBox, 205 | from, 206 | GeoBox::Corners(from.0, from.1) 207 | ); 208 | from_exp!( 209 | ((f64, f64), (f64, f64)), 210 | GeoBox, 211 | from, 212 | GeoBox::Corners( 213 | Location::LatLon({ from.0 }.0, { from.0 }.1), 214 | Location::LatLon({ from.1 }.0, { from.1 }.1) 215 | ) 216 | ); 217 | from_exp!( 218 | (f64, f64, f64, f64), 219 | GeoBox, 220 | from, 221 | GeoBox::Vertices(from.0, from.1, from.2, from.3) 222 | ); 223 | 224 | impl Serialize for GeoBox { 225 | fn serialize(&self, serializer: S) -> Result 226 | where 227 | S: Serializer, 228 | { 229 | use self::GeoBox::*; 230 | match self { 231 | Corners(ref top_left, ref bottom_right) => { 232 | let mut d = BTreeMap::new(); 233 | d.insert("top_left", top_left); 234 | d.insert("bottom_right", bottom_right); 235 | d.serialize(serializer) 236 | } 237 | Vertices(top, left, bottom, right) => { 238 | let mut d = BTreeMap::new(); 239 | d.insert("top", top); 240 | d.insert("left", left); 241 | d.insert("bottom", bottom); 242 | d.insert("right", right); 243 | d.serialize(serializer) 244 | } 245 | } 246 | } 247 | } 248 | 249 | /// A non-specific holder for an option which can either be a single thing, or 250 | /// multiple instances of that thing. 251 | #[derive(Debug, Deserialize)] 252 | pub enum OneOrMany { 253 | One(T), 254 | Many(Vec), 255 | } 256 | 257 | impl Default for OneOrMany { 258 | fn default() -> Self { 259 | OneOrMany::One(Default::default()) 260 | } 261 | } 262 | 263 | impl Serialize for OneOrMany 264 | where 265 | T: Serialize, 266 | { 267 | fn serialize(&self, serializer: S) -> Result 268 | where 269 | S: Serializer, 270 | { 271 | match self { 272 | OneOrMany::One(ref t) => t.serialize(serializer), 273 | OneOrMany::Many(ref t) => t.serialize(serializer), 274 | } 275 | } 276 | } 277 | 278 | impl From for OneOrMany { 279 | fn from(from: T) -> OneOrMany { 280 | OneOrMany::One(from) 281 | } 282 | } 283 | 284 | impl From> for OneOrMany { 285 | fn from(from: Vec) -> OneOrMany { 286 | OneOrMany::Many(from) 287 | } 288 | } 289 | 290 | /// DistanceType 291 | #[derive(Debug, Deserialize)] 292 | pub enum DistanceType { 293 | SloppyArc, 294 | Arc, 295 | Plane, 296 | } 297 | 298 | impl Serialize for DistanceType { 299 | fn serialize(&self, serializer: S) -> Result 300 | where 301 | S: Serializer, 302 | { 303 | match self { 304 | DistanceType::SloppyArc => "sloppy_arc", 305 | DistanceType::Arc => "arc", 306 | DistanceType::Plane => "plane", 307 | } 308 | .serialize(serializer) 309 | } 310 | } 311 | 312 | /// DistanceUnit 313 | #[derive(Debug, Deserialize)] 314 | pub enum DistanceUnit { 315 | Mile, 316 | Yard, 317 | Feet, 318 | Inch, 319 | Kilometer, 320 | Meter, 321 | Centimeter, 322 | Millimeter, 323 | NauticalMile, 324 | } 325 | 326 | impl Default for DistanceUnit { 327 | fn default() -> DistanceUnit { 328 | DistanceUnit::Kilometer 329 | } 330 | } 331 | 332 | impl ToString for DistanceUnit { 333 | fn to_string(&self) -> String { 334 | match *self { 335 | DistanceUnit::Mile => "mi", 336 | DistanceUnit::Yard => "yd", 337 | DistanceUnit::Feet => "ft", 338 | DistanceUnit::Inch => "in", 339 | DistanceUnit::Kilometer => "km", 340 | DistanceUnit::Meter => "m", 341 | DistanceUnit::Centimeter => "cm", 342 | DistanceUnit::Millimeter => "mm", 343 | DistanceUnit::NauticalMile => "NM", 344 | } 345 | .to_owned() 346 | } 347 | } 348 | 349 | impl Serialize for DistanceUnit { 350 | fn serialize(&self, serializer: S) -> Result 351 | where 352 | S: Serializer, 353 | { 354 | self.to_string().serialize(serializer) 355 | } 356 | } 357 | 358 | /// Distance, both an amount and a unit 359 | #[derive(Debug, Default, Deserialize)] 360 | pub struct Distance { 361 | amt: f64, 362 | unit: DistanceUnit, 363 | } 364 | 365 | impl Distance { 366 | pub fn new(amt: f64, unit: DistanceUnit) -> Self { 367 | Distance { amt, unit } 368 | } 369 | } 370 | 371 | impl Serialize for Distance { 372 | fn serialize(&self, serializer: S) -> Result 373 | where 374 | S: Serializer, 375 | { 376 | format!("{}{}", self.amt, self.unit.to_string()).serialize(serializer) 377 | } 378 | } 379 | 380 | /// A trait for types that can become JsonVals 381 | pub trait JsonPotential { 382 | fn to_json_val(&self) -> JsonVal; 383 | } 384 | 385 | macro_rules! json_potential { 386 | ($t:ty) => { 387 | impl JsonPotential for $t { 388 | fn to_json_val(&self) -> JsonVal { 389 | (*self).into() 390 | } 391 | } 392 | }; 393 | } 394 | 395 | impl<'a> JsonPotential for &'a str { 396 | fn to_json_val(&self) -> JsonVal { 397 | (*self).into() 398 | } 399 | } 400 | 401 | json_potential!(i64); 402 | json_potential!(i32); 403 | json_potential!(u64); 404 | json_potential!(u32); 405 | json_potential!(f64); 406 | json_potential!(f32); 407 | json_potential!(bool); 408 | 409 | /// A Json value that's not a structural thing - i.e. just String, i64 and f64, 410 | /// no array or object 411 | #[derive(Debug)] 412 | pub enum JsonVal { 413 | String(String), 414 | Number(Number), 415 | Boolean(bool), 416 | } 417 | 418 | impl JsonVal { 419 | pub fn from(from: &Value) -> Result { 420 | use serde_json::Value::*; 421 | Ok(match from { 422 | String(ref string) => JsonVal::String(string.clone()), 423 | Bool(b) => JsonVal::Boolean(*b), 424 | Number(ref i) => JsonVal::Number(i.clone()), 425 | _ => return Err(EsError::EsError(format!("Not a JsonVal: {:?}", from))), 426 | }) 427 | } 428 | } 429 | 430 | impl Default for JsonVal { 431 | fn default() -> Self { 432 | JsonVal::String(Default::default()) 433 | } 434 | } 435 | 436 | impl Serialize for JsonVal { 437 | fn serialize(&self, serializer: S) -> Result 438 | where 439 | S: Serializer, 440 | { 441 | match self { 442 | JsonVal::String(ref s) => s.serialize(serializer), 443 | JsonVal::Number(ref i) => i.serialize(serializer), 444 | JsonVal::Boolean(b) => b.serialize(serializer), 445 | } 446 | } 447 | } 448 | 449 | impl<'de> Deserialize<'de> for JsonVal { 450 | fn deserialize(deserializer: D) -> Result 451 | where 452 | D: Deserializer<'de>, 453 | { 454 | deserializer.deserialize_any(JsonValVisitor) 455 | } 456 | } 457 | 458 | struct JsonValVisitor; 459 | 460 | impl<'de> de::Visitor<'de> for JsonValVisitor { 461 | type Value = JsonVal; 462 | 463 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 464 | formatter.write_str("a json value") 465 | } 466 | 467 | fn visit_string(self, s: String) -> Result 468 | where 469 | E: de::Error, 470 | { 471 | Ok(JsonVal::String(s)) 472 | } 473 | 474 | fn visit_str(self, s: &str) -> Result 475 | where 476 | E: de::Error, 477 | { 478 | Ok(JsonVal::String(s.to_owned())) 479 | } 480 | 481 | fn visit_i64(self, i: i64) -> Result 482 | where 483 | E: de::Error, 484 | { 485 | Ok(JsonVal::Number(i.into())) 486 | } 487 | 488 | fn visit_u64(self, u: u64) -> Result 489 | where 490 | E: de::Error, 491 | { 492 | Ok(JsonVal::Number(u.into())) 493 | } 494 | 495 | fn visit_f64(self, f: f64) -> Result 496 | where 497 | E: de::Error, 498 | { 499 | Ok(JsonVal::Number( 500 | Number::from_f64(f).ok_or_else(|| de::Error::custom("not a float"))?, 501 | )) 502 | } 503 | 504 | fn visit_bool(self, b: bool) -> Result 505 | where 506 | E: de::Error, 507 | { 508 | Ok(JsonVal::Boolean(b)) 509 | } 510 | } 511 | 512 | // TODO - deprecated 513 | // impl ToJson for JsonVal { 514 | // fn to_json(&self) -> Json { 515 | // match self { 516 | // &JsonVal::String(ref str) => str.to_json(), 517 | // &JsonVal::I64(i) => Json::I64(i), 518 | // &JsonVal::U64(u) => Json::U64(u), 519 | // &JsonVal::F64(f) => Json::F64(f), 520 | // &JsonVal::Boolean(b) => Json::Boolean(b) 521 | // } 522 | // } 523 | // } 524 | 525 | from!(String, JsonVal, String); 526 | 527 | impl<'a> From<&'a str> for JsonVal { 528 | fn from(from: &'a str) -> JsonVal { 529 | JsonVal::String(from.to_owned()) 530 | } 531 | } 532 | 533 | from_exp!( 534 | f32, 535 | JsonVal, 536 | from, 537 | #[allow(clippy::cast_lossless)] 538 | JsonVal::Number(Number::from_f64(from as f64).unwrap()) 539 | ); 540 | from_exp!( 541 | f64, 542 | JsonVal, 543 | from, 544 | JsonVal::Number(Number::from_f64(from).unwrap()) 545 | ); 546 | from_exp!(i32, JsonVal, from, JsonVal::Number(from.into())); 547 | from_exp!(i64, JsonVal, from, JsonVal::Number(from.into())); 548 | from_exp!(u32, JsonVal, from, JsonVal::Number(from.into())); 549 | from_exp!(u64, JsonVal, from, JsonVal::Number(from.into())); 550 | from!(bool, JsonVal, Boolean); 551 | 552 | impl<'a> From<&'a Value> for JsonVal { 553 | fn from(from: &'a Value) -> Self { 554 | use serde_json::Value::*; 555 | match from { 556 | String(ref s) => JsonVal::String(s.clone()), 557 | Number(ref f) => JsonVal::Number(f.clone()), 558 | Bool(b) => JsonVal::Boolean(*b), 559 | _ => panic!("Not a String, F64, I64, U64 or Boolean"), 560 | } 561 | } 562 | } 563 | -------------------------------------------------------------------------------- /src/query/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-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 the ElasticSearch Query DSL. 18 | //! 19 | //! ElasticSearch offers a 20 | //! [rich DSL for searches](https://www.elastic.co/guide/en/elasticsearch/reference/1.x/query-dsl.html). 21 | //! It is JSON based, and therefore very easy to use and composable if using from a 22 | //! dynamic language (e.g. 23 | //! [Ruby](https://github.com/elastic/elasticsearch-ruby/tree/master/elasticsearch-dsl#features-overview)); 24 | //! but Rust, being a staticly-typed language, things are different. The `rs_es::query` 25 | //! module defines a set of builder objects which can be similarly composed to the same 26 | //! ends. 27 | //! 28 | //! For example: 29 | //! 30 | //! ```rust 31 | //! use rs_es::query::Query; 32 | //! 33 | //! let query = Query::build_bool() 34 | //! .with_must(vec![Query::build_term("field_a", 35 | //! "value").build(), 36 | //! Query::build_range("field_b") 37 | //! .with_gte(5) 38 | //! .with_lt(10) 39 | //! .build()]) 40 | //! .build(); 41 | //! ``` 42 | 43 | use std::collections::BTreeMap; 44 | 45 | use serde::ser::{SerializeMap, Serializer}; 46 | use serde::Serialize; 47 | 48 | use crate::{json::ShouldSkip, util::StrJoin}; 49 | 50 | #[macro_use] 51 | mod common; 52 | 53 | pub mod compound; 54 | pub mod full_text; 55 | pub mod functions; 56 | pub mod geo; 57 | pub mod joining; 58 | pub mod specialized; 59 | pub mod term; 60 | 61 | // Miscellaneous types required by queries go here 62 | 63 | // Enums 64 | 65 | /// Minimum should match - used in numerous queries 66 | /// TODO: should go somewhere specific 67 | #[derive(Debug)] 68 | pub struct CombinationMinimumShouldMatch { 69 | first: MinimumShouldMatch, 70 | second: MinimumShouldMatch, 71 | } 72 | 73 | impl CombinationMinimumShouldMatch { 74 | pub fn new(first: A, second: B) -> CombinationMinimumShouldMatch 75 | where 76 | A: Into, 77 | B: Into, 78 | { 79 | CombinationMinimumShouldMatch { 80 | first: first.into(), 81 | second: second.into(), 82 | } 83 | } 84 | } 85 | 86 | impl ToString for CombinationMinimumShouldMatch { 87 | fn to_string(&self) -> String { 88 | format!("{}<{}", self.first.to_string(), self.second.to_string()) 89 | } 90 | } 91 | 92 | impl Serialize for CombinationMinimumShouldMatch { 93 | fn serialize(&self, serializer: S) -> Result 94 | where 95 | S: Serializer, 96 | { 97 | self.to_string().serialize(serializer) 98 | } 99 | } 100 | 101 | #[derive(Debug)] 102 | pub enum MinimumShouldMatch { 103 | Integer(i64), 104 | Percentage(f64), 105 | Combination(Box), 106 | MultipleCombination(Vec), 107 | LowHigh(i64, i64), 108 | } 109 | 110 | from!(i64, MinimumShouldMatch, Integer); 111 | from!(f64, MinimumShouldMatch, Percentage); 112 | from_exp!( 113 | CombinationMinimumShouldMatch, 114 | MinimumShouldMatch, 115 | from, 116 | MinimumShouldMatch::Combination(Box::new(from)) 117 | ); 118 | from!( 119 | Vec, 120 | MinimumShouldMatch, 121 | MultipleCombination 122 | ); 123 | from_exp!( 124 | (i64, i64), 125 | MinimumShouldMatch, 126 | from, 127 | MinimumShouldMatch::LowHigh(from.0, from.1) 128 | ); 129 | 130 | impl ToString for MinimumShouldMatch { 131 | fn to_string(&self) -> String { 132 | match self { 133 | MinimumShouldMatch::Integer(val) => val.to_string(), 134 | MinimumShouldMatch::Percentage(val) => format!("{}%", val), 135 | _ => panic!("Can't convert {:?} to String", self), 136 | } 137 | } 138 | } 139 | 140 | impl Serialize for MinimumShouldMatch { 141 | fn serialize(&self, serializer: S) -> Result 142 | where 143 | S: Serializer, 144 | { 145 | match self { 146 | MinimumShouldMatch::Integer(val) => val.serialize(serializer), 147 | MinimumShouldMatch::Percentage(_) => self.to_string().serialize(serializer), 148 | MinimumShouldMatch::Combination(ref comb) => comb.serialize(serializer), 149 | MinimumShouldMatch::MultipleCombination(ref combs) => combs 150 | .iter() 151 | .map(ToString::to_string) 152 | .join(" ") 153 | .serialize(serializer), 154 | MinimumShouldMatch::LowHigh(low, high) => { 155 | let mut d = BTreeMap::new(); 156 | d.insert("low_freq", low); 157 | d.insert("high_freq", high); 158 | d.serialize(serializer) 159 | } 160 | } 161 | } 162 | } 163 | 164 | /// Fuzziness 165 | #[derive(Debug)] 166 | pub enum Fuzziness { 167 | Auto, 168 | LevenshteinDistance(i64), 169 | Proportionate(f64), 170 | } 171 | 172 | from!(i64, Fuzziness, LevenshteinDistance); 173 | from!(f64, Fuzziness, Proportionate); 174 | 175 | impl Serialize for Fuzziness { 176 | fn serialize(&self, serializer: S) -> Result 177 | where 178 | S: Serializer, 179 | { 180 | use self::Fuzziness::*; 181 | match self { 182 | Auto => "auto".serialize(serializer), 183 | LevenshteinDistance(dist) => dist.serialize(serializer), 184 | Proportionate(p) => p.serialize(serializer), 185 | } 186 | } 187 | } 188 | 189 | // Flags 190 | 191 | /// Flags - multiple operations can take a set of flags, each set is dependent 192 | /// on the operation in question, but they're all formatted to a similar looking 193 | /// String 194 | #[derive(Debug)] 195 | pub struct Flags(Vec) 196 | where 197 | A: AsRef; 198 | 199 | impl Serialize for Flags 200 | where 201 | A: AsRef, 202 | { 203 | fn serialize(&self, serializer: S) -> Result 204 | where 205 | S: Serializer, 206 | { 207 | self.0.iter().join("|").serialize(serializer) 208 | } 209 | } 210 | 211 | impl From> for Flags 212 | where 213 | A: AsRef, 214 | { 215 | fn from(from: Vec) -> Self { 216 | Flags(from) 217 | } 218 | } 219 | 220 | /// ScoreMode 221 | #[derive(Debug)] 222 | pub enum ScoreMode { 223 | Multiply, 224 | Sum, 225 | Avg, 226 | First, 227 | Max, 228 | Min, 229 | } 230 | 231 | impl Serialize for ScoreMode { 232 | fn serialize(&self, serializer: S) -> Result 233 | where 234 | S: Serializer, 235 | { 236 | match self { 237 | ScoreMode::Multiply => "multiply".serialize(serializer), 238 | ScoreMode::Sum => "sum".serialize(serializer), 239 | ScoreMode::Avg => "avg".serialize(serializer), 240 | ScoreMode::First => "first".serialize(serializer), 241 | ScoreMode::Max => "max".serialize(serializer), 242 | ScoreMode::Min => "min".serialize(serializer), 243 | } 244 | } 245 | } 246 | 247 | /// Query represents all available queries 248 | /// 249 | /// Each value is boxed as Queries can be recursive, they also vary 250 | /// significantly in size 251 | 252 | // TODO: Filters and Queries are merged, ensure all filters are included in this enum 253 | #[derive(Debug)] 254 | pub enum Query { 255 | MatchAll(Box), 256 | 257 | // Full-text queries 258 | Match(Box), 259 | MultiMatch(Box), 260 | Common(Box), 261 | QueryString(Box), 262 | SimpleQueryString(Box), 263 | 264 | // Term level queries 265 | Term(Box), 266 | Terms(Box), 267 | Range(Box), 268 | Exists(Box), 269 | // Not implementing the Missing query, as it's deprecated, use `must_not` and `Exists` 270 | // instead 271 | Prefix(Box), 272 | Wildcard(Box), 273 | Regexp(Box), 274 | Fuzzy(Box), 275 | Type(Box), 276 | Ids(Box), 277 | 278 | // Compound queries 279 | ConstantScore(Box), 280 | Bool(Box), 281 | DisMax(Box), 282 | FunctionScore(Box), 283 | Boosting(Box), 284 | Indices(Box), 285 | // Not implementing the And query, as it's deprecated, use `bool` instead. 286 | // Not implementing the Not query, as it's deprecated 287 | // Not implementing the Or query, as it's deprecated, use `bool` instead. 288 | // Not implementing the Filtered query, as it's deprecated. 289 | // Not implementing the Limit query, as it's deprecated. 290 | 291 | // Joining queries 292 | Nested(Box), 293 | HasChild(Box), 294 | HasParent(Box), 295 | 296 | // Geo queries 297 | GeoShape(Box), 298 | GeoBoundingBox(Box), 299 | GeoDistance(Box), 300 | // TODO: implement me - pending changes to range query 301 | //GeoDistanceRange(Box) 302 | GeoPolygon(Box), 303 | GeohashCell(Box), 304 | 305 | // Specialized queries 306 | MoreLikeThis(Box), 307 | // TODO: template queries 308 | // TODO: Search by script 309 | 310 | // Span queries 311 | // TODO: SpanTerm(Box), 312 | // TODO: Span multi term query 313 | // TODO: Span first query 314 | // TODO: Span near query 315 | // TODO: Span or query 316 | // TODO: Span not query 317 | // TODO: Span containing query 318 | // TODO: Span within query 319 | } 320 | 321 | impl Default for Query { 322 | fn default() -> Query { 323 | Query::MatchAll(Default::default()) 324 | } 325 | } 326 | 327 | impl Serialize for Query { 328 | fn serialize(&self, serializer: S) -> Result 329 | where 330 | S: Serializer, 331 | { 332 | use self::Query::*; 333 | 334 | let mut map_ser = serializer.serialize_map(Some(1))?; 335 | (match self { 336 | // All 337 | MatchAll(ref q) => map_ser.serialize_entry("match_all", q), 338 | 339 | // Full-text 340 | Match(ref q) => map_ser.serialize_entry("match", q), 341 | MultiMatch(ref q) => map_ser.serialize_entry("multi_match", q), 342 | Common(ref q) => map_ser.serialize_entry("common", q), 343 | QueryString(ref q) => map_ser.serialize_entry("query_string", q), 344 | SimpleQueryString(ref q) => map_ser.serialize_entry("simple_query_string", q), 345 | 346 | // Term 347 | Term(ref q) => map_ser.serialize_entry("term", q), 348 | Terms(ref q) => map_ser.serialize_entry("terms", q), 349 | Range(ref q) => map_ser.serialize_entry("range", q), 350 | Exists(ref q) => map_ser.serialize_entry("exists", q), 351 | Prefix(ref q) => map_ser.serialize_entry("prefix", q), 352 | Wildcard(ref q) => map_ser.serialize_entry("wildcard", q), 353 | Regexp(ref q) => map_ser.serialize_entry("regexp", q), 354 | Fuzzy(ref q) => map_ser.serialize_entry("fuzzy", q), 355 | Type(ref q) => map_ser.serialize_entry("type", q), 356 | Ids(ref q) => map_ser.serialize_entry("ids", q), 357 | 358 | // Compound 359 | ConstantScore(ref q) => map_ser.serialize_entry("constant_score", q), 360 | Bool(ref q) => map_ser.serialize_entry("bool", q), 361 | DisMax(ref q) => map_ser.serialize_entry("dis_max", q), 362 | FunctionScore(ref q) => map_ser.serialize_entry("function_score", q), 363 | Boosting(ref q) => map_ser.serialize_entry("boosting", q), 364 | Indices(ref q) => map_ser.serialize_entry("indices", q), 365 | 366 | // Joining 367 | Nested(ref q) => map_ser.serialize_entry("nested", q), 368 | HasChild(ref q) => map_ser.serialize_entry("has_child", q), 369 | HasParent(ref q) => map_ser.serialize_entry("has_parent", q), 370 | 371 | // Geo 372 | GeoShape(ref q) => map_ser.serialize_entry("geo_shape", q), 373 | GeoBoundingBox(ref q) => map_ser.serialize_entry("geo_bounding_box", q), 374 | GeoDistance(ref q) => map_ser.serialize_entry("geo_distance", q), 375 | GeoPolygon(ref q) => map_ser.serialize_entry("geo_polygon", q), 376 | GeohashCell(ref q) => map_ser.serialize_entry("geohash_cell", q), 377 | 378 | // Specialized 379 | MoreLikeThis(ref q) => map_ser.serialize_entry("more_like_this", q), 380 | })?; 381 | map_ser.end() 382 | } 383 | } 384 | 385 | // Specific query types go here 386 | 387 | /// Match all query 388 | 389 | #[derive(Debug, Default, Serialize)] 390 | pub struct MatchAllQuery { 391 | #[serde(skip_serializing_if = "ShouldSkip::should_skip")] 392 | boost: Option, 393 | } 394 | 395 | impl Query { 396 | pub fn build_match_all() -> MatchAllQuery { 397 | MatchAllQuery::default() 398 | } 399 | } 400 | 401 | impl MatchAllQuery { 402 | add_field!(with_boost, boost, f64); 403 | 404 | build!(MatchAll); 405 | } 406 | 407 | #[cfg(test)] 408 | mod tests { 409 | extern crate serde_json; 410 | 411 | use super::full_text::SimpleQueryStringFlags; 412 | use super::functions::Function; 413 | use super::term::TermsQueryLookup; 414 | use super::{Flags, Query}; 415 | 416 | #[test] 417 | fn test_simple_query_string_flags() { 418 | let opts = vec![SimpleQueryStringFlags::And, SimpleQueryStringFlags::Not]; 419 | let flags: Flags = opts.into(); 420 | let json = serde_json::to_string(&flags); 421 | assert_eq!("\"AND|NOT\"", json.unwrap()); 422 | } 423 | 424 | #[test] 425 | fn test_terms_query() { 426 | let terms_query = Query::build_terms("field_name") 427 | .with_values(vec!["a", "b", "c"]) 428 | .build(); 429 | assert_eq!( 430 | "{\"terms\":{\"field_name\":[\"a\",\"b\",\"c\"]}}", 431 | serde_json::to_string(&terms_query).unwrap() 432 | ); 433 | 434 | let terms_query_2 = Query::build_terms("field_name") 435 | .with_values(["a", "b", "c"].as_ref()) 436 | .build(); 437 | assert_eq!( 438 | "{\"terms\":{\"field_name\":[\"a\",\"b\",\"c\"]}}", 439 | serde_json::to_string(&terms_query_2).unwrap() 440 | ); 441 | 442 | let terms_query_3 = Query::build_terms("field_name") 443 | .with_values(TermsQueryLookup::new(123, "blah.de.blah").with_index("other")) 444 | .build(); 445 | assert_eq!("{\"terms\":{\"field_name\":{\"id\":123,\"index\":\"other\",\"path\":\"blah.de.blah\"}}}", 446 | serde_json::to_string(&terms_query_3).unwrap()); 447 | } 448 | 449 | #[test] 450 | fn test_function_score_query() { 451 | let function_score_query = Query::build_function_score() 452 | .with_function( 453 | Function::build_script_score("this_is_a_script") 454 | .with_lang("made_up") 455 | .add_param("A", 12) 456 | .build(), 457 | ) 458 | .build(); 459 | assert_eq!("{\"function_score\":{\"functions\":[{\"script_score\":{\"lang\":\"made_up\",\"params\":{\"A\":12},\"inline\":\"this_is_a_script\"}}]}}", 460 | serde_json::to_string(&function_score_query).unwrap()); 461 | } 462 | 463 | #[test] 464 | fn test_exists_query() { 465 | let exists_query = Query::build_exists("name").build(); 466 | assert_eq!( 467 | "{\"exists\":{\"field\":\"name\"}}", 468 | serde_json::to_string(&exists_query).unwrap() 469 | ); 470 | } 471 | } 472 | --------------------------------------------------------------------------------