In the previous recipe we read how to build an analyzer plugin that extends the query capabilities of Elasticsearch. In this recipe, we will see how to create one of the most common Elasticsearch plugins.
This kind of plugin allows the standard REST calls to be extended with custom ones to easily improve the capabilities of Elasticsearch.
In this recipe, we will see how to define a REST entry-point and create its action; in the next one, we'll see how to execute this action distributed in shards.
You need an up-and-running Elasticsearch installation as we described in the Downloading and installing Elasticsearch recipe in Chapter 2, Downloading and Setup.
A Maven tool, or an IDE that supports Java programming, such as Eclipse or IntelliJ IDEA. The code for this recipe is available in the chapter17/rest_plugin
directory.
To create a REST entry-point, we need to create the action and then register it in the plugin. We will perform the following steps:
simple
action (RestSimpleAction.java
):... public class RestSimpleAction extends BaseRestHandler { @Inject public RestSimpleAction(Settings settings, Client client, RestController controller) { super(settings); controller.registerHandler(POST, "/_simple", this); controller.registerHandler(POST, "/{index}/_simple", this); controller.registerHandler(POST, "/_simple/{field}", this); controller.registerHandler(GET, "/_simple", this); controller.registerHandler(GET, "/{index}/_simple", this); controller.registerHandler(GET, "/_simple/{field}", this); } @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { final SimpleRequest simpleRequest = new SimpleRequest(Strings.splitStringByCommaToArray (request.param("index"))); simpleRequest.setField(request.param("field")); return channel -> client.execute(SimpleAction.INSTANCE, simpleRequest, new RestBuilderListener<SimpleResponse> (channel){ @Override public RestResponse buildResponse(SimpleResponse simpleResponse, XContentBuilder builder) throws Exception { try { builder.startObject(); builder.field("ok", true); builder.array("terms", simpleResponse.getSimple().toArray()); builder.endObject(); } catch (Exception e) { onFailure(e); } return new BytesRestResponse(OK, builder); } }); } }
public class RestPlugin extends Plugin implements ActionPlugin { @Override public List<Class<? extends RestHandler>> getRestHandlers() { return singletonList(RestSimpleAction.class); } @Override public List<ActionHandler<? extends ActionRequest<?>, ? extends ActionResponse>> getActions() { return singletonList(new ActionHandler<> (SimpleAction.INSTANCE, TransportSimpleAction.class)); } }
mvn package
and manually install the ZIP. If we restart the Elasticsearch server, we should see the plugin loaded:[...][INFO ][o.e.n.Node ] [es] initializing ... [...][INFO ][o.e.e.NodeEnvironment ] [es] using [1] data paths, mounts [[/ (/dev/disk1)]], net usable_space [24.8gb], net total_space [930.7gb], spins? [unknown], types [hfs] [...][INFO ][o.e.e.NodeEnvironment ] [es] heap size [1.9gb], compressed ordinary object pointers [true] [...][INFO ][o.e.n.Node ] [es] version[5.0.2], pid[46225], build[f6b4951/2016-11-24T10:07:18.101Z], OS[Mac OS X/10.12.1/x86_64], JVM[Oracle Corporation/Java HotSpot(TM) 64- Bit Server VM/1.8.0_101/25.101-b13] [...][INFO ][o.e.p.PluginsService ] [es] loaded module [aggs-matrix-stats] [...][INFO ][o.e.p.PluginsService ] [es] loaded module [ingest-common] [...][INFO ][o.e.p.PluginsService ] [es] loaded module [lang-expression] [...][INFO ][o.e.p.PluginsService ] [es] loaded module [lang-groovy] [...][INFO ][o.e.p.PluginsService ] [es] loaded module [lang-mustache] [...][INFO ][o.e.p.PluginsService ] [es] loaded module [lang-painless] [...][INFO ][o.e.p.PluginsService ] [es] loaded module [percolator] [...][INFO ][o.e.p.PluginsService ] [es] loaded module [reindex] [...][INFO ][o.e.p.PluginsService ] [es] loaded module [transport-netty3] [...][INFO ][o.e.p.PluginsService ] [es] loaded module [transport-netty4] [...][INFO ][o.e.p.PluginsService ] [es] loaded plugin [simple-plugin] [...][INFO ][o.e.n.Node ] [es] initialized [...][INFO ][o.e.n.Node ] [es] starting ...
curl
:curl 'http://127.0.0.1:9200/_simple?field=mytest&pretty'
{ "ok" : true, "terms" : [ "mytest_[test-index][0]", "mytest_[test-index][3]", "mytest_[test-index][4]", "mytest_[test-index][1]", "mytest_[test-index][2]" ] }
Adding a REST action is very easy: We need to create a RestXXXAction
class that handles the calls.
The rest action is derived from the BaseRestHandler
class and needs to implement the handleRequest
method.
The constructor is very important. So let's start by writing:
@Inject public RestSimpleAction(Settings settings, RestController controller)
Its signature usually injects
via Guice (a lightweight dependency injection framework very popular in the Java ecosystem. See the library homepage for more details at https://code.google.com/p/google-guice/) with the following parameters:
Settings
, which can be used to load custom settings for your rest actionRestController
, which is used to register the REST action to the controllerIn the constructor of the REST action, the list of actions that must be handled is registered in the RestController
:
controller.registerHandler(POST, "/_simple", this); ...
To register an action, some parameters must be passed to the controller:
GET
/POST
/PUT
/DELETE
/HEAD
/OPTIONS
)RestHandler
, usually the same class, which must answer the callAfter having defined the constructor, if an action is fired, the class method prepareRequest
is called:
@Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
This method is the core of the REST action. It processes the request and sends back the result. The parameters passed to the method are:
RestRequest
: The REST request that hits the Elasticsearch serverRestChannel
: The channel used to send back the responseNodeClient
: The client used to communicate in the clusterThe returned value is a RestChannelConsumer
that is a FunctionalInterface
that accepts a RestChannel
- it's a simple Lambda.
A prepareRequest
method is usually composed of these phases:
In the preceding example, we created a SimpleRequest
processing the request:
final SimpleRequest simpleRequest = new SimpleRequest(Strings.splitStringByCommaToArray(request.param("index"))); simpleRequest.setField(request.param("field"));
As you can see, it accepts a list of indices (we split the classic comma-separated list of indices via the Strings.splitStringByCommaToArray
helper) and we had the field
parameter if available.
Now that we have a SimpleRequest
, we can send it to the cluster and get back SimpleResponse
via the Lambda closure:
return channel -> client.execute(SimpleAction.INSTANCE, simpleRequest, new RestBuilderListener<SimpleResponse>(channel){
client.execute
accepts an action, a request, and a RestBuilderListener
class that maps a future response. We can now process the response via the definition of a onResponse
method.
onResponse
receives a Response
object that must be converted in a JSON result.
@Override public RestResponse buildResponse(SimpleResponse simpleResponse, XContentBuilder builder) throws Exception {
The builder is the standard JSON XContentBuilder
that we have already seen in Chapter 14, Java Integration,
After having processed the cluster response and built the JSON, we can send the REST response.
return new BytesRestResponse(OK, builder);
Obviously, if something goes wrong during the JSON creation, an exception must be raised.
try {/* JSON building*/ } catch (Exception e) { onFailure(e); }
We will discuss SimpleRequest
in the next recipe.
3.145.202.27