ElasticSearch基础入门
在某些数据量非常大的项目中,一定会提供搜索功能。如果搜索功能仅仅使用数据库模糊查询实现,就会带来很多问题:
- 效率低。由于数据库模糊查询不走索引,在数据量较大的时候,查询性能很差。数据库模糊查询随着表数据量的增多,查询性能的下降会非常明显,
- 功能单一。数据库的模糊搜索功能单一,匹配条件非常苛刻,必须恰好包含用户搜索的关键字。
而使用搜索引擎就没有上述问题。
elasticsearch
ElasticSearch是一款非常强大的开源搜索引擎,支持的功能非常多
认识和安装
Elasticsearch是elastic技术栈中的一部分。完整的技术栈包括:
- Elasticsearch:用于数据存储、计算和搜索
- Logstash/Beats:用于数据收集
- Kibana:用于数据可视化
整套技术栈被称为ELK,经常用来做日志收集、系统监控和状态分析等等,其核心就是用来存储、搜索、计算的Elasticsearch
需要安装的内容有:
- Elasticsearch:提供核心的数据存储、搜索、分析功能。
- Kibana:Elasticsearch对外提供的是Restful风格的API,任何操作都可以通过发送http请求来完成。不过http请求的方式、路径、还有请求参数的格式都有严格的规范。要借助于Kibana服务
而且,Kibana的功能非常强大,包括:
- 对Elasticsearch数据的搜索、展示
- 对Elasticsearch数据的统计、聚合,并形成图形化报表、图形
- 对Elasticsearch的集群状态监控
- 它还提供了一个开发控制台(DevTools),在其中对Elasticsearch的Restful的API接口提供了语法提示
安装elasticsearch
通过Docker命令安装单机版本的elasticsearch,这里采用的是elasticsearch7版本:
docker run -d \ |
安装完成后,访问9200端口,即可看到响应的Elasticsearch服务的基本信息。
安装Kibana
通过Docker命令部署Kibana:
docker run -d \ |
安装完成后,直接访问5601端口,即可看到控制台页面,选择Explore on my own
之后,进入主页面,然后选中Dev tools
,进入开发工具页面,之后主要在这里预先输入ES查询语句。
倒排索引
ES之所以有如此高性能的搜索表现,正是得益于底层的倒排索引技术。倒排索引的概念是基于MySQL这样的正向索引而言的。
正向索引
例如有一张名为tb_goods
的表:
id | title | price |
---|---|---|
1 | 小米手机 | 3499 |
2 | 华为手机 | 4999 |
3 | 华为小米充电器 | 49 |
4 | 小米手环 | 49 |
… | … | … |
其中的id
字段已经创建了索引,由于索引底层采用了B+树结构,因此我们根据id搜索的速度会非常快。但是其他字段例如title
,只在叶子节点上存在。
因此要根据title
搜索的时候只能遍历树中的每一个叶子节点,判断title数据是否符合要求。
比如用户的SQL语句为:
select * from tb_goods where title like '%手机%'; |
那搜索的大概流程为:
- 1)检查到搜索条件为
like '%手机%'
,需要找到title
中包含手机
的数据 - 2)逐条遍历每行数据(每个叶子节点),比如第1次拿到
id
为1的数据 - 3)判断数据中的
title
字段值是否符合条件 - 4)如果符合则放入结果集,不符合则丢弃
- 5)回到步骤1
当搜索条件为模糊匹配时,由于索引无法生效,导致从索引查询退化为全表扫描,效率很差。因此,正向索引适合于根据索引字段的精确搜索,不适合基于部分词条的模糊匹配。而倒排索引恰好解决的就是根据部分词条模糊匹配的问题。
倒排索引
倒排索引中有两个概念:
- 文档(
Document
):用来搜索的数据,其中的每一条数据就是一个文档 - 词条(
Term
):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条
创建倒排索引是对正向索引的一种特殊处理和应用,流程为:
- 将每一个文档的数据利用分词算法根据语义拆分,得到一个个词条
- 创建表,每行数据包括词条、词条所在文档id、位置等信息
- 因为词条唯一性,可以给词条创建正向索引
此时形成的这张以词条为索引的表,就是倒排索引表,两者对比如下:
正向索引
id(索引) | title | price |
---|---|---|
1 | 小米手机 | 3499 |
2 | 华为手机 | 4999 |
3 | 华为小米充电器 | 49 |
4 | 小米手环 | 49 |
… | … | … |
倒排索引
词条(索引) | 文档id |
---|---|
小米 | 1,3,4 |
手机 | 1,2 |
华为 | 2,3 |
充电器 | 3 |
手环 | 4 |
倒排索引的搜索流程如下为:
1)用户输入条件"华为手机"
进行搜索。
2)对用户输入条件分词,得到词条:华为
、手机
3)拿着词条在倒排索引中查找(由于词条有索引,查询效率很高),即可得到包含词条的文档id:1、2、3
4)拿着文档id
到正向索引中查找具体文档即可(由于id
也有索引,查询效率也很高)
正向和倒排的优缺点
正向索引:
- 优点:
- 可以给多个字段创建索引
- 根据索引字段搜索、排序速度非常快
- 缺点:
- 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
倒排索引:
- 优点:
- 根据词条搜索、模糊搜索时,速度非常快
- 缺点:
- 只能给词条创建索引,而不是字段
- 无法根据字段做排序
基础概念
elasticsearch中有很多独有的概念,与mysql中略有差别,但也有相似之处。
文档和字段
elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json
格式后存储在elasticsearch
中:
{ |
因此,原本数据库中的一行数据就是ES中的一个JSON文档;而数据库中每行数据都包含很多列,这些列就转换为JSON文档中的字段(Field)。
索引和映射
随着业务发展,需要在es中存储的文档也会越来越多,比如有商品的文档、用户的文档、订单文档等等,后期将类型相同的文档集中在一起管理,称为索引(Index)。
例如:
- 所有用户文档,就可以组织在一起,称为用户的索引
- 所有商品的文档,可以组织在一起,称为商品的索引
- 所有订单的文档,可以组织在一起,称为订单的索引
可以把索引当做是数据库中的表。
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
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 |
- Mysql:擅长事务类型操作,可以确保数据的安全和一致性
- Elasticsearch:擅长海量数据的搜索、分析、计算
项目中往往两者结合使用:
- 对安全性要求较高的写操作,使用mysql实现
- 对查询性能要求较高的搜索需求,使用elasticsearch实现
- 两者再基于某种方式,实现数据的同步,保证一致性
IK分词器
Elasticsearch的关键就是倒排索引,而倒排索引依赖于对文档内容的分词,而分词则需要高效、精准的分词算法,IK分词器就是这样的中文分词算法。
安装IK分词器
运行命令:
docker exec -it es ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip |
重启es容器:
docker restart es |
使用IK分词器
IK分词器包含两种模式:
ik_smart
——智能语义切分:可以在词典库中识别词语,将句子分成一个个词语ik_max_word
——最细粒度切分:除了将句子分成一个个词语之外,还分出一个个字,便于精确查找
拓展词典
互联网发展伴随着越来越多的新词语,而这些新词语在ik分词器的原始词典中并不存在,所以要想正确分词,词库也需要不断的更新,所以分词器提供了扩展词汇的功能。
1)打开IK分词器config目录
注意:如果ik插件下没有config目录,要么自己手动创建config目录
和IKAnalyzer.cfg.xml
文件,要么需要下载复制到服务器中
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-v7.12.1.zip |
2)在IKAnalyzer.cfg.xml配置文件内容添加:
|
3)在IK分词器的config目录新建一个 ext.dic
,在其中写入想要扩展的词汇
4)重启elasticsearch
docker restart es |
索引库操作
Index就类似数据库表,Mapping映射就类似表的结构。我们要向es中存储数据,必须先创建Index和Mapping
Mapping映射属性
Mapping是对索引库中文档的约束,常见的Mapping属性包括:
type
:字段数据类型,常见的简单类型有:- 字符串:
text
(可分词的文本)、keyword
(精确值,例如:品牌、国家、ip地址) - 数值:
long
、integer
、short
、byte
、double
、float
、 - 布尔:
boolean
- 日期:
date
- 对象:
object
- 字符串:
index
:是否创建索引,默认为true
analyzer
:使用哪种分词器properties
:该字段的子字段
例如下面的json文档:
{ |
对应的每个字段映射(Mapping):
字段名 | 字段类型 | 类型说明 | 是否参与搜索 | 是否参与分词 | 分词器 |
---|---|---|---|---|---|
age | integer |
整数 | 是 | 否 | —— |
weight | float |
浮点数 | 是 | 否 | —— |
isMarried | boolean |
布尔 | 是 | 否 | —— |
info | text |
字符串,需要分词 | 是 | 是 | IK |
keyword |
字符串,但是不分词 | 否 | 否 | —— | |
score | float |
只看数组中元素类型 | 是 | 否 | —— |
firstName | keyword |
字符串,但是不分词 | 是 | 否 | —— |
lastName | keyword |
字符串,但是不分词 | 是 | 否 | —— |
索引库的CRUD
由于Elasticsearch采用的是Restful风格的API,因此其请求方式和路径相对都比较规范,而且请求参数也都采用JSON风格。
创建索引库和映射
基本语法:
- 请求方式:
PUT
- 请求路径:
/索引库名
,可以自定义 - 请求参数:
mapping
映射
格式:
PUT /索引库名 |
查询索引库
基本语法:
- 请求方式:GET
- 请求路径:/索引库名
- 请求参数:无
格式:
GET /索引库名 |
修改索引库
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引。因此索引库一旦创建,无法修改mapping。
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。因此修改索引库能做的就是向索引库中添加新字段,或者更新索引库的基础属性。
语法说明:
PUT /索引库名/_mapping |
删除索引库
语法:
- 请求方式:DELETE
- 请求路径:/索引库名
- 请求参数:无
格式:
DELETE /索引库名 |
文档操作
新增文档
语法:
POST /索引库名/_doc/文档id |
查询文档
根据rest风格,新增是post,查询是get,需在后面加上文档id。
语法:
GET /索引库名/_doc/id |
删除文档
删除使用DELETE请求,同样,需要根据id进行删除:
语法:
DELETE /索引库名/_doc/id |
修改文档
修改有两种方式:
- 全量修改:直接覆盖原来的文档
- 局部修改:修改文档中的部分字段
全量修改
语法与新增文档相同,全量修改是覆盖原来的文档,其本质是两步操作:
- 根据指定的id删除文档
- 新增一个相同id的文档
语法:
PUT /索引库名/_doc/文档id |
局部修改
局部修改是只修改指定id匹配的文档中的部分字段。
语法:
POST /索引库名/_update/文档id |
批处理
批处理采用POST请求,基本语法如下:
POST /_bulk |
其中:
index
代表新增操作_index
:指定索引库名_id
指定要操作的文档id{ "field1" : "value1" }
:则是要新增的文档内容
delete
代表删除操作_index
:指定索引库名_id
指定要操作的文档id
update
代表更新操作_index
:指定索引库名_id
指定要操作的文档id{ "doc" : {"field2" : "value2"} }
:要更新的文档字段
RestAPI
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES
初始化RestClient
在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient
的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接
1)引入es
的RestHighLevelClient
依赖:
<dependency> |
2)因为SpringBoot默认的ES版本是7.17.10
,需要覆盖默认的ES版本:
<properties> |
3)初始化RestHighLevelClient:
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder( |
这里为了单元测试方便,我们创建一个测试类IndexTest
,然后将初始化的代码编写在@BeforeEach
方法中:
public class IndexTest { |
创建索引库
由于要实现对商品搜索,所以需要将商品添加到Elasticsearch中,不过需要根据搜索业务的需求来设定索引库结构,而不是一股脑的把MySQL数据写入Elasticsearch
Mapping映射
实现搜索功能需要的字段包括三大部分:
- 搜索过滤字段
- 分类
- 品牌
- 价格
- 排序字段
- 默认:按照更新时间降序排序
- 销量
- 价格
- 展示字段
- 商品id:用于点击后跳转
- 图片地址
- 是否是广告推广商品
- 名称
- 价格
- 评价数量
- 销量
结合数据库表结构,以上字段对应的mapping映射属性如下:
字段名 | 字段类型 | 类型说明 | 是否参与搜索 | 是否参与分词 | 分词器 |
---|---|---|---|---|---|
id | long |
长整数 | 是 | 否 | —— |
name | text |
字符串,参与分词搜索 | 是 | 是 | IK |
price | integer |
以分为单位,所以是整数 | 是 | 否 | —— |
stock | integer |
字符串,但需要分词 | 是 | 否 | —— |
image | keyword |
字符串,但是不分词 | 否 | 否 | —— |
category | keyword |
字符串,但是不分词 | 是 | 否 | —— |
brand | keyword |
字符串,但是不分词 | 是 | 否 | —— |
sold | integer |
销量,整数 | 是 | 否 | —— |
commentCount | integer |
评价,整数 | 否 | 否 | —— |
isAD | boolean |
布尔类型 | 是 | 否 | —— |
updateTime | Date |
更新时间 | 是 | 否 | —— |
最终索引库文档结构应该是这样:
PUT /items |
创建索引
分为三步:
1)创建Request对象。因为是创建索引库的操作,因此Request是CreateIndexRequest
2)添加请求参数。Json格式的Mapping映射参数
3)发送请求。client.indices()
方法的返回值是IndicesClient
类型,封装了所有与索引库操作有关的方法。例如创建索引、删除索引、判断索引是否存在等
示例:
void testCreateIndex() throws IOException { |
删除索引库
与创建索引库相比:
- 请求方式从PUT变为DELTE
- 请求路径不变
- 无请求参数
所以代码的差异,注意体现在Request对象上。流程如下:
1)创建Request对象。DeleteIndexRequest对象
2)准备参数。省略
3)发送请求。delete方法
|
判断索引库是否存在
判断索引库是否存在,本质就是查询,对应的请求语句是:
GET /hotel |
因此与删除的Java代码流程是类似的,流程如下:
1)创建Request对象。GetIndexRequest对象
2)准备参数。省略
3)发送请求。exists方法
|
总结
JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()
方法来获取索引库的操作对象。
索引库操作的基本步骤:
- 初始化
RestHighLevelClient
- 创建XxxIndexRequest。XXX是
Create
、Get
、Delete
- 准备请求参数(
Create
时需要,其它是无参,可以省略) - 发送请求。调用
RestHighLevelClient#indices().xxx()
方法,xxx是create
、exists
、delete
RestClient操作文档
新增文档
新增文档的请求语法如下:
POST /索引库名/_doc/1 |
1)创建Request对象,这里是IndexRequest
,因为添加文档就是创建倒排索引的过程
2)准备请求参数,本例中就是Json文档
3)发送请求
这里直接使用client.xxx()
的API
整体步骤为:
- 1)根据id查询商品数据
Item
- 2)将
Item
封装为ItemDoc
- 3)将
ItemDoc
序列化为JSON - 4)创建IndexRequest,指定索引库名和id
- 5)准备请求参数,也就是JSON文档
- 6)发送请求
|
查询文档
查询的请求语句如下:
GET /索引库名/_doc/id |
- 创建Request对象
- 发送请求
流程如下:
1)准备Request对象。这次是查询,所以是GetRequest
2)发送请求,得到结果。因为是查询,这里调用client.get()
方法
3)解析结果,对JSON做反序列化
|
删除文档
删除的请求语句如下:
DELETE /hotel/_doc/{id} |
与查询相比,仅仅是请求方式从DELETE
变成GET
|
修改文档
有两种方式:
- 全量修改:本质是先根据id删除,再新增
- 局部修改:修改文档中的指定字段值
在RestClient的API中,全量修改与新增的API完全一致。
局部修改的请求语法如下:
POST /索引库名/_update/id |
1)准备Request
对象。这次是修改,所以是UpdateRequest
2)准备参数。也就是JSON文档,里面包含要修改的字段
3)更新文档。这里调用client.update()
方法
|
批量导入文档
如果要将大量数据导入索引库,肯定不能逐条导入,而是采用批处理方案。常见的方案有:
- 利用Logstash批量导入
- 需要安装Logstash
- 对数据的再加工能力较弱
- 无需编码
- 利用JavaAPI批量导入
- 需要编码,但基于JavaAPI,学习成本低
- 更加灵活,可以任意对数据做再加工处理后写入索引库
在这里使用JavaAPI导入。
语法说明
批处理与前面讲的文档的CRUD步骤基本一致:
- 创建Request,这次用的是
BulkRequest
- 准备请求参数
- 发送请求,用到
client.bulk()
方法
BulkRequest
本身其实并没有请求参数,其本质就是将多个普通的CRUD请求组合在一起发送。例如:
- 批量新增文档,就是给每个文档创建一个
IndexRequest
请求,然后封装到BulkRequest
中,一起发出。 - 批量删除,就是创建N个
DeleteRequest
请求,然后封装到BulkRequest
,一起发出
因此BulkRequest
中提供了add
方法,用以添加其它CRUD的请求:
能添加的请求有:
IndexRequest
UpdateRequest
DeleteRequest
因此Bulk中添加了多个IndexRequest
,就是批量新增功能了。
|
小结
文档操作的基本步骤:
- 初始化
RestHighLevelClient
- 创建XxxRequest。
- XXX是
Index
、Get
、Update
、Delete
、Bulk
- XXX是
- 准备参数(
Index
、Update
、Bulk
时需要) - 发送请求。
- 调用
RestHighLevelClient#.xxx()
方法,xxx是index
、get
、update
、delete
、bulk
- 调用
- 解析结果(
Get
时需要)