Let's start the journey of extending Elasticsearch by creating a custom REST action. We've chosen this as the first extension, because we wanted to take the simplest approach as the introduction to extending Elasticsearch.
We assume that you already have a Java project created and that you are using Maven, just like we did in the Creating the Apache Maven project structure section in the beginning of this chapter. If you would like to use an already created and working example and start from there, please look at the code for Chapter 9, Developing Elasticsearch Plugins that is available with the book.
In order to illustrate how to develop a custom REST action, we need to have an idea of how it should work. Our REST action will be really simple—it should return names of all the nodes or names of the nodes that start with the given prefix if the prefix
parameter is passed to it. In addition to that, it should only be available when using the HTTP GET method, so POST requests, for example, shouldn't be allowed.
We will need to develop two Java classes:
BaseRestHandler
Elasticsearch abstract class from the org.elasticsearch.rest
package that will be responsible for handling the REST action code—we will call it a CustomRestAction
.AbstractPlugin
class from the org.elasticsearch.plugin
package—we will call it CustomRestActionPlugin
.In addition to the preceding two, we will need a simple text file that we will discuss after implementing the two mentioned Java classes.
The most interesting class is the one that will be used to handle the user's requests—we will call it CustomRestAction
. In order to work, it needs to extend the BaseRestHandler
class from the org.elasticsearch.rest
package—the base class for REST actions in Elasticsearch. In order to extend this class, we need to implement the handleRequest
method in which we will process the user request and a three argument constructor that will be used to initialize the base class and register the appropriate handler under which our REST action will be visible.
The whole code for the CustomRestAction
class looks as follows:
public class CustomRestAction extends BaseRestHandler { @Inject public CustomRestAction(Settings settings, RestController controller, Client client) { super(settings, controller, client); controller.registerHandler(Method.GET,"/_mastering/nodes", this); } @Override public void handleRequest(RestRequest request, RestChannel channel, Client client) { final String prefix = request.param("prefix", ""); client.admin().cluster().prepareNodesInfo().all().execute(new RestBuilderListener<NodesInfoResponse>(channel) { @Override public RestResponse buildResponse( NodesInfoResponse response, XContentBuilder builder) throws Exception { List<String> nodes = new ArrayList<String>(); for (NodeInfo nodeInfo : response.getNodes()) { String nodeName = nodeInfo.getNode().getName(); if (prefix.isEmpty()) { nodes.add(nodeName); } else if (nodeName.startsWith(prefix)) { nodes.add(nodeName); } } builder.startObject() .field("nodes", nodes) .endObject(); return new BytesRestResponse(RestStatus.OK, builder); } }); } }
For each custom REST class, Elasticsearch will pass three arguments when creating an object of such type: the Settings
type object, which holds the settings; the RestController
type object that we will use to bind our REST action to the REST endpoint; and the Client
type object, which is an Elasticsearch client and entry point for cooperation with it. All of these arguments are also required by the super class, so we invoke the base class constructor and pass them.
There is one more thing: the @Inject
annotation. It allows us to inform Elasticsearch that it should put the objects in the constructor during the object creation. For more information about it, please refer to the Javadoc of the mentioned annotation, which is available at https://github.com/elasticsearch/elasticsearch/blob/master/src/main/java/org/elasticsearch/common/inject/Inject.java.
Now, let's focus on the following code line:
controller.registerHandler(Method.GET, "/_mastering/nodes", this);
What it does is that it registers our custom REST action implementation and binds it to the endpoint of our choice. The first argument is the HTTP method type, the REST action will be able to work with. As we said earlier, we only want to respond to GET requests. If we would like to respond to multiple types of HTTP methods, we should just include multiple registerHandler
method invocations with each HTTP method. The second argument specifies the actual REST endpoint our custom action will be available at; in our case, it will available under the /_mastering/nodes
endpoint. The third argument tells Elasticsearch which class should be responsible for handling the defined endpoint; in our case, this is the class we are developing, thus we are passing this
.
Although the handleRequest
method is the longest one in our code, it is not complicated. We start by reading the request parameter with the following line of code:
String prefix = request.param("prefix", "");
We store the prefix request parameter in the variable called prefix
. By default, we want an empty String
object to be assigned to the prefix
variable if there is no prefix parameter passed to the request (the default value is defined by the second parameter of the param
method of the request
object).
Next, we retrieve the NodesInfoResponse
object using the Elasticsearch client object and its abilities to run administrative commands. In this case, we have used the possibility of sending queries to Elasticsearch in an asynchronous way. Instead of the call execute().actionGet()
part, which waits for a response and returns it, we have used the execute()
call, which takes a future object that will be informed when the query finishes. So, the rest of the method is in the buildResponse()
callback of the RestBuilderListener
object. The NodesInfoResponse
object will contain an array of NodeInfo
objects, which we will use to get node names. What we need to do is return all the node names that start with a given prefix or all if the prefix
parameter was not present in the request. In order to do this, we create a new array:
List<String> nodes = new ArrayList<String>();
We iterate over the available nodes using the following for
loop:
for (NodeInfo nodeInfo : response.getNodes())
We get the node name using the getName
method of the DiscoveryNode
object, which is returned after invoking the getNode
method of NodeInfo
:
String nodeName = nodeInfo.getNode().getName();
If prefix
is empty or if it starts with the given prefix, we add the name of the node to the array we've created. After we iterate through all the NodeInfo
objects, we call the are starting build the response and sent it through the HTTP.
The last thing regarding our CustomRestAction
class is the response handling, which is the responsibility of the last part of the buildResponse()
method that we created. It is simple because an appropriate response builder is already provided by Elasticsearch under the builder
argument. It takes into consideration the format
parameter used by the client in the call, so by default, we send the response in a proper JSON format just like Elasticsearch does and also take the YAML (http://en.wikipedia.org/wiki/YAML) format for free.
Now, we use the builder
object we got to start the response object (using the startObject
method) and start a nodes
field (because the value of the field is a collection, it will automatically be formatted as an array). The nodes
field is created inside the initial object, and we will use it to return matching nodes names. Finally, we close the object using the endObject
method.
After we have our object ready to be sent as a response, we return the BytesRestResponse
object. We do this in the following line:
return new BytesRestResponse(RestStatus.OK, builder);
As you can see, to create the object, we need to pass two parameters: RestStatus
and the XContentBuilder
, which holds our response. The RestStatus
class allows us to specify the response code, which is RestStatus.OK
in our case, because everything went smoothly.
The
CustomRestActionPlugin
class will hold the code that is used by Elasticsearch to initialize the plugin itself. It extends the AbstractPlugin
class from the org.elasticsearch.plugin
package. Because we are creating an extension, we are obliged to implement the following code parts:
onModule
method: This is the method that includes the code that will add our custom REST action so that Elasticsearch will know about itname
method: This is the name of our plugindescription
method: This is a short description of our pluginThe code of the whole class looks as follows:
public class CustomRestActionPlugin extends AbstractPlugin { @Inject public CustomRestActionPlugin(Settings settings) { } public void onModule(RestModule module) { module.addRestAction(CustomRestAction.class); } @Override public String name() { return "CustomRestActionPlugin"; } @Override public String description() { return "Custom REST action"; } }
The constructor, name
, and
description
methods are very simple, and we will just skip discussing them, and we will focus on the onModule
method. This method takes a single argument: the RestModule
class object, which is the class that allows us to register our custom REST action. Elasticsearch will call the onModule
method for all the modules that are available and eligible (all REST actions). What we do is just a simple call to the RestModule
addRestAction
method, passing in our CustomRestAction
class as an argument. That's all when it comes to Java development.
We have our code ready, but we need one additional thing; we need to let Elasticsearch know what the class registering our plugin is—the one we've called CustomRestActionPlugin
. In order to do this, we create an es-plugin.properties
file in the src/main/resources
directory with the following content:
plugin=pl.solr.rest.CustomRestActionPlugin
We just specify the plugin
property there, which should have a value of the class we use to register our plugins (the one that extends the Elasticsearch AbstractPlugin
class). This file will be included in the jar file that will be created during the build process and will be used by Elasticsearch during the plugin load process.
Of course, we could leave it now and say that we are done, but we won't. We would like to show you how to build each of the plugins, install it, and finally, test it to see whether it actually works. Let's start with building our plugin.
We start with the easiest part—building our plugin. In order to do this, we run a simple command:
mvn compile package
We tell Maven that we want the code to be compiled and packaged. After the command finishes, we can find the archive with the plugin in the target/release
directory (assuming you are using a project setup similar to the one we've described at the beginning of the chapter).
In order to install the plugin, we will use the plugin
command that is located in the bin
directory of the Elasticsearch distributable package. Assuming that we have our plugin archive stored in the /home/install/es/plugins
directory, we will run the following command (we run it from the Elasticsearch home directory):
bin/plugin --install rest --url file:/home/install/es/plugins/elasticsearch-rest-1.4.1.zip
We need to install the plugin on all the nodes in our cluster, because we want to be able to run our custom REST action on each Elasticsearch instance.
In order to learn more about installing Elasticsearch plugins, please refer to our previous book, Elasticsearch Server Second Edition, or check out the official Elasticsearch documentation at http://www.elasticsearch.org/guide/reference/modules/plugins/.
After we have the plugin installed, we need to restart our Elasticsearch instance we were making the installation on. After the restart, we should see something like this in the logs:
[2014-12-12 21:04:48,348][INFO ][plugins ] [Archer] loaded [CustomRestActionPlugin], sites []
As you can see, Elasticsearch informed us that the plugin named CustomRestActionPlugin
was loaded.
We can finally check whether the plugin works. In order to do that, we will run the following command:
curl -XGET 'localhost:9200/_mastering/nodes?pretty'
As a result, we should get all the nodes in the cluster, because we didn't provide the prefix
parameter and this is exactly what we've got from Elasticsearch:
{ "nodes" : [ "Archer" ] }
Because we only had one node in our Elasticsearch cluster, we've got the nodes
array with only a single
entry.
Now, let's test what will happen if we add the prefix=Are
parameter to our request. The exact command we've used was as follows:
curl -XGET 'localhost:9200/_mastering/nodes?prefix=Are&pretty'
The response from Elasticsearch was as follows:
{ "nodes" : [ ] }
As you can see, the nodes
array is empty, because we don't have any node in the cluster that would start with the Are
prefix. At the end, let's check another format of response:
curl -XGET 'localhost:9200/_mastering/nodes?pretty&format=yaml'
Now the response is not in a JSON format. Look at the example output for a cluster consisting of two nodes:
--- nodes: - "Atalon" - "Slapstick"
As we can see, our REST plugin is not so complicated but already has several features.