什么是elasticsearch
elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容
elasticsearch结合kibana、Logstash、 Beats, 也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域。
elasticsearch可以将日志信息可视化展示出来,所以将来做日志分析时候非常方便。因此搜索引擎使用的场景非常广泛,ELK技术栈里面尽管有很多组件,核心就是elasticsearch,它复制数据的存储、计算、搜索分析。而Logstash,Beats主要负责数据抓取的, Kibana是一个数据可视化组件,用于数据展示,形成报表。但是可视化组件是否必须使用kibana?不一定,完全可以自己去实现。数据抓取也同理,完全可以自己去写java代码,抓取数据。所以,除了elasticsearch以外,都是可替代的。
elasticsearch的底层实现是一个名为Lucene的技术。
Lucene是- -个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发
官网地址: https://lucene.apache.org/。
Lucene的优势:易扩展,高性能(基于倒排索引)
Lucene的缺点:只限于java语言开发,学习难,不支持水平扩展。不支持高并发场景,不支持集群扩展。所以要实现必须进行二次开发。
elasticsearch的发展
2004年Shay Banon基于Lucene开发了Compassp
2010年Shay Banon重写了Compass,取名为Elasticsearch。
官网地址: https://www.elastic.co/cn/
目前最新的版本是: 7.12.1
相比与lucene, elasticsearch具备下列优势:
因此具备了处理海量数据和高并发场景的能力。
为什么学习elasticsearch?
搜索引擎技术排名:
Elasticsearch:开源的分布式搜索引擎
Splunk: 商业项目
Solr: Apache的开源搜索引擎
总结
什么是elasticsearch?
一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能
什么是elastic stack (ELK) ?
是以elasticsearch为核心的技术栈 ,包括beats、Logstash.kibana、elasticsearch
什么是Lucene?
是Apache的开源搜索引擎类库, 提供了搜索引擎的核心API
倒排索引是与mysql等传统数据库的正向索引去对比得出的一个名称,因此与传统数据库的索引是有比较大的差异的。
这个差异我们通过一个案例来看一下。
正向索引和倒排索引
传统数据库( 如MySQL)采用正向索引,例如给下表(tb_ goods)中的id创建索引:
一般情况下,会基于id去创建索引形成一颗B+树,检索的速度就会非常快,这种方式的索引就是一种正向索引,但是如果我现在搜索的不是id,而是一个普通的title字段,title内容比较长,你不会去给他加索引,而且即便你有索引,如果是模糊查询,索引也不会生效,这种情况下没有索引我们数据库怎么去比较和查询呢?就会采用逐条扫描的方式,如果你的表数据非常大,性能差,这就是正向索引,它进行局部内容索引的时候,效率比较差。那倒排索引又是怎么来做的呢?
elasticsearch采用倒排索引:
文档(document) : 每条数据就是一个文档
词条(term) : 文档按照语义分成的词语
比如说,小米是一个词条,手机也是一个词条,等等…
所以倒排索引,它会先把文档中的内容分成词条去存,比方说我拿到第一条数据,那么我要对标题创建倒排索引,我就要把标题做个分词,分成,小米、手机两个词,并记录它的文档id,因为是第一条数据,即文档id为1,存第二条的时候,手机已经存在词条,再记录一个文档id即可。
将来你有更多的词条,继续往下记录即可,并且这些词条肯定有大量的重复,只记录唯一的一个词条。这样能保证倒排索引词条字段是绝对不会出现重复的。因为其唯一性,那么我们就可以给他创建索引了。数据较少的时候使用哈希法,也可以使用B+树,去给词条创建唯一索引。那么将来我们根据词条查询的速度,就非常快了。
现在比如说我搜索华为手机,elasticsearch会对用户输入的进行一段分词,分为华为、手机,拿着这两个词条去倒排索引中进行查询,这是非常快的,查到手机,1,2;查到华为2,3;两组文档ID。这个时候我就知道了包含华为手机的所有的文档了,其中2号文档的关联度更高一点,将来就可以排序,2号文档往前排,1和3往后一点。然后我拿着这三个id我就可以去查询文档了,查询id为1,2,3的正向索引,也会很快。
所以,我们其实经过了两次查询,但是两次查询都经过了索引,这个效率是比逐条查询效率高的。
倒排索引之所以叫倒排索引,因为我们在正向索引中,我们要找到包含手机的,我们得一行行的看,先看文档,再看是否包含词条。倒排索引是倒过来的,先去看词条,再去关联到文档。
倒排索引更擅长于基于文档的内容进行搜索,更复杂的搜索需求,这就是为什么我们的搜索引擎底层都是基于倒排索引的原因。
总结
什么是文档什么是词条?
每一条数据就是一 个文档
对文档中的内容分词,得到的词语就是词条
什么是正向索引?
什么是倒排索引?
elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。
文档数据会被序列化为json格式后存储在elasticsearch中。
索引(Index)、映射(mapping)
索引(index) :相同类型的文档的集合
映射(mapping) :索引中文档的字段约束信息,类似表的结构约束
elasticsearch与mysql的概念对应关系
SQL和DSL在发送时的差别
在ES中,你写好了DSL以后,我们是基于HTTP请求发出去,因为ES对外暴露的restful接口,这种接口的好处的跟语言无关。任何的语言只要能发HTTP请求,你都可以把你的DSL发给我,我就能处理了。这样以来就彻底脱离了语言的束缚了。
是不是说有了ES以后,我们就能完全替代了我们的Mysql了呢?不是,他们两个擅长的事情是不一样的
Mysql:擅长事务类型操作,可以确保数据的安全和一-致性
Elasticsearch:擅长海量数据的搜索、分析、计算
ES没有事务的概念,它无法保证ACID,所有他们两个是各司其职的,如果说你现在做的是下单付款的业务,它对事务要求很高,数据的安全性、一致性要求很高,你就应该使用mysql去做数据的存储。但是你现在做的是商品的搜索或者页面的搜索,这个搜索比较复杂,你肯定得用ES去做。【是一种互补的关系】
将来我们的系统架构当中,两个都会存在。
比方说用户来一个商品查询的CRUD,它的请求访问到服务器以后,我们的服务器就可以作出一个判断,如果是增删写操作,就给到mysql,这样数据就比较安全了。如果你现在是查询的操作,就给到ES去做。
那么怎么确保ES和mysql都有数据呢?
mysql可以使用某种方式,将数据同步给ES,从而实现数据的双写。
总结
针对业务量比较大, 实现需求比较复杂的时候,才会考虑两个库里都去写。但一些简单的查询(根据ID查询)还是用数据库没问题的。
合适场景选择合适的技术
因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:
docker network create es-net
网络名es-net
可以任意取,在xshell输入,创建全新的网络。
输入结果:
[root@hadoop100 ~]# docker network create es-net
1394876ff1c3b5e2e0c7937daeb808324ed7be9ed8d09b781e0a8ef0bc689f31
这里我们采用elasticsearch的7.12.1
版本的镜像,这个镜像体积非常大,接近1G。不建议大家自己pull。
课前资料提供了镜像的tar包:
大家将其上传到虚拟机中,然后运行命令加载即可:
# 导入数据
docker load -i es.tar
docker load -i kibana.tar
同理还有kibana
的tar包也需要这样做。
加载情况
[root@hadoop100 software]# docker load -i es.tar
2653d992f4ef: Loading layer [==================================================>] 216.5MB/216.5MB
0ba8eff8aa04: Loading layer [==================================================>] 101.4MB/101.4MB
2a944434ad00: Loading layer [==================================================>] 314.9kB/314.9kB
ade95a7611c0: Loading layer [==================================================>] 543.9MB/543.9MB
09a575a6e776: Loading layer [==================================================>] 26.62kB/26.62kB
498ae65924d7: Loading layer [==================================================>] 7.68kB/7.68kB
36b3f8db7aaa: Loading layer [==================================================>] 490.5kB/490.5kB
Loaded image: elasticsearch:7.12.1
[root@hadoop100 software]# docker load -i kibana.tar
d797e87ed4ce: Loading layer [==================================================>] 112.9MB/112.9MB
80ce41fc1f8a: Loading layer [==================================================>] 26.62kB/26.62kB
3345a8ffd0ea: Loading layer [==================================================>] 3.584kB/3.584kB
d736a1702974: Loading layer [==================================================>] 20.34MB/20.34MB
570575469db2: Loading layer [==================================================>] 56.83kB/56.83kB
459d502a3562: Loading layer [==================================================>] 770.7MB/770.7MB
f22a9f0649d0: Loading layer [==================================================>] 2.048kB/2.048kB
4b66f24ba0de: Loading layer [==================================================>] 4.096kB/4.096kB
0a50faa06266: Loading layer [==================================================>] 15.36kB/15.36kB
8a310ff91413: Loading layer [==================================================>] 4.096kB/4.096kB
5997553ddc84: Loading layer [==================================================>] 479.2kB/479.2kB
f87dadd7c340: Loading layer [==================================================>] 309.8kB/309.8kB
Loaded image: kibana:7.12.1
运行docker命令,部署单点es:
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms1024m -Xmx1024m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
命令解释:
-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
:授予逻辑卷访问权--network es-net
:加入一个名为es-net的网络中-p 9200:9200
:端口映射配置,(暴露的Http协议的端口,将来供用户访问的)-p 9300:9300
:将来ES容器各个结点之间互联的端口。elasticsearch:7.12.1
: 镜像名称浏览器访问网站 http://192.168.10.100:9200/
ES安装就完成了!
安装Kibanna
kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。
运行docker命令,部署kibana
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1
--network es-net
:加入一个名为es-net的网络中,与elasticsearch在同一个网络中-e ELASTICSEARCH_HOSTS=http://es:9200"
:设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch-p 5601:5601
:端口映射配置kibana启动一般比较慢,需要多等待一会,可以通过命令:
docker logs -f kibana
查看运行日志,当查看到下面的日志,说明成功:
也就是这句话
{"type":"log","@timestamp":"2021-11-17T01:13:05+00:00","tags":["listening","info"],"pid":6,"message":"Server running at http://0.0.0.0:5601"}
此时,在浏览器输入地址访问:http://192.168.10.100:5601,即可看到结果
DevTools
kibana中提供了一个DevTools界面:
这个界面中可以编写DSL来操作elasticsearch。并且对DSL语句有自动补全功能。
分词器
es在创建倒排索引时需要对文档分词;在搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理并不友好。我们在kibana的DevTools中测试:
英语分词还是可以的,但是中文确是逐字分词。如果要分词中文就不能使用默认分词器。一般中文分词我们会采用IK分词器。https://github.com/medcl/elasticsearch-analysis-ik
在线安装ik插件(较慢)
# 进入容器内部
docker exec -it elasticsearch /bin/bash
# 在线下载并安装
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
#退出
exit
#重启容器
docker restart elasticsearch
离线安装ik插件(推荐)
1)查看数据卷目录
安装插件需要知道elasticsearch的plugins目录位置,而我们用了数据卷挂载,因此需要查看elasticsearch的数据卷目录,通过下面命令查看:
docker volume inspect es-plugins
显示结果:
[
{
"CreatedAt": "2022-05-06T10:06:34+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
"Name": "es-plugins",
"Options": null,
"Scope": "local"
}
]
说明plugins目录被挂载到了:/var/lib/docker/volumes/es-plugins/_data
这个目录中。
下面我们需要把课前资料中的ik分词器解压缩,重命名为ik
传到es容器的插件数据卷中
也就是/var/lib/docker/volumes/es-plugins/_data
:
重启容器
# 4、重启容器
docker restart es
# 查看es日志
docker logs -f es
测试:
IK分词器包含两种模式:
ik_smart
:最少切分 ( 粗粒度切分,从字数最多去到字数最少去看,比如说“程序员”是不会切分的)ik_max_word
:最细切分(细粒度切分,比如说“程序员”会切分成,“程序员”,“程序”,“员”)这两种带来的后果是什么?如果搜程序,按第一种是搜不到这篇文档的,搜索的概率就比较低,好处是分的词少占用的内存空间就少了。到时内存就能缓存更多的数据,效率更高一点。这就是smart模式优势。max_word占用内存空间会更多。
底层分词的原理是什么?
字典,字典里会有各种各样的词语罗列好了。IK分词器还有其他的中文分词器,都会依赖于一个字典去做分词。它这个字典中可能会包含我们不希望的分词(如 “的”,“了”,“哦”),也有我们希望有的新潮词汇(如“奥利给”,“白嫖”)等。我们应该如何扩展呢?
要拓展ik分词器的词库,只需要修改一个ik分词器 目录中的config目录中的IkAnalyzer.cfg.xml文件:
要禁用某些敏感词条,只需要修改一个ik分词器目录中的config目录中的IkAnalyzer.cfg.xml文件:
在ik分词器的config目录下修改以下3个文件
/var/lib/docker/volumes/es-plugins/_data/ik/config
其他更多的功能,参考官网文档
总结
mapping属性
mapping是对索引库中文档的约束,常见的mapping属性包括:
官方手册:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
常用:
type | 字段数据类型 | |
字符串 | text (可分词的文本) keyword (精确值,例如:品牌、国家、ip地址) 如果字段不需要拆分,就用keyword |
|
数值 | long,integer,short,byte,double,float | |
布尔 | boolean | |
日期 | date | |
对象 | object | |
index | 是否创建索引 | 默认为true,主要取决于某个字段是否参与搜索 |
analyzer | 使用哪种分词器 | 使用的较少,因为只有text需要分词,其他类型都不需要分词。 它的值就是分词器的名称(ik_smart,ik_max_word) |
properties | 该字段的子字段 | 处理"name":{ "firstName":"xx","lastName":"yy" } 这种情况 |
总结:
创建索引库
DSL
PUT /heima
{
"mappings": {
"properties": {
"info":{
"type": "text",
"analyzer": "ik_smart"
},
"email":{
"type": "keyword",
"index": false
},
"name":{
"properties": {
"firstName":{
"type":"keyword"
},
"lastName":{
"type":"keyword"
}
}
}
}
}
}
结果
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "heima"
}
查询、删除索引库
修改索引库
事实上在ES中是不允许 修改的,因为索引库创建完成了以后,它的数据结构也就是mapping映射都已经定义好了,我们ES会基于这些mapping去创建倒排索引,那么如果说你要去修改一个字段,就会导致原有的倒排索引彻底失效。
【在ES里禁止修改索引库】
ES虽然禁止修改原有字段,但允许你添加新字段。
# 查询
GET /heima
# 修改索引库,添加新字段
PUT /heima/_mapping
{
"properties":{
"age":{
"type":"integer"
}
}
}
# 删除
DELETE /heima
总结:
添加文档
查看、删除文档
修改文档
所有DSL
# 插入文档
POST /heima/_doc/1
{
"info":"黑马程序员java讲师",
"email":"[email protected]",
"name":{
"firstName":"y",
"lastName":"z"
}
}
# 查询文档
GET /heima/_doc/1
# 删除文档
DELETE /heima/_doc/1
# 修改文档方式1,文档存在的情况:updated
PUT /heima/_doc/1
{
"info":"黑马程序员java讲师",
"email":"[email protected]",
"name":{
"firstName":"y",
"lastName":"z"
}
}
# 修改文档方式1,文档不存在的情况:created
PUT /heima/_doc/3
{
"info":"黑马程序员java讲师",
"email":"[email protected]",
"name":{
"firstName":"y",
"lastName":"z"
}
}
# 修改文档方式2-局部修改 updated
POST /heima/_update/1
{
"doc":{
"email":"[email protected]"
}
}
总结:
什么是RestClient
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址: https://www.elastic.co/guide/en/elasticsearch/client/index.html
案例
这个hotel-demo的application.yml默认的mysql地址 是 mysql:3306意味着连接的是虚拟机上docker的mysql,所以如果你是在windows机器上导入的sql文件,你应该改成localhost
分析数据结构
# 酒店的mapping
PUT /hotel
{
"mappings": {
"properties": {
"id":{
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "ik_max_word"
},
"address":{
"type": "keyword",
"index": false
},
"price":{
"type":"integer"
},
"score":{
"type":"integer"
},
"brand":{
"type": "keyword"
},
"city":{
"type": "keyword"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword"
},
"location":{
"type":"geo_point"
},
"pic":{
"type": "keyword",
"index": false
}
}
}
}
我是根据多个字段搜索效率高,还是只根据一个字段搜索效率高?显然是一个字段效率高。我现在需求是用户搜名称、搜品牌、搜地址都能搜到,而且我还希望性能好怎么办?
字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段。示例:
你可以在一个字段里搜到多个字段的内容了,而且这种拷贝还做了优化,并不是把文档拷贝进去了,而只是基于它创建倒排索引,所有你将来查询是查不到这个字段的,但搜却可以根据其搜。
即:"copy_to": "all"
# 酒店的mapping
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"
},
"starName":{
"type": "keyword"
},
"business":{
"type": "keyword",
"copy_to": "all"
},
"location":{
"type":"geo_point"
},
"pic":{
"type": "keyword",
"index": false
},
"all":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
初始化RestClient
引入依赖:
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
<version>7.12.1version>
dependency>
为什么有些依赖是7.6.2版本?
因为依赖被Springboot管理,所以想要覆盖Springboot的版本定义,找到自己的pom文件,在properties标签下定义
<elasticsearch.version>7.12.1elasticsearch.version>
所以用springboot管理时,一定要去properties里指明版本。
那么这里的版本信息不写也行
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
dependency>
在test包下,创建测试类,以下是基本代码,包含了初始化与最终销毁的基本代码。(后面测试的代码都写在这个类中,并省略下面这段代码。)
package cn.itcast.hotel;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import java.io.IOException;
/**
* 酒店索引测试
* @author:whd
* @createTime: 2021/11/17
*/
public class HotelIndexTest {
private RestHighLevelClient client;
/**
* 在一开始就完成client的初始化
*/
@BeforeEach
void setUp() {
//如果是集群,这里的HttpHost.create("http://192.168.10.100:9200")可以用逗号分割
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.10.100:9200")
));
}
/**
* 用完后销毁
*/
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
测试初始化
@Test
void testInit() {
System.out.println("client = " + client);
}
创建索引库
这里将DSL语句定义在一个常量类里面
package cn.itcast.hotel.constants;
/**
* @author:whd
* @createTime: 2021/11/17
*/
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" +
" },\n" +
" \"starName\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"business\":{\n" +
" \"type\": \"keyword\",\n" +
" \"copy_to\": \"all\"\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" +
"}";
}
创建酒店索引
/**
* 创建酒店索引
*/
@Test
void createHotelIndex() throws IOException {
//1.创建Request对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
//2.准备请求的参数,DSL语句
request.source(MAPPING_TEMPLATE, XContentType.JSON);
//3.发送请求
client.indices().create(request, RequestOptions.DEFAULT);
}
然后查询
GET /hotel
没问题,成功建立
删除、判断索引库是否存在
/**
* 删除索引
*/
@Test
void testDeleteHotelIndex() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
client.indices().delete(request,RequestOptions.DEFAULT);
System.out.println("删除成功!");
}
/**
* 是否存在索引
*/
@Test
void testExistsHotelIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest("hotel");
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}
总结
索引库操作的基本步骤:
初始化RestHighLevelClient
创建XxxIndexRequest。XXX是Create、Get、Delete
准备DSL( Create时需要)
发送请求。调用RestHighLevelClient.indices().xxx()方法,xxx是create、exists、delete
@SpringBootTest
public class HotelDocumentTest {
@Autowired
private IHotelService iHotelService;
private RestHighLevelClient client;
/**
* 添加文档
*/
@Test
void testAddDocument() throws IOException {
//根据ID查询酒店数据
Hotel hotel = iHotelService.getById(36934L);
//转换为文档类型(以处理location与经纬度不一致)
HotelDoc hotelDoc = new HotelDoc(hotel);
//1.准备request对象
IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
//2.准备JSON文档
request.source(JSON.toJSONString(hotelDoc),XContentType.JSON);
//3.发送请求
client.index(request,RequestOptions.DEFAULT);
}
/**
* 在一开始就完成client的初始化
*/
@BeforeEach
void setUp() {
//如果是集群,这里的HttpHost.create("http://192.168.10.100:9200")可以用逗号分割
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.10.100:9200")
));
}
/**
* 用完后销毁
*/
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
回到浏览器
发送GET /hotel/_doc/36934
查询成功!
{
"_index" : "hotel",
"_type" : "_doc",
"_id" : "36934",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"address" : "静安交通路40号",
"brand" : "7天酒店",
"business" : "四川北路商业区",
"city" : "上海",
"id" : 36934,
"location" : "31.251433, 121.47522",
"name" : "7天连锁酒店(上海宝山路地铁站店)",
"pic" : "https://m.tuniucdn.com/fb2/t1/G1/M00/3E/40/Cii9EVkyLrKIXo1vAAHgrxo_pUcAALcKQLD688AAeDH564_w200_h200_c1_t0.jpg",
"price" : 336,
"score" : 37,
"starName" : "二钻"
}
}
/**
* 查询文档
*/
@Test
void testGetDocumentById() throws IOException {
//1.准备request对象
GetRequest request = new GetRequest("hotel","36934");
//2.发送请求得到响应
GetResponse response = client.get(request, RequestOptions.DEFAULT);
//3.解析响应结果
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
}
/**
* 更新文档
*/
@Test
void testUpdateDocument() throws IOException {
//1.准备Request
UpdateRequest request = new UpdateRequest("hotel","36934");
//2.准备请求参数
request.doc(
"price","337",
"starName","五星"
);
//3.发送请求
client.update(request,RequestOptions.DEFAULT);
}
/**
* 删除文档
*/
@Test
void testDeleteDocument() throws IOException {
//1.准备Request
DeleteRequest request = new DeleteRequest("hotel","36934");
//2.发送请求
client.delete(request,RequestOptions.DEFAULT);
}
总结
文档操作的基本步骤:
/**
* 批量导入数据到文档
*/
@Test
void testBulkRequest() throws IOException {
List<Hotel> hotelList = iHotelService.list();
BulkRequest request = new BulkRequest();
for (Hotel hotel : hotelList) {
HotelDoc hotelDoc = new HotelDoc(hotel);
//创建文档request对象
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc),XContentType.JSON)
);
}
//发送请求
client.bulk(request,RequestOptions.DEFAULT);
}
在ES的DEVtools输入以下查询指令,即可得到结果
GET /hotel/_search
Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
查询所有:查询出所有数据,一般测试用。
例如:match_all
全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。
例如:
match_query
multi_match_query
精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。
例如:
ids
range
term
地理(geo)查询:根据经纬度查询。
例如:
geo_distance
geo_bounding_box
复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。
例如:
bool
function_score
默认只查询出来10条展示。
# match查询
GET /hotel/_search
{
"query": {
"match": {
"all": "外滩"
}
}
}
# multi_match查询
# "brand","name","business"只要有一个满足就行
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "外滩如家",
"fields": ["brand","name","business"]
}
}
}
搜索字段越多,查询的效率越低,因此要想办法把多个字段弄到一个字段里去查,比如说all字段
这个all字段实际上是我们索引库操作时"copy_to": "all"
的结果
# term查询
GET /hotel/_search
{
"query": {
"term": {
"city": {
"value": "上海"
}
}
}
}
# range查询
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gte": 100,
"lte": 200
}
}
}
}
gte 大于等于,gt大于, lte同理
精确查询常见的有哪些?
- term查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段
- range查询:根据数值范围查询,可以是数值、日期的范围
第一种
第二种
# distance 查询
GET /hotel/_search
{
"query": {
"geo_distance":{
"distance": "5km",
"location" : "31.21,121.5"
}
}
}
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_ score) ,返回结果时按照分值降序排列。
例如,我们搜索"虹桥如家",结果如下:
可见词条出现次数越多,TF越高,相关性就越高。所以早期我们计算文档得分就是计算TF。因为【虹桥,如家】两个分词,就先计算虹桥,然后计算如家的TF,相加就可以了。
但是这种算法有一种问题,【如家】这个词在三篇文档中都有出现,再去把【如家】进行累加毫无意义。后面为了避免这种在每个文档中都出现这个词的情况,这种词的权重比较低,所以我们引入新的算法:
逆文档频率
文档总数/ 包含词条的文档总数。比方说我们拿【如家】为例,包含【如家】的文档有3个,而文档总数也是3个,3除3为1,因此Log1 = 0,代表这个如家的权重就是0。
相反如果是【虹桥】呢?包含【虹桥】的文档有1个,而文档总和是3个,3除1=3,Log 3 = 0.477,所以【虹桥】的权重就比较高,因为这个词在文档中出现的次数越少,权重则越高。将来得分也就越高。
最终得分就是TF乘IDF,再累加。
这就是业界常用的 TF-IDF算法。
但是在我们ES中并没有使用这种算法,(早期ES是有使用过),从ES的5.1开始就已经没有再使用这种算法。而采用了新的算法:
这种算法不会受词频影响较大,在传统TF算法中,词频越高,将来得分会无限增加。但是BM25算法最终得分趋于一种水平。
使用function score query,可以修改文档的相关性算分(query score) ,根据新得到的算分排序。
# 算分函数
GET /hotel/_search
{
"query": {
"function_score": {
"query": {
"match": {
"all": "外滩"
}
},
"functions": [
{
"filter": {
"term": {
"brand": "如家"
}
},
"weight": 10
}
]
}
}
}
其他使用的案例查阅官方文档
官方文档
修改前
# 复合查询
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "如家"
}
},
{
"geo_distance":{
"distance":"10km",
"location":{
"lat":31.21,
"lon":121.5
}
}
}
],
"must_not": [
{
"range":{
"price": {
"gt": 400
}
}
}
]
}
}
}
修改后:(将geo_distance 这种不需要参与算法的,放入filter里,可以提高查询效率)
# 复合查询
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "如家"
}
}
],
"must_not": [
{
"range":{
"price": {
"gt": 400
}
}
}
],
"filter": [
{
"geo_distance":{
"distance":"10km",
"location":{
"lat":31.21,
"lon":121.5
}
}
}
]
}
}
}
一般情况下,关键字搜索放到must里,其他的尽量放到must_not和filter里
# 按用户评价降序,评价相同按照价格升序排序
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"score": {
"order": "desc"
},
"price": {
"order": "asc"
}
}
]
}
获取经纬度的方式 - 高德开放平台
# 找到121.393598,31.316488周围的酒店,距离升序排序
# 113.260791,23.128016 广州 (查询结果都是深圳的)
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"order": "asc",
"unit": "km",
"location": {
"lat": 23.128016,
"lon": 113.260791
}
}
}
]
}
你一旦做了排序,相关性打分就没有意义了。所以这时候ES会放弃打分, 效率提高。
elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。
elasticsearch中通过修改from、size参数来控制要返回的分页结果:
ES使用的倒排索引,所以它是不适合分页的,它其实是逻辑上的分页,
比方说我现在要查询990到1000这10条数据,对于ES来说只能是查出从0到1000的所有数据,然后再去截取990到1000的这一部分,这是因为其数据结构决定的。这种在单点查询是没有问题的,在生产环境为了让ES存储更多的数据,一定会做集群,而且ES天生就是支持集群的。一旦做了集群,ES就会把数据做拆分。
放到不同的机器上,拆分出的每一份我们叫做分片,每一片上的数据是不一样的。现在我要按照价格做排序,集群ES就不知道是找哪个分片上的前1000条。而是把所有分片上的前1000名,都取出来合并重新作个排序,才是前1000条。
首先在每个数据分片上都排序并查询前1000条文档。
然后将所有节点的结果聚合,在内存中重新排序选出前1000条文档
最后从这1000条中,选取从990开始的10条文档
在生成环境下,像百度,ES集群达到数千台,意味着要在每一台上截取1000个,至少百万级别的截取量。还要排序五百万条记录。内存消耗非常大。
如果搜索页数过深,或者结果集(from + size)越大,对内存和CPU的消耗也越高。因此ES设定结果集查询的上限是10000
面临深度分页问题,要在业务上杜绝。
如果有这样的需求怎么办?
针对深度分页,ES提供了两种解决方案,官方文档:
•search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
•scroll:原理将排序数据形成快照,保存在内存。官方已经不推荐使用。
from + size:
•优点:支持随机翻页
•缺点:深度分页问题,默认查询上限(from + size)是10000
•场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
after search:
•优点:没有查询上限(单次查询的size不超过10000)
•缺点:只能向后逐页查询,不支持随机翻页
•场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
scroll:
•优点:没有查询上限(单次查询的size不超过10000)
•缺点:会有额外内存消耗,并且搜索结果是非实时的
•场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 after search方案。
# 高亮查询 默认情况下,ES搜索字段必须与高亮字段一致
GET /hotel/_search
{
"query": {
"match": {
"all": "如家"
}
},
"highlight": {
"fields": {
"name": {
"require_field_match": "false"
}
}
}
}
/**
* 测试matchall
*/
@Test
void MatchAll() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders.matchAllQuery());
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits().value;
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
System.out.println("json = " + json);
}
}
/**
* 测试match和multimatch
*/
@Test
void Match() throws IOException {
SearchRequest request = new SearchRequest("hotel");
//单字段查询
// QueryBuilders builder1 = QueryBuilders.matchQuery("all", "如家");
//多字段查询
MultiMatchQueryBuilder builder2 = QueryBuilders.multiMatchQuery("如家","name","business");
request.source().query(builder2);
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits().value;
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
System.out.println("json = " + json);
}
}
/**
* 测试termAndRange
*/
@Test
void termAndRange() throws IOException {
SearchRequest request = new SearchRequest("hotel");
//TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("city", "深圳");
RangeQueryBuilder price = QueryBuilders.rangeQuery("price").gte(100).lte(150);
request.source().query(price);
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits().value;
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
System.out.println("json = " + json);
}
}
/**
* boolQuery
*/
@Test
void boolQuery() throws IOException {
SearchRequest request = new SearchRequest("hotel");
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.must(QueryBuilders.termQuery("city","深圳"));
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lte(250));
request.source().query(boolQueryBuilder);
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits().value;
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
System.out.println("json = " + json);
}
}
/**
* fenyeANDpaixu
*/
@Test
void fenyeANDpaixu() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders.matchAllQuery());
//分页
request.source().from(0).size(5);
//价格排序
request.source().sort("price", SortOrder.ASC);
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits().value;
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
System.out.println("json = " + json);
}
}
/**
* 高亮
*/
@Test
void highlight() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().highlighter(
new HighlightBuilder().field("name").requireFieldMatch(false));
//搜索如家,把如家高亮
MatchQueryBuilder builder1 = QueryBuilders.matchQuery("all", "如家");
request.source().query(builder1);
SearchResponse response = client.search(request,RequestOptions.DEFAULT);
SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits().value;
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
HotelDoc hotelDoc = JSON.parseObject(hit.getSourceAsString(),HotelDoc.class);
//处理高亮
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (!CollectionUtils.isEmpty(highlightFields)) {
//高亮字段
HighlightField highlightField = highlightFields.get("name");
if(highlightField != null){
//取出高亮字段第一个
String name = highlightField.getFragments()[0].string();
hotelDoc.setName(name);
}
}
System.out.println("hotelDoc = " + hotelDoc);
}
}