Module Elasticsearch pour Play Framework 2

Publié le Mis à jour le

Play Framework et Elasticsearch sont deux outils récents très populaires. Afin d’intégrer facilement ces deux outils, nous utilisons dans nos projets un module pour Play Framework 2 que nous avons développé. Voyons ensemble comment fonctionne ce module.

Historique

Lors de différents projets développés sous Play Framework 2, nous avons eu besoin d’intégrer Elasticsearch. Aucun module n’existait permettant de rendre cette intégration simple, nous avons donc décidé de bâtir le notre.

Tout d’abord développé dans un dépôt privé, après quelques itérations nous avons obtenu un module suffisamment stable pour être utilisé par d’autres personnes et nous l’avons donc mis à disposition sous licence MIT sur le compte Github de Clever Age.

Dernièrement, une API dédiée à Scala a été mise en place autour de l’API initiale du module.
Nous allons présenter dans cet article les grands principes de fonctionnement de ce module.

Que fait le module ?

Ce module permet d’utiliser Elasticsearch soit:

  • en mode local : pour embarquer un serveur Elasticsearch ( très utile pour les tests )
  • en mode client : pour se connecter à un ( ou plusieurs ) noeuds Elasticsearch

Lors du démarrage de l’application Play, le module va instancier un “Client” qui permettra ensuite d’intérargir avec Elasticsearch.

On peut ensuite créer un (ou plusieurs) Index(s), Type(s) dont on peut initialiser les “settings” et les “mappings” si besoin.

Le module permet d’effectuer des opérations d’indexation, de lecture et de suppression sur un “Index”/“Type” donné.

Il permet principalement d’effectuer des recherches avec la possibilité de lui demander des facettes.
Nous avons également mis en place des méthodes pour faciliter l’utilisation des “Percolator”.

Installation

Comme tout module Play, il suffit de déclarer la dépendance dans le fichier Build.scala pour pouvoir récupérer et utiliser le module. Il est actuellement publié sur le dépôt communautaire de SBT, il faut donc également déclarer ce dépôt. Voici donc les dépendances à déclarer :

import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {
  val appName = "elasticsearch-sample"
  val appVersion = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // Add your project dependencies here,
    "com.clever-age" % "play2-elasticsearch" % "0.5.3"
  )

  val main = play.Project(appName, appVersion, appDependencies).settings(
    // Add your own project settings here
    resolvers += Resolver.url("play-plugin-releases", new URL("http://repo.scala-sbt.org/scalasbt/sbt-plugin-releases/"))(Resolver.ivyStylePatterns),
    resolvers += Resolver.url("play-plugin-snapshots", new URL("http://repo.scala-sbt.org/scalasbt/sbt-plugin-snapshots/"))(Resolver.ivyStylePatterns)
  )
}

Activation

Il faut ensuite activer le module en le définissant dans le fichier “conf/play.plugins”

11000:com.github.cleverage.elasticsearch.plugin.IndexPlugin

Configuration

Une fois la dépendance récupérée, différentes options peuvent être ajoutées dans le fichier application.conf ou dans un fichier de configuration dédié inclus dans ce dernier.

elasticsearch { # permet d'utiliser elasticsearch de manière "embarquée" à l'application
local: true

# liste des Nodes elasticsearch auxquels se connecter
client: "127.0.0.1:9300,127.0.0.1:9301"

# cluster.name elasticsearch auquel se connecter
cluster.name: "elasticsearch"

index {
# le(s) nom(s) d'index qui sera(ont) utilisé(s) dans l'application
name: play2-elasticsearch,log

# pattern définissant les classes "indexables"
clazzs: "indexing.*"

# mapping elasticsearch qui seront appliqués au démarrage de l'application
mappings: {
# la clé est le "type" elasticsearch et la valeur est le mapping
"indexTest": "{"indexTest":{"properties":{"category":{"type":"string","analyzer":"keyword"}}}"
}

# activation du log des requêtes (logs effectués au niveau "DEBUG")
show_request: true,
}

# paramètres additionnels qui seront appliqués sur l'index au démarrage
play2-elasticsearch.settings: "analysis: { analyzer: { team_name_analyzer: { type: "custom", tokenizer: "standard"  } } }"

}

Java

En Java, pour indexer et requêter Elasticsearch, il faut définir une classe qui correspond aux données que vous souhaitez indexer. Cette classe doit hériter de la classe « Index » définie dans le module et définir les méthodes toIndex() et fromIndex().

On définit le « type » Elasticsearch à l’aide de l’annotation @IndexType(). Optionnellement, on peut spécifier l’index utilisé à l’aide de l’annotation @IndexName (par défaut, le premier défini dans la configuration sera utilisé).

Ex :

@IndexType(name = "indexTest")
  public class IndexTest extends Index {
    public String name;

    // Finder used to request ElasticSearch
    public static Finder find = new Finder(IndexTest.class);

    @Override
    public Map toIndex() {
      Map map = new HashMap();
      map.put("name", name);
      return map;
    }

    @Override
    public Indexable fromIndex(Map map) {
      this.name = (String) map.get("name");
      return this;
    }
}

Lors de la création du Finder, il est possible également de cibler un index spécifique. Ex :

new Finder(IndexTest.class, "log");

Il est alors possible d’indexer et de requêter elasticsearch comme ceci :


