一、ElasticSearch 简介

1、简介

ElasticSearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多员工能力的全文搜索引擎,基于 RESTful web 接口。Elasticsearch 是用 Java 语言开发的,并作为 Apache 许可条款下的开放源码发布,是一种流行的企业级搜索引擎。
ElasticSearch 用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

2、特性

  • 分布式的文档存储引擎
  • 分布式的搜索引擎和分析引擎
  • 分布式,支持 PB 级数据

3、使用场景

  • 搜索领域:如百度、谷歌,全文检索等。
  • 门户网站:访问统计、文章点赞、留言评论等。
  • 广告推广:记录员工行为数据、消费趋势、员工群体进行定制推广等。
  • 信息采集:记录应用的埋点数据、访问日志数据等,方便大数据进行分析。

二、ElasticSearch 基础概念

1、ElaticSearch 和 DB 的关系

在 Elasticsearch 中,文档归属于一种类型 type,而这些类型存在于索引 index 中,可以列一些简单的不同点,来类比传统关系型数据库:

  • Relational DB -> Databases -> Tables -> Rows -> Columns
  • Elasticsearch -> Indices -> Types -> Documents -> Fields

Elasticsearch 集群可以包含多个索引 indices,每一个索引可以包含多个类型 types,每一个类型包含多个文档 documents,然后每个文档包含多个字段 Fields。而在 DB 中可以有多个数据库 Databases,每个库中可以有多张表 Tables,没个表中又包含多行 Rows,每行包含多列 Columns。

ES MySql
字段
文档 一行数据
类型(已废弃)
索引 数据库

2、索引

索引基本概念(indices)

索引是含义相同属性的文档集合,是 ElasticSearch 的一个逻辑存储,可以理解为关系型数据库中的数据库,ElasticSearch 可以把索引数据存放到一台服务器上,也可以 sharding 后存到多台服务器上,每个索引有一个或多个分片,每个分片可以有多个副本。

索引类型(index_type)

索引可以定义一个或多个类型,文档必须属于一个类型。在 ElasticSearch 中,一个索引对象可以存储多个不同用途的对象,通过索引类型可以区分单个索引中的不同对象,可以理解为关系型数据库中的表。每个索引类型可以有不同的结构,但是不同的索引类型不能为相同的属性设置不同的类型。

3、文档(document

文档是可以被索引的基本数据单位。存储在 ElasticSearch 中的主要实体叫文档 document,可以理解为关系型数据库中表的一行记录。每个文档由多个字段构成,ElasticSearch 是一个非结构化的数据库,每个文档可以有不同的字段,并且有一个唯一的标识符。

4、映射(mapping)

ElasticSearch 的 Mapping 非常类似于静态语言中的数据类型:声明一个变量为 int 类型的变量,以后这个变量都只能存储 int 类型的数据。同样的,一个 number 类型的 mapping 字段只能存储 number 类型的数据。
同语言的数据类型相比,Mapping 还有一些其他的含义,Mapping 不仅告诉 ElasticSearch 一个 Field 中是什么类型的值, 它还告诉 ElasticSearch 如何索引数据以及数据是否能被搜索到。
ElaticSearch 默认是动态创建索引和索引类型的 Mapping 的。这就相当于无需定义 Solr 中的 Schema,无需指定各个字段的索引规则就可以索引文件,很方便。但有时方便就代表着不灵活。比如,ElasticSearch 默认一个字段是要做分词的,但有时要搜索匹配整个字段却不行。如有统计工作要记录每个城市出现的次数。对于 name 字段,若记录 new york 文本,ElasticSearch 可能会把它拆分成 new 和 york 这两个词,分别计算这个两个单词的次数,而不是期望的 new york。

三、Spring Data Elasticsearch

Spring Data Elasticsearch 是 Spring 提供的一种以 Spring Data 风格来操作数据存储的方式,它可以避免编写大量的样板代码。
Spring Data 的官网:https://spring.io/projects/spring-data

1、常用注解

映射:Spring Data 通过注解来声明字段的映射属性,有下面的三个注解:

  • @Document 作用在类,标记实体类为文档对象,一般有四个属性
    • indexName:对应索引库名称
    • type:对应在索引库中的类型
    • shards:分片数量,默认 5
    • replicas:副本数量,默认 1
  • @Id 作用在成员变量,标记一个字段作为 id 主键
  • @Field 作用在成员变量,标记为文档的字段,并指定字段映射属性:
    • type:字段类型,取值是枚举:FieldType
    • index:是否索引,布尔类型,默认是 true
    • store:是否存储,布尔类型,默认是 false
    • analyzer:分词器名称:ik_max_word
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "article")
public class ArticleSearchDTO {
/**
* 文章id
*/
@Id
private Integer id;

/**
* 文章标题
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String articleTitle;

/**
* 文章内容
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String articleContent;

/**
* 是否删除
*/
@Field(type = FieldType.Integer)
private Integer isDelete;

/**
* 文章状态
*/
@Field(type = FieldType.Integer)
private Integer status;
}

2、ElasticsearchRestTemplate 用法

