Elasticsearch, Scala, Gradle. Writing plugin step-by-step

According to wiki

Elasticsearch is a search server based on Lucene. It provides a distributed, multitenant-capable full-text search engine with a RESTful web interface and schema-free JSON documents.

Elasticsearch developers made very good job not only to develop high quality search database but also to keep it out of new features which are not necessary and be focused on critical and generic functionality. At the same time they provide very good plugin system to extend elasticsearch for you needs when basic functionality is not enough for you. You can find a lot of different plugins on Github which monitor elasticsearch cluster, collect data from different sources like Twitter, RabbitMQ, MongoDB (so called “rivers”), Forsquare published on github plugin for custom geo-based scoring (written in Scala by the way) and many more. I’m going to describe how to build simple hello world plugin in Scala using Gradle, release it on Github and start to use it.

Install Environment

I will use IntelliJ IDE Community edition. It has built in support for Gradle. If you are not familliar with Gradle yet and don’t know why you should spend time on it, just read top 3 from Google search “Why Gradle”. I like it for most of its advantages, but I love it for flexibility given by Groovy (you can code tasks in your build script the same way using Rake for Ruby or FAKE for .NET) and easy to read syntax in comparison to old XML based build tools. We are going to develop elasticsearch plugin in Scala. There is a nice build tool for Scala called SBT. It’s definetelly tool to go on pure Scala projects, but usually we have already Java project (and got lucky to have Gradle as a build tool. if not, you may consider migration from Maven). Your team is suttisfied to use Java for mainstream development, but new plugin is more likely will be easier to write in functional style, because you need to do a custom text analysis or processing, and functional languages are proved suitable tool for that. You are most likely don’t think even to change build tool just for ~3% of you codebase. With Gradle it’s very easy to build your plugin in Scala. So make sure you have Scala, Gradle and elasticsearch installed and of course Java installed.

Building elasticsearch plugin with Gradle

Elasticsearch plugin is zip file which contains jar file with main plugin class and all dependencies in it. There is another option just _site folder with website content (see bigdesk pluging repository as example), but we will write real scala plugin which will integrate into elasticsearch and extend its functionality using provided Java API. Basic build process is the following: compile plugin code, run tests and archive with all necessary dependencies. Resulting archive is a valid plugin distribution.

Create Gradle project in IntelliJ or just create build.gradle file. Gradle build script with annotations:

apply plugin: 'scala' /* to build scala code.
    if you have mixed project with scala and java,
    you can add second line with java instead of scala */

sourceCompatibility = 1.6
version = '1.0'

repositories {
    mavenCentral()
    mavenLocal()
}
// additional configuration to tag dependencies to be archived with plugin jar
configurations {
    includeJars
}

dependencies {
    compile 'org.scala-lang:scala-library:2.11.4'
    compile 'org.elasticsearch:elasticsearch:1.5.2' // same the server version of your elasticsearch
    testCompile 'junit:junit:4.11'
    includeJars 'org.scala-lang:scala-library:2.11.4' // include this dependency
}
// task to archive plugin jars
task buildPluginZip(type: Zip, dependsOn:[':jar']) {
    baseName = 'hello-plugin'
    classifier = 'plugin'
    from files(libsDir) // include output dirictory into archive
    from { configurations.includeJars.collect { it } } // include dependencies to archive
}
// define artifacts
artifacts {
    archives buildPluginZip
}

Run Gradle build with the following console command:

gradle build buildPluginZip

As the result we will have a zip file ready to install to elasticsearch

bin\plugin --install hello-plugin --url=file://path_to_zip/hello-plugin.zip

Elasticsearch Hello plugin

1) Our first step will be to create the main class of the plugin extended from org.elasticsearch.plugins.AbstractPlugin, define name (line 7) and description (line 9) for your plugin. Since we want to build REST endpoint we have to import org.elasticsearch.rest._ and add method onModule(module:RestModule):Unit (line 11). Elasticsearch dependency injection is based on Google’s DI framework Guice, it will call this method and pass RestModule instance, so you can register your class which contains definition of REST action.

package hello.elasticsearch

import org.elasticsearch.plugins.AbstractPlugin
import org.elasticsearch.rest._

class HelloPlugin extends AbstractPlugin {
  override def name(): String = "hello-plugin"

  override def description(): String = "Hello plugin"

  def onModule(module: RestModule): Unit = {
    module.addRestAction(classOf[HelloAction])
  }
}

2) Create HelloAction.scala file with class inherited from BaseRestHandler. Annotation @Inject tells DI container to inject appropriate dependencies (line 9). We will define the same arguments we have to pass to base class constructor. Scala’s primary contractor which is basically body of the class looks pretty laconic and beautiful, doesn’t it?

