Une simple implémentation de marquage avec Elasticsearch

Une mise en œuvre simple du marquage avec Elasticsearch

1. Vue d'ensemble

Le balisage est un modèle de conception courant qui nous permet de catégoriser et de filtrer les éléments de notre modèle de données.

Dans cet article, nous allons mettre en œuvre le balisage à l'aide de Spring et Elasticsearch. Nous utiliserons à la fois Spring Data et l'API Elasticsearch.

Tout d'abord, nous n'allons pas couvrir les bases de l'obtention d'Elasticsearch et de Spring Data - vous pouvez explorer ceshere.

2. Ajout de balises

The simplest implementation of tagging is an array of strings. Nous pouvons implémenter ceci en ajoutant un nouveau champ à notre modèle de données comme ceci:

@Document(indexName = "blog", type = "article")
public class Article {

    // ...

    @Field(type = Keyword)
    private String[] tags;

    // ...
}

Notez l'utilisation du type de champKeyword . Nous voulons seulement que les correspondances exactes de nos balises filtrent un résultat. Cela nous permet d'utiliser des balises similaires mais séparées commeelasticsearchIsAwesome etelasticsearchIsTerrible.

Les champs analysés renverraient des résultats partiels, ce qui est un comportement incorrect dans ce cas.

3. Créer des requêtes

Les tags nous permettent de manipuler nos requêtes de manière intéressante. Nous pouvons les rechercher comme n'importe quel autre champ, ou nous pouvons les utiliser pour filtrer nos résultats sur les requêtesmatch_all. Nous pouvons également les utiliser avec d'autres requêtes pour améliorer nos résultats.

3.1. Recherche de tags

Le nouveau champtag que nous avons créé sur notre modèle est comme tous les autres champs de notre index. Nous pouvons rechercher n'importe quelle entité ayant une balise spécifique comme celle-ci:

@Query("{\"bool\": {\"must\": [{\"match\": {\"tags\": \"?0\"}}]}}")
Page
findByTagUsingDeclaredQuery(String tag, Pageable pageable);

Cet exemple utilise un référentiel de données Spring pour construire notre requête, mais nous pouvons tout aussi rapidement utiliser unRest Template pour interroger manuellement le cluster Elasticsearch.

De même, nous pouvons utiliser l'API Elasticsearch:

boolQuery().must(termQuery("tags", "elasticsearch"));

Supposons que nous utilisions les documents suivants dans notre index:

[
    {
        "id": 1,
        "title": "Spring Data Elasticsearch",
        "authors": [ { "name": "John Doe" }, { "name": "John Smith" } ],
        "tags": [ "elasticsearch", "spring data" ]
    },
    {
        "id": 2,
        "title": "Search engines",
        "authors": [ { "name": "John Doe" } ],
        "tags": [ "search engines", "tutorial" ]
    },
    {
        "id": 3,
        "title": "Second Article About Elasticsearch",
        "authors": [ { "name": "John Smith" } ],
        "tags": [ "elasticsearch", "spring data" ]
    },
    {
        "id": 4,
        "title": "Elasticsearch Tutorial",
        "authors": [ { "name": "John Doe" } ],
        "tags": [ "elasticsearch" ]
    },
]

Maintenant nous pouvons utiliser cette requête:

Page
articleByTags = articleService.findByTagUsingDeclaredQuery("elasticsearch", PageRequest.of(0, 10)); // articleByTags will contain 3 articles [ 1, 3, 4] assertThat(articleByTags, containsInAnyOrder( hasProperty("id", is(1)), hasProperty("id", is(3)), hasProperty("id", is(4))) );

3.2. Filtrer tous les documents

Un modèle de conception courant consiste à créer unFiltered List View dans l'interface utilisateur qui affiche toutes les entités, mais permet également à l'utilisateur de filtrer en fonction de différents critères.

Supposons que nous souhaitons renvoyer tous les articles filtrés en fonction de la balise sélectionnée par l'utilisateur:

@Query("{\"bool\": {\"must\": " +
  "{\"match_all\": {}}, \"filter\": {\"term\": {\"tags\": \"?0\" }}}}")
Page
findByFilteredTagQuery(String tag, Pageable pageable);

Une fois de plus, nous utilisons Spring Data pour construire notre requête déclarée.

