menu icon

Java pour Elasticsearch, épisode 2. Chercher des données

Mutualiser le code créé précédemment puis chercher des données sur des critères précis. Utiliser la notation lambda pour l'utilisation de la boîte à outils. Stocker les résutats dans des objets spécifiques.

Java pour Elasticsearch, épisode 2.  Chercher des données

Dans ce deuxième article de la série, nous allons voir comment mutualiser le code, utiliser les lambdas et faire des requêtes avec des critères précis.

Le code présenté est disponible sur Gitlab. La branche correspondante à cet article est la branche 02-enhancing-requesting.

Etape 1 : Mutualiser le code et l’améliorer

Dans l’article précédent, le client Elastic était défini dans la classe Indices.

Nous allons déporter cette création dans une classe de configuration, via bean dédié.

@Configuration  
public class ElasticClientBean {  

    @Value("${elastic.host}")  
    private String elasticHost;  

    @Value("${elastic.port}")  
    private int elasticPort;  

    @Value("${elastic.ca.fingerprint}")  
    private String fingerPrint;  

    @Value("${elastic.apikey}")
    private String apiKey;

    @Value("${elastic.scheme}")  
    private String elasticScheme;  

    @Bean  
    @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)  
    public ElasticsearchClient elasticClient() {  

        RestClient restClient = RestClient
                .builder(new HttpHost(elasticHost, elasticPort, elasticScheme))
                .setHttpClientConfigCallback(hccc -> hccc.setSSLContext(sslContext))
                .setDefaultHeaders(new Header[] {
                        new BasicHeader("Authorization", "ApiKey " + apiKey),
                })
                .build(); 

        ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());  

        return new ElasticsearchClient(transport);  

    }  

}

Au passage, le mode d’authentification a été modifié.

Au lieu d’utiliser les identifiants classiques pour la connexion, nous allons créer une clé d’API.

Le menu est accessible dans Kibana via Stack Management -> Security -> API keys.

Il faut commencer par créer la clé d’API en définissant les droits associés et la durée de validité si besoin.

alt text
Création de la clé d'API

La valeur de la clé n’est affichée qu’une seule fois au moment de sa création. **Attention : La valeur affichée ne correspond pas à la valeur copiée en cliquant sur l’icône de copie. **

alt text
Récupération de la valeur de la clé

Etape 2 : Créer le service de recherche

La première chose à faire dans ce service est d’initialiser le client Elasticsearch.

@Service  
public class OrderService {  

    private ElasticsearchClient elasticClient;  

    @Autowired  
    public void setElasticClient(ElasticsearchClient elasticClient) {  
        this.elasticClient = elasticClient;  
    }  

Dans ce service, nous allons définir deux méthodes :

  • Une méthode permettant de récupérer le nombre de commandes pour une adresse mail donnée.

  • Une méthode permettant d’obtenir le détail des commandes pour une adresse mail donnée.

On va donc commencer par définir une constante qui va contenir le nom du champ qui permettre de filtrer les résultats de recherche :

public static final String EMAIL_FIELD = "email";

public static final String INDEX_NAME = "kibana_sample_data_ecommerce";

Ensuite, la première méthode qui permet de compter les résultats.

public long findNumberOfOrdersForMail(String email) {  

    try {  

        CountResponse response = elasticClient.count(c -> c  
                .index(INDEX_NAME)  
                .query(q -> q  
                        .match(m -> m  
                                .field(EMAIL_FIELD)  
                                .query(email))));  

        return response.count();  
    } catch (IOException e) {  
        throw new RuntimeException(e);  
    }  

}

On peut voir que pour chaque d’opération, il existe un type de réponse dédié.

La CountResponse indique donc clairement que nous n’allons pas ramener d’autres données qu’un nombre de document.

La requête est faite en utilisant une notation lambda.

La méthode de recherche utilisée est une des plus simples : un match, qui va aller chercher une valeur précise (que ce soit en terme de casse ou de contenu) dans un champ précis de l’index.

On peut alors créer notre classe de test pour vérifier que les résultat renvoyé correspond aux attentes.

@SpringBootTest  
class OrderTest {  

    @Autowired  
    OrderService orderService;  

    @Test  
    void countOrdersByEmail() {  

        long count = orderService.findNumberOfOrdersForMail("mary@bailey-family.zzz");  
        assertNotEquals(0, count);  
        assertEquals(3, count);  

    }

}

Dans le Dev Tools, la requête correspondante est la suivante :

GET kibana_sample_data_ecommerce/_count
{
  "query": {
    "match": {
      "email": "mary@bailey-family.zzz"
    }
  }
}

La réponse doit alors être :

{
  "count": 3,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  }
}

