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
字段类型。我们只希望精确匹配标签来过滤结果,这样就能区分类似elasticsearchIsAwesome
和elasticsearchIsTerrible
这样的不同标签。
如果使用分析字段(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仓库获取。