本文最后更新于:4 个月前
                  
                
              
            
            
              
                
                前言
- 我很荣幸能够分享一篇关于 Elasticsearch 的博客笔记,部分正文内容转载自黑马程序员的优质笔记
- 该笔记在该博文的正文栏目,在正文之前,是我在实际使用ES的过程中,所做的相关经验分享
正文
经验分享
分词器
- 分词器是干啥用的?指定了分词的规则(2023/09/20午)
内置分词器
空格分词器
| 12
 3
 4
 5
 
 | POST _analyze{
 "analyzer": "whitespace",
 "text":     "The quick brown fox."
 }
 
 | 
关键词分词器
标准分词规则
| 12
 3
 4
 5
 6
 
 | POST _analyze{
 "tokenizer": "standard",
 "filter":  [ "lowercase", "asciifolding" ],
 "text":      "Is this déja vu?"
 }
 
 | 
- 分词器 analyze 和分词规则 tokenizer 有什么区别呢?
| 12
 3
 4
 5
 6
 7
 
 | 在搜索引擎和文本分析领域中,分词器(Analyzer)和分词规则器(Tokenizer)是两个不同的概念。
 分词器(Analyzer)是一种将文本转换为单词(Term)序列的工具。它通常包含多个处理步骤,例如词法分析、去除停用词、小写转换、词干提取等。分词器的作用是将原始的文本输入转换为可供索引和搜索的标记流。例如,在Elasticsearch中,分词器被用于预处理文本数据并将其存储在倒排索引中,以支持全文搜索。
 
 分词规则器(Tokenizer)是分词器的一个组成部分。它是文本分析的第一个处理步骤,将输入的文本按照指定的规则拆分为单词。常见的分词规则器有基于空格拆分的空格分词器、基于标点符号拆分的标点分词器等。分词规则器负责定义文本拆分的方式,决定了哪些字符会被视为词条的分隔。
 
 总结来说,分词规则器(Tokenizer)是分词器(Analyzer)的组成部分,用于定义文本的拆分方式;而分词器(Analyzer)则包含多个处理步骤,用于将输入文本转换为标记流。
 
 | 
IK分词器(ES内置插件)
下载安装

- 下载完成,将压缩包解压在 Elasticsearch 的 plugins / ik目录下即可 
修改版本一致


启动ES、Kibana


测试分词效果


- 测试成功,这里也能看出来 ik_smart 和 ik_max_word 这两种不同分词模式的区别了(2023/09/20午)- 
- ik_smart模式是 IK 分词器的简单模式,它会对文本进行较为粗粒度的切分,主要以将句子切分为一些较短的词语为目标,适用于快速搜索和一般文本处理场景。该模式下的分词结果倾向于保留短词
- ik_max_word模式是 IK 分词器的细粒度模式,它会尽可能多地将文本切分为更小的词语,包括一些更细致的切分,如拆分复合词和词组等。该模式下的分词结果倾向于将文本切分为更多的词
 
ES 调用方式
Java 操作 ES
ES实现搜索接口
建立索引
- 在ES中,也存在和 MySQL类似的表结构,这里可以将二者对比一下:
| MySQL | Elasticsearch | 说明 | 
| Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) | 
| Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 | 
| Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) | 
| Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) | 
| SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | create table post(
 id         bigint auto_increment comment 'id'
 primary key,
 title      varchar(512)                       null comment '标题',
 content    text                               null comment '内容',
 tags       varchar(1024)                      null comment '标签列表(json 数组)',
 thumbNum   int      default 0                 not null comment '点赞数',
 favourNum  int      default 0                 not null comment '收藏数',
 userId     bigint                             not null comment '创建用户 id',
 createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间',
 updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
 isDelete   tinyint  default 0                 not null comment '是否删除'
 )
 comment '帖子' collate = utf8mb4_unicode_ci;
 
 | 
- 建立索引语句:
- ES Mapping: 
- id(可以不放到字段设置里) 
- ES 中,尽量存放需要用户筛选(搜索)的数据 
- aliases:别名(为了后续方便数据迁移) 
- 字段类型是 text,这个字段是可被分词的、可模糊查询的;而如果是 keyword,只能完全匹配、精确查询。 
- analyzer(存储时生效的分词器):用 ik_max_word,拆的更碎、索引更多,更有可能被搜出来 
- search_analyzer(查询时生效的分词器):用 ik_smart,更偏向于用户想搜的分词 
- 如果想要让 text 类型的分词字段也支持精确查询,可以创建 keyword 类型的子字段: 
| 12
 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
 
 | POST post_v1{
 "aliases": {
 "post": {}
 },
 "mappings": {
 "properties": {
 "title": {
 "type": "text",
 "analyzer": "ik_max_word",
 "search_analyzer": "ik_smart",
 "fields": {
 "keyword": {
 "type": "keyword",
 "ignore_above": 256
 }
 }
 },
 "content": {
 "type": "text",
 "analyzer": "ik_max_word",
 "search_analyzer": "ik_smart",
 "fields": {
 "keyword": {
 "type": "keyword",
 "ignore_above": 256
 }
 }
 },
 "tags": {
 "type": "keyword"
 },
 "userId": {
 "type": "keyword"
 },
 "createTime": {
 "type": "date"
 },
 "updateTime": {
 "type": "date"
 },
 "isDelete": {
 "type": "keyword"
 }
 }
 }
 }
 
 | 
| 12
 3
 4
 
 | <dependency><groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
 </dependency>
 
 | 
| 12
 3
 4
 
 | elasticsearch:uris: http://localhost:9200
 username: root
 password: 123456
 
 | 
| 12
 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
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 
 | @Document(indexName = "post")@Data
 public class PostEsDTO implements Serializable {
 private static final String DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
 
 
 
 
 @Id
 private Long id;
 
 
 
 
 private String title;
 
 
 
 
 private String content;
 
 
 
 
 private List<String> tags;
 
 
 
 
 private Integer thumbNum;
 
 
 
 
 private Integer favourNum;
 
 
 
 
 private Long userId;
 
 
 
 
 @Field(index = false, store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN)
 private Date createTime;
 
 
 
 
 @Field(index = false, store = true, type = FieldType.Date, format = {}, pattern = DATE_TIME_PATTERN)
 private Date updateTime;
 
 
 
 
 private Integer isDelete;
 
 private static final long serialVersionUID = 1L;
 
 private static final Gson GSON = new Gson();
 
 
 
 
 
 
 
 public static PostEsDTO objToDto(Post post) {
 if (post == null) {
 return null;
 }
 PostEsDTO postEsDTO = new PostEsDTO();
 BeanUtils.copyProperties(post, postEsDTO);
 String tagsStr = post.getTags();
 if (StringUtils.isNotBlank(tagsStr)) {
 postEsDTO.setTags(GSON.fromJson(tagsStr, new TypeToken<List<String>>() {
 }.getType()));
 }
 return postEsDTO;
 }
 
 
 
 
 
 
 
 public static Post dtoToObj(PostEsDTO postEsDTO) {
 if (postEsDTO == null) {
 return null;
 }
 Post post = new Post();
 BeanUtils.copyProperties(postEsDTO, post);
 List<String> tagList = postEsDTO.getTags();
 if (CollectionUtils.isNotEmpty(tagList)) {
 post.setTags(GSON.toJson(tagList));
 }
 return post;
 }
 }
 
 | 
| 12
 3
 
 | public interface PostEsDao extends ElasticsearchRepository<PostEsDTO, Long> {List<PostEsDTO> findByUserId(Long userId);
 }
 
 | 
