1.初识Elasticsearch

什么是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具备下列优势:

  1. 支持分布式,可水平扩展.
  2. 提供Restful接口,可被任何语言调用

因此具备了处理海量数据和高并发场景的能力。

为什么学习elasticsearch?

搜索引擎技术排名:

  1. Elasticsearch:开源的分布式搜索引擎

  2. Splunk: 商业项目

  3. Solr: Apache的开源搜索引擎

总结

什么是elasticsearch?

​ 一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能

什么是elastic stack (ELK) ?

​ 是以elasticsearch为核心的技术栈 ,包括beats、Logstash.kibana、elasticsearch

什么是Lucene?

​ 是Apache的开源搜索引擎类库, 提供了搜索引擎的核心API

2.倒排索引

倒排索引是与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的正向索引,也会很快。

所以,我们其实经过了两次查询,但是两次查询都经过了索引,这个效率是比逐条查询效率高的。

倒排索引之所以叫倒排索引,因为我们在正向索引中,我们要找到包含手机的,我们得一行行的看,先看文档,再看是否包含词条。倒排索引是倒过来的,先去看词条,再去关联到文档。

倒排索引更擅长于基于文档的内容进行搜索,更复杂的搜索需求,这就是为什么我们的搜索引擎底层都是基于倒排索引的原因。

总结

什么是文档什么是词条?

  • 每一条数据就是一 个文档

  • 对文档中的内容分词,得到的词语就是词条

什么是正向索引?

  • 基于文档id创建索引。查询词条时必须先找到文档,而后判断是否包含词条

什么是倒排索引?

  • 对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时先根据词条查询到文档id,而后获取到文档

3.ES与Mysql的概念对比

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查询)还是用数据库没问题的。

合适场景选择合适的技术

4.安装elasticsearch,kibana

4.1 部署单点ES

因为我们还需要部署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语句有自动补全功能。

4.2 安装IK分词器

分词器

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分词器还有其他的中文分词器,都会依赖于一个字典去做分词。它这个字典中可能会包含我们不希望的分词(如 “的”,“了”,“哦”),也有我们希望有的新潮词汇(如“奥利给”,“白嫖”)等。我们应该如何扩展呢?

4.3 IK分词器 - 扩展词库

要拓展ik分词器的词库,只需要修改一个ik分词器 目录中的config目录中的IkAnalyzer.cfg.xml文件:

要禁用某些敏感词条,只需要修改一个ik分词器目录中的config目录中的IkAnalyzer.cfg.xml文件:

在ik分词器的config目录下修改以下3个文件

/var/lib/docker/volumes/es-plugins/_data/ik/config

其他更多的功能,参考官网文档

总结

5.索引库操作

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

总结:

6.文档的CRUD

添加文档

查看、删除文档

修改文档

所有DSL


# 插入文档
POST /heima/_doc/1
{"info":"黑马程序员java讲师","email":"zy@123.com","name":{"firstName":"y","lastName":"z"}
}# 查询文档
GET /heima/_doc/1# 删除文档
DELETE /heima/_doc/1# 修改文档方式1,文档存在的情况:updated
PUT /heima/_doc/1
{"info":"黑马程序员java讲师","email":"zhaoyun@123.com","name":{"firstName":"y","lastName":"z"}
}# 修改文档方式1,文档不存在的情况:created
PUT /heima/_doc/3
{"info":"黑马程序员java讲师","email":"12312321313@123.com","name":{"firstName":"y","lastName":"z"}
}# 修改文档方式2-局部修改 updated
POST /heima/_update/1
{"doc":{"email":"zzyy@123.com"}
}

总结:

7.RestClient操作索引库

什么是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

引入依赖:

 <!--elasticsearch-->
<dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId><version>7.12.1</version>
</dependency>

为什么有些依赖是7.6.2版本?

因为依赖被Springboot管理,所以想要覆盖Springboot的版本定义,找到自己的pom文件,在properties标签下定义

<elasticsearch.version>7.12.1</elasticsearch.version>

所以用springboot管理时,一定要去properties里指明版本。

那么这里的版本信息不写也行

 <!--elasticsearch-->
<dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId>
</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的初始化*/@BeforeEachvoid setUp() {//如果是集群,这里的HttpHost.create("http://192.168.10.100:9200")可以用逗号分割this.client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.10.100:9200")));}/*** 用完后销毁*/@AfterEachvoid 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

8.RestClient操作文档

添加


@SpringBootTest
public class HotelDocumentTest {@Autowiredprivate IHotelService iHotelService;private RestHighLevelClient client;/*** 添加文档*/@Testvoid 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的初始化*/@BeforeEachvoid setUp() {//如果是集群,这里的HttpHost.create("http://192.168.10.100:9200")可以用逗号分割this.client = new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.10.100:9200")));}/*** 用完后销毁*/@AfterEachvoid 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.准备RequestUpdateRequest request = new UpdateRequest("hotel","36934");//2.准备请求参数request.doc("price","337","starName","五星");//3.发送请求client.update(request,RequestOptions.DEFAULT);
}

删除

/*** 删除文档*/
@Test
void testDeleteDocument() throws IOException {//1.准备RequestDeleteRequest request = new DeleteRequest("hotel","36934");//2.发送请求client.delete(request,RequestOptions.DEFAULT);
}

总结

文档操作的基本步骤:

  1. 初始化RestHighLevelClient
  2. 创建XxxRequest。XXX是Index、Get、Update、Delete
  3. 准备参数(Index和Update时需要)
  4. 发送请求。调用RestHighLevelClient#.xxx()方法,xxx是index、get、update、delete
  5. 解析结果(Get时需要)

批量处理文档

 /*** 批量导入数据到文档*/@Testvoid 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

9.分布式搜索引擎

1.DSL查询语法

DSL Query的分类

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

DSL Query - Match查询

默认只查询出来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同理

精确查询常见的有哪些?

  1. term查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段
  2. 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

使用function score query,可以修改文档的相关性算分(query score) ,根据新得到的算分排序。


# 算分函数
GET /hotel/_search
{"query": {"function_score": {"query": {"match": {"all": "外滩"}},"functions": [{"filter": {"term": {"brand": "如家"}},"weight": 10}]}}
}

其他使用的案例查阅官方文档

官方文档

复合查询 Boolean Query

修改前


# 复合查询
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里

2.搜索结果处理

1.排序

# 按用户评价降序,评价相同按照价格升序排序
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会放弃打分, 效率提高。

2.分页

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方案。

3.高亮

# 高亮查询 默认情况下,ES搜索字段必须与高亮字段一致
GET /hotel/_search
{"query": {"match": {"all": "如家"}},"highlight": {"fields": {"name": {"require_field_match": "false"}}}
}

4.总结

3.RestClient查询文档

match_all

/*** 测试matchall*/@Testvoid 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和multi_match

/*** 测试match和multimatch*/@Testvoid 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);}}

term和range

