1. 概述

标签是一种常见的设计模式,用于对数据模型中的项目进行分类和过滤。

本文将使用Spring和Elasticsearch实现标签功能,同时结合Spring Data和Elasticsearch API两种方式。

需要说明的是,本文不会涉及Elasticsearch和Spring Data的基础配置——这些内容可以参考官方教程

2. 添加标签

最简单的标签实现方式是字符串数组。 我们可以在数据模型中添加新字段:

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

    // ...

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

    // ...
}

注意这里使用了Keyword字段类型。我们只希望精确匹配标签来过滤结果,这样就能区分类似elasticsearchIsAwesomeelasticsearchIsTerrible这样的不同标签。

如果使用分析字段(Analyzed fields),会导致部分匹配,这在标签场景中是错误的行为。

3. 构建查询

标签提供了多种有趣的查询操作方式。我们可以像普通字段一样搜索标签,也可以在match_all查询中使用它们过滤结果,还能与其他查询组合来缩小结果范围。

3.1. 搜索标签

我们在模型中创建的tags字段与索引中的其他字段无异。可以这样查询包含特定标签的实体:

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

这个示例使用Spring Data Repository构建查询,但同样可以使用Rest Template手动查询Elasticsearch集群。

使用Elasticsearch API的等效写法:

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 将包含3篇文章 [ 1, 3, 4]
assertThat(articleByTags, containsInAnyOrder(
 hasProperty("id", is(1)),
 hasProperty("id", is(3)),
 hasProperty("id", is(4)))
);

3.2. 过滤所有文档

常见的设计模式是在UI中创建"过滤列表视图",显示所有实体但允许用户根据不同条件过滤。

假设我们要返回所有文章,但按用户选择的标签过滤:

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

再次使用Spring Data构建声明式查询。

该查询分为两部分:评分查询(这里是match_all)和过滤查询(告诉Elasticsearch丢弃哪些结果)。

使用方式:

Page<Article> articleByTags =
  articleService.findByFilteredTagQuery("elasticsearch", PageRequest.of(0, 10));

// articleByTags 将包含3篇文章 [ 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);

Spring Data再次为我们完成所有工作。

手动构建查询的写法:

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 包含 [ 1, 4 ]
assertThat(articleByTags, containsInAnyOrder(
 hasProperty("id", is(1)),
 hasProperty("id", is(4)))
);

4. 过滤上下文

构建查询时需要区分查询上下文(Query Context)和过滤上下文(Filter Context)。Elasticsearch中的每个查询都有查询上下文,但并非所有查询类型都支持过滤上下文。

bool查询有两种方式访问过滤上下文

  • 第一个参数filter(上文已使用)
  • must_not参数也能激活过滤上下文

**下一个可过滤的查询类型是constant_score**。当需要用过滤结果替换查询上下文,并为每个结果分配相同分数时特别有用。

**最后可基于标签过滤的是filter aggregation**。这允许我们根据过滤结果创建聚合分组——例如按标签对所有文章分组。

5. 高级标签

目前我们只讨论了最基础的标签实现。下一步是创建键值对形式的标签,这能实现更复杂的查询和过滤。

例如,可以将标签字段改为:

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

然后将过滤器改为使用nestedQuery类型。

掌握键值对用法后,使用复杂对象作为标签就是小步升级。虽然多数实现不需要完整对象作为标签,但知道有这个选项总是好的。

6. 总结

本文介绍了使用Elasticsearch实现标签功能的基础知识。

完整示例代码可在GitHub仓库获取。


原始标题:A Simple Tagging Implementation with Elasticsearch