Etape 3 : Travailler avec des documents

Maintenant que nous savons compter des documents, nous allons pouvoir en récupérer pour les exploiter.

public List<Object> listOrdersByEmail(String email) {  

    try {  
        SearchResponse<Object> response = elasticClient.search(s -> s  
                .index(INDEX_NAME)  
                        .query(q -> q  
                                .match(m -> m  
                                        .field(EMAIL_FIELD)  
                                        .query(email)))  
                , Object.class);  

        if (response.hits().total() == null || response.hits().total().value() == 0) {  
            return new ArrayList<>();  
        }  

        return new ArrayList<>(response.hits().hits());  

    } catch (IOException e) {  
        throw new RuntimeException(e);  
    }  

}

Le type de la réponse passe donc à SearchResponse.

Avec ce code, on récupère un liste d’objets, mais il est possible de spécialiser le code pour récupérer un type d’objet précis qui sera désérialisé grâce à Jackson.

Il faut commencer par définir l’objet :

public class Order {  

    @JsonProperty("currency")  
    private String currency;  

    @JsonProperty("customer_first_name")  
    private String customerFirstName;  

    @JsonProperty("customer_last_name")  
    private String customerLastName;  

    @JsonProperty("customer_full_name")  
    private String customerFullName;  

    @JsonProperty("email")  
    private String email;  

    @JsonProperty("order_date")  
    private ZonedDateTime orderDate;  

    @JsonProperty("taxful_total_price")  
    private double taxfulTotalPrice;  

    @JsonProperty("taxless_total_price")  
    private double taxlessTotalPrice;  

    public Order(String currency, String customerFirstName, String customerLastName, String customerFullName,  
                 String email, ZonedDateTime orderDate, double taxfulTotalPrice, double taxlessTotalPrice) {  
        this.currency = currency;  
        this.customerFirstName = customerFirstName;  
        this.customerLastName = customerLastName;  
        this.customerFullName = customerFullName;  
        this.email = email;  
        this.orderDate = orderDate;  
        this.taxfulTotalPrice = taxfulTotalPrice;  
        this.taxlessTotalPrice = taxlessTotalPrice;  
    }  

    public Order() {  
    }

    @Override
    public String toString() {
        return "Order{" +
                "currency='" + currency + '\'' +
                ", customerFirstName='" + customerFirstName + '\'' +
                ", customerLastName='" + customerLastName + '\'' +
                ", customerFullName='" + customerFullName + '\'' +
                ", email='" + email + '\'' +
                ", orderDate=" + orderDate +
                ", taxfulTotalPrice=" + taxfulTotalPrice +
                ", taxlessTotalPrice=" + taxlessTotalPrice +
                '}';
    }

    /* Getters and setters */

J’attire l’attention sur la définition de orderDate.

Si on regarde le champ dans le Dev Tools, on peut voir que la date est à un format particulier :

2024-03-03T21:59:02+00:00

Il s’agit d’un format zoné, le type de la date doit donc intégrer cette particularité et être ZonedDateTime.

Pour que la désérialisation se passe bien, il faut modifier la création du client Elasticsearch et paramétrer le JsonpMapper. La ligne

        ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());  

Devient alors

        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
        mapper.registerModule(new JavaTimeModule());
        JacksonJsonpMapper jacksonJsonpMapper = new JacksonJsonpMapper(mapper);

        ElasticsearchTransport transport = new RestClientTransport(restClient, jacksonJsonpMapper);

Il faut alors modifier la requête pour la spécialiser :

public List<Hit<Order>> listOrdersByEmail(String email) {  

    try {  
        SearchResponse<Order> response = elasticClient.search(s -> s  
                .index(INDEX_NAME)  
                        .query(q -> q  
                                .match(m -> m  
                                        .field(EMAIL_FIELD)  
                                        .query(email)))  
                , Order.class);  

        if (response.hits().total() == null || response.hits().total().value() == 0) {  
            return new ArrayList<>();  
        }  

        return response.hits().hits();  

    } catch (IOException e) {  
        throw new RuntimeException(e);  
    }  

}

Il reste donc à écrire la méthode de test correspondante :