Par conséquent, la requête que nous utilisons est divisée en deux parties. La requête de notation est le premier terme, dans ce cas,match_all. La requête de filtre est la suivante et indique à Elasticsearch les résultats à supprimer.

Voici comment nous utilisons cette requête:

Page
articleByTags = articleService.findByFilteredTagQuery("elasticsearch", PageRequest.of(0, 10)); // articleByTags will contain 3 articles [ 1, 3, 4] assertThat(articleByTags, containsInAnyOrder( hasProperty("id", is(1)), hasProperty("id", is(3)), hasProperty("id", is(4))) );

Il est important de réaliser que, même si cela renvoie les mêmes résultats que notre exemple ci-dessus, cette requête fonctionnera mieux.

3.3. Filtrage des requêtes

Parfois, une recherche renvoie trop de résultats pour pouvoir être utilisée. Dans ce cas, il est intéressant d’exposer un mécanisme de filtrage permettant de réexécuter la même recherche, en réduisant simplement les résultats.

Voici un exemple dans lequel nous restreignons les articles qu'un auteur a rédigés à ceux qui comportent une balise spécifique:

@Query("{\"bool\": {\"must\": " +
  "{\"match\": {\"authors.name\": \"?0\"}}, " +
  "\"filter\": {\"term\": {\"tags\": \"?1\" }}}}")
Page
findByAuthorsNameAndFilteredTagQuery( String name, String tag, Pageable pageable);

Encore une fois, Spring Data fait tout le travail pour nous.

Voyons également comment construire nous-mêmes cette requête:

QueryBuilder builder = boolQuery().must(
  nestedQuery("authors", boolQuery().must(termQuery("authors.name", "doe")), ScoreMode.None))
  .filter(termQuery("tags", "elasticsearch"));

Bien entendu, nous pouvons utiliser cette même technique pour filtrer tout autre champ du document. Mais les tags se prêtent particulièrement bien à ce cas d'utilisation.

Voici comment utiliser la requête ci-dessus:

SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(builder)
  .build();
List
articles = elasticsearchTemplate.queryForList(searchQuery, Article.class); // articles contains [ 1, 4 ] assertThat(articleByTags, containsInAnyOrder( hasProperty("id", is(1)), hasProperty("id", is(4))) );

4. Contexte du filtre

Lorsque nous construisons une requête, nous devons différencier le contexte de la requête du contexte de filtrage. Chaque requête dans Elasticsearch a un contexte de requête, nous devrions donc être habitués à les voir.

Tous les types de requêtes ne prennent pas en charge le contexte de filtre. Par conséquent, si nous voulons filtrer les balises, nous devons savoir quels types de requête nous pouvons utiliser.

The bool query has two ways to access the Filter Context. Le premier paramètre,filter, est celui que nous utilisons ci-dessus. Nous pouvons également utiliser un paramètremust_not pour activer le contexte.

The next query type we can filter is constant_score. Cela est utile lorsque vous souhaitez remplacer le contexte de la requête par les résultats du filtre et attribuer le même score à chaque résultat.

The final query type that we can filter based on tags is the filter aggregation. Cela nous permet de créer des groupes d'agrégation basés sur les résultats de notre filtre. En d'autres termes, nous pouvons regrouper tous les articles par balises dans le résultat de notre agrégation.

5. Balisage avancé

Jusqu'ici, nous avons seulement parlé du marquage en utilisant la mise en œuvre la plus élémentaire. La prochaine étape logique consiste à créer des balises qui sont elles-mêmeskey-value pairs. Cela nous permettrait d’être encore plus sophistiqués avec nos requêtes et nos filtres.

Par exemple, nous pourrions changer notre champ de balise en ceci:

@Field(type = Nested)
private List tags;

Ensuite, nous changerons simplement nos filtres pour utiliser les typesnestedQuery.

Une fois que nous comprenons comment utiliserkey-value pairs, c'est un petit pas vers l'utilisation d'objets complexes comme balise. Peu d’implémentations auront besoin d’un objet complet comme balise, mais il est bon de savoir que nous avons cette option si nous en avons besoin.

6. Conclusion

Dans cet article, nous avons abordé les bases de la mise en œuvre du balisage à l'aide d'Elasticsearch.

Comme toujours, des exemples peuvent être trouvésover on GitHub.