Currently, rs-es implements the most common Elasticsearch APIs, including searching and indexing. The implementation of other APIs is planned, as are other improvements, including to performance. RS-ES supports Elasticsearch versions 2.0 and up. Finally, it’s worth noting that neither rs-es nor elastic has support for asynchronous calls, though async support in planned for later versions of elastic

The answer is yes! There are actually two Rust Elasticsearch clients under active development, rs-es and elastic . In this tutorial, we will use RS-ES. While both libraries are idiomatic Elasticsearch, only RS-ES is also idiomatic Rust. For example, the type of an Elasticsearch document in rs-es is referred to as doc_type since type is a reserved keyword in Rust. By contrast, though documents in elastic are strongly-typed, queries are weakly-typed, meaning some errors are not caught until runtime.

Meanwhile, systems programming language Rust has been gaining more widespread use in production and its library ecosystem is growing. So, is it now possible to interact with the Elasticsearch API using a Rust client?

Previous tutorials have discussed how to use native clients in languages like Java and Python to interact with Elasticsearch via REST API.

Tutorial

For this post, we will be using hosted Elasticsearch on Qbox.io. You can sign up or launch your cluster here, or click “Get Started” in the header navigation. If you need help setting up, refer to “Provisioning a Qbox Elasticsearch Cluster.”

We also assume Rust is installed and configured on your system. If not, check out the Rust Install Guide for further instruction.

First, we add the rs-es dependency to our Cargo.toml. We specify 0.10.0, the most recent version:

[dependencies] rs-es = “0.10.0”

Now, in our main.rs, we create a function to make a Client using the Client struct. You need to set up one client in rs-es for each connection you have (connection pooling is not supported at this time).

Our make_client() function provides error handling in case the hostname does not exist or any other problems arise.

We must specify both hostname and port.

extern crate rs_es; use rs_es::Client; fn main() { pub fn make_client() -> Client { let client = match Client::new("YOUR_QBOX_HOSTNAME") { Ok(c) => c, Err(_) => panic!("Something went wrong!") }; client } let mut client = make_client(); }

Operations, or methods, on our Client correspond to Elasticsearch APIs. Calling an operation on a Client will return a builder-pattern object. Mandatory parameters must be passed to each operation, but optional parameters must be passed to the operation itself. For example, assuming operation is a builder-pattern object that supports custom routing, routing can be enabled with operation.with_routing(“username”) .

Let’s also create a custom post object which we will populate with custom fields and values so that we can return some meaningful, if minimal, results for our index and search queries. To do so, we need to use the Serde crate, or library, since it provides support for JSON serialization and deserialization. We’ll add the serde_json dependency to the dependencies section of our Cargo.toml:

serde_json = "1.0"

and the necessary imports to our main.rs:

#[macro_use] extern crate serde_json; use serde_json::Value;

Now, we’ll create a custom serde_json::Value object using Serde’s json! macro. While this step does allow for compile-checking of whether or not the values can be represented as JSON, for larger projects, you’ll want to serialize your own data structures so they can be strongly-typed. The details of how to do so are described thoroughly here and here. We will also show how to do so in a separate tutorial.

Our object has two fields, company and title, and corresponding values. Here’s how we can create it in main.rs:

// The type of `post` is `serde_json::Value` let post = json!({ "company": "qbox", "title": "Elasticsearch rest client" });

Example of the Features of RS-ES

Index a New Document

let result_wrapped = client .index("blog", "post") .with_doc(&post) .with_ttl("100d") .with_id("1") .send(); println!("INDEX RESULT: {:?}", result_wrapped);

Note that our document must implement serde’s Serialize trait in order to be indexed. In our case, because we created the Value object above, we know it implements Serialize. However, as mentioned before, in production, deriving Serialize for a custom serde object is a more robust solution.

.index() returns an IndexOperation which can be modified using with_ttl and with_id. send() returns an IndexResult .

Output:

INDEX RESULT: Ok(IndexResult { index: "blog", doc_type: "post", id: "1", version: 18, created: false })

Search the Document Using Query Params

For a value that exists:

let doc_a:SearchResult<Value> = client .search_uri() .with_indexes(&["blog"]) .with_query("qbox") .send() .unwrap(); println!("SUCCESSFUL SEARCH RESULT: {:?}", doc_a);

For a value that doesn’t exist:

let doc_b:SearchResult<Value> = client .search_uri() .with_indexes(&["blog"]) .with_query("trick") .send() .unwrap(); println!("FAILED SEARCH RESULT: {:?}", doc_b);

Output:

Result of successful search:

SUCCESSFUL SEARCH RESULT: SearchResult { took: 3, timed_out: false, shards: ShardCountResult { total: 4, successful: 4, failed: 0 }, hits: SearchHitsResult { total: 1, hits: [SearchHitsHitsResult { index: "blog", doc_type: "post", id: "1", score: Some(0.2876821), version: None, source: Some(Object({"company": String("qbox"), "title": String("Elasticsearch rest client")})), timestamp: None, routing: None, fields: None, highlight: None }] }, aggs: None, scroll_id: None }

Result of failed search:

FAILED SEARCH RESULT: SearchResult { took: 1, timed_out: false, shards: ShardCountResult { total: 4, successful: 4, failed: 0 }, hits: SearchHitsResult { total: 0, hits: [] }, aggs: None, scroll_id: None }

Search the Document Using QueryDSL

In order to use the QueryDSL, the Query module must be imported:

use rs_es::query::Query;

The Query must be composed using a single or set of builder objects.

For a value that exists:

let doc_c:SearchResult<Value> = client.search_query() .with_indexes(&["blog"]) .with_query(&Query::build_match("company", "qbox").build()) .send() .unwrap(); println!("SUCCESSFUL SEARCH: {:?}", doc_c);

For a value that doesn’t exist:

let doc_d:SearchResult<Value> = client.search_query() .with_indexes(&["blog"]) .with_query(&Query::build_match("company", "supergiant").build()) .send() .unwrap();

Output:

Result of successful search:

SUCCESSFUL SEARCH RESULT: SearchResult { took: 11, timed_out: false, shards: ShardCountResult { total: 4, successful: 4, failed: 0 }, hits: SearchHitsResult { total: 1, hits: [SearchHitsHitsResult { index: "blog", doc_type: "post", id: "1", score: Some(0.2876821), version: None, source: Some(Object({"company": String("qbox"), "title": String("Elasticsearch rest client")})), timestamp: None, routing: None, fields: None, highlight: None }] }, aggs: None, scroll_id: None }

Result of failed search:

FAILED SEARCH RESULT: SearchResult { took: 1, timed_out: false, shards: ShardCountResult { total: 4, successful: 4, failed: 0 }, hits: SearchHitsResult { total: 0, hits: [] }, aggs: None, scroll_id: None }

Conclusion

While rs-es may not be quite as mature as other Elasticsearch clients yet, it is well able to handle basic use cases, as well as some more advanced ones. In later tutorials, we’ll look into some of the latter, including scanning, scrolling, and aggregations.

Other Helpful Tutorials

Give It a Whirl!

It’s easy to spin up a standard hosted Elasticsearch cluster on any of our 47 Rackspace, Softlayer, or Amazon data centers. And you can now provision your own AWS Credits on Qbox Private Hosted Elasticsearch.

Questions? Drop us a note, and we’ll get you a prompt response.

Not yet enjoying the benefits of a hosted ELK-stack enterprise search on Qbox? We invite you to create an account today and discover how easy it is to manage and scale your Elasticsearch environment in our cloud hosting service.