增删改查
| 12
 
 | @Resourceprivate PostEsDao postEsDao;
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | @Test
 void testAdd() {
 PostEsDTO postEsDTO = new PostEsDTO();
 postEsDTO.setId(5L);
 postEsDTO.setTitle("test");
 postEsDTO.setContent("test");
 postEsDTO.setTags(Arrays.asList("java", "python"));
 postEsDTO.setThumbNum(1);
 postEsDTO.setFavourNum(1);
 postEsDTO.setUserId(1L);
 postEsDTO.setCreateTime(new Date());
 postEsDTO.setUpdateTime(new Date());
 postEsDTO.setIsDelete(0);
 postEsDao.save(postEsDTO);
 System.out.println(postEsDTO.getId());
 }
 
 
 | 
| 12
 3
 4
 5
 6
 
 | @Test
 void testFindById() {
 Optional<PostEsDTO> postEsDTO = postEsDao.findById(1L);
 System.out.println(postEsDTO);
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | @Test
 void testSelect() {
 System.out.println(postEsDao.count());
 Page<PostEsDTO> PostPage = postEsDao.findAll(
 PageRequest.of(0, 5, Sort.by("createTime")));
 List<PostEsDTO> postList = PostPage.getContent();
 System.out.println(postList);
 }
 
 | 
- 简单的增、删、改、查测试通过:(2023/09/20晚)

DSL查询
- 参考文档:
- [Query and filter context | Elasticsearch Guide 7.17] | Elastic
- [Boolean query | Elasticsearch Guide 7.17] | Elastic
- 详细的DSL查询学习可以看官网学习,待我学成归来,就在此留下我的学习笔记(2023/09/21晚)
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | GET post/_search{
 "query": {
 "bool": { // 组合条件
 "must": [ // 必须都满足
 { "match": { "title": "鱼皮" }}, // match 模糊查询
 { "match": { "content": "知识星球" }}
 ],
 "filter": [
 { "term": { "status": "published" }}, // term 精确查询
 { "range": { "publish_date": { "gte": "2015-01-01" }}} // range 范围
 ]
 }
 }
 }
 
 | 
数据同步
- 一般情况下,如果做查询搜索功能,使用 ES 来模糊搜索 (2023/09/21晚) 
- 但是数据是存放在数据库 MySQL 里 的,所以说我们需要把 MySQL 中的数据和 ES 进行同步,保证数据一致(以 MySQL 为主) 
- MySQL => ES (单向)
- 首次安装完 ES,把 MySQL 数据全量同步到 ES 里,写一个单次脚本 4 种方式,全量同步(首次)+ 增量同步(新数据): - 
- 定时任务:比如 1 分钟 1 次,找到 MySQL 中过去几分钟内(至少是定时周期的 2 倍)发 生改变的数据,然后更新到 ES。  - 
- 优点:简单易懂、占用资源少、不用引入第三方中间件 
- 缺点:有时间差 应用场景:数据短时间内不同步影响不大、或者数据几乎不发生修改 
 
- 双写:写数据的时候,必须也去写 ES;更新删除数据库同理。 - 
- 事务:建议先保证  MySQL 写成功
- 如果 ES 写失败了,可以通过定时任务 + 日志 + 告警进行检测和修复 (补偿)
 
- Logstash 数据同步管道:(一般要配合 kafka 消息队列 + beats 采集器) 
- Canal 监听 MySQL Binlog:实时同步  
 
Logstash
下载安装
demo测试

| 1
 | logstash.bat -e "input { stdin { } } output { stdout {} }"
 | 

自定义配置
- 快速开始:[Running Logstash on Windows | Logstash Reference 7.17] | Elastic(2023/09/22晚)
- 在官方文档中,找到这一段简单的示例配置: 

- 将这段配置粘贴进 config 下的 logstash-sample.conf 配置文件(可以保留该原文件,复制一份重命名)中:
- 这几行配置是干什么的呢?简单来讲就是定义了输入和输出:监听 UDP,并输出

- 按官方文档的操作来,尝试加载这个配置文件 并 启动 Logstash:
| 1
 | .\bin\logstash.bat -f .\config\myTask.conf
 | 

- 运行这行命令,可以看到 Logstash 成功启动运行了

同步MySQL
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | input {jdbc {
 jdbc_driver_library => "mysql-connector-java-5.1.36-bin.jar"
 jdbc_driver_class => "com.mysql.jdbc.Driver"
 jdbc_connection_string => "jdbc:mysql://localhost:3306/mydb"
 jdbc_user => "mysql"
 parameters => { "favorite_artist" => "Beethoven" }
 schedule => "* * * * *"
 statement => "SELECT * from songs where artist = :favorite_artist"
 }
 }
 
 | 

- 原因很简单,就是配置中的 mysql jar包找不到,我们需要自己配置一个 mysql jar包,并正确配置它的路径
- 这里有个技巧:在 IDEA 中找到项目所依赖的 jar 包- 
- 如图所示,选择对应的依赖后,可以直接在文件管理器中打开
  
 - 
- 然后直接在文件管理器中复制,粘贴到 Logstash 目录下即可
 
- 加载配置、启动 Logstash,启动成功了:

- 聊聊我在这段配置上踩过的坑吧:- 
- mysql jar 包路径外层多加了一层双引号
- 用户名、密码配置错误
- SQL 语句中 where 多写了一个
- timestamp 写成 timestampe
 
- 这段配置绝对不能出现任何问题,否则就会出现严重的报错。我的最终配置是这样的:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | 
 
 input {
 jdbc {
 jdbc_driver_library => "D:\softWare\logstash\logstash-7.17.9\config\mysql-connector-java-8.0.29.jar"
 jdbc_driver_class => "com.mysql.jdbc.Driver"
 jdbc_connection_string => "jdbc:mysql://localhost:3306/memory_search"
 jdbc_user => "root"
 jdbc_password => "Dw990831"
 statement => "SELECT * from post where 1 = 1"
 schedule => "*/5 * * * *"
 }
 }
 
 output {
 stdout { codec => rubydebug }
 }
 
 | 
| 12
 3
 4
 5
 6
 
 | statement => "SELECT * from post where updateTime > :sql_last_value"use_column_value => true
 tracking_column_type => "timestamp"
 tracking_column => "updateTime"
 schedule => "*/5 * * * * *"
 jdbc_default_timezone => "Asia/Shanghai"
 
 | 
- 这段配置就是根据 updateTime 字段的最新值,同步updateTime 大于最新值的数据:

- 所以说,sql_last_value 指定的是上次查到的数据的最后一行的指定字段,新的查询就是比较这个指定字段与sql_last_value的大小
- 但是经过多次查询发现,这里的 sql_last_value 始终不变
- 我们可以在 data\plugins\inputs\jdbc\logstash_jdbc_last_run 看到 sql_last_value 指定的数据,确实没有变化:
 

- 将 tracking_column => “updateTime” 的 updateTime 修改为 updatetime,日期同步成功

- 更新下数据库中的最新值,再看看效果,确实拿到了数据库中最新修改的值(参照上次修改后的最新值):

同步ES

- 跟着官网简单的 demo 学就行,配置过一次就会了,这是我完成同步 ES 后的配置:(部分私密信息已做处理)
| 12
 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
 
 | 
 
 input {
 jdbc {
 jdbc_driver_library => "D:\softWare\logstash\logstash-7.17.9\config\mysql-connector-java-8.0.29.jar"
 jdbc_driver_class => "com.mysql.jdbc.Driver"
 jdbc_connection_string => "jdbc:mysql://localhost:3306/"******""
 jdbc_user => "******"
 jdbc_password => ""******""
 statement => "SELECT * from post where updateTime > :sql_last_value"
 use_column_value => true
 tracking_column_type => "timestamp"
 tracking_column => "updatetime"
 schedule => "*/5 * * * * *"
 jdbc_default_timezone => "Asia/Shanghai"
 }
 }
 
 output {
 stdout { codec => rubydebug }
 
 elasticsearch {
 hosts => "127.0.0.1:9200"
 index => "post_v1"
 document_id => "%{id}"
 }
 }
 
 | 
- 这里简单介绍下这几个配置的作用:- 
- host:标识要进行同步的 ES 地址,即指定了:数据从 MySQL 中取出后,发送到哪
- index:目标索引
- document_id:指定目标索引内,每一个文档的 id,就是从 SELECT * 中解构出 id 值
- data_stream:特殊的数据格式,我们从数据库中取到的都是普通类型,不需要这行配置
 
- 其他的目前暂且不需要了解,日后再进一步学习 
- 加载配置,运行 Logstash,可以看到运行成功了,数据库中最新更新的数据也成功同步到了本地的 ES 上了:

- 从同步结果来看,我们还需要解决几个问题:- 
- 排除某些不需要同步的字段
- ES 中同步过来的文档数据字段都是全小写,不是驼峰式
- 查询结果按 updateTime 降序排列,避免重排序,导致多同步了不必要的数据,造成性能浪费
 
- 解决这三个问题当然很简单: - 
- 首先修改下 SQL 语句:(2023/09/23午)
 | 1
 | statement => "SELECT * from post where updateTime > :sql_last_value and updateTime < now() order by updateTime desc"
 |  
 
- 
- 再写入如下过滤配置,将对应字段进行驼峰式转换,并排除不需要的字段:
 | 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | filter {mutate {
 rename => {
 "updatetime" => "updateTime"
 "userid" => "userId"
 "createtime" => "createTime"
 "isdelete" => "isDelete"
 }
 remove_field => ["thumbnum", "favournum"]
 }
 }
 
 |  
 
- 重新进行同步,结果完美,顺利完成: 

Logstash 配置多个输入 / 输出源(小丑的第一次尝试)
🔥 最近在优化 Memory 聚合搜索平台,尝试实现博文 article 的快速搜索和关键词高亮显示等功能
主要工作如下:
- 新增 article 实体,表结构已给出 👇
- 新增博文的 ES 包装类(ArticleEsDao)、博文查询参数(ArticleQueryRequest)、博文高亮字段(ArticleEsHighlightData)
- 使用 Spring Data Elasticsearch 的 QueryBuilder 组合条件查询,实现使用 ES 快速搜索博文和关键词高亮显示
- 新增博文数据源接口(ArticleDataSource),供聚合搜索调用
- 配置 Logstash 实现 MySQL 和 ES 数据同步
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | create table article
 (
 id          bigint                                 not null comment '文章id',
 title       varchar(256)                           not null comment '文章标题',
 description varchar(256)                           not null comment '文章摘要',
 content     varchar(2048)                          not null comment '文章内容',
 author_id   bigint                                 not null comment '创作者',
 view        int          default 0                 not null comment '浏览量',
 likes       int          default 0                 not null comment '点赞量',
 comments    varchar(256) default '0'               null comment '评论量',
 create_time datetime     default CURRENT_TIMESTAMP not null comment '创建时间',
 update_time datetime     default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
 is_delete   tinyint      default 0                 not null comment '逻辑删除',
 collects    int                                    not null comment '收藏量',
 article_url varchar(2048)                          null comment '封面图片',
 tags        varchar(256)                           not null comment '文章标签'
 )
 comment '博文';
 
 | 
同步配置
- 新增 article 相关实体的过程这里先不细讲,重点记录:如何实现 MySQL 和 ES 数据同步
- 在 Logstash 的 config 目录下,我们作如下配置:(2023/11/07晚)
| 12
 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
 
 | 
 
 input {
 jdbc {
 jdbc_driver_library => "D:\softWare\logstash\logstash-7.17.9\config\mysql-connector-java-8.0.29.jar"
 jdbc_driver_class => "com.mysql.jdbc.Driver"
 jdbc_connection_string => "jdbc:mysql://localhost:3306/******"
 jdbc_user => "******"
 jdbc_password => "******"
 statement => "SELECT * from article where update_time > :sql_last_value and update_time < now() order by update_time desc"
 use_column_value => true
 tracking_column_type => "timestamp"
 tracking_column => "update_time"
 schedule => "*/5 * * * * *"
 jdbc_default_timezone => "Asia/Shanghai"
 }
 }
 
 input {
 jdbc {
 jdbc_driver_library => "D:\softWare\logstash\logstash-7.17.9\config\mysql-connector-java-8.0.29.jar"
 jdbc_driver_class => "com.mysql.jdbc.Driver"
 jdbc_connection_string => "jdbc:mysql://localhost:3306/******"
 jdbc_user => "******"
 jdbc_password => "******"
 statement => "SELECT * from post where updateTime > :sql_last_value and updateTime < now() order by updateTime desc"
 use_column_value => true
 tracking_column_type => "timestamp"
 tracking_column => "updatetime"
 schedule => "*/5 * * * * *"
 jdbc_default_timezone => "Asia/Shanghai"
 }
 }
 
 filter {
 mutate {
 rename => {
 "updatetime" => "updateTime"
 "userid" => "userId"
 "createtime" => "createTime"
 "isdelete" => "isDelete"
 }
 remove_field => ["thumbnum", "favournum"]
 }
 }
 
 output {
 stdout { codec => rubydebug }
 
 elasticsearch {
 hosts => "127.0.0.1:9200"
 index => "post_v1"
 document_id => "%{id}"
 }
 }
 
 output {
 stdout { codec => rubydebug }
 
 elasticsearch {
 hosts => ["localhost:9200"]
 index => "article_v1"
 document_id => "%{id}"
 }
 }
 
 | 
🥣 我们废话少说,看清楚如上配置 👆
- 比较有趣的是,新增的 article 实体的字段是下划线命名法,而 post 实体的字段却是驼峰命名法:

- 这样的属性名肯定是不规范的(当然,是因为 article 是我从 Memory 缘忆交友社区下直接粘贴过来的)
- 不过,正好可以比对下不同命名规范的属性,在 Logstash 配置中的写法区别了:(2023/11/07晚)
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | -- 下划线命名法input {
 jdbc {
 jdbc_driver_library => "D:\softWare\logstash\logstash-7.17.9\config\mysql-connector-java-8.0.29.jar"
 jdbc_driver_class => "com.mysql.jdbc.Driver"
 jdbc_connection_string => "jdbc:mysql://localhost:3306/******"
 jdbc_user => "******"
 jdbc_password => "******"
 statement => "SELECT * from article where update_time > :sql_last_value and update_time < now() order by update_time desc"
 use_column_value => true
 tracking_column_type => "timestamp"
 tracking_column => "update_time"
 schedule => "*/5 * * * * *"
 jdbc_default_timezone => "Asia/Shanghai"
 }
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | -- 驼峰命名法input {
 jdbc {
 jdbc_driver_library => "D:\softWare\logstash\logstash-7.17.9\config\mysql-connector-java-8.0.29.jar"
 jdbc_driver_class => "com.mysql.jdbc.Driver"
 jdbc_connection_string => "jdbc:mysql://localhost:3306/******"
 jdbc_user => "******"
 jdbc_password => "******"
 statement => "SELECT * from post where updateTime > :sql_last_value and updateTime < now() order by updateTime desc"
 use_column_value => true
 tracking_column_type => "timestamp"
 tracking_column => "updatetime"
 schedule => "*/5 * * * * *"
 jdbc_default_timezone => "Asia/Shanghai"
 }
 
 | 
ES 查询
- Logstash 同步配置写完之后,当然要进行测试了,看看数据是否成功从 MySQL 成功同步到了 ES 中
- 首先新增 ES 索引,在 Kibana 监控面板下,执行如下 DSL 语句:
| 12
 3
 4
 5
 6
 7
 
 | PUT /article_v1  {
 "settings": {
 "number_of_shards": 3,
 "number_of_replicas": 2
 }
 }
 
 | 
🔥 注意:
- 索引名要跟 Logstash 配置中 output 块下的 - index属性对应:
- 跟 ArticleEsDao 的 - Document字段对应:
| 1
 | @Document(indexName = "article_v1")
 | 
- 按官方文档的操作来,尝试加载这个配置文件 并 启动 Logstash:
| 1
 | .\bin\logstash.bat -f .\config\myTask.conf
 | 
- 随便修改一条记录(下面的实现 updateTime 字段自动更新一栏中有提到,数据开始同步 👇:

- 在 Kibana 监控面板下,使用 DSL 语句执行查询,效果如下:

- 成功完成 article实体的数据同步 (2023/11/07晚)
实现 updateTime 字段自动更新
- 什么意思呢?我们希望在修改完数据库表中的记录后,该条记录对应的 uodateTime 字段实现自动更新
- 实现方法很简单,在 IDEA 中,直接修改表的 updateTime 字段属性,如下:

| 1
 | updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
 | 
- 这样,我们更新记录后,该记录 updateTime 字段会自动更新为最近修改时间:(2023/11/07晚)

Logstash 配置多个输入输出源Ⅱ
| 12
 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
 
 | 
 
 input {
 jdbc {
 jdbc_driver_library => "D:\softWare\logstash\logstash-7.17.9\config\mysql-connector-java-8.0.29.jar"
 jdbc_driver_class => "com.mysql.jdbc.Driver"
 jdbc_connection_string => "jdbc:mysql://localhost:3306/memory_search"
 jdbc_user => "root"
 jdbc_password => "Dw990831"
 statement => "SELECT * from post where updateTime > :sql_last_value and updateTime < now() order by updateTime desc"
 use_column_value => true
 tracking_column_type => "timestamp"
 tracking_column => "updatetime"
 schedule => "*/5 * * * * *"
 jdbc_default_timezone => "Asia/Shanghai"
 }
 }
 
 filter {
 mutate {
 rename => {
 "updatetime" => "updateTime"
 "userid" => "userId"
 "createtime" => "createTime"
 "isdelete" => "isDelete"
 }
 remove_field => ["thumbnum", "favournum"]
 }
 }
 
 output {
 stdout { codec => rubydebug }
 elasticsearch {
 hosts => "127.0.0.1:9200"
 index => "post_v1"
 document_id => "%{id}"
 }
 }
 
 
 
 | 
| 12
 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
 
 | input {jdbc {
 jdbc_driver_library => "D:\softWare\logstash\logstash-7.17.9\config\mysql-connector-java-8.0.29.jar"
 jdbc_driver_class => "com.mysql.jdbc.Driver"
 jdbc_connection_string => "jdbc:mysql://localhost:3306/memory_search"
 jdbc_user => "root"
 jdbc_password => "Dw990831"
 statement => "SELECT * from article where updateTime > :sql_last_value and updateTime < now() order by updateTime desc"
 use_column_value => true
 tracking_column_type => "timestamp"
 tracking_column => "updatetime"
 schedule => "*/5 * * * * *"
 jdbc_default_timezone => "Asia/Shanghai"
 }
 }
 
 filter {
 mutate {
 rename => {
 "updatetime" => "updateTime"
 "userid" => "userId"
 "createtime" => "createTime"
 "isdelete" => "isDelete"
 }
 remove_field => ["thumbnum", "favournum"]
 }
 }
 
 output {
 stdout { codec => rubydebug }
 elasticsearch {
 hosts => ["127.0.0.1:9200"]
 index => "article_v1"
 document_id => "%{id}"
 }
 }
 
 | 
| 1
 | .\bin\logstash.bat -f .\config\myTask.conf -f .\config\myTask2.conf
 | 
- 有关 Logstash 的配置,还需要更多了解,目前知识掌握了 MSQL 向 ES 的映射配置(2023/11/16晚)
SQL,多输入,多输出

Kibana 监控面板




如下图所示:
看板的命名很有意思,看板的命名必须要匹配到已经创建的索引名,还不能重复,也就是说:
每个索引只可以创建一个看板,至少我目前的看法是这样的(2023/12/03午)


- 创建好新的看板之后,就可以再次进入 DashBorad界面了,我们创建的可视化看板可以投入使用了
- 这里简单地介绍下各个板块的作用吧,其他没有什么好讲的,有时间玩玩就可以

- 使用 Kibana可视化监控看板的教程到这里就结束了(2023/12/03午)
其他
搜索建议
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | POST my-index-000001/_search{
 "query" : {
 "match": {
 "message": "tring out Elasticsearch"
 }
 },
 "suggest" : {
 "my-suggestion" : {
 "text" : "tring out Elasticsearch",
 "term" : {
 "field" : "message"
 }
 }
 }
 }
 
 | 
搜索高亮
- 官方文档:[Highlighting | Elasticsearch Guide 7.17] | Elastic
- 如何使搜索词高亮?ES文档里有现成的demo:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | GET /_search{
 "query": {
 "match": { "content": "kimchy" }
 },
 "highlight": {
 "fields": {
 "content": {}
 }
 }
 }
 
 | 

后端
- 我们使用 Java客户端,这样编写:
- 使所有字段内匹配的关键字高亮: (2023/10/01晚)
| 12
 3
 4
 5
 6
 
 | 搜索关键词高亮HighlightBuilder highlightBuilder = new HighlightBuilder();
 highlightBuilder.field("*")
 .preTags("<font color='#eea6b7'>")
 .postTags("</font>");
 highlightBuilder.requireFieldMatch(false);
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 
 | HighlightBuilder highlightBuilder = new HighlightBuilder()
 .field("content")
 .requireFieldMatch(false)
 .preTags("<font color='#eea6b7'>")
 .postTags("</font>");
 highlightBuilder.field("title")
 .requireFieldMa
 
 | 
| 12
 3
 4
 5
 6
 
 | NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
 .withQuery(boolQueryBuilder)
 .withHighlightBuilder(highlightBuilder)
 .withPageable(pageRequest)
 .withSorts(sortBuilder).build();
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | List<SearchHit<PostEsDTO>> searchHitList = searchHits.getSearchHits();
 Map<Long, PostEsHighlightData> highlightDataMap = new HashMap<>();
 for (SearchHit hit : searchHits.getSearchHits()) {
 PostEsHighlightData data = new PostEsHighlightData();
 data.setId(Long.valueOf(hit.getId()));
 if (hit.getHighlightFields().get("title") != null) {
 String highlightTitle = String.valueOf(hit.getHighlightFields().get("title"));
 data.setTitle(highlightTitle.substring(1, highlightTitle.length() - 1));
 System.out.println(data.getTitle());
 }
 if (hit.getHighlightFields().get("content") != null) {
 String highlightContent = String.valueOf(hit.getHighlightFields().get("content"));
 data.setContent(highlightContent.substring(1, highlightContent.length() - 1));
 System.out.println(data.getContent());
 }
 highlightDataMap.put(data.getId(), data);
 }
 
 | 
- 根据 id 拿到每一个 Post对象,使用高亮关键词替换原文本,返回结果:
| 12
 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
 
 | List<Long> postIdList = searchHitList.stream().map(searchHit -> searchHit.getContent().getId())
 .collect(Collectors.toList());
 
 List<Post> postList = baseMapper.selectBatchIds(postIdList);
 if (postList != null) {
 Map<Long, List<Post>> idPostMap = postList.stream().collect(Collectors.groupingBy(Post::getId));
 postIdList.forEach(postId -> {
 if (idPostMap.containsKey(postId)) {
 
 Post post = idPostMap.get(postId).get(0);
 String hl_title = highlightDataMap.get(postId).getTitle();
 String hl_content = highlightDataMap.get(postId).getContent();
 if (hl_title != null && hl_title.trim() != "") {
 post.setTitle(hl_title);
 }
 if (hl_content != null && hl_content.trim() != "") {
 post.setContent(hl_content);
 }
 resourceList.add(post);
 } else {
 
 String delete = elasticsearchRestTemplate.delete(String.valueOf(postId), PostEsDTO.class);
 log.info("delete post {}", delete);
 }
 });
 }
 }
 
 
 | 
前端
- 后端已经将关键词高亮特殊处理了,前端应该做什么呢?
- 将后端响应的文本信息,放入 v-html 属性中,即可解析出文本的 CSS 样式:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | <!--标题--><template #title>
 <a href="https://www.antdv.com/" v-html="item.title"></a>
 </template>
 <!--头像-->
 <template #avatar>
 <a-avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"/>
 </template>
 <template #description>
 <div v-html="item.content" style="margin-bottom: 10px"></div>
 </template>
 
 | 

- 这让我想起了前两天刚实现过的前端解析 Markdown 格式文件的方法: (2023/10/01晚)
| 12
 3
 4
 5
 6
 7
 
 | import MarkdownIt from 'markdown-it';
 
 const parsedContent = ref()
 const md = new MarkdownIt();
 
 parsedContent.value = md.render(articleInfo.value.content);
 
 | 
| 12
 3
 
 | <div v-html="parsedContent"style="position: absolute; margin-left: 10px; margin-right: 10px; margin-top: 20px;">
 </div>
 
 | 
搜索词建议
订阅数据库流水的同步方式 Canel
下载、安装
配置
项目导入Canel依赖
简单测试
Jmeter压力测试
- 流程记录(2023/09/25晚) 
  
 
1.初识elasticsearch
1.1.了解ES
1.1.1.elasticsearch的作用
elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容
例如:
- 在GitHub搜索代码  
 
- 在电商网站搜索商品  
 
- 在百度搜索答案  
 
- 在打车软件搜索附近的车  
 
1.1.2.ELK技术栈
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:

而elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。

1.1.3.elasticsearch和lucene
elasticsearch底层是基于lucene来实现的。
Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。官网地址:https://lucene.apache.org/ 。

elasticsearch的发展历史:
- 2004年Shay Banon基于Lucene开发了Compass
- 2010年Shay Banon 重写了Compass,取名为Elasticsearch。

1.1.4.为什么不是其他搜索技术?
目前比较知名的搜索引擎技术排名:

虽然在早期,Apache Solr是最主要的搜索引擎技术,但随着发展elasticsearch已经渐渐超越了Solr,独占鳌头:

1.1.5.总结
什么是elasticsearch?
- 一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能
什么是elastic stack(ELK)?
- 是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch
什么是Lucene?
- 是Apache的开源搜索引擎类库,提供了搜索引擎的核心API
1.2.倒排索引
倒排索引的概念是基于MySQL这样的正向索引而言的。
1.2.1.正向索引
那么什么是正向索引呢?例如给下表(tb_goods)中的id创建索引:

如果是根据id查询,那么直接走索引,查询速度非常快。
但如果是基于title做模糊查询,只能是逐行扫描数据,流程如下:
1)用户搜索数据,条件是title符合"%手机%"
2)逐行获取数据,比如id为1的数据
3)判断数据中的title是否符合用户搜索条件
4)如果符合则放入结果集,不符合则丢弃。回到步骤1
逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。
1.2.2.倒排索引
倒排索引中有两个非常重要的概念:
- 文档(Document):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息
- 词条(Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
创建倒排索引是对正向索引的一种特殊处理,流程如下:
- 将每一个文档的数据利用算法分词,得到一个个词条
- 创建表,每行数据包括词条、词条所在文档id、位置等信息
- 因为词条唯一性,可以给词条创建索引,例如hash表结构索引
如图:

倒排索引的搜索流程如下(以搜索”华为手机”为例):
1)用户输入条件"华为手机"进行搜索。
2)对用户输入内容分词,得到词条:华为、手机。
3)拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3。
4)拿着文档id到正向索引中查找具体文档。
如图:

