如今,不管是面试,还是工作中,ES的出场越来越频繁,基于兴趣,迫于现实,开始学习ES,网上找了《ElasticSearch权威指南》,也在bilibili找了写学习视频,以此记录学习过程,总结学习经验。如有不对之处,还请大佬们指点一下。
学习前,使用docker安装了7.7.0版本的ES,顺便也安装了kibana工具(可以理解为ES的图形化管理、操作工具),后面的学习也基于这个版本。安装很简单,可以参考这些连接Docker安装ES。这里就不过多讲解了。
然后是一个数据的准备,我自己写了个demo,造了8000W数据(花了差不多6个小时),以便后面数据。demo代码地址,有兴趣的小伙伴可以直接拉取,修改配置文件中信息后调用接口/user/init?size=xx(需要多少数据size就传多少)即可造数据。

数据实体类(基本的类型都有了,如果有需要其他的,可以自己加):
public class User {
private String name;
private Integer age;
private String nativePlace;
private String phone;
private String school;
// 0 :男 1 :女
private Integer sex;
private Double height;
private Double weight;
private List<String> hobbys;
private LocalDate birthday;
}
好了,接下来就开始我们的ES之旅吧。。。。
ES是什么呢? 《Elasticsearch权威指南》中是这样去描述它的
Elasticsearch是一个基于Apache Lucene™的开源搜索引擎。无论在开源还是专有领域,
Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。
但是,Lucene只是一个库。想要使用它,你必须使用Java来作为开发语言并将其直接集成到
你的应用中,更糟糕的是,Lucene非常复杂,你需要深入了解检索的相关知识来理解它是如
何工作的。
Elasticsearch也使用Java开发并使用Lucene作为其核心来实现所有索引和搜索的功能,但是
它的目的是通过简单的 RESTful API 来隐藏Lucene的复杂性,从而让全文搜索变得简单。
不过,Elasticsearch不仅仅是Lucene和全文搜索,我们还能这样去描述它:
分布式的实时文件存储,每个字段都被索引并可被搜索
分布式的实时分析搜索引擎
可以扩展到上百台服务器,处理PB级结构化或非结构化数据
ES里面有一个索引的概念,在Elasticsearch中存储数据的行为就叫做索引(indexing),不过在索引之前,我们需要明确数据应该存储在哪里。
在Elasticsearch中,文档归属于一种类型(type),而这些类型存在于索引(index)中,我们可以画一些简单的对比图来类比传统关系型数据库:
Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices -> Types -> Documents -> Fields
Elasticsearch集群可以包含多个索引(indices)(数据库),每一个索引可以包含多个类型(types)(表),每一个类型包含多个文档(documents)(行),然后每个文档包含多个字段(Fields)(列)。前文中提到的数据准备的数据我是放在了user的这个索引中。
添加索引(此处不理解可以先看后面的怎么与ES交互的介绍):
PUT /user
{
"settings" : {
"number_of_shards" : 3, // 该值一旦设置后不可修改
"number_of_replicas" : 0 // 备份的数量,因为我是单机部署的,这里设置为0
}
}
为了将数据添加到Elasticsearch,我们需要索引(index)——一个存储关联数据的地方。实际上,索引只是一个用来指向一个或多个分片(shards)的“逻辑命名空间(logical namespace)”.
一个分片(shard)是一个最小级别“工作单元(worker unit)”,它只是保存了索引中所有数据的一部分。在接下来的《深入分片》一章,我们将详细说明分片的工作原理,但是现在我们只要知道分片就是一个Lucene实例,并且它本身就是一个完整的搜索引擎。我们的文档存储在分
片中,并且在分片中被索引,但是我们的应用程序不会直接与它们通信,取而代之的是,直接与索引通信。
分片是Elasticsearch在集群中分发数据的关键。把分片想象成数据的容器。文档存储在分片中,然后分片分配到你集群中的节点上。当你的集群扩容或缩小,Elasticsearch将会自动在你的节点间迁移分片,以使集群保持平衡。分片可以是主分片(primary shard)或者是复制分片(replica shard)。你索引中的每个文档属于一个单独的主分片,所以主分片的数量决定了索引最多能存储多少数据。
理论上主分片能存储的数据大小是没有限制的,限制取决于你实际的使用情况。分片的最大容量完全取决于你的使用状况:硬件存储的大小、文档的大小和复杂度、如何索引和查询你的文档,以及你期望的响应时间。
复制分片只是主分片的一个副本,它可以防止硬件故障导致的数据丢失,同时可以提供读请求,比如搜索或者从别的shard取回文档。
当索引创建完成的时候,主分片的数量就固定了,但是复制分片的数量可以随时调整。让我们在集群中唯一一个空节点上创建一个叫做 blogs 的索引。默认情况下,一个索引被分配5个主分片,但是为了演示的目的,我们只分配3个主分片.
可以在kibana工具中查看索引的信息,Primaries就是主分片的数量:

