Vous ne connaissez-pas ElasticSearch ? Et si ce module était pourtant le meilleur candidat pour créer des moteurs de recherche pour trouver des instruments financiers ? J’ai eu l’occasion de le déployer dans le cadre de plusieurs missions professionnelles. Voici quelques astuces pour indexer et rechercher des “options” avec ElasticSearch et son client Java, d’accès bien plus robuste que son API Rest.
Faisons les présentations. ElasticSearch est un moteur de recherche open source basé sur Apache Lucene qui permet l’indexation de données sous forme de documents, leur recherche et leur analyse en temps réel. Il dispose de plusieurs clients qui vont de la simple API Rest aux Client Java ou .Net. On le retrouve fréquemment dans les architectures logicielles modernes à l’instar de la stack ELK (ElasticSearch, Logstash & Kibana), une solution très utilisée pour le monitoring de logs applicatif. Et pour cause, elle offre beaucoup d’avantages qui vont du système distribué à la scalabilité en passant par une vraie facilité de déploiement et une excellente visualisation.
A la recherche des options
Dans ma mission, l’un des besoins de mes utilisateurs finaux est de rechercher des options, ces contrats par lesquels une partie accorde à une autre le droit de lui acheter ou de lui vendre un actif à un prix déterminé. Les opérateurs font cette recherche appelée screening en fonction de caractéristiques particulières. Traditionnellement, cette opération est effectuée manuellement. Concrètement, l’opérateur consulte sur son écran la ou les options qui correspondent à sa recherche grâce à un système de filtres ou de tris. Il sélectionne ensuite un ensemble d’options qui rentrera dans le calcul d’indicateurs financiers quotidiens. Inutile de vous préciser que mes utilisateurs rêvent que cette recherche soit automatisée, non seulement pour limiter le risque d’erreur, mais aussi pour augmenter le nombre de recherches et industrialiser ainsi la création de ces indicateurs.
Une solution historique laborieuse…
La modélisation, forcément historique, des options est liée à celle des instruments financiers de la banque et représentée dans de multiples tables en base de données. La solution existante génère dynamiquement, à partir de critères, une requête comportant de nombreuses jointures pour interroger la base de données. Cette solution est très fortement liée à la représentation en base de données et exige des ajustements permanents pour suivre les évolutions du modèle. Elle souffre également de performances de plus en plus dégradées, liées au volume croissant d’instruments et de tables à interroger via des jointures coûteuses. Les index de la base ne sont, en effet, pas optimisés pour ces requêtes transversales.
… alors qu’il existe un moteur simple à installer
Une solution consiste à conserver dans le serveur l’ensemble des instruments, de les parcourir et de les filtrer. Cela nécessite de dimensionner le serveur pour qu’il conserve tous les instruments. Une autre repose sur la mise en place d’index inversés pour améliorer les performances en filtrant plus rapidement les instruments selon des caractéristiques. Toutes les deux se rapprochent en réalité de la création d’un moteur de recherche.
Dans le cadre de mes missions, ma solution a été d’utiliser un moteur existant, ElasticSearch, et de gérer mes besoins à travers son client Java. Mon choix a été influencé par les deux avantages d’ElasticSearch. La simplicité de sa mise en oeuvre, l’installation d’un cluster est simple et bien documentée, et l’utilisation du client Java qui évite de rajouter des dépendances dans mon application vers un client REST. Accessoirement cela me permet aussi d’utiliser directement l’API ElasticSearch.
De la création du client…
La création d’un client en Java peut se faire de deux manières. Soit notre application Java devient un noeud du cluster, soit seulement un client. Lors d’une mission, j’ai choisi de créer un client simple, pour les noeuds client se référant à NodeBuilder. Une connexion à ElasticSearch en Java donne alors ceci par exemple :
Settings settings = Settings.settingsBuilder().put(“cluster.name”, clusterNale).build(); TransportClient elasticClient = TransportClient.builder().settings(settings).build(); try { elasticClient = elasticClient.addTransportAdress(new InetSocketTransportAdress(InetAddress.getByName(clusterUrl), clusterPort)); } catch (UnknownHostException e) { // handle exception }
… à celle du mapping
D’une manière générale, je préfère aider ElasticSeach pour la création du mapping. Cela me permet de définir les champs qui ne seront requêtables qu’en valeur exacte (mot clé). Ici, par exemple, le type d’option CALL ou PUT. Je définis également le format de mes dates pour ne conserver que les jours, mois et années. Je crée même un second élément pour rechercher uniquement par mois/année, un format plus souple.
ElasticSearch indexe de la sorte des documents et je dé-normalise mes données en ajoutant de la duplication si nécessaire. Cette dé-normalisation est recommandée pour créer des documents et éviter les jointures. En réalité la création explicite du mapping permet à ElasticSearch d’indexer plus efficacement et rapidement des documents. Un point à vérifier évidemment, surtout si l’indexation est particulièrement lente ! Voici un exemple de création d’index ElasticSeach :
elasticClient.admin().indices().create( new CreateIndexRequest(“instruments”)).actionGet(); XContentBuilder source = createMapping(); elasticClient.admin() .indices() .preparePutMapping(“instruments”) .setType(“instr”) .setSource(source) .execute().actionGet();
Notez que les requêtes au cluster Elastic sont asynchrones par défaut. Il faut ici appeler actionGet pour attendre le retour et ordonner la création puis le mapping avant l’indexation. Voici un exemple de création mapping sous ElasticSeach :
public XContentBuilder createMapping() { return XContentFactory.jsonBuilder().prettyPrint() .startObject() .startObject(“instr”) // Nom du type .startObject(“properties”) .startObject(“name”) .field(“type”, “string”) .endObject() .startObject(“underlying_name”) .field(“type”, “string”) .endObject().startObject(“underlying_id”) .field(“type”, “long”) .endObject() .startObject(“underlying_isin”) // Code Instrument .field(“type”, “string”) // Recherche Exacte seulement .field(“index”, “not_analyzed”) .endObject() .startObject(“callPut”) // CALL ou PUT .field(“type”, “string”) // Recherche Exacte seulement .field(“index”, “not_analyzed”) .endObject() .startObject(“maturity”) .field(“type”, “date”) .field(“format”, “yyyy-MM-dd”) .startObject(“fields”) // denormalization .startObject(“maturity_ym”) .field(“type”, “string”) // Recherche exacte seulement .field(“index”, “not_analyzed”) .endObject() .endObject() .endObject() .endObject() .endObject() .endObject(); }
Le mapping string analyse la chaîne de caractères et permet des recherches partielles. J’ai créé ce mapping avec l’interface fluent XContentBuilder. Cette étape, assez verbeuse, construit un document qui représente le mapping. On peut relever dans cet exemple plusieurs points importants :
- le mode not_analysed permet de créer des keywords
- le champ date prend un ou plusieurs formats en paramètres
- pour faciliter les recherches sur un mois calendaire donné, un sous-type de maturity est créé en mode chaîne de caractères non analysée
L’insertion de données se fait soit à l’unité, soit en Bulk via les méthodes de prepareIndex. Les objets à indexer doivent être transformés en Map.
Recherche et résultat
Pour la recherche, je génère à présent dynamiquement la requête ElasticSearch à partir des paramètres. Voici par exemple la création d’une requête via ElasticSearchClient :
SearchResponse reponse = elasticClient.prepareSearch(“instruments”) .setTypes(“instr”) .setSearchType(“DFS_QUERY_THEN_FETCH”) .setQuery(QueryBuilders.queryStringQuery( “underlying_name:’AIR FRANCE’ AND maturity_ym:/2019-06.*/”)) .setFetchSource(new String[]{“*”}, null) // include, exclude .setFrom(0).setSize(100) // paging .setExplain(false) // may be used in dev .execute() .actionGet();
Il ne me reste qu’à lire la réponse. Elle pourra prendre cette forme :
SearchHits hits = reponse.getHits(); for (SearchHit hit : hits) { Map result = hit.getSource(); // Do something nice with result }
Le résultat est une map de clé valeur correspondant au document, sur plusieurs niveaux pour des structures arborescentes. Ce result est trouvé dans les hts de la réponse avec des informations sur la requête qu’ElasticSearch a réellement effectuée ainsi que sur les documents qu’il a matchés.
Et en plus c’est fluide
L’API ElasticSearch m’a ainsi permis de séparer le modèle de données entre la persistance et la recherche. La recherche est plus rapide qu’un ensemble de jointures et la volumétrie est gérée par ElasticSearch et son système de clusters. L’API du client permet une intégration fluide dans l’application Java. Le modèle de requête reste toutefois très proche de la construction d’un document request et peut nécessiter une abstraction supplémentaire avec les requêtes fonctionnelles de notre domaine.
Références : Elasticsearch 2.3.3, Java 8
Vos commentaires
je tenais à vous remercier pour ce contenu. je suis en effet etudiant en big data , je souhaiterais approfondir mes connaissances sur elasticsearch. j´ai eu une chance d’assister à un cours de 4h sur elasticsearch et j’ai vraiment été impressionner. je souhaite en savoir davantage.
merci de me fournir tous documents permettant de me fournir la pratique de ce open source.