Eine einfache Tagging-Implementierung mit Elasticsearch

Eine einfache Tagging-Implementierung mit Elasticsearch

1. Überblick

Tagging ist ein gängiges Entwurfsmuster, mit dem wir Elemente in unserem Datenmodell kategorisieren und filtern können.

In diesem Artikel implementieren wir das Tagging mit Spring und Elasticsearch. Wir werden sowohl Spring Data als auch die Elasticsearch-API verwenden.

Zunächst werden wir uns nicht mit den Grundlagen des Abrufs von Elasticsearch- und Spring-Daten befassen. Sie können diesehere untersuchen.

2. Tags hinzufügen

The simplest implementation of tagging is an array of strings. Wir können dies implementieren, indem wir unserem Datenmodell ein neues Feld wie folgt hinzufügen:

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

    // ...

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

    // ...
}

Beachten Sie die Verwendung des FeldtypsKeyword . Wir möchten nur exakte Übereinstimmungen unserer Tags, um ein Ergebnis zu filtern. Dies ermöglicht es uns, ähnliche, aber separate Tags wieelasticsearchIsAwesome undelasticsearchIsTerrible zu verwenden.

Analysierte Felder würden Teiltreffer zurückgeben, was in diesem Fall ein falsches Verhalten ist.

3. Abfragen erstellen

Mithilfe von Tags können wir unsere Abfragen auf interessante Weise bearbeiten. Wir können sie wie jedes andere Feld durchsuchen oder sie verwenden, um unsere Ergebnisse nachmatch_all-Anfragen zu filtern. Wir können sie auch mit anderen Abfragen verwenden, um unsere Ergebnisse zu verbessern.

3.1. Tags suchen

Das neuetag-Feld, das wir in unserem Modell erstellt haben, ist genau wie jedes andere Feld in unserem Index. Wir können nach jeder Entität suchen, die ein bestimmtes Tag wie das folgende hat:

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

In diesem Beispiel wird ein Spring Data Repository zum Erstellen unserer Abfrage verwendet, aber wir können genauso schnell einRest Template verwenden, um den Elasticsearch-Cluster manuell abzufragen.

Ebenso können wir die Elasticsearch-API verwenden:

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

Angenommen, wir verwenden die folgenden Dokumente in unserem 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" ]
    },
]

Jetzt können wir diese Abfrage verwenden:

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. Alle Dokumente filtern

Ein gängiges Entwurfsmuster besteht darin, in der Benutzeroberfläche einFiltered List View zu erstellen, das alle Entitäten anzeigt, dem Benutzer jedoch auch das Filtern nach verschiedenen Kriterien ermöglicht.

Angenommen, wir möchten alle Artikel zurückgeben, die nach dem vom Benutzer ausgewählten Tag gefiltert sind:

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

Wir verwenden erneut Spring Data, um unsere deklarierte Abfrage zu erstellen.

Folglich ist die von uns verwendete Abfrage in zwei Teile geteilt. Die Bewertungsabfrage ist der erste Term, in diesem Fallmatch_all. Die Filterabfrage ist die nächste und teilt Elasticsearch mit, welche Ergebnisse verworfen werden sollen.

So verwenden wir diese Abfrage:

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))) );

Es ist wichtig zu wissen, dass diese Abfrage zwar die gleichen Ergebnisse wie in unserem obigen Beispiel liefert, jedoch eine bessere Leistung erbringt.

3.3. Abfragen filtern

Manchmal gibt eine Suche zu viele Ergebnisse zurück, um verwendet werden zu können. In diesem Fall ist es hilfreich, einen Filtermechanismus bereitzustellen, mit dem dieselbe Suche erneut ausgeführt werden kann, nur wenn die Ergebnisse eingegrenzt sind.

Hier ist ein Beispiel, in dem wir die Artikel, die ein Autor geschrieben hat, auf diejenigen mit einem bestimmten Tag eingrenzen:

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

Auch hier leistet Spring Data die ganze Arbeit für uns.

Schauen wir uns auch an, wie Sie diese Abfrage selbst erstellen:

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

Wir können natürlich dieselbe Technik verwenden, um nach jedem anderen Feld im Dokument zu filtern. Tags eignen sich jedoch besonders gut für diesen Anwendungsfall.

So verwenden Sie die obige Abfrage:

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. Kontext filtern

Wenn wir eine Abfrage erstellen, müssen wir zwischen dem Abfragekontext und dem Filterkontext unterscheiden. Jede Abfrage in Elasticsearch hat einen Abfragekontext, sodass wir es gewohnt sein sollten, sie anzuzeigen.

Nicht jeder Abfragetyp unterstützt den Filterkontext. Wenn wir also nach Tags filtern möchten, müssen wir wissen, welche Abfragetypen wir verwenden können.

The bool query has two ways to access the Filter Context. Der erste Parameter,filter, ist der oben verwendete. Wir können auch einenmust_not-Parameter verwenden, um den Kontext zu aktivieren.

The next query type we can filter is constant_score. Dies ist nützlich, wenn Sie den Abfragekontext durch die Ergebnisse des Filters ersetzen und jedem Ergebnis die gleiche Bewertung zuweisen möchten.

The final query type that we can filter based on tags is the filter aggregation. Auf diese Weise können wir Aggregationsgruppen basierend auf den Ergebnissen unseres Filters erstellen. Mit anderen Worten, wir können alle Artikel in unserem Aggregationsergebnis nach Tags gruppieren.

5. Erweiterte Kennzeichnung

Bisher haben wir nur über das Markieren mit der grundlegendsten Implementierung gesprochen. Der nächste logische Schritt besteht darin, Tags zu erstellen, die selbstkey-value pairs sind. Dies würde es uns ermöglichen, noch ausgefeilter mit unseren Abfragen und Filtern zu werden.

Zum Beispiel könnten wir unser Tag-Feld folgendermaßen ändern:

@Field(type = Nested)
private List tags;

Dann würden wir einfach unsere Filter ändern, um die Typen vonnestedQueryzu verwenden.

Sobald wir verstanden haben, wie mankey-value pairs verwendet, ist es ein kleiner Schritt, komplexe Objekte als unser Tag zu verwenden. Nicht viele Implementierungen benötigen ein vollständiges Objekt als Tag, aber es ist gut zu wissen, dass wir diese Option haben, falls wir sie benötigen.

6. Fazit

In diesem Artikel haben wir die Grundlagen der Implementierung von Tagging mit Elasticsearch behandelt.

Beispiele finden sich wie immer inover on GitHub.