与ES交互的方式我目前了解到的有3种
postman直接调用接口,如下图所示,可以查看user索引的一个信息
3. 使用kibana工具

使用java代码,elasticsearch-rest-high-level-client工具包。前面造数据的demo有相关的代码演示。
增加数据有两种方式,一种是制定ID的,我们可以自己定义ID,也可以使用ES自动生成的ID。
指定ID的方式用PUT请求 PUT /{index}/{type}/{id} 或者 PUT /{index}/_doc/{id}
PUT /user/type1/222
{
"age": 54,
"birthday": "1967-11-29",
"height": 159.7,
"hobbys": [
"单机游戏",
"听音乐",
"跑步",
"绘画"
],
"name": "张三四五",
"nativePlace": "天津市",
"phone": "13658233947",
"school": "华东师范大学",
"sex": 0,
"weight": 101.26
}
创建成功之后我们可以得到这样的信息
{
"_index" : "user",
"_type" : "_doc",
"_id" : "222",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 29134582,
"_primary_term" : 1
}
不指定ID的方式就需要使用POST请求 POST /{index}/{type} 或者 POST/{index}/_doc
POST /user/_doc
{
"age": 54,
"birthday": "1967-11-29",
"height": 159.7,
"hobbys": [
"单机游戏",
"听音乐",
"跑步",
"绘画"
],
"name": "张三四五六",
"nativePlace": "天津市",
"phone": "13658233947",
"school": "华东师范大学",
"sex": 0,
"weight": 101.26
}
创建成果后,我们同样可以得到这样的信息,注意,这时候的ID就是ES帮我们生成的了
{
"_index" : "user",
"_type" : "_doc",
"_id" : "ZkfHpoIBJl3IJPYzIxVG",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 29134583,
"_primary_term" : 1
}
只要执行HTTP GET请求并指出文档的“地址”——索引、类型和ID既可。根据这三部分信息,我们就可以返回原始JSON文档,默认会返回10条数据,GET /{index}/{type}/_search比如:GET /user/type1/_search,但是高版本中其实已经不建议使用type了,直接用GET /user/_search

也可以对指定的某一个id进行查询/{index}/{type}/{id},例如:GET /user/type1/0IcjloIBJl3IJPYzIjol,在我们使用的这个版本中已经不推荐使用这种方式来查询,推荐使用_doc替换type另外一种方式:GET /{index}/_doc/{id},例如:GET /user/_doc/0IcjloIBJl3IJPYzIjol。
也可以对指定的字段进行搜索GET /user/_search?q=name:张三
比如查询我们刚才创建的数据
GET /user/_doc/ZkfHpoIBJl3IJPYzIxVG
查询的数据如下:
{
"_index" : "user",
"_type" : "_doc",
"_id" : "ZkfHpoIBJl3IJPYzIxVG",
"_version" : 1,
"_seq_no" : 29134583,
"_primary_term" : 1,
"found" : true,
"_source" : {
"age" : 54,
"birthday" : "1967-11-29",
"height" : 159.7,
"hobbys" : [
"单机游戏",
"听音乐",
"跑步",
"绘画"
],
"name" : "张三四五六",
"nativePlace" : "天津市",
"phone" : "13658233947",
"school" : "华东师范大学",
"sex" : 0,
"weight" : 101.26
}
}
可能有的同学只想查其中的几个字段,可以这样操作
GET /user/_doc/ZkfHpoIBJl3IJPYzIxVG?_source=age,height

今天只是先简单的将个例子,便于理解,不用纠结语法,后面会详细的介绍。
需要注意的是,我们可以看到hits.total.value的值是10000,这是因为默认最大是1000,如果需要展示全部的信息,我们需要用另外一种搜索方式,带JSON参数的查询,设置track_total_hits为true。
查询字符串搜索便于通过命令行完成特定(ad hoc)的搜索,但是它也有局限性。Elasticsearch提供丰富且灵活的查询语言叫做DSL查询(Query DSL),它允许你构建更加复杂、强大的查询。DSL(Domain Specific Language特定领域语言)以JSON请求体的形式出现。我们可以这样表示之前关于“张三”的查询:
到目前为止搜索都很简单:搜索特定的名字,通过年龄筛选。让我们尝试一种更高级的搜索,全文搜索——一种传统数据库很难实现的功能。
GET /user/_search
{
"track_total_hits": true,
"query":{
"match": { // 全文搜索,可以搜索到张三思,也可以搜索到张思三,只要名字中有这两个字或者其中一个就可以,只是他们的_score值不一样,关于_score后面有介绍
"name":"张三"
}
}
}