虽然要先查询倒排索引,再查询倒排索引,但是无论是词条、还是文档id都建立了索引,查询速度非常快!无需全表扫描。
1.2.3.正向和倒排
那么为什么一个叫做正向索引,一个叫做倒排索引呢?
是不是恰好反过来了?
那么两者方式的优缺点是什么呢?
正向索引:
- 优点:
- 可以给多个字段创建索引
- 根据索引字段搜索、排序速度非常快
 
- 缺点:
- 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
 
倒排索引:
- 优点:
- 缺点:
- 只能给词条创建索引,而不是字段
- 无法根据字段做排序
 
1.3.es的一些概念
elasticsearch中有很多独有的概念,与mysql中略有差别,但也有相似之处。
1.3.1.文档和字段
elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中:

而Json文档中往往包含很多的字段(Field),类似于数据库中的列。
1.3.2.索引和映射
索引(Index),就是相同类型的文档的集合。
例如:
- 所有用户文档,就可以组织在一起,称为用户的索引;
- 所有商品的文档,可以组织在一起,称为商品的索引;
- 所有订单的文档,可以组织在一起,称为订单的索引;

因此,我们可以把索引当做是数据库中的表。
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
1.3.3.mysql与elasticsearch
我们统一的把mysql与elasticsearch的概念做一下对比:
| MySQL | Elasticsearch | 说明 | 
| Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) | 
| Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 | 
| Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) | 
| Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) | 
| SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD | 
是不是说,我们学习了elasticsearch就不再需要mysql了呢?
并不是如此,两者各自有自己的擅长支出:
- Mysql:擅长事务类型操作,可以确保数据的安全和一致性
- Elasticsearch:擅长海量数据的搜索、分析、计算
因此在企业中,往往是两者结合使用:
- 对安全性要求较高的写操作,使用mysql实现
- 对查询性能要求较高的搜索需求,使用elasticsearch实现
- 两者再基于某种方式,实现数据的同步,保证一致性

