ElasticSearch安装与基本使用¶
约 2688 个字 485 行代码 预计阅读时间 15 分钟
ElasticSearch介绍¶
Elasticsearch是基于Lucene的分布式搜索与分析引擎,能够对大规模数据进行近实时的全文检索、结构化查询与聚合分析。它采用JSON文档存储模型,支持水平扩展、分片与副本机制,提供高可用与高性能,并常与Logstash、Kibana组成ELK/Elastic Stack,用于日志分析、全文检索、监控与数据可视化等场景
ElasticSearch相关概念¶
- index(索引):具有相同结构的文档集合,类似于关系型数据库的数据库实例(6.0.0版本type废弃后,索引的概念下降到等同于数据库表的级别)。一个集群里可以定义多个索引,如客户信息索引、商品分类索引、商品索引、订单索引、评论索引等等,分别定义自己的数据结构。索引命名要求全部使用小写,建立索引、搜索、更新、删除操作都需要用到索引名称
- type(类型):原本是在索引内进行的逻辑细分,但后来发现企业研发为了增强可阅读性和可维护性,制订的规范约束,同一个索引下很少还会再使用type进行逻辑拆分(如同一个索引下既有订单数据,又有评论数据),因而在6.0.0版本之后,此定义废弃
- document(文档):一个文档是一个可被索引的基础信息单元,document(文档)是JSON格式的,document就像是MySQL中某个Table里面每一行的数据,document中可以包含多个字段,每个字段可以是文本、数字、日期等类型
- field(字段):文档中的一个元素或属性,每个字段都有一个数据类型,如字符串、整数、日期等
- mapping(映射):类似于传统关系型数据中table的schema(定义了数据库中的数据如何组织,包括表的结构、字段的数据类型、键(如主键、外键)的设置等),用于定义一个索引的数据的结构(mapping中主要包括字段名、字段数据类型和字段索引类型)。在ES中,可以手动创建mapping,也可以采用默认创建方式。在默认配置下,ES可以根据插入的数据自动地创建mapping
将MySQL中的概念与ES概念进行类比:
| MySQL | ElasticSearch |
|---|---|
| 表Table | 索引Index |
| 数据行Row | 文档Document |
| 数据列Column | 字段Field |
| 模式Schema | 映射Mapping |
安装ElasticSearch¶
下面主要介绍在Docker上安装ElasticSearch,首先拉取ES镜像:
| Bash | |
|---|---|
1 | |
为了便于后续操作和维护ES,可以顺便安装Kibana,同样拉取Kibana镜像:
| Bash | |
|---|---|
1 | |
接着,需要保证ES和Kibana连接到同一个网络,这里可以创建一个自定义网络:
| Bash | |
|---|---|
1 | |
接着,先启动ES,再启动Kibana:
| Bash | |
|---|---|
1 2 3 4 5 6 7 8 9 | |
注意,需要确保ES启动成功后再启动Kibana,否则Kibana无法连接上ES,启动ES后,可以访问http://localhost:9200/进行验证
如果需要对中文句子进行分词,可以使用ES插件,ik分词器,点击网址(推荐下载8.5.3版本)进行下载,下载完成后,将这个压缩包放在容器映射的宿主机目录/es-plugins下,需要注意,要先创建一个子目录,例如elasticsearch-analysis-ik-8.5.3,再在该子目录中将压缩包进行解压,解压完成后重启ES容器即可
ik分词器默认有两种分词模式:
ik_smart:分词数量少,倾向于输出更语义完整的词,更适合短词或者关键词的检索场景ik_max_word:尽可能细地拆分,会输出所有可能的词,适合需要尽可能匹配更多结果、做词云、关键词统计、召回优先的场景
在Kibana开发工具中基本使用ElasticSearch¶
在Kibana的侧边栏找到“开发工具”,新增如下数据:
| JSON | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | |
接着,使用下面的命令进行所有数据的获取:
| JSON | |
|---|---|
1 | |
如果要查询单个数据,可以使用下面的命令:
| JSON | |
|---|---|
1 | |
如果要更新指定数据,使用下面的命令:
| JSON | |
|---|---|
1 2 3 4 5 6 | |
如果要删除指定数据,使用下面的命令:
| JSON | |
|---|---|
1 | |
如果要删除整个索引,可以使用下面的命令:
| JSON | |
|---|---|
1 | |
SpringBoot项目与ElasticSearch¶
首先引入Maven依赖:
| XML | |
|---|---|
1 2 3 4 | |
本次演示实现一个基于MySQL数据库获取用户列表,并可以支持根据用户用户名进行模糊搜索。数据库设计如下:
| SQL | |
|---|---|
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 | |
接着,在ElasticSearch中配置连接地址:
| YAML | |
|---|---|
1 2 3 | |
要想操作ElasticSearch首先得有一个索引,在开发工具中操作ElasticSearch时,如果不存在指定索引会报错提示索引不存在,但是使用代码操作时可以通过实体类让ElasticSearch创建索引,例如下面的实体类:
| Java | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
Note
需要注意,在ElasticSearch 8.5.3版本中存在使用@Id注解后会覆盖实际在Java代码中声明的类型(在ElasticSearch,_id默认为keyword类型),例如上面的代码,如果给userId用@Id,那么@Field(type = FieldType.Integer)就会失效,其他版本不确定是否有这种情况
有了索引之后,还需要操作索引的方法,这些方法可以通过一个父接口提供,但是首先需要创建子接口继承这个父接口:
| Java | |
|---|---|
1 2 3 4 5 | |
可以类比一下MybatisPlus的Mapper:
| Java | |
|---|---|
1 2 3 | |
在ElasticsearchRepository提供了一些接口:
| 方法 | 功能说明 |
|---|---|
save(T entity) | 保存单个实体:不存在则新增,已存在则更新覆盖 |
saveAll(Iterable<S> entities) | 批量保存:逐个执行save语义,新增或更新 |
deleteById(ID id) | 按主键删除指定文档 |
delete(T entity) | 按实体删除(根据实体 id),等价于 deleteById(entity.getId()) |
findAll(Sort sort) | 查询全部并按排序返回 |
findAll(Pageable pageable) | 分页查询全部,返回Page |
但是,如果要按照指定字段进行查询,需要自行编写接口,有两种思路:
- 在Spring Data中,可以按照方法名派生查询规则编写方法名
- 使用
@Query注解自行编写查询条件
在Spring Data中,有一下常见的规则:
- 字段前缀:
find/get/query/read/count/exists/delete+ 字段名 +By,其中方法名里的字段必须与实体字段一致(如Difficulty对应difficulty),例如findByDifficulty - 连接词:
And/Or/Not,例如findByTitleAndDifficulty/findByTitleOrContent - 比较关键字:
Equals(可省略)、GreaterThan、LessThan、Between、Like、Containing、In、IsNull等,例如findByDifficultyGreaterThan - 排序:
OrderByXxxAsc/Desc,方法参数可带Pageable/Sort,例如findByDifficultyOrderByCreateTimeDesc(Pageable pageable) - 限制条数:
findTop10By.../findFirst5By...
根据上面的规则,本次需要按照nickname进行查询,可以写为:
| Java | |
|---|---|
1 2 3 4 5 | |
在@Query中需要直接编写ElasticSearch的DSL JSON,常见规则如下:
-
@Query的内容就是ES查询DSLmatch/term/bool/should/must等,含义如下:match:全文检索,会对字段进行分词后匹配,适用于text类型字段term:精确匹配,不进行分词,适用于keyword/数值/布尔等字段bool:组合查询容器,用于把多个查询条件进行组合must:必须满足的条件(“且”逻辑),即必须包含关键词才返回should:可选条件(“或”逻辑),命中越多得分越高,即可匹配可不匹配,匹配了就更靠前,如果只有一个should则相当于must
bool包must/should,must/should里放match/term,格式如下:JSON 1 2 3 4 5 6 7 8 9 10 11
{ "bool": { "must": [ { "term": { "difficulty": 1 } } ], "should": [ { "match": { "title": "foo" } }, { "match": { "content": "bar" } } ] } } -
参数使用占位符从
?0开始依次对应方法参数,依次为?1、?2等 match适合全文分词匹配(text),term适合精确匹配(keyword/数字/布尔)- JSON字符串内双引号需要转义(
\"),否则会解析失败 - 方法参数可带
Pageable/Sort,分页与排序由Spring自动追加
例如:
| JSON | |
|---|---|
1 2 3 4 5 6 7 8 9 | |
其中:
bool:组合查询容器should:两个可选条件(标题匹配?0或 内容匹配?1)match:分词后的全文匹配minimum_should_match: 1:至少满足一个should条件即可命中
对应的方法为:
| Java | |
|---|---|
1 2 | |
回到当前项目,根据nickname进行模糊查询可以写为:
| JSON | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 | |
对应的方法为:
| Java | |
|---|---|
1 2 | |
接着编写测试的Controller、Service,如下:
| Java | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 | |
| Java | |
|---|---|
1 2 3 | |
| Java | |
|---|---|
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 | |
在PostMan测试分别测试不搜索和搜索数据:
| JSON | |
|---|---|
1 2 3 4 5 6 | |
结果:
| JSON | |
|---|---|
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 78 79 80 81 82 83 84 85 86 87 | |
| JSON | |
|---|---|
1 2 3 4 5 6 | |
结果:
| JSON | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
上面的搜索对于多字匹配较为精确,但是有时用户会输入一个单字,这个单字可能没被分词器当做一个词分离,例如:
| JSON | |
|---|---|
1 2 3 4 5 6 | |
结果是:
| JSON | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
但是根据数据库中的数据,nickname有“十”的不止一个,原因就是“十一”、“十二”等被当做一个完整的词语所以没有拆出“十”,解决这个问题的方式很简单粗暴,对于单字,直接用n-gram进行单字拆解,对于多字查询条件,就用ik:
首先,添加一个自定义分词配置文件,并在实体类引入这个配置文件,然后修改针对nickname字段的配置,确保可以动态指定字段:
在src/main/resources中添加文件名为elasticsearch-settings.json的文件,内容如下:
| JSON | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
| Java | |
|---|---|
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 | |
接着,修改nickname不为空时的查询逻辑:
| Java | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
Sort类介绍¶
Sort 是 Spring Data 提供的排序工具类,用于构建数据库/搜索引擎查询的排序规则
常见的方法如下:
| Java | |
|---|---|
1 | |
其中,direction表示升序和降序:
| Java | |
|---|---|
1 2 | |
properties表示排序字段名,可以使用单字段:
| Java | |
|---|---|
1 2 3 4 5 | |
也可以多字段排序:
| Java | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 | |
常用的其他方法:
| 方法 | 作用 |
|---|---|
sort.ascending() | 改为升序 |
sort.descending() | 改为降序 |
sort.and(otherSort) | 合并多个排序 |
Sort.unsorted() | 无排序(默认) |
特殊处理方法:
| Java | |
|---|---|
1 2 3 4 5 6 | |
Pageable类介绍¶
Pageable 是 Spring Data 提供的分页参数接口,用于封装分页查询的页码、每页大小、排序等信息。
创建Pageable的方式:
| Java | |
|---|---|
1 2 3 4 5 | |
常用方法如下:
| 方法 | 返回值 | 说明 |
|---|---|---|
getPageNumber() | int | 当前页码(从0开始) |
getPageSize() | int | 每页大小 |
getOffset() | long | 偏移量(跳过多少条) |
getSort() | Sort | 排序信息 |
next() | Pageable | 下一页 |
previous() | Pageable | 上一页 |
first() | Pageable | 第一页 |
需要注意的时页码问题:
| 前端习惯 | 后端(Pageable) | 转换 |
|---|---|---|
| 第1页 | 0 | page - 1 |
| 第2页 | 1 | page - 1 |
| 第n页 | n-1 | page - 1 |
| Java | |
|---|---|
1 2 3 4 | |
Page类介绍¶
| 方法 | 返回值类型 | 说明 | 示例值 |
|---|---|---|---|
getNumber() | int | 当前页码(从0开始) | 0, 1, 2... |
getNumber() + 1 | int | 当前页码(转为从1开始) | 1, 2, 3... |
getSize() | int | 每页大小 | 10 |
getTotalElements() | long | 总记录数 | 100 |
getTotalPages() | int | 总页数 | 10 |
getContent() | List<T> | 当前页数据列表 | List<UserDocument> |
hasContent() | boolean | 当前页是否有数据 | true / false |
hasNext() | boolean | 是否有下一页 | true / false |
hasPrevious() | boolean | 是否有上一页 | true / false |
isFirst() | boolean | 是否是第一页 | true / false |
isLast() | boolean | 是否是最后一页 | true / false |
nextPageable() | Pageable | 下一页的分页对象 | PageRequest |
previousPageable() | Pageable | 上一页的分页对象 | PageRequest |
示例:
| Java | |
|---|---|
1 2 3 4 5 6 7 8 9 | |