IndexTest indexTest = new IndexTest();
indexTest.name = "hello World";
indexTest.index();

IndexTest byId = IndexTest.find.byId("1");

IndexResults all = IndexTest.find.all();

IndexQuery indexQuery = IndexTest.find.query();
indexQuery.setBuilder(QueryBuilders.queryString("hello"));
IndexResults results = IndexTest.find.search(indexQuery);

Toutes les possibilités offertes par l’API Elasticsearch peuvent être passées à l’object IndexQuery, ainsi que les FacetBuilder et SortBuilder.

Si vous souhaitez appliquer un mapping particulier sur un type donnée, il est possible de le faire à l’aide de l’annotation @IndexMapping. Ex :

@IndexType(name = "team")
@IndexMapping(value = "{players : { properties : { players : { type : "nested" }, name : {type : "string", analyzer : "team_name_analyzer" } } } }")
public class Team extends Index {
  public String name;
  public Date dateCreate;
  public String level;
  public Country country;
  public List players = new ArrayList();

  // Finder used to request ElasticSearch
  public static Finder find = new Finder(Team.class);
}

Scala

Pour utiliser le module en profitant pleinement de Scala, il est possible d’utiliser l’API définie dans le module ScalaHelpers. Les principes sont à peu près les mêmes, mais il est possible d’utiliser des case class pour définir les objets à indexer et les résultats de recherche seront accessibles sous forme de collections Scala permettant d’utiliser toutes les méthodes associées (map(), filter(), …).

Il vous faut donc tout d’abord créer une case class qui devra étendre le trait ScalaHelpers.Indexable :

case class IndexableTest(id: String, name: String, category: String) extends Indexable

Associé à cette classe, un object qui étend le trait ScalaHelpers.IndexableManager[T] permettra d’intéragir avec Elasticsearch. :


object IndexableTestManager extends IndexableManager[IndexableTest] {
import play.api.libs.json._

// Obligatoire : le type Elasticsearch à utiliser
val indexType = "indexableTest"
// Optionnel : le nom de l'index si on ne veut pas utiliser le premier déclarer dans la conf
// val index = "log"
val reads: Reads[IndexableTest] = Json.reads[IndexableTest]
val writes: Writes[IndexableTest] = Json.writes[IndexableTest]

Ce singleton doit définir des parsers JSON (Reads et Writes). Dans le cas classique, l’utilisation des macros Json.reads[T] & Json.writes[T] feront tout ce qu’il faut. Si un formattage JSON particulier est nécessaire, il faudra les définir manuellement.

Une fois ces éléments définis, il est possible d’intéragir avec l’index de cette manière :


IndexableTestManager.index(IndexableTest("1", "first name", "cateogory A"))
assert(IndexableTestManager.get("1") == IndexableTest("1", "first name", "cateogory A")

val indexQuery = IndexQuery[IndexableTest]().withBuilder(QueryBuilders.matchQuery("name", "first"))
//.withSize(...)
//.withFrom(...)
//.addFacet(...)
//.addSort(...)

val queryResults: IndexResults[IndexableTest] = IndexableTestManager.search(indexQuery)
println(queryResults.results.map(_.name).mkString(",")) val count = queryResults.totalCount

IndexableTestManager.delete("1")

Asynchrone

Toutes les actions (indexation, recherche, …) peuvent être éxécutées de manière non-bloquante. Les différentes API retournent alors des « Futures ». Il est alors possible de les utiliser dans des actions de controlleurs non-bloquantes. C’est le cas pour l’API Java et l’API Scala, il suffit d’utiliser les méthodes suffixés par « Async ».

Ex en Scala :


object Application extends Controller {
def async = Action {
IndexTestManager.index(IndexTest("1", "Here is the first name", "First category"))
IndexTestManager.index(IndexTest("2", "Then comes the second name", "First category"))
IndexTestManager.index(IndexTest("3", "Here is the third name", "Second category"))
IndexTestManager.index(IndexTest("4", "Finnaly is the fourth name", "Second category"))
}

IndexTestManager.refresh()

val indexQuery = IndexTestManager.query
.withBuilder(QueryBuilders.matchQuery("name", "Here"))
val indexQuery2 = IndexTestManager.query
.withBuilder(QueryBuilders.matchQuery("name", "third"))

// Combining futures
val l: Future[(IndexResults[IndexTest], IndexResults[IndexTest])] = for {
result1 <- IndexTestManager.searchAsync(indexQuery)
result2 <- IndexTestManager.searchAsync(indexQuery2)
yield (result1, result2)

Async {
l.map { case (r1, r2) =>
Ok(r1.totalCount + " - " + r2.totalCount)
}
}

}
}

Conclusion

Si vous avez besoin d’intégrer facilement Elasticsearch dans votre projet Playframework, n’hésitez pas à tester ce module. Vous le trouverez sur notre dépôt Github – Les tests sont éxécutés automatiquement à l’aide de Travis-CI.

Les futures versions devraient proposer les améliorations suivantes :

  • Gestion des documents Parent/Child de manière simple
  • Développement d’une application d’exemple permettant d’illustrer les différentes fonctionnalités
  • Amélioration de la documentation

Toutes les remontées et les Pull Requests sont les bienvenues !


  • Elasticsearch

  • Play