Software Craftmanship

MasterClass : Comment sauvegarder des données et les retrouver sur ElasticSearch

Découvrez nos jobs
Vous ambitionnez de devenir Tech Lead ou de faire du conseil de haut-niveau ? Nous avons des challenges à votre hauteur !

Installation

Le code du projet est disponible sur le repository suivant:

https://github.com/patrick-hg/demo-spring-elasticsearch

Ajouter les dependances suivantes dans un projet Spring Boot.

<dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>

A la rédaction de cet article nous avions obtenons la version suivante du projet:
    spring-data-elasticsearch: 3.2.6

Nous ajouteront aussi Swagger pour appeler nos services, ainsi que OpenCSV qui nous aidera par la suite à parser un fichier de données au format CSV.

<dependency>             <groupId>io.springfox</groupId>             <artifactId>springfox-swagger2</artifactId>             <version>2.9.2</version> </dependency> <dependency>             <groupId>io.springfox</groupId>             <artifactId>springfox-swagger-ui</artifactId>             <version>2.9.2</version> </dependency> <dependency>             <groupId>com.opencsv</groupId>             <artifactId>opencsv</artifactId>             <version>4.6</version> </dependency>

Projet Spring Data Elasticsearch sur GitHub:

https://github.com/spring-projects/spring-data-elasticsearch

En se référant à la table si dessous, on télécharge la version correspondante d’Elasticsearch. Dans notre cas ce sera la 6.8.1

Spring Data ElasticsearchElasticsearch
3.2.x6.8.1
3.1.x6.2.2
3.0.x5.5.0
2.1.x2.4.0
2.0.x2.2.0
1.3.x1.5.2

Télécharger et démarrer Elasticsearch

https://www.elastic.co/downloads/past-releases/elasticsearch-6-8-1

Présentation d’Elasticsearch

Elasticsearch est une base de données documents, sous forme de simple texte JSON, orientée vers la recherche de données, notamment le full text search. Elasticsearch est écrite en Java et basée sur la technologie Apache Lucene.

Avec Elasticsearch nous avons aussi QueryDSL qui permet de requêter la base de donnée via une syntaxe Json.

Si on veut comparer avec les bases de données relationelles, une table est un index, un tuple dans une table est un document.

Elasticsearch fait partie de la stack elastic ELK, comptant plusieurs produits qui peuvent interagir ensemble.

Nous pouvons citer plusieurs outils connus, dont les suivantes:

 Kibana est une plateforme pour construire des graphiques utiles pour le monitoring d’applications. 
 Logstash est une plateforme de traitement les logs, et gestion d’évenements

Interragir avec elasticsearch via les commandes:

Retourner un document spécifique en utilisant son identifiantcurl -X GET <host>/<index>/<type>/<doc_id> ex: curl -X GET « localhost:9200/messages/message/NqdFMH0B3IoxXuQ8Ubht »
Supprimer un documentcurl -X DELETE localhost:9200/<index>/<type>/<documentID>
Supprimer un indexcurl -X DELETE localhost:9200/<index>
ex:       curl -X DELETE localhost:9200/users             curl -X DELETE localhost:9200/posts Cette commande supprime un index avec tout les documents qu’il contient.
Retourner le nombre de documents sur un indexcurl -X GET <host>/<index>/_count
Ex: curl -X GET « localhost:9200/users/_count?pretty »
Lister tout les documents sur in indexcurl -X GET <host>/<index>/_search
ex:   curl -X GET ‘localhost:9200/users/_search?pretty’ retournera une liste de tout les utilisateurs.
Retourne des informations sur le cluster Elasticsearchcurl -X GET http://localhost:9200/_cluster/stats?pretty

Le fait d’ajouter pretty à toute requêtte, rendra le retour (Json) plus facile a lire.

Requêter Elasticsearch via QueryDSL

Rechercher un utilisateur par son usernamecurl -X GET « localhost:9200/users/_search?pretty » -H ‘Content-Type: application/json’ -d’ {   « query »: {     « term »: {       « username »: « kim.lan »     }   } } ‘
Rechercher les utilisateurs qui ont moins de 25 ans  curl -X GET « localhost:9200/users/_search?pretty » -H ‘Content-Type: application/json’ -d’ {   « query »: {     « range »: {       « age »: {         « lt »: « 25 »       }     }   } } ‘
Rechercher les messages d’un utilisateurcurl -X GET « localhost:9200/messages/_search?pretty » -H ‘Content-Type: application/json’ -d’ {   « query »: {     « term »: {       « username »: « kim.lan »     }   } } ‘