@Test  
void listOrdersByEmail() {  

    List<Hit<Order>> orders = orderService.listOrdersByEmail("mary@bailey-family.zzz");  
    assertNotNull(orders);  
    assertEquals(3, orders.size());  
    for (Hit<Order> hit : orders) {
        System.out.println(hit.source());
    }

}

On peut voir que pour accéder à l’objet “Order”, il faut utiliser la méthode “source()” de l’objet Hit.

Java pour Elasticsearch, épisode 1. Requêter le cluster

18/01/2024

Découvrir comment intégrer Elasticsearch dans votre code est une aventure passionnante et, en réalité, plus simple qu'il n'y paraît. Dans ce premier article de notre série, nous allons explorer ensemble la mise en place d'un cluster de trois nœuds et la manière de s'y connecter en utilisant des certificats auto-générés, démontrant ainsi l'accessibilité et l'efficacité de ce processus.

Lire l'article

Retour de la conférence DevFest Toulouse

19/11/2023

Nous sommes de retour du DevFest Toulouse, occasion pour nous d'assister à plusieurs conférences, nous former et partager une version personnalisée de notre presentation Cloner ChatGPT avec Hugging Face et Elasticsearch.

Lire l'article

Plonger dans le NLP avec Elastic

01/04/2023

Un aperçu sur le NLP et un guide pratique sur la façon dont il peut être utilisé avec la Suite Elastic pour améliorer les capacités de recherche.

Lire l'article

Assurer la scalabilité d’un moteur de recherche pour des milliers de magasins en ligne – retour sur la conférence ElasticON

10/03/2023

Retour sur la présentation Assurer la scalabilité d’un moteur de recherche pour des milliers de magasins en ligne par Roudy Khoury et Aline Paponaud à ElasticON 2023

Lire l'article

Question answering, une approche plus humaine à nos recherches sur all.site.

19/01/2023

Tout sur les Question-Answering et comment l'implémenter en utilisant flask et elasticsearch.

Lire l'article

Retour d’Expérience - Fine-tuning d’un modèle VOSK

05/01/2022

all.site est un moteur de recherche collaboratif. Il fonctionne comme Bing ou Google mais il a l’avantage de pouvoir aller plus loin en indexant par exemple les contenus média et en organisant les données de systèmes comme Slack, Confluence ou l’ensemble des informations présentes dans l’intranet d’une entreprise.

Lire l'article

Retour d’Expérience - Indexation des transcriptions de fichiers média

17/12/2021

all.site est un moteur de recherche collaboratif. Il fonctionne comme Bing ou Google mais il a l’avantage de pouvoir aller plus loin en indexant par exemple les contenus média et en organisant les données de systèmes comme Slack, Confluence ou l’ensemble des informations présentes dans l’intranet d’une entreprise.

Lire l'article

La revue de presse du 25 Novembre 2021

25/11/2021

Bientôt le weekend, bientôt l'hiver, alors une petite revue de presse pour occuper vos longues soirées...

Lire l'article

Nouveau meetup Search & Data - E-Commerce Search et Open Source

28/10/2021

La cinquième édition du meetup Search and Data est dédiée au search e-commerce et à l'open source. Un bel agenda pour cette édition de rentrée et de reprise.

Lire l'article

Expédition vers Synonym Graph dans Elasticsearch

21/04/2021

Dans cet article, nous expliquons comment nous sommes passés des anciens filtres de synonymes d'Elasticsearch aux nouveaux filtres de type graphe, les Synonym Graph Token Filter.

Lire l'article

Quand les requêtes sont très verbeuses

22/02/2021

Dans cet article, nous présentons une méthode simple pour réécrire les requêtes utilisateurs afin qu'un moteur de recherche basé sur des mots clés puisse mieux les comprendre. Cette méthode est très utile dans le contexte d'une recherche vocale ou une conversation avec un chatbot, contexte dans lequel les requêtes utilisateur sont généralement plus verbeuses.

Lire l'article

Enrichir les données et réécrire les requêtes avec le percolator Elasticsearch

26/04/2019

Cet article est une transcription de notre intervention cette semaine à Haystack - une conférence sur l'amélioration de la pertinence des moteurs de recherche. Nous avons montré une méthode permettant d'enrichir et de réécrire les requêtes des utilisateurs en utilisant Wikidata et le percolator Elasticsearch.

Lire l'article

A2 le moteur qui sublime Elasticsearch

13/06/2018

Elasticsearch est une technologie ouverte qui permet aux intégrateurs de construire des solutions toujours plus innovantes et puissantes.

Lire l'article