Contents

Elasticsearch 标记简介

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

分析的字段将返回部分命中,在这种情况下这是错误的行为。

3. 构建查询

标签允许我们以有趣的方式操纵我们的查询。我们可以像任何其他字段一样搜索它们,或者我们可以使用它们来过滤match_all查询的结果。我们还可以将它们与其他查询一起使用以收紧我们的结果。

3.1. 搜索标签

我们在模型上创建的新标签字段就像我们索引中的所有其他字段一样。我们可以搜索任何具有特定标签的实体,如下所示:

@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 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 中创建一个过滤列表视图,显示所有实体,但也允许用户根据不同的条件进行过滤。

假设我们要返回由用户选择的任何标签过滤的所有文章:

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

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

4.过滤上下文

当我们构建查询时,我们需要区分查询上下文和过滤上下文。Elasticsearch 中的每个查询都有一个查询上下文,所以我们应该习惯于看到它们。

并非每种查询类型都支持过滤上下文。因此,如果我们想过滤标签,我们需要知道我们可以使用哪些查询类型。

bool查询有两种访问 Filter Context 的方法。第一个参数filter就是我们上面使用的那个。我们还可以使用must_not参数来激活上下文。

我们可以过滤的下一个查询类型是constant_score。当您想用过滤器的结果替换查询上下文并为每个结果分配相同的分数时,这很有用。

我们可以根据标签过滤的最后一种查询类型是过滤聚合。这允许我们根据过滤器的结果创建聚合组。换句话说,我们可以在聚合结果中按标签对所有文章进行分组。

5. 高级标记

到目前为止,我们只讨论了使用最基本的实现进行标记。下一个合乎逻辑的步骤是创建本身就是键值对的标签。这将使我们的查询和过滤器更加出色。

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

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

然后我们只需将过滤器更改为使用nestedQuery类型。

一旦我们了解了如何使用键值对,使用复杂对象作为我们的标签是一小步。没有多少实现需要一个完整的对象作为标签,但很高兴知道我们有这个选项,如果我们需要它。