1. 概要

タグ付けは、データモデル内のアイテムを分類およびフィルタリングできるようにする一般的なデザインパターンです。

この記事では、SpringとElasticsearchを使用してタグ付けを実装します。 Spring DataとElasticsearchAPIの両方を使用します。

まず、ElasticsearchとSpring Dataの取得の基本については説明しません。これらのここを調べることができます。

2. タグの追加

タグ付けの最も簡単な実装は文字列の配列です。次のようにデータモデルに新しいフィールドを追加することでこれを実装できます。

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

    // ...

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

    // ...
}

キーワードフィールドタイプの使用に注意してください。 結果をフィルタリングするには、タグの完全一致のみが必要です。 これにより、elasticsearchIsAwesomeelasticsearchIsTerribleのような類似しているが別々のタグを使用できます。

分析されたフィールドは部分的なヒットを返しますが、この場合は間違った動作です。

3. クエリの作成

タグを使用すると、クエリを興味深い方法で操作できます。 他のフィールドと同じようにそれらを検索することも、match_allクエリで結果をフィルタリングするために使用することもできます。 他のクエリでそれらを使用して、結果を引き締めることもできます。

3.1. タグを検索する

モデルに作成した新しいtagフィールドは、インデックス内の他のすべてのフィールドとまったく同じです。 次のような特定のタグを持つエンティティを検索できます。

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

この例では、Spring Data Repositoryを使用してクエリを作成していますが、 RestTemplateを使用してElasticsearchクラスターに手動でクエリを実行することもできます。

同様に、ElasticsearchAPIを使用できます。

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

インデックスで次のドキュメントを使用するとします。

[
    {
        "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" ]
    },
]

これで、このクエリを使用できます。

Page<Article> 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. すべてのドキュメントのフィルタリング

一般的なデザインパターンは、UIに Filtered List View を作成して、すべてのエンティティを表示することですが、ユーザーはさまざまな基準に基づいてフィルタリングすることもできます。

ユーザーが選択したタグでフィルタリングされたすべての記事を返したいとしましょう。

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

ここでも、Spring Dataを使用して宣言されたクエリを作成しています。

したがって、使用しているクエリは2つに分割されます。 スコアリングクエリは最初の用語であり、この場合はmatch_allです。 次はフィルタークエリで、Elasticsearchにどの結果を破棄するかを指示します。

このクエリの使用方法は次のとおりです。

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

これにより上記の例と同じ結果が返されますが、このクエリのパフォーマンスは向上することを理解することが重要です。

3.3. クエリのフィルタリング

検索で返される結果が多すぎて使用できない場合があります。 その場合、結果を絞り込んで、同じ検索を再実行できるフィルタリングメカニズムを公開すると便利です。

これは、著者が書いた記事を特定のタグが付いた記事だけに絞り込む例です。

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

繰り返しになりますが、SpringDataは私たちのためにすべての作業を行っています。

このクエリを自分で作成する方法も見てみましょう。

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

もちろん、これと同じ手法を使用して、ドキュメント内の他のフィールドをフィルタリングすることもできます。 ただし、タグはこのユースケースに特に適しています。

上記のクエリの使用方法は次のとおりです。

SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(builder)
  .build();
List<Article> articles = 
  elasticsearchTemplate.queryForList(searchQuery, Article.class);

// articles contains [ 1, 4 ]
assertThat(articleByTags, containsInAnyOrder(
 hasProperty("id", is(1)),
 hasProperty("id", is(4)))
);

4. フィルタコンテキスト

クエリを作成するときは、クエリコンテキストとフィルターコンテキストを区別する必要があります。 Elasticsearchのすべてのクエリにはクエリコンテキストがあるため、それらを確認することに慣れている必要があります。

すべてのクエリタイプがフィルターコンテキストをサポートしているわけではありません。 したがって、タグでフィルタリングする場合は、使用できるクエリタイプを知る必要があります。

ブールクエリには、フィルターコンテキストにアクセスする2つの方法があります。 最初のパラメーターfilterは、上記で使用したパラメーターです。 must_notパラメーターを使用してコンテキストをアクティブ化することもできます。

フィルタリングできる次のクエリタイプはconstant_scoreです。 これは、クエリコンテキストをフィルターの結果に置き換え、各結果に同じスコアを割り当てる場合に便利です。

タグに基づいてフィルタリングできる最後のクエリタイプは、フィルター集約です。 これにより、フィルターの結果に基づいて集計グループを作成できます。 つまり、集計結果のタグですべての記事をグループ化できます。

5. 高度なタグ付け

これまで、最も基本的な実装を使用したタグ付けについてのみ説明してきました。 次の論理的な手順は、それ自体がキーと値のペアであるタグを作成することです。 これにより、クエリとフィルターをさらに使いこなせるようになります。

たとえば、タグフィールドを次のように変更できます。

@Field(type = Nested)
private List<Tag> tags;

次に、nestedQueryタイプを使用するようにフィルターを変更します。

キーと値のペアの使用方法を理解したら、複雑なオブジェクトをタグとして使用するための小さなステップです。 タグとして完全なオブジェクトを必要とする実装は多くありませんが、必要に応じてこのオプションがあることを知っておくとよいでしょう。

6. 結論

この記事では、Elasticsearchを使用してタグ付けを実装するための基本について説明しました。

いつものように、例はGitHubにあります。