elasticsearch
1. Elasticsearch 入门
1.1 什么是 Elasticsearch
Elasticsearch 是由 elastic 公司开发的一套搜索引擎技术,它是 elastic 技术栈中的一部分。完整的技术栈包括:
- Elasticsearch:用于数据存储、计算和搜索
- Logstash/Beats:用于数据收集
- Kibana:用于数据可视化
1.1.1 安装 Elasticsearch
1 |
|
1.1.2.安装 Kibana
通过下面的 Docker 命令,即可部署 Kibana:
1 |
|
1.2 倒排索引
1.2.1 正序索引
1 |
|
- 1)检查到搜索条件为 like ‘%手机%’,需要找到 title 中包含手机的数据
- 2)逐条遍历每行数据(每个叶子节点),比如第 1 次拿到 id 为 1 的数据
- 3)判断数据中的 title 字段值是否符合条件
- 4)如果符合则放入结果集,不符合则丢弃
- 5)回到步骤 1
1.2.2 倒排索引
倒排索引中有两个非常重要的概念:
- 文档(Document):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息
- 词条(Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
创建倒排索引是对正向索引的一种特殊处理和应用,流程如下:
- 将每一个文档的数据利用分词算法根据语义拆分,得到一个个词条
- 创建表,每行数据包括词条、词条所在文档 id、位置等信息
- 因为词条唯一性,可以给词条创建正向索引
此时形成的这张以词条为索引的表,就是倒排索引表,两者对比如下:
那么为什么一个叫做正向索引,一个叫做倒排索引呢?
- 正向索引是最传统的,根据 id 索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。
- 而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的 id,然后根据 id 获取文档。是根据词条找文档的过程。
1.3 IK 分词器
Elasticsearch 的关键就是倒排索引,而倒排索引依赖于对文档内容的分词,而分词则需要高效、精准的分词算法,IK 分词器就是这样一个中文分词算法。
1.3.1 IK 分词器的安装
首先,查看之前安装的 Elasticsearch 容器的 plugins 数据卷目录:
1 |
|
可以看到 elasticsearch 的插件挂载到了/var/lib/docker/volumes/es-plugins/_data 这个目录。我们需要把 IK 分词器上传至这个目录。
最后重启 Elasticsearch 容器
1 |
|
1.3.2 IK 分词器的使用
在 Kibana 的 DevTools 上来测试分词器的效果:
1 |
|
2.索引库操作
2.1 Mapping 映射属性
Mapping 是对索引库中文档的约束,常见的 Mapping 属性包括:
- type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip 地址)
- 数值:long、integer、short、byte、double、float、
- 布尔:boolean
- 日期:date
- 对象:object
- index:是否创建索引,默认为 true
- analyzer:使用哪种分词器
- properties:该字段的子字段
2.2.索引库的 CRUD
由于 Elasticsearch 采用的是 Restful 风格的 API,因此其请求方式和路径相对都比较规范,而且请求参数也都采用 JSON 风格。
我们直接基于 Kibana 的 DevTools 来编写请求做测试,由于有语法提示,会非常方便。
2.2.1.创建索引库和映射
基本语法:
- 请求方式:PUT
- 请求路径:/索引库名,可以自定义
- 请求参数:mapping 映射
1 |
|
2.2.2.查询索引库
基本语法:
- 请求方式:GET
- 请求路径:/索引库名
- 请求参数:无
1 |
|
2.2.3.修改索引库
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改 mapping。
虽然无法修改 mapping 中已有的字段,但是却允许添加新的字段到 mapping 中,因为不会对倒排索引产生影响。因此修改索引库能做的就是向索引库中添加新字段,或者更新索引库的基础属性。
1 |
|
2.2.4.删除索引库
语法:
- 请求方式:DELETE
- 请求路径:/索引库名
- 请求参数:无
1 |
|
3.文档操作
有了索引库,接下来就可以向索引库中添加数据了。
Elasticsearch 中的数据其实就是 JSON 风格的文档。操作文档自然保护增、删、改、查等几种常见操作
3.1 新增文档
1 |
|
3.2.查询文档
1 |
|
3.3 删除文档
1 |
|
3.4 修改文档
3.4.1.全量修改
全量修改是覆盖原来的文档,其本质是两步操作:
- 根据指定的 id 删除文档
- 新增一个相同 id 的文档
注意:如果根据 id 删除时,id 不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
1 |
|
3.4.2.局部修改
局部修改是只修改指定 id 匹配的文档中的部分字段。
1 |
|
3.5 批处理
1 |
|
- index 代表新增操作
- _index:指定索引库名
- _id 指定要操作的文档 id
- { “field1” : “value1” }:则是要新增的文档内容
- delete 代表删除操作
- _index:指定索引库名
- _id 指定要操作的文档 id
- update 代表更新操作
- _index:指定索引库名
- _id 指定要操作的文档 id
- { “doc” : {“field2” : “value2”} }:要更新的文档字段
4. RestApi
4.1 引入依赖
1 |
|
因为 SpringBoot 默认的 ES 版本是 7.17.10,所以我们需要覆盖默认的 ES 版本
1 |
|
5. DSL 查询
5.1 叶子查询
5.1.1.全文检索查询
- 全文检索查询(Full Text Queries):利用分词器对用户输入搜索条件先分词,得到词条,然后再利用倒排索引搜索词条。例如:
- match:
- multi_match
- 精确查询(Term-level queries):不对用户输入搜索条件分词,根据字段内容精确值匹配。但只能查找 keyword、数值、日期、boolean 类型的字段。例如:
- ids
- term
- range
- 地理坐标查询:用于搜索地理位置,搜索方式很多,例如:
- geo_bounding_box:按矩形搜索
- geo_distance:按点和半径搜索
1 |
|
5.1.2.精确查询
精确查询,英文是 Term-level query,顾名思义,词条级别的查询。也就是说不会对用户输入的搜索条件再分词,而是作为一个词条,与搜索的字段内容精确值匹配。因此推荐查找 keyword、数值、日期、boolean 类型的字段。例如:
- id
- price
- 城市
- 地名
- 人名
等等,作为一个整体才有含义的字段。
1 |
|
5.1.3 range 查询
再来看下 range 查询,语法如下:
1 |
|
range 是范围查询,对于范围筛选的关键字有:
- gte:大于等于
- gt:大于
- lte:小于等于
- lt:小于
5.2 复合查询
5.2.1 bool 查询
bool 查询,即布尔查询。就是利用逻辑运算来组合一个或多个查询子句的组合。bool 查询支持的逻辑运算有:
- must:必须匹配每个子查询,类似“与”
- should:选择性匹配子查询,类似“或”
- must_not:必须不匹配,不参与算分,类似“非”
- filter:必须匹配,不参与算分
1 |
|
5.3 排序
elasticsearch 默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。不过分词字段无法排序,能参与排序字段类型有:keyword 类型、数值类型、地理坐标类型、日期类型等。
1 |
|
5.4 分页
elasticsearch 默认情况下只返回 top10 的数据。而如果要查询更多数据就需要修改分页参数了。
5.4.1 基础分页
elasticsearch 中通过修改 from、size 参数来控制要返回的分页结果:
- from:从第几个文档开始
- size:总共查询几个文档
类似于 mysql 中的 limit ?, ?
5.4.2 深度分页
elasticsearch 的数据一般会采用分片存储,也就是把一个索引中的数据分成 N 份,存储到不同节点上。这种存储方式比较有利于数据扩展,但给分页带来了一些麻烦。
比如一个索引库中有 100000 条数据,分别存储到 4 个分片,每个分片 25000 条数据。现在每页查询 10 条,查询第 99 页。那么分页查询的条件如下:
1 |
|
试想一下,假如我们现在要查询的是第 999 页数据呢,是不是要找第 9990~10000 的数据,那岂不是需要把每个分片中的前 10000 名数据都查询出来,汇总在一起,在内存中排序?如果查询的分页深度更深呢,需要一次检索的数据岂不是更多?
由此可知,当查询分页深度较大时,汇总数据过多,对内存和 CPU 会产生非常大的压力。
因此 elasticsearch 会禁止 from+ size 超过 10000 的请求。
针对深度分页,elasticsearch 提供了两种解决方案:
- search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
- scroll:原理将排序后的文档 id 形成快照,保存下来,基于快照做分页。官方已经不推荐使用。
假设你有一张按时间排序的日志表,想分页查询每页 10 条记录。
传统分页(from 和 size)
第一页:from=0, size=10
ES 返回第 1 到 10 条记录。
第二页:from=10, size=10
ES 需要从头遍历前 10 条记录,跳过它们,再返回第 11 到 20 条。
第 100 页:from=1000, size=10
ES 需要从头遍历前 1000 条记录,跳过它们,再返回第 1001 到 1010 条。
search_after 分页
第一页:size=10, sort=时间戳
ES 返回第 1 到 10 条记录,并记录第 10 条的时间戳(比如 2023-10-01 12:00:00)。
第二页:size=10, sort=时间戳, search_after=[2023-10-01 12:00:00]
ES 直接从 2023-10-01 12:00:00 之后开始查询,返回第 11 到 20 条记录。
第 100 页:size=10, sort=时间戳, search_after=[上次最后一条的时间戳]
ES 直接从上一次的最后一条记录开始查询,不需要遍历前面的数据。
总结:
大多数情况下,我们采用普通分页就可以了。查看百度、京东等网站,会发现其分页都有限制。例如百度最多支持 77 页,每页不足 20 条。京东最多 100 页,每页最多 60 条。
因此,一般我们采用限制分页深度的方式即可,无需实现深度分页。
5.5 高亮
观察页面源码,你会发现两件事情:
- 高亮词条都被加了
<em>
标签 <em>
标签都添加了红色样式
css 样式肯定是前端实现页面的时候写好的,但是前端编写页面的时候是不知道页面要展示什么数据的,不可能给数据加标签。而服务端实现搜索功能,要是有 elasticsearch 做分词搜索,是知道哪些词条需要高亮的。
因此词条的高亮标签肯定是由服务端提供数据的时候已经加上的。
因此实现高亮的思路就是:
- 用户输入搜索关键字搜索数据
- 服务端根据搜索关键字到 elasticsearch 搜索,并给搜索结果中的关键字词条添加 html 标签
- 前端提前给约定好的 html 标签添加 CSS 样式
1 |
|
- 搜索必须有查询条件,而且是全文检索类型的查询条件,例如 match
- 参与高亮的字段必须是 text 类型的字段
- 默认情况下参与高亮的字段要与搜索字段一致,除非添加:required_field_match=false