1.4.安装es、kibana
1.4.1.安装
参考课前资料:
 
 
1.4.2.分词器
参考课前资料:
 
 
1.4.3.总结
分词器的作用是什么?
- 创建倒排索引时对文档分词
- 用户搜索时,对输入的内容分词
IK分词器有几种模式?
- ik_smart:智能切分,粗粒度
- ik_max_word:最细切分,细粒度
IK分词器如何拓展词条?如何停用词条?
- 利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典
- 在词典中添加拓展词条或者停用词条
2.索引库操作
索引库就类似数据库表,mapping映射就类似表的结构。
我们要向es中存储数据,必须先创建“库”和“表”。
2.1.mapping映射属性
mapping是对索引库中文档的约束,常见的mapping属性包括:
- type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
- 数值:long、integer、short、byte、double、float、
- 布尔:boolean
- 日期:date
- 对象:object
 
- index:是否创建索引,默认为true
- analyzer:使用哪种分词器
- properties:该字段的子字段
例如下面的json文档:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | {"age": 21,
 "weight": 52.1,
 "isMarried": false,
 "info": "黑马程序员Java讲师",
 "email": "zy@itcast.cn",
 "score": [99.1, 99.5, 98.9],
 "name": {
 "firstName": "云",
 "lastName": "赵"
 }
 }
 
 | 