3) We just need to register this class as a handler. We will use _ before the url to avoid possible conflicts with usage hello as index name. So the next line after class definition is controller.registerHandler(GET, "/_hello", this) (line 10).

4) HelloAction class is not going to be abstract, so we have to implement handleRequest(RestRequest, RestChannel, Client):Unit (line 11). Using RestRequest we obtain name query parameter, execute answer method and send response to RestChannel. answer(String):String method is implemented using awesome pattern matching.

package hello.elasticsearch
/* all imports */
class HelloAction @Inject() (settings: Settings, controller: RestController, client:Client)
    extends BaseRestHandler(settings, controller, client) {

  controller.registerHandler(GET, "/_hello", this)

  def handleRequest(request: RestRequest, channel: RestChannel, client: Client): Unit =
    channel.sendResponse(new BytesRestResponse(RestStatus.OK, answer(request.param("name"))))

  private def answer(who: String) = Option(who) match {
    case Some("Robert") => "Your Grace!"
    case None | Some("") => "I don't talk to strangers."
    case _ => "Hello, " + who + "!"
  }
}

5) Probably the most important step to make your plugin visible to elasticsearch is to add es-plugin.properties file to the resources directory with the following content:

plugin=hello.elasticsearch.HelloPlugin

If you forget to do so, even after successful installation of the plugin, elasticsearch will ignore your plugin and you may waste your time trying to figure out what’s wrong with your code.

Debugging elasticsearch plugin

We already have elasticsearch dependency in our project with full functional elasticsearch node. At the matter of fact one of the options to connect to elasticsearch cluster using Java API is to start embedded node instance inside your application. Thus debugging of your plugin is very easy. You just need to add run configuration with main class org.elasticsearch.bootstrap.ElasticsearchF. You can use VM options to change elasticsearch configuration. Any option in elasticsearch.yml file can be used with es. prefix. So for instance if you want to change cluster name to debug your plugin, add -Des.cluster.name=my-cluster to VM options when it’s just cluster.name in elasticsearch.yml file.

You can also add run task to your Gradle build script and run you app using gradle build run command.

task run(type: JavaExec, dependsOn: classes) {
    main = 'org.elasticsearch.bootstrap.ElasticsearchF'
    classpath sourceSets.main.runtimeClasspath
    classpath configurations.runtime
}

Define modules as alternative solution

Usually plugin is something more than just REST endpoint and it’s better to split our plugin on modules. First module can be our REST hello endpoint. The Hello module class must be inherited from org.elasticsearch.common.inject.AbstractModule and have overridden configure method to register the handler

def override configure():Unit = bind(classOf[HelloRestHandler]).asEagerSingleton

You can register your modules by overriding Collection<Class<? extends Module>> modules() java method of the plugin class. First of all we need to define the list of modules our plugin contains. We have to convert Scala list to Java list to satisfy Java interface. When you import scala.collection.JavaConverters conversion will happen implicitly, but according to Effective Scala book by Twitter it’s recommended to use explicit asJava method, aiding reader. Finally the method will look like:

def override modules() {
  List(
       classOf[HelloModule],
       classOf[UselessModule]
  ).asJava
}

Release!

Next step will be release our plugin to be able to install plugin using standard elasticsearch commnand. This command will look like ./bin/plugin --install mylifeecho/hello-plugin/0.0.1. Version number of course should be according Semantic Versioning, but keep in mind that your plugin builded for elasticsearch 1.3.x may not work on elasticsearch 1.4.x. In my case I had issue due to changes in interface of BaseRestHandler contructor between 1.3 and 1.4 versions. So you probably would like to have plugin version per minor elasticsearch version like these guys do. When you run ./bin/plugin --install command elasticsearch will try to access download.elastic.co first and than maven central in order to download your plugin. Follow OSSRH Guide to deploy plugin and take a look at OSSRH Gradle. After that you can install your plugin.

Restart elasticsearch after installation. Output will be similar to

[2015-05-31 21:34:56,276][INFO ][node    ] [Varys] version[1.5.2], pid[8572], build[62ff986/2015-04-27T09:21:06Z]
[2015-05-31 21:34:56,277][INFO ][node    ] [Varys] initializing ...
[2015-05-31 21:34:56,296][INFO ][plugins ] [Varys] loaded [hello-plugin], sites []
[2015-05-31 21:34:59,593][INFO ][node    ] [Varys] initialized
[2015-05-31 21:34:59,740][INFO ][node    ] [Varys] starting ...

You can find the source code of hello plugin on Github or install plugin with the command bin\plugin --install hello-plugin --url http://bit.ly/1ADC0bB.

In my next blog post we are going to add support of new script language into elasticsearch.

blog comments powered by Disqus