在 ElasticsearchTemplate 中,执行查询的大多都是 query 开头,而 query 方法,第一个参数是 Query 的实现类

image.png
源码中,NativeSearchQuery 的构造方法中,参数是 QueryBuilder

这个又是什么?
从名字分析,Query 查询,builder 构建,很清楚的分析出来,QueryBuilder 用来构建查询条件,过滤条件。就好比 SQL 语句后面 where name = “张三” 跟这个是一个意思。
image.png
分析到这里基本上就差不多了,这里只是说简单使用,Spring 中提供了一个类 QueryBuilders ,里面有很多方法来完成各种各样的 QueryBuilder 的构建,字符串型的,Boolean 型的,match,Term 等。

##

四、整合 Elasticsearch 实现博客搜索

1、引入依赖

1
2
3
4
5
6
<!--Elasticsearch相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch<artifactId>
</dependency>

2、修改 SpringBoot 配置文件

1
2
3
4
spring:
elasticsearch:
rest:
uris: http://localhost:9200

3、搜索实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@Slf4j
@Service("esSearchStrategyImpl")
public class EsSearchStrategyImpl implements SearchStrategy {

@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;

@Override
public List<ArticleSearchDTO> searchArticle(String keywords) {
if(StringUtils.isBlank(keywords)){
return new ArrayList<>();
}
return search(buildQuery(keywords));
}

/**
* 搜索文章构造
*
* @param keywords 关键字
* @return es条件构造器
*/
private NativeSearchQueryBuilder buildQuery(String keywords) {
// 条件构造器
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 根据关键词搜索文章标题或内容
boolQueryBuilder.must(QueryBuilders.boolQuery().should(QueryBuilders.matchQuery("articleTitle", keywords))
.should(QueryBuilders.matchQuery("articleContent", keywords)))
.must(QueryBuilders.termQuery("isDelete", FALSE))
.must(QueryBuilders.termQuery("status", PUBLIC.getStatus()));
nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
return nativeSearchQueryBuilder;

}
/**
* 文章搜索结果高亮
*
* @param nativeSearchQueryBuilder es条件构造器
* @return 搜索结果
*/
private List<ArticleSearchDTO> search(NativeSearchQueryBuilder nativeSearchQueryBuilder) {
// 添加文章标题高亮
HighlightBuilder.Field titleField = new HighlightBuilder.Field("articleTitle");
titleField.preTags(PRE_TAG);
titleField.postTags(POST_TAG);
// 添加文章内容高亮
HighlightBuilder.Field contentField = new HighlightBuilder.Field("articleContent");
contentField.preTags(PRE_TAG);
contentField.postTags(POST_TAG);
contentField.fragmentSize(200);
nativeSearchQueryBuilder.withHighlightFields(titleField, contentField);
// 搜索
try {
SearchHits<ArticleSearchDTO> search = elasticsearchRestTemplate.search(nativeSearchQueryBuilder.build(), ArticleSearchDTO.class);
return search.getSearchHits().stream().map(hit -> {
ArticleSearchDTO article = hit.getContent();
// 获取文章标题高亮数据
List<String> titleHighLightList = hit.getHighlightFields().get("articleTitle");
if (CollectionUtils.isNotEmpty(titleHighLightList)) {
// 替换标题数据
article.setArticleTitle(titleHighLightList.get(0));
}
// 获取文章内容高亮数据
List<String> contentHighLightList = hit.getHighlightFields().get("articleContent");
if (CollectionUtils.isNotEmpty(contentHighLightList)) {
// 替换内容数据
article.setArticleContent(contentHighLightList.get(contentHighLightList.size() - 1));
}
return article;
}).collect(Collectors.toList());
} catch (Exception e) {
log.error(e.getMessage());
}
return new ArrayList<>();
}

}

4、使用 Maxwell 同步数据库与 Elasticsearch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component
@RabbitListener(queues = MAXWELL_QUEUE)
public class MaxWellConsumer {
@Autowired
private ElasticsearchMapper elasticsearchMapper;

@RabbitHandler
public void process(byte[] data) {
// 获取监听信息
MaxwellDataDTO maxwellDataDTO = JSON.parseObject(new String(data), MaxwellDataDTO.class);
// 获取文章数据
Article article = JSON.parseObject(JSON.toJSONString(maxwellDataDTO.getData()), Article.class);
// 判断操作类型
switch (maxwellDataDTO.getType()) {
case "insert":
case "update":
// 更新es文章
elasticsearchMapper.save(BeanCopyUtils.copyObject(article, ArticleSearchDTO.class));
break;
case "delete":
// 删除文章
elasticsearchMapper.deleteById(article.getId());
break;
default:
break;
}
}
}

添加 ElasticsearchMapper 接口用于操作 Elasticsearch

继承 ElasticsearchRepository 接口,这样就拥有了一些基本的 Elasticsearch 数据操作方法,同时定义了一个衍生查询方法。

1
2
3
4
@Repository
public interface ElasticsearchMapper extends ElasticsearchRepository<ArticleSearchDTO,Integer> {
}