学习目标
搜索技术在我们日常生活的方方面面都会用到,例如:
这些搜索业务有一些可以使用数据库来完成,有一些却不行。因此我们今天会学习一种新的搜索方案,解决海量数据、复杂业务的搜索。
因为复杂搜索往往是模糊的查找,因此数据库索引基本都会实效,只能逐条数据判断。基本流程如下:
1)用户搜索数据,条件是title符合"谷歌创始人跳槽"
2)逐行获取数据,比如id为10的数据
3)判断数据中的title是否符合用户搜索条件
4)如果符合则放入结果集,不符合则丢弃。回到步骤1
如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ngKWz7d2-1614743158449)(assets/image-20200103154627038.png)]
如果有5条数据,则需要遍历并判断5次。如果有100万数据,则需要循环遍历和判断100万次。线性查找和判断,效率极差,一个10mb的硬盘文件,遍历一遍需要3秒。
要实现类似百度的复杂搜索,或者京东的商品搜索,如果使用传统的数据库存储数据,那么会存在一系列的问题:
但是,并不是说数据库就一无是处。在一些对业务有强数据一致性需求,事物需求的情况下,数据库是不可替代的。
只是在海量数据的搜索方面,需要有新的技术来解决,就是我们今天要学习的倒排索引技术。
全文检索:全部都检索,可以按照用户定义的查询规则任意查询,得到目标数据。
内部实现的原理:倒排索引是一种特殊的数据索引方式,虽然于数据库存储的数据结构没有太大差别,但是在检索方式上却大不一样。
Document
):用来检索的海量数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息Term
):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如,数据库中有下面的数据:
id | title | url |
---|---|---|
10 | 谷歌地图之父跳槽FaceBook | http://www.itcast.cn/10.html |
20 | 谷歌地图之父加盟FaceBook | http://www.itcast.cn/20.html |
30 | 谷歌地图创始人拉斯离开谷歌加盟Facebook | http://www.itcast.cn/30.html |
40 | 谷歌地图之父跳槽Facebook与Wave项目取消有关 | http://www.itcast.cn/40.html |
50 | 谷歌地图之父拉斯加盟社交网站Facebook | http://www.itcast.cn/50.html |
那么这里的每一行数据就是一条文档,如:
id | title | url |
---|---|---|
10 | 谷歌地图之父跳槽FaceBook | http://www.itcast.cn/10.html |
把标题字段分词,可以得到词语如:谷歌
就是一个词条
现在,假设用户要搜索"谷歌创始人跳槽"
,来看看倒排索引的传统查找在检索时的区别
倒排索引的数据存储方式与数据库类似,但检索方式不同。
全文检索主要是通过倒排索引的方式实现,倒排索引主要是分成两大步骤完成:
先看看倒排索引如何处理数据。
文档列表:
首先,倒排索引需要把文档数据逐个编号(从0递增),存储到文档表中。并且给每一个编号创建索引,这样根据编号检索文档的速度会非常快。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-smSMNcFr-1614743158453)(assets/image-20200103160217964.png)]
词条列表(Term Dictionary):
然后,对文档中的数据按照算法做分词,得到一个个的词条,记录词条和词条出现的文档的编号、位置、频率信息,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8isecZ5x-1614743158456)(assets/image-20200103161448460.png)]
然后给词条创建索引,这样根据词条匹配和检索的速度就非常快。
倒排索引的检索流程如下:
1)用户输入条件"谷歌创始人跳槽"
进行搜索。
2)对用户输入内容分词,得到词条:谷歌
、创始人
、跳槽
。
3)拿着词条到词条列表中查找,可以得到包含词条的文档编号:0、1、2、3、4。
4)拿着词条的编号到文档列表中查找具体文档。
如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1zna0noz-1614743158458)(assets/image-20200103163447165.png)]
虽然搜索会在两张表进行,但是每次都是根据索引查找,因此速度比传统搜索时的全表扫描速度要快的多。
在java语言中,对倒排索引的实现中最广为人知的就是Lucene了,目前主流的java搜索框架都是依赖Lucene来实现的。
Solr是建立在Apache Lucene ™之上的流行,快速,开放源代码的企业搜索平台。
Solr具有高度的可靠性,可伸缩性和容错性,可提供分布式索引,复制和负载平衡查询,自动故障转移和恢复,集中式配置等。Solr为许多世界上最大的互联网站点提供搜索和导航功能。
官网:https://lucene.apache.org/solr/
Elastic官网:https://www.elastic.co/cn/
Elastic是一系列产品的集合,比较知名的是ELK技术栈,其核心就是ElasticSearch:
Elasticsearch官网:https://www.elastic.co/cn/products/elasticsearch
Elasticsearch历史版本:https://www.elastic.co/cn/downloads/past-releases
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Thw1PgyQ-1614743158459)(assets/1526464283575.png)]
Elasticsearch是一个基于Lucene的搜索web服务,对外提供了一系列的Rest风格的API接口。因此任何语言的客户端都可以通过发送Http请求来实现ElasticSearch的操作。
ES的主要优势特点如下:
使用ES的案例场景:
1、GitHub抛弃了Solr,采取ElasticSearch 来做PB级的搜索。 “GitHub使用ElasticSearch搜索20TB 的数据,包括13亿文件和1300亿行代码”
2、维基百科:启动以elasticsearch为基础的核心搜索架构
3、SoundCloud:“SoundCloud使用ElasticSearch为1.8亿用户提供即时而精准的音乐搜索服务”
4、百度:百度目前广泛使用ElasticSearch作为文本数据分析,采集百度所有服务器上的各类指标数据及用户自定义数据,通过对各种数据进行多维分析展示,辅助定位分析实例异常或业务层面异常。目前覆盖百度内部20多个业务线(包括casio、云分析、网盟、预测、文库、直达号、钱包、风控等),单集群最大100台机器,200个ES节点,每天导入30TB+数据。 新浪使用ES 分析处理32亿条实时日志。
5、阿里使用ES 构建自己的日志采集和分析体系
6、京东到家订单中心 Elasticsearch 演进历程
Elasticsearch 做为一款功能强大的分布式搜索引擎,支持近实时的存储、搜索数据,在京东到家订单系统中发挥着巨大作用,目前订单中心ES集群存储数据量达到10亿个文档,日均查询量达到5亿。
7、滴滴 2016 年初开始构建 Elasticsearch 平台,如今已经发展到超过 3500+ Elasticsearch 实例,超过 5PB 的数据存储,峰值写入 tps 超过了 2000w/s 的超大规模。
8、携程Elasticsearch应用案例
9、去哪儿:订单中心基于elasticsearch 的解决方案
Solr和ElasticSearch如何选择?
企业生产中一般都会使用成熟的搜索产品,例如:Solr或者Elasticsearch,不过从性能来看Elasticsearch略胜一筹,因此我们今天的学习目标就是elasticsearch。
如果把Lucene比喻成一台发动机,那么Solr就是一台家用汽车,而Elasticsearch就是一台c超级跑车。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b5UXEj9z-1614743158461)(assets/image-20201115165603142.png)]
本章是基于Docker安装
docker run -id --name elasticsearch \
-e "cluster.name=elastic-cluster" \
-e "http.host=0.0.0.0" \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-e http.cors.enabled=true \
-e http.cors.allow-origin="*" \
-e http.cors.allow-headers=X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization \
-e http.cors.allow-credentials=true \
-v es-data:/usr/share/elasticsearch/data \
-v es-logs:/usr/share/elasticsearch/logs \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--hostname elasticsearch \
-p 9200:9200 \
-p 9300:9300 \
-p 5601:5601 \
elasticsearch:7.4.2
命令解释:
-e "cluster.name=es-docker-cluster"
:设置集群名称-e "http.host=0.0.0.0"
:监听的地址,可以外网访问-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"
:内存大小-e "discovery.type=single-node"
:非集群模式-v es-data:/usr/share/elasticsearch/data
:挂载逻辑卷,绑定es的数据目录-v es-logs:/usr/share/elasticsearch/logs
:挂载逻辑卷,绑定es的日志目录-v es-plugins:/usr/share/elasticsearch/plugins
:挂载逻辑卷,绑定es的插件目录--privileged
:授予逻辑卷访问权-p 9200:9200
:端口映射配置restart相关参数如下:
选项 作用 –restart=no 不自动重启容器. (默认) –restart=on-failure 容器发生error而退出(容器退出状态不为0)重启容器 on-failure:3 在容器非正常退出时重启容器,最多重启3次 –restart=unless-stopped 在容器退出时总是重启容器,但是不考虑在Docker守护进程启动时就已经停止了的容器 –restart=always 在容器已经stop掉或Docker stoped/restarted的时候才重启容器 如果已经启动的容器项目,则使用update更新:
docker update --restart=always 容器名/ID
防火墙配置:
# 开放端口 9200 9300 5601 firewall-cmd --zone=public --add-port=9200/tcp --permanent # 重启防火墙 firewall-cmd --reload # 查看放行端口 firewall-cmd --list-ports
ES提供了多种访问方式,官方推荐使用REST API,也是使用人数最多的访问方式。就是通过http协议,使用Restful的风格,按照es的api去操作数据,访问时我们需要传递给es json 参数,es处理后会给我们返回 json 的结果,不过浏览器不方便操作es 官方推荐使用kibana来操作ES
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-98yDvxqi-1614743158462)(assets/1526481256534-5439065.png)]
Kibana是一个基于Node.js的Elasticsearch索引库数据统计工具,可以利用Elasticsearch的聚合功能,生成各种图表,如柱形图,线状图,饼图等。
而且还提供了操作Elasticsearch索引数据的控制台,并且提供了一定的API提示,非常有利于我们学习Elasticsearch的语法。
docker run -di --name kibana -e ELASTICSEARCH_HOSTS=http://127.0.0.1:9200 --network=container:elasticsearch kibana:7.4.2
# Head 插件(了解) docker run -d --name es_admin -p 9100:9100 mobz/elasticsearch-head:5
输入地址访问:http://192.168.200.129:5601
根据我们之前讲解的倒排索引原理,当我们向elasticsearch插入一条文档数据时,elasticsearch需要对数据分词,分词到底如何完成呢?
ElasticSearch 内置分词器
kibana中可以测试分词器效果,我们来看看elasticsearch中的默认分词器。
在kibana的DevTools中输入一段命令:
POST /_analyze
{
"text": "黑马程序员学习Java太棒了!!!",
"analyzer": "standard"
}
请求代表的含义:
POST
_analyze
,前面省略了http://127.0.0.1:9200,这个由Kibana帮我们补充analyzer
:分词器名称,standard是默认的标准分词器text
:要分词的文本内容效果:
标准分词器并不能很好处理中文,一般我们会用第三方的分词器,例如:IK分词器。
IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包,是一个基于Maven构建的项目,具有60万字/秒的高速处理能力,支持用户词典扩展定义。
IK分词器的 地址:https://github.com/medcl/elasticsearch-analysis-ik, 安装非常简单。
IK分词器可以用ik_max_word
和ik_smart
两种方式,分词粒度不同。
1、安装ik插件(在线较慢)
# 进入容器内部
docker exec -it elasticsearch /bin/bash
# 在线下载并安装
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
#退出
exit
#重启容器
docker restart elasticsearch
2、离线安装ik插件(推荐)
# 1、进入/var/lib/docker/volumes/es-plugins/_data/
cd /var/lib/docker/volumes/es-plugins/_data/
# 2、新建文件目录 ik 并且进入
mkdir ik
cd ik
# 3、解压elasticsearch-analysis-ik-7.4.2.zip
yum -y install unzip
unzip elasticsearch-analysis-ik-7.4.2.zip
# 4、重启容器
docker restart elasticsearch
# 查看es日志
docker logs -f elasticsearch
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UbhPR1Pe-1614743158462)(assets/image-20200405012258985.png)]
# 重启 Kibana
docker restart kibana
测试:
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "黑马程序员学习java太棒了"
}
结果:
{
"tokens" : [
{
"token" : "黑马",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "程序员",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "程序",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "员",
"start_offset" : 4,
"end_offset" : 5,
"type" : "CN_CHAR",
"position" : 3
},
{
"token" : "学习",
"start_offset" : 5,
"end_offset" : 7,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "java",
"start_offset" : 7,
"end_offset" : 11,
"type" : "ENGLISH",
"position" : 5
},
{
"token" : "太棒了",
"start_offset" : 11,
"end_offset" : 14,
"type" : "CN_WORD",
"position" : 6
},
{
"token" : "太棒",
"start_offset" : 11,
"end_offset" : 13,
"type" : "CN_WORD",
"position" : 7
},
{
"token" : "了",
"start_offset" : 13,
"end_offset" : 14,
"type" : "CN_CHAR",
"position" : 8
}
]
}
小结:
ik_smart
:最小切分
ik_max_word
:最细切分
随着社会的发展,在原有的词汇列表中不是一个词的,但是现在已经是一个词了。比如:“白富美”,“高富帅”,“黑马程序员”,“传智播客” 等。
所以我们的词汇也需要不断的更新,IK分词器提供了扩展词汇的功能。
1)打开IK分词器config目录:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9p7R7GgV-1614743158464)(assets/image-20201115230147799.png)]
2)IKAnalyzer.cfg.xml配置文件内容添加:
<properties>
<comment>IK Analyzer 扩展配置comment>
<entry key="ext_dict">ext.dicentry>
<entry key="ext_stopwords">entry>
properties>
3)新建一个 ext.dic,可以参考config目录下复制一个配置文件进行修改
黑马程序员
传智播客
高富帅
白富美
4)重启elasticsearch
docker restart elasticsearch
docker restart kibana
# 查看 日志
docker logs -f elasticsearch
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3WK9cF1N-1614743158465)(assets/image-20201115230900504.png)]
日志中已经成功加载ext.dic配置文件
5)测试效果:
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "传智播客已经有19家分校啦,里面也有高富帅,不知道有没有白富美"
}
注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑
在互联网项目中,在网络间传输的速度很快,所以很多语言是不允许在网络上传递的,如:关于宗教、政治等敏感词语,那么我们在搜索时也应该忽略当前词汇。
IK分词器也提供了强大的停用词功能,让我们在索引时就直接忽略当前的停用词汇表中的内容。
1)IKAnalyzer.cfg.xml配置文件内容添加:
<properties>
<comment>IK Analyzer 扩展配置comment>
<entry key="ext_dict">ext.dicentry>
<entry key="ext_stopwords">stopword.dicentry>
properties>
3)在 stopword.dic 添加停用词
特朗普选举
4)重启elasticsearch
docker restart elasticsearch
docker restart kibana
# 查看 日志
docker logs -f elasticsearch
日志中已经成功加载stopword.dic配置文件
5)测试效果:
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "特朗普选举失败"
}
注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑
Elasticsearch虽然是一种NoSql库,但最终的目的是存储数据、检索数据。因此很多概念与MySQL类似的。
ES中的概念 | 数据库概念 | 说明 |
---|---|---|
索引库(indices) | 数据库(Database) | ES中可以有多个索引库,就像Mysql中有多个Database一样。 |
类型(Type) | 表(table) | mysql中database可以有多个table,table用来约束数据结构。而ES中的每个索引库中只有一个类型 ,类型 中用来约束字段属性的叫做映射(mapping ),7.X之后将会被取消,取默认值:_doc |
映射(mappings) | 表的字段约束 | mysql表对字段有约束,ES中叫做映射,用来约束字段属性,包括:字段名称、数据类型等信息 |
文档(document) | 行(Row) | 存入索引库原始的数据,比如每一条商品信息,就是一个文档。对应mysql中的每行数据 |
字段(field) | 列(Column) | 文档中的属性,一个文档可以有多个属性。就像mysql中一行数据可以有多个列。 |
因此,我们对ES的操作,就是对索引库、类型映射、文档数据的操作:
操作MySQL,主要是database操作、表操作、数据操作,对应在elasticsearch中,分别是对索引库操作、类型映射操作、文档数据的操作:
而ES中完成上述操作都可以通过Rest风格的API来完成,符合请求要求的Http请求就可以完成数据的操作。
详见官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
按照Rest风格,增删改查分别使用:POST、DELETE、PUT、GET等请求方式,路径一般是资源名称。因此索引库操作的语法类似。
创建索引库的请求格式:
请求方式:PUT
请求路径:/索引库名
请求参数:格式:
{
"settings": {
"属性名": "属性值"
}
}
settings:就是索引库设置,其中可以定义索引库的各种属性,目前我们可以不设置,都走默认。
示例:
put /索引库名
{
"settings": {
"属性名": "属性值"
}
}
在Kibana中测试一下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BDNFBF0c-1614743158466)(assets/image-20200103111100436.png)]
这里我们没有写settings属性,索引库配置都走默认。
请求方式:GET
请求路径:/索引库名
请求参数:无
格式:
GET /索引库名
在Kibana中测试一下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-valTU8dd-1614743158466)(assets/image-20200103111317109.png)]
可以看到返回的信息也是JSON格式,其中包含这么几个属性:
请求方式:DELETE
请求路径:/索引库名
请求参数:无
格式:
DELETE /索引库名
在Kibana中测试一下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yEEqz9jO-1614743158467)(assets/image-20200103111453582.png)]
索引库操作:
PUT /库名称
GET /索引库名称
DELETE /索引库名称
MySQL中有表,并且表中有对字段的约束,对应到elasticsearch中就是类型映射mapping
.
索引库数据类型是松散的,不过也需要我们指定具体的字段及字段约束信息。而约束字段信息的就叫做映射(mapping
)。
映射属性包括很多:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hr17Q6Pw-1614743158468)(assets/image-20200529170444227.png)]
参考官网:https://www.elastic.co/guide/en/elasticsearch/reference/7.x/mapping-params.html
elasticsearch字段的映射属性该怎么选,除了字段名称外,我们一般要考虑这样几个问题:
1)数据的类型是什么?
type
属性来指定2)数据是否参与搜索?
index
属性来指定是否参与搜索,默认为true,也就是每个字段都参与搜索3)数据是否需要分词?
一个字段的内容如果不是一个不可分割的整体,例如国家,一般都需要分词存储。
如果是身份证号则不需要分词
如果分词的话用什么分词器?
分词器类型很多,中文一般选择IK分词器
指定分词器类型可以通过analyzer
属性指定
注意:在同一个域上 分词和搜索时 建议使用同一个分词器
4)数据是否存储到es库中
elasticsearch提供了非常丰富的数据类型:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FlBDslrD-1614743158469)(assets/image-20200529170805336.png)]
比较常见的有:
string类型,又分两种:
Numerical:数值类型,分两类
Date:日期类型
Object:对象,对象不便于搜索。因此ES会把对象数据扁平化处理再存储。
我们可以给一个已经存在的索引库添加映射关系,也可以创建索引库的同时直接指定映射关系。
我们假设已经存在一个索引库,此时要给索引库添加映射。
请求方式依然是PUT
PUT /索引库名/_mapping
{
"properties": {
"字段名1": {
"type": "类型",
"index": true,
"analyzer": "分词器"
},
"字段名2": {
"type": "类型",
"index": true,
"analyzer": "分词器"
},
...
}
}
发起请求:
PUT /heima/_mapping
{
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word"
},
"images": {
"type": "keyword",
"index": "false"
},
"price": {
"type": "float"
},
"des":{
"type":"text",
"index":"true",
"analyzer":"ik_max_word",
"store":"false"
}
}
}
响应结果:
{
"acknowledged": true
}
上述案例中,就给heima这个索引库中设置了3个字段:
并且给这些字段设置了一些属性
如果一个索引库是不存在的,我们就不能用上面的语法,而是这样:
PUT /索引库名
{
"mappings":{
"properties": {
"字段名1": {
"type": "类型",
"index": true,
"analyzer": "分词器"
},
"字段名2": {
"type": "类型",
"index": true,
"analyzer": "分词器"
},
...
}
}
}
# 创建索引库和映射
PUT /heima2
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word"
},
"images": {
"type": "keyword",
"index": "false"
},
"price": {
"type": "float"
}
}
}
}
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "heima2"
}
查看使用Get请求
GET /索引库名/_mapping
GET /heima/_mapping
{
"heima" : {
"aliases" : { },
"mappings" : {
"properties" : {
"images" : {
"type" : "keyword",
"index" : false
},
"price" : {
"type" : "float"
},
"title" : {
"type" : "text",
"analyzer" : "ik_max_word"
}
}
},
"settings" : {
"index" : {
"creation_date" : "1590744589271",
"number_of_shards" : "1",
"number_of_replicas" : "1",
"uuid" : "v7AHmI9ST76rHiCNHb5KQg",
"version" : {
"created" : "7040299"
},
"provided_name" : "heima"
}
}
}
}
我们把数据库中的每一行数据查询出来,存入索引库,就是文档。文档的主要操作包括新增、查询、修改、删除。
通过POST请求,可以向一个已经存在的索引库中添加文档数据。ES中,文档都是以JSON格式提交的。
POST /{索引库名}/_doc
{
"key":"value"
}
# 新增文档数据
POST /heima/_doc
{
"title":"小米手机",
"images":"http://image.leyou.com/12479122.jpg",
"price":2699.00
}
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "rGFGbm8BR8Fh6kyTbuq8",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
结果解释:
_index
:新增到了哪一个索引库。_id
:这条文档数据的唯一标示
,文档的增删改查都依赖这个id作为唯一标示。此处是由ES随即生成的,我们也可以指定使用某个IDresult
:执行结果,可以看到结果显示为:created
,说明文档创建成功。通过POST请求,可以向一个已经存在的索引库中添加文档数据。ES中,文档都是以JSON格式提交的。
POST /{索引库名}/_doc/{id}
{
"key":"value"
}
# 新增文档数据并指定id
POST /heima/_doc/1
{
"title":"小米手机",
"images":"http://image.leyou.com/12479122.jpg",
"price":2699.00
}
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把刚刚生成数据的id带上。
语法:
GET /{索引库名称}/_doc/{id}
通过kibana查看数据:
GET /heima/_doc/rGFGbm8BR8Fh6kyTbuq8
查看结果:
{
"_index": "heima", # 索引库
"_type": "_doc",
"_id": "rGFGbm8BR8Fh6kyTbuq8", # id
"_version": 1,
"found": true,
"_source": { # 查询数据存放在 _source 中
"title": "小米手机",
"images": "http://image.leyou.com/12479122.jpg",
"price": 2699
}
}
_source
:源文档信息,所有的数据都在里面。词条查询不会分析查询条件,只有当词条和查询字符串完全匹配时才匹配搜索
语法:
GET /{索引库}/_search
{
"query": {
"term": {
"field字段": {
"value": "查询的关键词"
}
}
}
}
通过kibana查看数据:
GET /heima/_search
{
"query": {
"term": {
"title": {
"value": "华为手机"
}
}
}
}
查看结果:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 0,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
}
}
全文查询会分析查询条件,先将查询条件进行分词,然后查询,求并集
语法:
GET /{索引库}/_search
{
"query": {
"match": {
"查询的字段":"查询关键词"
}
}
}
通过kibana查看数据:
GET /heima/_search
{
"query": {
"match": {
"title":"华为手机"
}
}
}
查看结果:
{
"took" : 7,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 0.7985077,
"hits" : [
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "3",
"_score" : 0.7985077,
"_source" : {
"title" : "华为手机",
"images" : "http://image.leyou.com/12479122.jpg",
"price" : 2699.0
}
},
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.105360515,
"_source" : {
"title" : "小米手机",
"images" : "http://image.leyou.com/12479122.jpg",
"price" : 2699.0
}
},
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.105360515,
"_source" : {
"title" : "苹果手机",
"images" : "http://image.leyou.com/12479122.jpg",
"price" : 2699.0
}
}
]
}
}
语法:
# 查询所有
GET /heima/_search
{
"query": {
"match_all": {}
}
}
把刚才新增的请求方式改为PUT,就是修改了。不过修改必须指定id,
比如,我们把使用id为3,不存在,则应该是新增:
# 新增
PUT /heima/_doc/2
{
"title":"大米手机",
"images":"http://image.leyou.com/12479122.jpg",
"price":2899.00
}
结果:
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "2",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1
}
可以看到是created
,是新增。
我们再次执行刚才的请求,不过把数据改一下:
# 修改
PUT /heima/_doc/2
{
"title":"大米手机Pro",
"images":"http://image.leyou.com/12479122.jpg",
"price":3099.00
}
查看结果:
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "2",
"_version" : 2,
"result" : "updated", # updated
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 3,
"_primary_term" : 1
}
可以看到结果是:updated
,显然是更新数据
说明:
es对API的要求并没有那么严格
如:
POST 新增文档时:如果指定了id,并且id在索引库不存在,直接存入索引库;如果id已经存在,则执行修改
PUT修改文档时:先根据id删除指定文档,然后加入新的文档
PUT修改文档时: 如果对应的文档不存在时,会添加该文档
删除使用DELETE请求,同样,需要根据id进行删除:
DELETE /索引库名/类型名/id值
# 根据id删除数据
DELETE /heima/_doc/rGFGbm8BR8Fh6kyTbuq8
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "rGFGbm8BR8Fh6kyTbuq8",
"_version" : 2,
"result" : "deleted",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 4,
"_primary_term" : 1
}
可以看到结果result为:deleted,说明删除成功了。
刚刚我们在新增数据时,添加的字段都是提前在类型中通过mapping定义过的,如果我们添加的字段并没有提前定义过,能够成功吗?
事实上Elasticsearch有一套默认映射规则,如果新增的字段从未定义过,那么就会按照默认映射规则来存储。
测试一下:
# 新增未映射字段
POST /heima/_doc/3
{
"title":"超大米手机",
"images":"http://image.leyou.com/12479122.jpg",
"price":3299.00,
"stock": 200,
"saleable":true,
"subTitle":"超级双摄,亿级像素"
}
我们额外添加了stock库存,saleable是否上架,subtitle副标题、3个字段。
来看结果:
{
"_index" : "heima",
"_type" : "_doc",
"_id" : "3",
"_version" : 1,
"result" : "created", # created
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 5,
"_primary_term" : 1
}
成功了!在看下索引库的映射关系:
{
"heima" : {
"mappings" : {
"properties" : {
"images" : {
"type" : "keyword",
"index" : false
},
"price" : {
"type" : "float"
},
"saleable" : {
"type" : "boolean"
},
"stock" : {
"type" : "long"
},
"subTitle" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"title" : {
"type" : "text",
"analyzer" : "ik_max_word"
}
}
}
}
}
stock、saleable、subtitle都被成功映射了:
如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3XhpFskn-1614743158470)(assets/image-20200104102125708.png)]
默认映射规则不一定符合我们的需求,我们可以按照自己的方式来定义默认规则。这就需要用到动态模板了。
动态模板的语法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d3sJNUe0-1614743158471)(assets/1547005993592.png)]
match_mapping_type
:按照数据类型匹配,如:string匹配字符串类型,long匹配整型match
和unmatch
:按照名称通配符匹配,如:t_*
匹配名称以t开头的字段凡是映射规则中未定义,而符合2中的匹配条件的字段,就会按照3中定义的映射方式来映射
在kibana中定义一个索引库,并且设置动态模板:
# 动态模板
PUT heima3
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word"
}
},
"dynamic_templates": [
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword"
}
}
}
]
}
}
这个动态模板的意思是:凡是string类型的自动,统一按照 keyword来处理。
接下来新增一个数据试试:
POST /heima2/_doc/1
{
"title":"超大米手机",
"images":"http://image.leyou.com/12479122.jpg",
"price":3299.00
}
然后查看映射:
GET /heima2/_mapping
结果:
{
"heima2" : {
"mappings" : {
"dynamic_templates" : [
{
"strings" : {
"match_mapping_type" : "string",
"mapping" : {
"type" : "keyword"
}
}
}
],
"properties" : {
"images" : {
"type" : "keyword"
},
"price" : {
"type" : "float"
},
"title" : {
"type" : "text",
"analyzer" : "ik_max_word"
}
}
}
}
}
可以看到images是一个字符串,被映射成了keyword类型。
在elasticsearch官网中提供了各种语言的客户端:https://www.elastic.co/guide/en/elasticsearch/client/index.html
而Java的客户端就有两个:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EXyJd1E2-1614743158472)(assets/image-20200104164045946.png)]
不过Java API这个客户端(Transport Client)已经在7.0以后过期了,而且在8.0版本中将直接废弃。所以我们会学习Java REST Client:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s5Jpwzhr-1614743158473)(assets/image-20200104164428873.png)]
然后再选择High Level REST Client这个。
Java REST Client 其实就是利用Java语言向 ES服务发 Http的请求,因此请求和操作与前面学习的REST API 一模一样。
新建基于Maven的Java项目,相关信息如下:
pom.xml:
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
<version>7.4.2version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.8version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.49version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.8.1version>
dependency>
dependencies>
实体类:
com.itheima.esdemo.pojo.User
package com.itheima.sh.esdemo.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* @Description:
* @Version: V1.0
*/
@Data
@AllArgsConstructor
public class User {
private Long id;
private String name;// 姓名
private Integer age;// 年龄
private String gender;// 性别
private String note;// 备注
}
扩展:
使用Lombok需要两个条件:
1)引入依赖:
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.8version>
dependency>
2)编辑器idea安装插件:
在线装,参考:https://plugins.jetbrains.com/plugin/6317-lombok
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qw0yvwwZ-1614743158474)(assets/image-20200220095157760.png)]
在官网上可以看到连接ES的教程:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-getting-started-initialization.html
首先需要与ES建立连接,ES提供了一个客户端RestHighLevelClient。
代码如下:
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http"),
new HttpHost("localhost", 9201, "http")));
ES中的所有操作都是通过RestHighLevelClient来完成的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ZlPKgU0-1614743158474)(assets/image-20200105103815463.png)]
为了后面测试方便,我们写到一个单元测试中,并且通过@Before
注解来初始化客户端连接。
com.itheima.sh.esdemo.ElasticSearchTest
package com.itheima.sh.esdemo;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.After;
import org.junit.Before;
import java.io.IOException;
//ES测试类
public class ElasticSearchTest {
//客户端对象
private RestHighLevelClient client;
/**
* 建立连接
*/
@Before
public void init() throws IOException {
//创建Rest客户端
client = new RestHighLevelClient(
RestClient.builder(
//如果是集群,则设置多个主机,注意端口是http协议的端口
new HttpHost("localhost", 9200, "http")
// ,new HttpHost("localhost", 9201, "http")
// ,new HttpHost("localhost", 9202, "http")
)
);
}
/**
* 创建索引库-测试
* @throws Exception
*/
@Test
public void testCreateIndex() throws Exception{
System.out.println(client);
// org.elasticsearch.client.RestHighLevelClient@6c61a903
}
/**
* 关闭客户端连接
*/
@After
public void close() throws IOException {
client.close();
}
}
开发中,往往库和映射的操作一起完成,官网详细文档地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/_index_apis.html
这里我们主要实现库和映射的创建。查询、删除等功能大家可参考文档自己实现。
按照官网给出的步骤,创建索引包括下面四个步骤:
其实仔细分析,与我们在Kibana中的Rest风格API完全一致:
PUT /heima
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
}
}
Java代码中设置mapping,依然与REST中一致,需要JSON风格的映射规则。因此我们先在kibana中给User实体类定义好映射规则。
谨记三个是否原则
User包括下面的字段:
使用ik_max_word
映射如下:
PUT /user
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"id": {
"type": "long"
},
"name":{
"type": "keyword"
},
"age":{
"type": "integer"
},
"gender":{
"type": "keyword"
},
"note":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
我们在上面新建的ElasticDemo类中新建单元测试,完成代码,思路就是之前分析的4步骤:
package com.itheima.sh.esdemo;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
private RestHighLevelClient client;
/**
* 创建索引
* @throws IOException
*/
@Test
public void testCreateIndex() throws IOException {
// 1.创建CreateIndexRequest对象,并指定索引库名称
CreateIndexRequest request = new CreateIndexRequest("user");
// 2.指定settings配置(可以默认)
request.settings(Settings.builder()
.put("index.number_of_shards", 3)
.put("index.number_of_replicas", 1)
);
// 3.指定mapping配置
request.mapping(
"{\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"long\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"age\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"gender\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"note\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }",
//指定映射的内容的类型为json
XContentType.JSON);
// 4.发起请求,得到响应(同步操作)
CreateIndexResponse response = client.indices()
.create(request, RequestOptions.DEFAULT);
//打印结果
System.out.println("response = " + response.isAcknowledged());
}
返回结果:
response = true
文档操作包括:新增文档、查询文档、修改文档、删除文档等。
CRUD官网地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-supported-apis.html
新增的官网地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-document-index.html
根据官网文档,实现的步骤如下:
新增文档:
/**
* 测试插入一个文档
* @throws IOException
*/
@Test
public void testAddDocument() throws Exception{
//1. 准备文档数据
User user = new User(110L, "张三", 22, "0", "上海市浦东新区航头镇黑马程序员");
//2. 创建IndexRequest对象,并指定索引库名称
IndexRequest indexRequest = new IndexRequest("user");
//3. 指定新增的数据的id
indexRequest.id(user.getId().toString());
//4. 将新增的文档数据变成JSON格式
// user.setAge(null);
String userJson = JSON.toJSONString(user);
//5. 将JSON数据添加到IndexRequest中
indexRequest.source(userJson, XContentType.JSON);
//6. 发起请求,得到结果
IndexResponse response = client.index(indexRequest, RequestOptions.DEFAULT);
System.out.println("indexResponse= "+response.getResult());
}
结果:
indexResponse = CREATED
注意:新增的ID一致时,是执行修改操作
我们直接测试过,新增的时候如果ID存在则变成修改,我们试试,再次执行刚才的代码,可以看到结果变了:
indexResponse = UPDATED
结论:在ES中如果ID一致则执行修改操作,其实质是先删除后添加。
官网地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-document-get.html
这里的查询是根据id查询,必须知道文档的id才可以。
根据官网文档,实现的步骤如下:
/**
* 测试根据id查询一个文档
* @throws IOException
*/
@Test
public void testfindDocumentById() throws Exception{
//1. 创建GetRequest 对象,并指定索引库名称、文档ID
GetRequest getRequest = new GetRequest("user", "110");
//2. 发起请求,得到结果
GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
//3. 从结果中得到source,是json字符串
String sourceAsString = response.getSourceAsString();
//4. 将JSON反序列化为对象
User user = JSON.parseObject(sourceAsString, User.class);
System.out.println(user);
}
结果如下:
User(id=110, name=张三, age=null, gender=0, note=上海市浦东新区航头镇黑马程序员)
官网地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-document-delete.html
/**
* 根据id删除文档
* @throws IOException
*/
@Test
public void testDeleteDocumentById() throws IOException {
// 1.创建DeleteRequest对象,指定索引库名称、文档ID
DeleteRequest request = new DeleteRequest(
"user",
"110");
// 2.发起请求
DeleteResponse deleteResponse = client.delete(
request, RequestOptions.DEFAULT);
System.out.println("deleteResponse = " + deleteResponse.getResult());
}
结果:
deleteResponse = DELETED
如果我们需要把数据库中的所有用户信息都导入索引库,可以批量查询出多个用户,但是刚刚的新增文档是一次新增一个文档,这样效率太低了。
因此ElasticSearch提供了批处理的方案:BulkRequest
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-document-bulk.html
# 批量导入的脚本
POST _bulk
{"index":{"_index":"user","_type":"_doc","_id":"1"}}
{"age":18,"gender":"1","id":1,"name":"Rose","note":"Rose同学在传智播客学表演11"}
{"index":{"_index":"user","_type":"_doc","_id":"2"}}
{"age":38,"gender":"1","id":2,"name":"Jack","note":"Jack同学在黑马学JavaEE"}
{"index":{"_index":"user","_type":"_doc","_id":"3"}}
{"age":38,"gender":"1","id":2,"name":"Jack","note":"Jack同学在黑马学JavaEE"}
{"index":{"_index":"user","_type":"_doc","_id":"4"}}
{"age":23,"gender":"0","id":3,"name":"小红","note":"小红同学在传智播客学唱歌"}
{"index":{"_index":"user","_type":"_doc","_id":"5"}}
{"age":20,"gender":"1","id":4,"name":"小明","note":"小明同学在黑马学JavaSE"}
{"index":{"_index":"user","_type":"_doc","_id":"6"}}
{"age":33,"gender":"1","id":5,"name":"达摩","note":"达摩和尚在达摩院学唱歌"}
{"index":{"_index":"user","_type":"_doc","_id":"7"}}
{"age":24,"gender":"1","id":6,"name":"鲁班","note":"鲁班同学走在乡间小路上"}
{"index":{"_index":"user","_type":"_doc","_id":"8"}}
{"age":26,"gender":"0","id":7,"name":"孙尚香","note":"孙尚香同学想带阿斗回东吴"}
{"index":{"_index":"user","_type":"_doc","_id":"9"}}
{"age":27,"gender":"1","id":8,"name":"李白","note":"李白同学在山顶喝着酒唱着歌"}
{"index":{"_index":"user","_type":"_doc","_id":"10"}}
{"age":28,"gender":"0","id":9,"name":"甄姬","note":"甄姬同学弹奏一曲东风破"}
{"index":{"_index":"user","_type":"_doc","_id":"11"}}
{"age":27,"gender":"0","id":10,"name":"虞姬","note":"虞姬同学在和项羽谈情说爱"}
A
BulkRequest
can be used to execute multiple index, update and/or delete operations using a single request.
一个BulkRequest可以在一次请求中执行多个 新增、更新、删除请求。
所以,BulkRequest就是把多个其它增、删、改请求整合,然后一起发送到ES来执行。
我们拿批量新增来举例,步骤如下:
/**
* 大量数据批量添加
* @throws IOException
*/
@Test
public void testBulkAddDocumentList() throws IOException {
// 1.从数据库查询文档数据
//第一步:准备数据源。本案例使用List来模拟数据源。
List<User> users = Arrays.asList(
new User(1L, "Rose", 18, "1", "Rose同学在传智播客学表演"),
new User(2L, "Jack", 38, "1", "Jack同学在黑马学JavaEE"),
new User(3L, "小红", 23, "0", "小红同学在传智播客学唱歌"),
new User(4L, "小明", 20, "1", "小明同学在黑马学JavaSE"),
new User(5L, "达摩", 33, "1", "达摩和尚在达摩院学唱歌"),
new User(6L, "鲁班", 24, "1", "鲁班同学走在乡间小路上"),
new User(7L, "孙尚香", 26, "0", "孙尚香同学想带阿斗回东吴"),
new User(8L, "李白", 27, "1", "李白同学在山顶喝着酒唱着歌"),
new User(9L, "甄姬", 28, "0", "甄姬同学弹奏一曲东风破"),
new User(10L, "虞姬", 27, "0", "虞姬同学在和项羽谈情说爱")
);
// 2.创建BulkRequest对象
BulkRequest bulkRequest = new BulkRequest();
// 3.创建多个IndexRequest对象,并添加到BulkRequest中
for (User user : userList) {
bulkRequest.add(new IndexRequest("user")
.id(user.getId().toString())
.source(JSON.toJSONString(user), XContentType.JSON)
);
}
// 4.发起请求
BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
System.out.println("status: " + bulkResponse.status());
}
结果如下:
status: OK
可以再Kibana中通过GET /user/_search
看到查询的结果。
提示:
可以批量处理增删改:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xxZMv9U1-1614743158475)(assets/image-20200220105716117.png)]