对应的每个字段映射(mapping):
- age:类型为 integer;参与搜索,因此需要index为true;无需分词器
- weight:类型为float;参与搜索,因此需要index为true;无需分词器
- isMarried:类型为boolean;参与搜索,因此需要index为true;无需分词器
- info:类型为字符串,需要分词,因此是text;参与搜索,因此需要index为true;分词器可以用ik_smart
- email:类型为字符串,但是不需要分词,因此是keyword;不参与搜索,因此需要index为false;无需分词器
- score:虽然是数组,但是我们只看元素的类型,类型为float;参与搜索,因此需要index为true;无需分词器
- name:类型为object,需要定义多个子属性
- name.firstName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
- name.lastName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
 
2.2.索引库的CRUD
这里我们统一使用Kibana编写DSL的方式来演示。
2.2.1.创建索引库和映射
基本语法:
- 请求方式:PUT
- 请求路径:/索引库名,可以自定义
- 请求参数:mapping映射
格式:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | PUT /索引库名称{
 "mappings": {
 "properties": {
 "字段名":{
 "type": "text",
 "analyzer": "ik_smart"
 },
 "字段名2":{
 "type": "keyword",
 "index": "false"
 },
 "字段名3":{
 "properties": {
 "子字段": {
 "type": "keyword"
 }
 }
 },
 
 }
 }
 }
 
 | 