下面我们再试一个更复杂一点的查询,查询名字是"张三”,年龄在20-30之间的数据,那么查询条件可以这么写,
GET /user/_search
{
"query": {
"bool": { // 5.0之前是filtered
"filter": {
"range": {
"age": {
"gt": 20,
"lt": 30
}
}
},
"must": {
"match_phrase": { // match_phrase表示完全匹配的精确查询 如:张三思可以匹配,但是张思三就匹配不到
"name": "张三"
}
}
}
}
}
很多应用喜欢从每个搜索结果中高亮(highlight)匹配到的关键字,这样用户可以知道为什么这些文档和查询相匹配。在Elasticsearch中高亮片段是非常容易的。让我们在之前的语句上增加 highlight 参数:
GET /user/_search
{
"query": {
"match": {
"name": "张三"
}
},
"highlight": {
"fields": {
"name": {}
}
}
}
当我们运行这个语句时,会命中与之前相同的结果,但是在返回结果中会有一个新的部分叫做 highlight ,这里包含了来自 about 字段中的文本,并且用 来标识匹配到的单词。

最后,我们还有一个需求需要完成:允许管理者在职员目录中进行一些分析。 Elasticsearch有一个功能叫做聚合(aggregations),它允许你在数据上生成复杂的分析统计。它很像SQL中的 GROUP BY 但是功能更强大。
举个例子,让我们来看看爱好最多的是哪几个:
GET /user/_search
{
"aggs": {
"all_hobbys": {
"terms": {
"field": "hobbys.keyword"
}
}
}
}
可以看到,在查询结果中多了“aggregations”的数据,排在前面的就是我们想要的结果

因为没有带条件,默认是返回10条,这里为了方便查看,我把多余的数据删除了只保留了一条数据。
{
"took" : 1, // 查询花费的时间,单位是ms
"timed_out" : false, // 查询是否超时
"_shards" : { // 执行请求时查询的分片信息
"total" : 3,// 查询的分片数量
"successful" : 3, // 成果返回结果的分片数量
"skipped" : 0, // 失败的分片数量
"failed" : 0
},
"hits" : {
"total" : {
"value" : 87404591, // 查询返回的文档总数
"relation" : "eq"
},
"max_score" : 1.0, // 计算所得的最高分
"hits" : [ // 返回文档的hits数组
{
"_index" : "user", // 索引
"_type" : "type1", // 属性
"_id" : "0IcjloIBJl3IJPYzIjol", // 唯一ID,每条数据都会有一个唯一的ID
"_score" : 1.0, // 这条数据的得分
"_source" : { // 发送到索引的JSON对象
"age" : 49,
"birthday" : "1972-10-22",
"height" : 150.2,
"hobbys" : [
"萨克斯",
"上网聊天",
"看韩剧"
],
"name" : "徐稣",
"nativePlace" : "辽宁省",
"phone" : "19873829888",
"school" : "西安培华学院",
"sex" : 1,
"weight" : 63.35
}
}
]
}
}
第一种方式,我们可以使用一个新的数据把原来的覆盖,还是使用PUT请求 PUT /{index}/{type}/{id} 或者 PUT /{index}/_doc/{id}
下面的例子中我们把刚才的数据age改为888:
PUT /user/_doc/ZkfHpoIBJl3IJPYzIxVG
{
"age" : 888,
"birthday" : "1967-11-29",
"height" : 159.7,
"hobbys" : [
"单机游戏",
"听音乐",
"跑步",
"绘画"
],
"name" : "张三四五六",
"nativePlace" : "天津市",
"phone" : "13658233947",
"school" : "华东师范大学",
"sex" : 0,
"weight" : 101.26
}
可以看到返回的结果中_version字段变成了2,再去查询可以看到结果也变成了age=888
{
"_index" : "user",
"_type" : "_doc",
"_id" : "ZkfHpoIBJl3IJPYzIxVG",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 29134584,
"_primary_term" : 1
}
可以用POST的请求方式对某一个字段直接更新,POST /{index}/_update/{id}
比如,下面我们将age改为37
POST /user/_update/ZkfHpoIBJl3IJPYzIxVG
{
"doc":{
"age":37
}
}
执行结果如下,可以看到,我们每做一次修改,version就会+1
{
"_index" : "user",
"_type" : "_doc",
"_id" : "ZkfHpoIBJl3IJPYzIxVG",
"_version" : 3,
"result" : "updated",
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 29134587,
"_primary_term" : 1
}
删除就相对来说比较简单啦,前面应该已经看出来了规律,没错,就是用DELET /{index}/_doc/{id}
下面,我们把刚才创建的数据删除掉
DELETE /user/_doc/ZkfHpoIBJl3IJPYzIxVG
{
"_index" : "user",
"_type" : "_doc",
"_id" : "ZkfHpoIBJl3IJPYzIxVG",
"_version" : 4,
"result" : "deleted",
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 29134588,
"_primary_term" : 1
}
再去查询的时候,就会看到已经查不到了
{
"_index" : "user",
"_type" : "_doc",
"_id" : "ZkfHpoIBJl3IJPYzIxVG",
"found" : false
}