/*** 测试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);}
}

bool查询

/*** 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);}
}

07SpringCloud-Elasticsearch相关推荐

  1. Elasticsearch学习之路(一)

    一.前序 1.1正向索引和倒排索引 ** 正向索引通常用于数据库中,在搜索引擎领域使用的最多的就是倒排索引 ** 通过例子表示: 我爱编程, 我爱编程,我是小码农 1.1.1 正向索引 假设我们使用m ...

  2. 2021年大数据ELK(二十五):添加Elasticsearch数据源

    全网最详细的大数据ELK文章系列,强烈建议收藏加关注! 新文章都已经列出历史文章目录,帮助大家回顾前面的知识重点. 目录 添加Elasticsearch数据源 一.Kibana索引模式 添加Elast ...

  3. 2021年大数据ELK(十九):使用FileBeat采集Kafka日志到Elasticsearch

    全网最详细的大数据ELK文章系列,强烈建议收藏加关注! 新文章都已经列出历史文章目录,帮助大家回顾前面的知识重点. 目录 使用FileBeat采集Kafka日志到Elasticsearch 一.需求分 ...

  4. 2021年大数据ELK(十七):Elasticsearch SQL 订单统计分析案例

    全网最详细的大数据ELK文章系列,强烈建议收藏加关注! 新文章都已经列出历史文章目录,帮助大家回顾前面的知识重点. 目录 订单统计分析案例 一.案例介绍 二.创建索引 三.导入测试数据 四.统计不同支 ...

  5. 2021年大数据ELK(十六):Elasticsearch SQL(职位查询案例)

    全网最详细的大数据ELK文章系列,强烈建议收藏加关注! 新文章都已经列出历史文章目录,帮助大家回顾前面的知识重点. 目录 职位查询案例 一.查询职位索引库中的一条数据 二.将SQL转换为DSL 三.职 ...

  6. 2021年大数据ELK(十五):Elasticsearch SQL简单介绍

    全网最详细的大数据ELK文章系列,强烈建议收藏加关注! 新文章都已经列出历史文章目录,帮助大家回顾前面的知识重点. 目录 Elasticsearch SQL简单介绍 一.SQL与Elasticsear ...

  7. 2021年大数据ELK(十三):Elasticsearch编程(添加职位数据)

    全网最详细的大数据ELK文章系列,强烈建议收藏加关注! 新文章都已经列出历史文章目录,帮助大家回顾前面的知识重点. 目录 Elasticsearch编程 一.添加职位数据 1.初始化客户端连接 2.实 ...

  8. 2021年大数据ELK(十二):Elasticsearch编程(环境准备)

    全网最详细的大数据ELK文章系列,强烈建议收藏加关注! 新文章都已经列出历史文章目录,帮助大家回顾前面的知识重点. 目录 Elasticsearch编程 一.环境准备 1.准备IDEA项目结构 2.准 ...

  9. 2021年大数据ELK(十一):Elasticsearch架构原理

    全网最详细的大数据ELK文章系列,强烈建议收藏加关注! 新文章都已经列出历史文章目录,帮助大家回顾前面的知识重点. 目录 Elasticsearch架构原理 一.Elasticsearch的节点类型 ...

  10. 2021年大数据ELK(八):Elasticsearch安装IK分词器插件

    全网最详细的大数据ELK文章系列,强烈建议收藏加关注! 新文章都已经列出历史文章目录,帮助大家回顾前面的知识重点. 目录 系列历史文章 安装IK分词器 一.下载Elasticsearch IK分词器 ...

最新文章

  1. cavium公司xPliant芯片
  2. python调用dll函数_从Python调用DLL函数
  3. opencv 通过标定摄像头测量物体大小_视觉激光雷达信息融合与联合标定
  4. BJUI验证Input非空和是否为数字
  5. java map存储对象_JAVA:查找存储在hashMap中的对象的最佳性能方法
  6. 自学Java编程要做好哪些准备?
  7. c语言 手动实现sizeof,sizeof究竟是怎样实现的?
  8. 微软对 Windows 10 Mobile 的支持将于12月10日结束
  9. android 辐射动画_Android 四种动画效果的调用实现代码
  10. nero刻录软件linux,下载:Linux平台刻录工具NeroLINUX 3.5.2.0版
  11. 12306网站抢票机制攻与防
  12. python截图并识别文字
  13. 电脑图标出现蓝色问号解决方法
  14. Android手机截图怎么做,手机截屏怎么弄,教您手机截图方法
  15. SMT32标准库函数——GPIO_ReadInputDataBit的使用(类比HAL库函数:HAL_GPIO_ReadPin函数)
  16. Android WiFi 以太网同时上内外网
  17. 有没有微信相互投票群?
  18. python简单爬取安居客的新房和二手房信息
  19. vector BLF 文件读写
  20. 关于form与表单提交

热门文章

  1. 【Android 系统】--- 下载 Android源码
  2. terracotta安装配置与集群过程
  3. 北京市新型冠状病毒疫情区域图
  4. 使用SfntTool制作字体剪辑工具1 - 直接使用sfnttool.jar
  5. 教你一招:Win10切换输入法与Win7一样(Ctrl + 空格)
  6. Interpreter(解释器模式)行为型
  7. 项目上线,部署到服务器(腾讯服务器),http协议及https协议(微信小程序必须https协议才可发布)、Nginx配置
  8. 【warm up】热身训练 的学习率设置
  9. 【python】幼儿园分班
  10. 在c语言中pwm的作用,详细注解的PWM c程序初学者适用