示例:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | PUT /heima{
 "mappings": {
 "properties": {
 "info":{
 "type": "text",
 "analyzer": "ik_smart"
 },
 "email":{
 "type": "keyword",
 "index": "falsae"
 },
 "name":{
 "properties": {
 "firstName": {
 "type": "keyword"
 }
 }
 },
 // ... 略
 }
 }
 }
 
 | 
2.2.2.查询索引库
基本语法:
- 请求方式:GET 
- 请求路径:/索引库名 
- 请求参数:无 
格式:
示例:

2.2.3.修改索引库
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。
语法说明:
| 12
 3
 4
 5
 6
 7
 8
 
 | PUT /索引库名/_mapping{
 "properties": {
 "新字段名":{
 "type": "integer"
 }
 }
 }
 
 | 
示例:

2.2.4.删除索引库
语法:
- 请求方式:DELETE 
- 请求路径:/索引库名 
- 请求参数:无 
格式:
在kibana中测试:

2.2.5.总结
索引库操作有哪些?
- 创建索引库:PUT /索引库名
- 查询索引库:GET /索引库名
- 删除索引库:DELETE /索引库名
- 添加字段:PUT /索引库名/_mapping
3.文档操作
3.1.新增文档
语法:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | POST /索引库名/_doc/文档id{
 "字段1": "值1",
 "字段2": "值2",
 "字段3": {
 "子属性1": "值3",
 "子属性2": "值4"
 },
 
 }
 
 | 
示例:
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | POST /heima/_doc/1{
 "info": "黑马程序员Java讲师",
 "email": "zy@itcast.cn",
 "name": {
 "firstName": "云",
 "lastName": "赵"
 }
 }
 
 | 
响应:

3.2.查询文档
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。
语法:
通过kibana查看数据:
查看结果:

3.3.删除文档
删除使用DELETE请求,同样,需要根据id进行删除:
语法:
示例:
| 12
 
 | # 根据id删除数据DELETE /heima/_doc/1
 
 | 
结果:

3.4.修改文档
修改有两种方式:
- 全量修改:直接覆盖原来的文档
- 增量修改:修改文档中的部分字段
3.4.1.全量修改
全量修改是覆盖原来的文档,其本质是:
注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
语法:
| 12
 3
 4
 5
 6
 7
 
 | PUT /{索引库名}/_doc/文档id{
 "字段1": "值1",
 "字段2": "值2",
 
 }
 
 
 | 
示例:
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | PUT /heima/_doc/1{
 "info": "黑马程序员高级Java讲师",
 "email": "zy@itcast.cn",
 "name": {
 "firstName": "云",
 "lastName": "赵"
 }
 }
 
 | 
3.4.2.增量修改
增量修改是只修改指定id匹配的文档中的部分字段。
语法:
| 12
 3
 4
 5
 6
 
 | POST /{索引库名}/_update/文档id{
 "doc": {
 "字段名": "新的值",
 }
 }
 
 | 
示例:
| 12
 3
 4
 5
 6
 
 | POST /heima/_update/1{
 "doc": {
 "email": "ZhaoYun@itcast.cn"
 }
 }
 
 | 
3.5.总结
文档操作有哪些?
- 创建文档:POST /{索引库名}/_doc/文档id   { json文档 }
- 查询文档:GET /{索引库名}/_doc/文档id
- 删除文档:DELETE /{索引库名}/_doc/文档id
- 修改文档:
- 全量修改:PUT /{索引库名}/_doc/文档id { json文档 }
- 增量修改:POST /{索引库名}/_update/文档id { “doc”: {字段}}
 
4.RestAPI
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html
其中的Java Rest Client又包括两种:
- Java Low Level Rest Client
- Java High Level Rest Client

我们学习的是Java HighLevel Rest Client客户端API
4.0.导入Demo工程
4.0.1.导入数据
首先导入课前资料提供的数据库数据:
 
 
数据结构如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | CREATE TABLE `tb_hotel` (`id` bigint(20) NOT NULL COMMENT '酒店id',
 `name` varchar(255) NOT NULL COMMENT '酒店名称;例:7天酒店',
 `address` varchar(255) NOT NULL COMMENT '酒店地址;例:航头路',
 `price` int(10) NOT NULL COMMENT '酒店价格;例:329',
 `score` int(2) NOT NULL COMMENT '酒店评分;例:45,就是4.5分',
 `brand` varchar(32) NOT NULL COMMENT '酒店品牌;例:如家',
 `city` varchar(32) NOT NULL COMMENT '所在城市;例:上海',
 `star_name` varchar(16) DEFAULT NULL COMMENT '酒店星级,从低到高分别是:1星到5星,1钻到5钻',
 `business` varchar(255) DEFAULT NULL COMMENT '商圈;例:虹桥',
 `latitude` varchar(32) NOT NULL COMMENT '纬度;例:31.2497',
 `longitude` varchar(32) NOT NULL COMMENT '经度;例:120.3925',
 `pic` varchar(255) DEFAULT NULL COMMENT '酒店图片;例:/img/1.jpg',
 PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 | 
4.0.2.导入项目
然后导入课前资料提供的项目:
 
 
项目结构如图:

4.0.3.mapping映射分析
创建索引库,最关键的是mapping映射,而mapping映射要考虑的信息包括:
- 字段名
- 字段数据类型
- 是否参与搜索
- 是否需要分词
- 如果分词,分词器是什么?
其中:
- 字段名、字段数据类型,可以参考数据表结构的名称和类型
- 是否参与搜索要分析业务来判断,例如图片地址,就无需参与搜索
- 是否分词呢要看内容,内容如果是一个整体就无需分词,反之则要分词
- 分词器,我们可以统一使用ik_max_word
来看下酒店数据的索引库结构:
| 12
 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
 
 | PUT /hotel{
 "mappings": {
 "properties": {
 "id": {
 "type": "keyword"
 },
 "name":{
 "type": "text",
 "analyzer": "ik_max_word",
 "copy_to": "all"
 },
 "address":{
 "type": "keyword",
 "index": false
 },
 "price":{
 "type": "integer"
 },
 "score":{
 "type": "integer"
 },
 "brand":{
 "type": "keyword",
 "copy_to": "all"
 },
 "city":{
 "type": "keyword",
 "copy_to": "all"
 },
 "starName":{
 "type": "keyword"
 },
 "business":{
 "type": "keyword"
 },
 "location":{
 "type": "geo_point"
 },
 "pic":{
 "type": "keyword",
 "index": false
 },
 "all":{
 "type": "text",
 "analyzer": "ik_max_word"
 }
 }
 }
 }
 
 | 
几个特殊字段说明:
- location:地理坐标,里面包含精度、纬度
- all:一个组合字段,其目的是将多字段的值 利用copy_to合并,提供给用户搜索
地理坐标说明:

copy_to说明:

4.0.4.初始化RestClient
在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。
分为三步:
1)引入es的RestHighLevelClient依赖:
| 12
 3
 4
 
 | <dependency><groupId>org.elasticsearch.client</groupId>
 <artifactId>elasticsearch-rest-high-level-client</artifactId>
 </dependency>
 
 | 