Modèle

Dans notre exemple, on prendra le modele suivant:

Nous avons des utilisateur et des messages.

Un utilisateur “User” a un identifiant, un nom, un pseudo unique (username) et des données personnelles.

Un message “Message” a un identifiant, du contenu, des données de géolocalisation, etc…

Les utilisateurs publient des messages.

User

@Document(indexName = "users", type = "user") public class User {     @Id     private String id;     private String username;     private String name;     private int age;     private String nationality;     private Date memberSince;     protected User() {     }     public User(String username, String name, int age, String nationality, Date memberSince) {         this.username = username;         this.name = name;         this.age = age;         this.nationality = nationality;         this.memberSince = memberSince;     }     // Getters & setters ...

Message

@Document(indexName = "messages", type = "message") public class Message {     @Id     private String id;       private String content;     private List<String> tags;     private String localization;     private Boolean available;     @Field(type = FieldType.Date)     private String creationDate;     private String username;     private String language;     protected Message() {     }     public Message(String localization, String username, String content, String language, String creationDate) {         this.localization = localization;         this.username = username;         this.content = content;         this.creationDate = creationDate;         this.available = true;         this.language = language;         this.tags = Utils.tagsFromText(content);     }     // Getters & setters ...

Configuration

Application Properties

# Local Elasticsearch config spring.data.elasticsearch.repositories.enabled=true spring.data.elasticsearch.cluster-nodes=localhost:9300   # App config server.port=8102 spring.application.name=BootElastic

SwaggerConfig

@Configuration @EnableSwagger2 public class SwaggerConfig {     @Bean     public Docket api() {         return new Docket(DocumentationType.SWAGGER_2)                 .select()                 .apis(RequestHandlerSelectors.any())                 .paths(PathSelectors.any())                 .build();     } }

Repositories et Requêtes

UserRepository

@Repository public interface UserRepository extends ElasticsearchRepository<User, String> {     Optional<User> existsByUsername(String username); }

MessageRepository

@Repository public interface MessageRepository extends ElasticsearchRepository<Message, String> {    etc...
Page<Message> findByUsernameOrderByCreationDate(String name, Pageable pageable);

Ici Spring-data construira automatiquement la requete pour nous.

/* "query": {     "term": {         "username": "?0"     } }   */ @Query("{\"term\": {\"username\": \"?0\"}}") Page<Message> findByUsernameUsingCustomQuery(String name, Pageable pageable); // elasticsearch term query

Ici on déclare une requete QueryDSL pour elasticsearch.

On recherche les messages dont le username est égal à la valeur du paramètre.

/* "query": {     "bool": {         "should": [             {                 "match": {                     "content": "?0"                 }             },             {                 "match": {                     "username": "?0"                 }             },             {                 "match": {                     "localization": "?0"                 }             },             {                 "match": {                     "language": "?0"                 }             }         ]     } }  */ @Query("{\"bool\": {\"should\": [{\"match\": {\"content\": \"?0\"}}, {\"match\": {\"username\": \"?0\"}}, {\"match\": {\"localization\": \"?0\"}}, {\"match\": {\"language\": \"?0\"}}]}}") Page<Message> search(String text, Pageable pageable);  

Dans ce cas on recherche les messages qui devraient matcher avec les différentes conditions mais sans que ce soit stricte. Les documents avec le plus de matchs seront premiers parmis les résultats de recherche.

Services

UserService

@Service public class UserService {     @Autowired     private UserRepository userRepository;     public List<User> getAll() {         List<User> users = new ArrayList<>();         userRepository.findAll().forEach(users::add);         return users;     }     public Optional<User> findByUsername(String username) {         return userRepository.existsByUsername(username);     }     public void persistUser(User user) {         if (findByUsername(user.getUsername()).isEmpty()) {     // seulement s'il n'existe pas             userRepository.save(user);         }     }     public void persistAll(List<User> users) {         users.forEach(this::persistUser);     } }

MessageService

@Service public class MessageService {     @Autowired     private MessageRepository messageRepository;     public List<Message> getAll() {         List<Message> tweets = new ArrayList<>();         messageRepository.findAll().forEach(tweets::add);         return tweets;     }       public List<Message> findByUsername(String username, Integer pageNum, Integer pageSize) {         Page<Message> pageResult = messageRepository.findByUsernameOrderByCreationDate( username, pageableOf(pageNum, pageSize));         return pageResult != null ? pageResult.getContent() : Collections.emptyList();     }       public List<Message> findByUsernameUsingCustomQuery(String username, Integer pageNum, Integer pageSize) {         Page<Message> pageResult = messageRepository.findByUsernameUsingCustomQuery( username, pageableOf(pageNum, pageSize));         return pageResult != null ? pageResult.getContent() : Collections.emptyList();     }       public SearchResult search(String text, String from, String until, Integer pageNum, Integer pageSize) {         Page<Message> pageResult = Strings.isEmpty(from) && Strings.isEmpty(until)                 ? messageRepository.search(text, pageableOf(pageNum, pageSize))                 : messageRepository.searchWithDateRange(text, from, until, pageableOf(pageNum, pageSize));         return new SearchResult(pageResult.getContent(), pageResult.getTotalElements(), pageResult.getTotalPages(), pageResult.getNumberOfElements());     } }

Controllers

UserController

@RestController @RequestMapping("/user") public class UserController {     @Autowired     private UserService userService;     @GetMapping     public List<User> getAll() {         return userService.getAll();     }     @RequestMapping(value = "/new", method = RequestMethod.POST)     public void persist(@RequestBody User user) {         userService.persistUser(user);     } }

MessageController

@RestController @RequestMapping("/message") public class MessageController {     @Autowired     private MessageRepository messageRepository;     @Autowired     private MessageService messageService;     @GetMapping     public List<Message> getAll() {         return messageService.getAll();     }     @RequestMapping(value = "/new", method = RequestMethod.POST)     public Message persist(@RequestBody Message message) {         if (Strings.isEmpty(message.getId())) {             message.setCreationDate(LocalDate.now().toString());         }         message.setTags(Utils.tagsFromText(message.getContent()));         return messageRepository.save(message);     }     @GetMapping(value = "/find-by-username")     public List<Message> findByUsernameWithPagination(@RequestParam String username,                                                       @RequestParam(required = false) Integer pageNum,                                                       @RequestParam(required = false) Integer pageSize,                                                       @RequestParam Boolean useCustomQuery) {         if (useCustomQuery) {             return messageService.findByUsernameUsingCustomQuery(username, pageNum, pageSize);         }         return messageService.findByUsername(username, pageNum, pageSize);     }     @GetMapping(value = "/search")     public SearchResult search(@RequestParam String text,                                @RequestParam(required = false) Integer pageNum,                                @RequestParam(required = false) Integer pageSize,                                @RequestParam(required = false) String from,                                @RequestParam(required = false) String until) {         return messageService.search(text, from, until, pageNum, pageSize);     } }

Générer des Données de test

ImportFromCSV: On charge des utilisateurs et des messages depuis les deux fichiers de données users.csv et messages.csv

users.csv

username    ; name           ; age ; nationalite  ; membreDepuis jean.dupond ; Jean DUPOND    ; 23  ; FRANCAISE    ; 2019-05-12 john.doe    ; John DOE       ; 24  ; ANGLAISE     ; 2019-07-12 rick.martin ; Rick MARTIN    ; 25  ; ANGLAISE     ; 2020-04-12 paul.martin ; Paul MARTIN    ; 27  ; FRANCAISE    ; 2020-10-12
etc ...

messages.csv

localization; username      ; language ; content Paris       ; jean.dupond   ; ANGLAIS  ; this is a message text with #someHashtags London      ; john.doe      ; FRANCAIS ; Le @RCLens retrouve sa place New York    ; rick.martin   ; ANGLAIS  ; We just launched our #Kickstarter pre-launch Miami       ; paul.martin   ; ANGLAIS  ; XRP Toolkit v2 expands Xumm's feature set even Los Angeles ; aadesh.patel  ; ANGLAIS  ; I've been fooling around with a Kuwahara Bombay      ; marc.martin   ; ANGLAIS  ; Definitely the clearest, and most starter- London      ; marc.henry    ; ANGLAIS  ; Asynchronous RDBMS access with #Spring Data
etc ...

Sources:

0