2)因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:
| 12
 3
 4
 
 | <properties><java.version>1.8</java.version>
 <elasticsearch.version>7.12.1</elasticsearch.version>
 </properties>
 
 | 
3)初始化RestHighLevelClient:
初始化的代码如下:
| 12
 3
 
 | RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.150.101:9200")
 ));
 
 | 
这里为了单元测试方便,我们创建一个测试类HotelIndexTest,然后将初始化的代码编写在@BeforeEach方法中:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 
 | package cn.itcast.hotel;
 import org.apache.http.HttpHost;
 import org.elasticsearch.client.RestHighLevelClient;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
 
 public class HotelIndexTest {
 private RestHighLevelClient client;
 
 @BeforeEach
 void setUp() {
 this.client = new RestHighLevelClient(RestClient.builder(
 HttpHost.create("http://192.168.150.101:9200")
 ));
 }
 
 @AfterEach
 void tearDown() throws IOException {
 this.client.close();
 }
 }
 
 | 
4.1.创建索引库
4.1.1.代码解读
创建索引库的API如下:

代码分为三步:
- 1)创建Request对象。因为是创建索引库的操作,因此Request是CreateIndexRequest。
- 2)添加请求参数,其实就是DSL的JSON参数部分。因为json字符串很长,这里是定义了静态字符串常量MAPPING_TEMPLATE,让代码看起来更加优雅。
- 3)发送请求,client.indices()方法的返回值是IndicesClient类型,封装了所有与索引库操作有关的方法。
4.1.2.完整示例
在hotel-demo的cn.itcast.hotel.constants包下,创建一个类,定义mapping映射的JSON字符串常量:
| 12
 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
 
 | package cn.itcast.hotel.constants;
 public class HotelConstants {
 public static final String MAPPING_TEMPLATE = "{\n" +
 "  \"mappings\": {\n" +
 "    \"properties\": {\n" +
 "      \"id\": {\n" +
 "        \"type\": \"keyword\"\n" +
 "      },\n" +
 "      \"name\":{\n" +
 "        \"type\": \"text\",\n" +
 "        \"analyzer\": \"ik_max_word\",\n" +
 "        \"copy_to\": \"all\"\n" +
 "      },\n" +
 "      \"address\":{\n" +
 "        \"type\": \"keyword\",\n" +
 "        \"index\": false\n" +
 "      },\n" +
 "      \"price\":{\n" +
 "        \"type\": \"integer\"\n" +
 "      },\n" +
 "      \"score\":{\n" +
 "        \"type\": \"integer\"\n" +
 "      },\n" +
 "      \"brand\":{\n" +
 "        \"type\": \"keyword\",\n" +
 "        \"copy_to\": \"all\"\n" +
 "      },\n" +
 "      \"city\":{\n" +
 "        \"type\": \"keyword\",\n" +
 "        \"copy_to\": \"all\"\n" +
 "      },\n" +
 "      \"starName\":{\n" +
 "        \"type\": \"keyword\"\n" +
 "      },\n" +
 "      \"business\":{\n" +
 "        \"type\": \"keyword\"\n" +
 "      },\n" +
 "      \"location\":{\n" +
 "        \"type\": \"geo_point\"\n" +
 "      },\n" +
 "      \"pic\":{\n" +
 "        \"type\": \"keyword\",\n" +
 "        \"index\": false\n" +
 "      },\n" +
 "      \"all\":{\n" +
 "        \"type\": \"text\",\n" +
 "        \"analyzer\": \"ik_max_word\"\n" +
 "      }\n" +
 "    }\n" +
 "  }\n" +
 "}";
 }
 
 | 
在hotel-demo中的HotelIndexTest测试类中,编写单元测试,实现创建索引:
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | @Testvoid createHotelIndex() throws IOException {
 
 CreateIndexRequest request = new CreateIndexRequest("hotel");
 
 request.source(MAPPING_TEMPLATE, XContentType.JSON);
 
 client.indices().create(request, RequestOptions.DEFAULT);
 }
 
 | 
4.2.删除索引库
删除索引库的DSL语句非常简单:
与创建索引库相比:
- 请求方式从PUT变为DELTE
- 请求路径不变
- 无请求参数
所以代码的差异,注意体现在Request对象上。依然是三步走:
- 1)创建Request对象。这次是DeleteIndexRequest对象
- 2)准备参数。这里是无参
- 3)发送请求。改用delete方法
在hotel-demo中的HotelIndexTest测试类中,编写单元测试,实现删除索引:
| 12
 3
 4
 5
 6
 7
 
 | @Testvoid testDeleteHotelIndex() throws IOException {
 
 DeleteIndexRequest request = new DeleteIndexRequest("hotel");
 
 client.indices().delete(request, RequestOptions.DEFAULT);
 }
 
 | 
4.3.判断索引库是否存在
判断索引库是否存在,本质就是查询,对应的DSL是:
因此与删除的Java代码流程是类似的。依然是三步走:
- 1)创建Request对象。这次是GetIndexRequest对象
- 2)准备参数。这里是无参
- 3)发送请求。改用exists方法
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | @Testvoid testExistsHotelIndex() throws IOException {
 
 GetIndexRequest request = new GetIndexRequest("hotel");
 
 boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
 
 System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
 }
 
 | 
4.4.总结
JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。
索引库操作的基本步骤:
- 初始化RestHighLevelClient
- 创建XxxIndexRequest。XXX是Create、Get、Delete
- 准备DSL( Create时需要,其它是无参)
- 发送请求。调用RestHighLevelClient#indices().xxx()方法,xxx是create、exists、delete
5.RestClient操作文档
为了与索引库操作分离,我们再次参加一个测试类,做两件事情:
- 初始化RestHighLevelClient
- 我们的酒店数据在数据库,需要利用IHotelService去查询,所以注入这个接口
| 12
 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
 
 | package cn.itcast.hotel;
 import cn.itcast.hotel.pojo.Hotel;
 import cn.itcast.hotel.service.IHotelService;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 
 import java.io.IOException;
 import java.util.List;
 
 @SpringBootTest
 public class HotelDocumentTest {
 @Autowired
 private IHotelService hotelService;
 
 private RestHighLevelClient client;
 
 @BeforeEach
 void setUp() {
 this.client = new RestHighLevelClient(RestClient.builder(
 HttpHost.create("http://192.168.150.101:9200")
 ));
 }
 
 @AfterEach
 void tearDown() throws IOException {
 this.client.close();
 }
 }
 
 
 | 
5.1.新增文档
我们要将数据库的酒店数据查询出来,写入elasticsearch中。
5.1.1.索引库实体类
数据库查询后的结果是一个Hotel类型的对象。结构如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | @Data@TableName("tb_hotel")
 public class Hotel {
 @TableId(type = IdType.INPUT)
 private Long id;
 private String name;
 private String address;
 private Integer price;
 private Integer score;
 private String brand;
 private String city;
 private String starName;
 private String business;
 private String longitude;
 private String latitude;
 private String pic;
 }
 
 | 
与我们的索引库结构存在差异:
- longitude和latitude需要合并为location
因此,我们需要定义一个新的类型,与索引库结构吻合:
| 12
 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
 
 | package cn.itcast.hotel.pojo;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
 @Data
 @NoArgsConstructor
 public class HotelDoc {
 private Long id;
 private String name;
 private String address;
 private Integer price;
 private Integer score;
 private String brand;
 private String city;
 private String starName;
 private String business;
 private String location;
 private String pic;
 
 public HotelDoc(Hotel hotel) {
 this.id = hotel.getId();
 this.name = hotel.getName();
 this.address = hotel.getAddress();
 this.price = hotel.getPrice();
 this.score = hotel.getScore();
 this.brand = hotel.getBrand();
 this.city = hotel.getCity();
 this.starName = hotel.getStarName();
 this.business = hotel.getBusiness();
 this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
 this.pic = hotel.getPic();
 }
 }
 
 
 | 
5.1.2.语法说明
新增文档的DSL语句如下:
| 12
 3
 4
 5
 
 | POST /{索引库名}/_doc/1{
 "name": "Jack",
 "age": 21
 }
 
 | 
对应的java代码如图:

可以看到与创建索引库类似,同样是三步走:
- 1)创建Request对象
- 2)准备请求参数,也就是DSL中的JSON文档
- 3)发送请求
变化的地方在于,这里直接使用client.xxx()的API,不再需要client.indices()了。
5.1.3.完整代码
我们导入酒店数据,基本流程一致,但是需要考虑几点变化:
- 酒店数据来自于数据库,我们需要先查询出来,得到hotel对象
- hotel对象需要转为HotelDoc对象
- HotelDoc需要序列化为json格式
因此,代码整体步骤如下:
- 1)根据id查询酒店数据Hotel
- 2)将Hotel封装为HotelDoc
- 3)将HotelDoc序列化为JSON
- 4)创建IndexRequest,指定索引库名和id
- 5)准备请求参数,也就是JSON文档
- 6)发送请求
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | @Testvoid testAddDocument() throws IOException {
 
 Hotel hotel = hotelService.getById(61083L);
 
 HotelDoc hotelDoc = new HotelDoc(hotel);
 
 String json = JSON.toJSONString(hotelDoc);
 
 
 IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
 
 request.source(json, XContentType.JSON);
 
 client.index(request, RequestOptions.DEFAULT);
 }
 
 | 
5.2.查询文档
5.2.1.语法说明
查询的DSL语句如下:
非常简单,因此代码大概分两步:
不过查询的目的是得到结果,解析为HotelDoc,因此难点是结果的解析。完整代码如下:

可以看到,结果是一个JSON,其中文档放在一个_source属性中,因此解析就是拿到_source,反序列化为Java对象即可。
与之前类似,也是三步走:
- 1)准备Request对象。这次是查询,所以是GetRequest
- 2)发送请求,得到结果。因为是查询,这里调用client.get()方法
- 3)解析结果,就是对JSON做反序列化
5.2.2.完整代码
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | @Testvoid testGetDocumentById() throws IOException {
 
 GetRequest request = new GetRequest("hotel", "61082");
 
 GetResponse response = client.get(request, RequestOptions.DEFAULT);
 
 String json = response.getSourceAsString();
 
 HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
 System.out.println(hotelDoc);
 }
 
 | 
5.3.删除文档
删除的DSL是这样的:
与查询相比,仅仅是请求方式从DELETE变成GET,可以想象Java代码应该依然是三步走:
- 1)准备Request对象,因为是删除,这次是DeleteRequest对象。要指定索引库名和id
- 2)准备参数,无参
- 3)发送请求。因为是删除,所以是client.delete()方法
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
| 12
 3
 4
 5
 6
 7
 
 | @Testvoid testDeleteDocument() throws IOException {
 
 DeleteRequest request = new DeleteRequest("hotel", "61083");
 
 client.delete(request, RequestOptions.DEFAULT);
 }
 
 | 
5.4.修改文档
5.4.1.语法说明
修改我们讲过两种方式:
- 全量修改:本质是先根据id删除,再新增
- 增量修改:修改文档中的指定字段值
在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:
- 如果新增时,ID已经存在,则修改
- 如果新增时,ID不存在,则新增
这里不再赘述,我们主要关注增量修改。
代码示例如图:

与之前类似,也是三步走:
- 1)准备Request对象。这次是修改,所以是UpdateRequest
- 2)准备参数。也就是JSON文档,里面包含要修改的字段
- 3)更新文档。这里调用client.update()方法
5.4.2.完整代码
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | @Testvoid testUpdateDocument() throws IOException {
 
 UpdateRequest request = new UpdateRequest("hotel", "61083");
 
 request.doc(
 "price", "952",
 "starName", "四钻"
 );
 
 client.update(request, RequestOptions.DEFAULT);
 }
 
 | 
5.5.批量导入文档
案例需求:利用BulkRequest批量将数据库数据导入到索引库中。
步骤如下:
5.5.1.语法说明
批量处理BulkRequest,其本质就是将多个普通的CRUD请求组合在一起发送。
其中提供了一个add方法,用来添加其他请求:

可以看到,能添加的请求包括:
- IndexRequest,也就是新增
- UpdateRequest,也就是修改
- DeleteRequest,也就是删除
因此Bulk中添加了多个IndexRequest,就是批量新增功能了。示例:

其实还是三步走:
- 1)创建Request对象。这里是BulkRequest
- 2)准备参数。批处理的参数,就是其它Request对象,这里就是多个IndexRequest
- 3)发起请求。这里是批处理,调用的方法为client.bulk()方法
我们在导入酒店数据时,将上述代码改造成for循环处理即可。
5.5.2.完整代码
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | @Testvoid testBulkRequest() throws IOException {
 
 List<Hotel> hotels = hotelService.list();
 
 
 BulkRequest request = new BulkRequest();
 
 for (Hotel hotel : hotels) {
 
 HotelDoc hotelDoc = new HotelDoc(hotel);
 
 request.add(new IndexRequest("hotel")
 .id(hotelDoc.getId().toString())
 .source(JSON.toJSONString(hotelDoc), XContentType.JSON));
 }
 
 client.bulk(request, RequestOptions.DEFAULT);
 }
 
 | 
5.6.小结
文档操作的基本步骤:
- 初始化RestHighLevelClient
- 创建XxxRequest。XXX是Index、Get、Update、Delete、Bulk
- 准备参数(Index、Update、Bulk时需要)
- 发送请求。调用RestHighLevelClient#.xxx()方法,xxx是index、get、update、delete、bulk
- 解析结果(Get时需要)