1. ElasticSearch概述
1.1 ElasticSearch是什么
Elaticsearch,简称为 ES,ES 是一个开源的高扩展的分布式全文搜索引擎,是整个Elastic Stack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上 百台服务器,处理PB级别的数据。
1.2 全文搜索引擎
全文搜索引擎指的是目前广泛应用的主流搜索引擎。它的工作原理是计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的 次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反 馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。
2. ElasticSearch入门
2.1 ElasticSearch安装
参考这篇博客:https://blog.csdn.net/weixin_44308006/article/details/130544345?fromshare=blogdetail&sharetype=blogdetail&sharerId=130544345&sharerefer=PC&sharesource=qq_51313170&sharefrom=from_link
2.2 ElasticSearch基本操作
2.2.1 RESTful
REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就 是 RESTful。Web 应用程序最重要的 REST 原则是,客户端和服务器之间的交互在请求之 间是无状态的。从客户端到服务器的每个请求都必须包含理解请求所必需的信息。如果服务 器在请求之间的任何时间点重启,客户端不会得到通知。此外,无状态请求可以由任何可用 服务器回答,这十分适合云计算之类的环境。客户端可以缓存数据以改进性能。
在服务器端,应用程序状态和功能可以分为各种资源。资源是一个有趣的概念实体,它 向客户端公开。资源的例子有:应用程序对象、数据库记录、算法等等。每个资源都使用 URI (Universal Resource Identifier) 得到一个唯一的地址。所有资源都共享统一的接口,以便在客 户端和服务器之间传输状态。使用的是标准的 HTTP 方法,比如 GET、PUT、POST 和 DELETE。
在 REST 样式的 Web 服务中,每个资源都有一个地址。资源本身都是方法调用的目 标,方法列表对所有资源都是一样的。这些方法都是标准方法,包括 HTTP GET、POST、 PUT、DELETE,还可能包括 HEAD 和 OPTIONS。简单的理解就是,如果想要访问互联 网上的资源,就必须向资源所在的服务器发出请求,请求体中必须包含资源的网络路径,以 及对资源进行的操作(增删改查)。
2.2.2 数据格式
Elasticsearch 是面向文档型数据库,一条数据在这里就是一个文档。为了方便大家理解, 我们将Elasticsearch 里存储文档数据和关系型数据库MySQL存储数据的概念进行一个类比
ES 里的Index可以看做一个库,而Types相当于表,Documents则相当于表的行。 这里Types的概念已经被逐渐弱化,Elasticsearch 6.X中,一个index下已经只能包含一个 type,Elasticsearch 7.X 中, Type 的概念已经被删除了。
2.2.3 HTTP操作
2.2.3.1 索引操作
创建索引
对比关系型数据库,创建索引就等同于创建数据库
在Postman 中,向 ES 服务器发 PUT 请求 http://127.0.0.1:9200/shopping
{"acknowledged": true, # 响应结果,true:操作成功"shards_acknowledged": true, # 分片结果,true:分片操作成功"index": "shopping" # 索引名称
}
注意:创建索引库的分片数默认1片,在7.0.0之前的ElasticSearch版本中,默认5片。
如果重复添加索引,会返回错误信息。
查看所有索引
在Postman 中,向 ES 服务器发 GET 请求 http://127.0.0.1:9200/_cat/indices?v
这里请求路径中的_cat 表示查看的意思, indices 表示索引,所以整体含义就是查看当前 ES服务器中的所有索引,就好像 MySQL 中的 show tables 的感觉,服务器响应结果如下
[外链图片转存中…(img-Wh9O222A-1733750133973)]
查看单个索引
在Postman 中,向 ES 服务器发 GET 请求 http://127.0.0.1:9200/shopping
{"shopping": { # 索引名"aliases": {}, # 别名"mappings": {}, # 映射"settings": { # 设置"index": { # 设置-索引"creation_date": "1733230880479", # 设置-索引-创建时间"number_of_shards": "1", # 设置-索引-主分片数量"number_of_replicas": "1", # 设置-索引-副分片数量"uuid": "P8N_RyNkTbmJAiBAxydpPA", # 设置-索引-唯一标识"version": { # 设置-索引-版本"created": "7080099"},"provided_name": "shopping" # 设置-索引-名称}}}
}
删除索引
在Postman 中,向 ES 服务器发 DELETE 请求 http://127.0.0.1:9200/shopping
重新访问索引时,服务器返回响应:索引不存在
[外链图片转存中…(img-ouAjU6Da-1733750133973)]
2.2.3.2 文档操作
创建文档
索引已经创建好了,接下来我们来创建文档,并添加数据。这里的文档可以类比为关系型数
据库中的表数据,添加的数据格式为 JSON 格式
在Postman 中,向 ES 服务器发 POST 请求 http://127.0.0.1:9200/shopping/_doc
请求体内容为:
{"title":"小米手机","category":"小米","images":"http://www.gulixueyuan.com/xm.jpg","price":3999.00
}
注意:这里的请求方式不能用PUT,因为上述请求发出多次返回的_id不同,说明POST操作不是幂等性,然而PUT是幂等性,所以这里不能使用PUT
服务器响应结果如下:
{"_index": "shopping",//索引"_type": "_doc",//类型-文档"_id": "ANQqsHgBaKNfVnMbhZYU",//唯一标识,可以类比为 MySQL 中的主键,随机生成"_version": 1,//版本"result": "created",//结果,这里的 create 表示创建成功"_shards": {//"total": 2,//分片 - 总数"successful": 1,//分片 - 总数"failed": 0//分片 - 总数},"_seq_no": 0,"_primary_term": 1
}
上面的数据创建后,由于没有指定数据唯一性标识(ID),默认情况下, ES 服务器会随机生成一个。
如果想要自定义唯一性标识,需要在创建时指定: http://127.0.0.1:9200/shopping/_doc/1
,请求体JSON内容为:
{"title":"小米手机","category":"小米","images":"http://www.gulixueyuan.com/xm.jpg","price":3999.00
}
返回结果如下:
{"_index": "shopping","_type": "_doc","_id": "1",//<------------------自定义唯一性标识"_version": 1,"result": "created","_shards": {"total": 2,"successful": 1,"failed": 0},"_seq_no": 1,"_primary_term": 1
}
此处需要注意:如果增加数据时明确数据主键,那么请求方式也可以为 PUT。
查看文档
查看文档时,需要指明文档的唯一性标识,类似于 MySQL 中数据的主键查询
在 Postman 中,向 ES 服务器发 GET 请求 : http://127.0.0.1:9200/shopping/_doc/1
。
返回结果如下:
{"_index": "shopping","_type": "_doc","_id": "1","_version": 1,"_seq_no": 1,"_primary_term": 1,"found": true,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 3999}
}
查找不存在的内容,向 ES 服务器发 GET 请求 : http://127.0.0.1:9200/shopping/_doc/1001。
返回结果如下:
查看索引下所有数据,向 ES 服务器发 GET 请求 : http://127.0.0.1:9200/shopping/_search。
返回结果如下:
{"took": 133,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 2,"relation": "eq"},"max_score": 1,"hits": [{"_index": "shopping","_type": "_doc","_id": "ANQqsHgBaKNfVnMbhZYU","_score": 1,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 3999}},{"_index": "shopping","_type": "_doc","_id": "1","_score": 1,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 3999}}]}
}
修改文档
全量修改
和新增文档一样,输入相同的 URL 地址请求,如果请求体变化,会将原有的数据内容覆盖
在 Postman 中,向 ES 服务器发 PUT 请求 : http://127.0.0.1:9200/shopping/_doc/1
请求体JSON内容为:
{"title":"华为手机","category":"华为","images":"http://www.gulixueyuan.com/hw.jpg","price":1999.00
}
返回结果如下:
{"_index": "shopping","_type": "_doc","_id": "1","_version": 4,"result": "updated","_shards": {"total": 2,"successful": 1,"failed": 0},"_seq_no": 3,"_primary_term": 1
}
局部修改
修改数据时,也可以只修改某一给条数据的局部信息
在 Postman 中,向 ES 服务器发 POST 请求 : http://127.0.0.1:9200/shopping/_update/1
。
请求体JSON内容为:
{"doc": {"title":"小米手机","category":"小米"}
}
返回结果如下:
{"_index": "shopping","_type": "_doc","_id": "1","_version": 5,"result": "updated","_shards": {"total": 2,"successful": 1,"failed": 0},"_seq_no": 4,"_primary_term": 2
}
在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_doc/1
,查看修改内容:
{"_index": "shopping","_type": "_doc","_id": "1","_version": 3,"_seq_no": 3,"_primary_term": 1,"found": true,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/hw.jpg","price": 1999}
}
删除文档
删除一个文档不会立即从磁盘上移除,它只是被标记成已删除(逻辑删除)。
在Postman 中,向 ES 服务器发 DELETE 请求 http://127.0.0.1:9200/shopping/_doc/1
返回结果如下:
{"_index": "shopping","_type": "_doc","_id": "1","_version": 6,"result": "deleted","_shards": {"total": 2,"successful": 1,"failed": 0},"_seq_no": 5,"_primary_term": 2
}
如果删除一个并不存在的文档,会返回如下结果:
{"_index": "shopping","_type": "_doc","_id": "1","_version": 7,"result": "not_found","_shards": {"total": 2,"successful": 1,"failed": 0},"_seq_no": 6,"_primary_term": 2
}
条件查询文档
URL带参查询
在 Postman 中,向 ES 服务器发 GET请求 :http://127.0.0.1:9200/shopping/_search?q=category:小米
返回结果如下:
{"took": 1,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 1,"relation": "eq"},"max_score": 1.3862942,"hits": [{"_index": "shopping","_type": "_doc","_id": "1001","_score": 1.3862942,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 3999.00}}]}
}
带请求体方式的查找所有内容
在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search
,附带JSON体如下:
{"query" : {"match" : {"category" : "小米"}}
}
返回结果如下:
{"took": 0,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 1,"relation": "eq"},"max_score": 1.3862942,"hits": [{"_index": "shopping","_type": "_doc","_id": "1001","_score": 1.3862942,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 3999.00}}]}
}
全量查询
在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search
,附带JSON体如下:
{"query" : {"match_all" : {}}
}
返回结果如下:
{"took": 0,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 1,"relation": "eq"},"max_score": 1.0,"hits": [{"_index": "shopping","_type": "_doc","_id": "1001","_score": 1.0,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 3999.00}}]}
}
分页查询
在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search
,附带JSON体如下:
{"query" : {"match_all" : {}},"from" : 0,"size" : 2
}
返回结果如下:
{"took": 1,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 1,"relation": "eq"},"max_score": 1.0,"hits": [{"_index": "shopping","_type": "_doc","_id": "1001","_score": 1.0,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 3999.00}}]}
}
查询指定字段
在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search
,附带JSON体如下:
{"query" : {"match_all" : {}},"from" : 0,"size" : 2,"_source" : ["title"]
}
返回的结果如下:
{"took": 900,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 6,"relation": "eq"},"max_score": 1.0,"hits": [{"_index": "shopping","_type": "_doc","_id": "1001","_score": 1.0,"_source": {"title": "小米手机"}},{"_index": "shopping","_type": "_doc","_id": "1002","_score": 1.0,"_source": {"title": "小米手机"}}]}
}
查询排序
如果你想通过排序查出价格最高的手机,在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search
,附带JSON体如下:
{"query" : {"match_all" : {}},"from" : 0,"size" : 2,"_source" : ["title"],"sort" : {"price" : {"order" : "desc"}}
}
返回的结果如下:
{"took": 316,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 7,"relation": "eq"},"max_score": null,"hits": [{"_index": "shopping","_type": "_doc","_id": "1007","_score": null,"_source": {"title": "小米手机"},"sort": [4999.0]},{"_index": "shopping","_type": "_doc","_id": "1001","_score": null,"_source": {"title": "小米手机"},"sort": [3999.0]}]}
}
多条件查询
假设想找出小米牌子,价格为3999元的。(must相当于数据库的&&)
在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search
,附带JSON体如下:
{"query" : {"bool" : {"must" : [{"match" : {"category" : "小米"}},{"match" : {"price" : 4999.00}}]}}
}
返回的结果如下:
{"took": 1,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 1,"relation": "eq"},"max_score": 1.1290771,"hits": [{"_index": "shopping","_type": "_doc","_id": "1007","_score": 1.1290771,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 4999.00}}]}
}
范围查询
假设想找出小米和华为的牌子,价格大于2000元的手机。
在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search
,附带JSON体如下:
{"query" : {"bool" : {"should" : [{"match" : {"category" : "小米"}},{"match" : {"category" : "华为"}}],"filter" : {"range" : {"price" : {"gt" : 4000}}}}}
}
返回的结果如下:
{"took": 1,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 3,"relation": "eq"},"max_score": 3.583519,"hits": [{"_index": "shopping","_type": "_doc","_id": "1001","_score": 3.583519,"_source": {"title": "华为手机","category": "华为","images": "http://www.gulixueyuan.com/xm.jpg","price": 4999.00}},{"_index": "shopping","_type": "_doc","_id": "1007","_score": 0.6508448,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 4999.00}},{"_index": "shopping","_type": "_doc","_id": "1008","_score": 0.0,"_source": {"title": "华为手机","category": "荣耀","images": "http://www.gulixueyuan.com/xm.jpg","price": 4999.00}}]}
}
全文检索
这功能像搜索引擎那样,如品牌输入“小华”,返回结果带回品牌有“小米”和华为的。
在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search
,附带JSON体如下:
{"query":{"match":{"category" : "小华"}}
}
返回结果如下:
{"took": 4,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 7,"relation": "eq"},"max_score": 1.7917595,"hits": [{"_index": "shopping","_type": "_doc","_id": "1001","_score": 1.7917595,"_source": {"title": "华为手机","category": "华为","images": "http://www.gulixueyuan.com/xm.jpg","price": 4999.00}},{"_index": "shopping","_type": "_doc","_id": "1002","_score": 0.3254224,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 3999.00}},{"_index": "shopping","_type": "_doc","_id": "1003","_score": 0.3254224,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 3999.00}},{"_index": "shopping","_type": "_doc","_id": "1004","_score": 0.3254224,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 3999.00}},{"_index": "shopping","_type": "_doc","_id": "1005","_score": 0.3254224,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 1999.00}},{"_index": "shopping","_type": "_doc","_id": "1006","_score": 0.3254224,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 2999.00}},{"_index": "shopping","_type": "_doc","_id": "1007","_score": 0.3254224,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 4999.00}}]}
}
完全匹配
在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search
,附带JSON体如下:
{"query":{"match_phrase":{"category" : "为"}}
}
返回结果如下:
{"took": 1,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 1,"relation": "eq"},"max_score": 1.7917595,"hits": [{"_index": "shopping","_type": "_doc","_id": "1001","_score": 1.7917595,"_source": {"title": "华为手机","category": "华为","images": "http://www.gulixueyuan.com/xm.jpg","price": 4999.00}}]}
}
高亮查询
在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search,附带JSON体如下:
{"query":{"match_phrase":{"category" : "为"}},"highlight":{"fields":{"category":{}//<----高亮这字段}}
}
返回结果如下:
{"took": 24,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 1,"relation": "eq"},"max_score": 1.7917595,"hits": [{"_index": "shopping","_type": "_doc","_id": "1001","_score": 1.7917595,"_source": {"title": "华为手机","category": "华为","images": "http://www.gulixueyuan.com/xm.jpg","price": 4999.00},"highlight": {"category": ["华<em>为</em>"]}}]}
}
聚合查询
聚合允许使用者对 es 文档进行统计分析,类似与关系型数据库中的 group by,当然还有很多其他的聚合,例如取最大值max、平均值avg等等。
接下来按price字段进行分组:
在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search
,附带JSON体如下:
{"aggs":{//聚合操作"price_group":{//名称,随意起名"terms":{//分组"field":"price"//分组字段}}}
}
返回结果如下:
{"took": 8,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 8,"relation": "eq"},"max_score": 1.0,"hits": [{"_index": "shopping","_type": "_doc","_id": "1002","_score": 1.0,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 3999.00}},{"_index": "shopping","_type": "_doc","_id": "1003","_score": 1.0,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 3999.00}},{"_index": "shopping","_type": "_doc","_id": "1004","_score": 1.0,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 3999.00}},{"_index": "shopping","_type": "_doc","_id": "1005","_score": 1.0,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 1999.00}},{"_index": "shopping","_type": "_doc","_id": "1006","_score": 1.0,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 2999.00}},{"_index": "shopping","_type": "_doc","_id": "1007","_score": 1.0,"_source": {"title": "小米手机","category": "小米","images": "http://www.gulixueyuan.com/xm.jpg","price": 4999.00}},{"_index": "shopping","_type": "_doc","_id": "1001","_score": 1.0,"_source": {"title": "华为手机","category": "华为","images": "http://www.gulixueyuan.com/xm.jpg","price": 4999.00}},{"_index": "shopping","_type": "_doc","_id": "1008","_score": 1.0,"_source": {"title": "华为手机","category": "荣耀","images": "http://www.gulixueyuan.com/xm.jpg","price": 4999.00}}]},"aggregations": {"price_group": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": 3999.0,"doc_count": 3},{"key": 4999.0,"doc_count": 3},{"key": 1999.0,"doc_count": 1},{"key": 2999.0,"doc_count": 1}]}}
}
上面返回结果会附带原始数据的。若不想要不附带原始数据的结果,在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search
,附带JSON体如下:
{"aggs":{"price_group":{"terms":{"field":"price"}}},"size":0
}
返回结果如下:
{"took": 1,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 8,"relation": "eq"},"max_score": null,"hits": []},"aggregations": {"price_group": {"doc_count_error_upper_bound": 0,"sum_other_doc_count": 0,"buckets": [{"key": 3999.0,"doc_count": 3},{"key": 4999.0,"doc_count": 3},{"key": 1999.0,"doc_count": 1},{"key": 2999.0,"doc_count": 1}]}}
}
若想对所有手机价格求平均值。
在 Postman 中,向 ES 服务器发 GET请求 : http://127.0.0.1:9200/shopping/_search
,附带JSON体如下:
{"aggs":{"price_avg":{//名称,随意起名"avg":{//求平均"field":"price"}}},"size":0
}
返回结果如下:
{"took": 2,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 8,"relation": "eq"},"max_score": null,"hits": []},"aggregations": {"price_avg": {"value": 3999.0}}
}
2.2.3.3 映射关系
有了索引库,等于有了数据库中的 database。
接下来就需要建索引库(index)中的映射了,类似于数据库(database)中的表结构(table)。
创建数据库表需要设置字段名称,类型,长度,约束等;索引库也一样,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做映射(mapping)。
先创建一个索引:
返回结果:
{"acknowledged": true,"shards_acknowledged": true,"index": "user"
}
创建映射
在 Postman 中,向 ES 服务器发 PUT 请求 : http://127.0.0.1:9200/user/_mapping
,附带JSON体如下:
{"properties" : {"name" : {"type" : "text","index" : true},"sex" : {"type" : "keyword","index" : true},"tel" : {"type" : "keyword","index" : false}}
}
返回结果如下:
{"acknowledged": true
}
查询映射
在 Postman 中,向 ES 服务器发 GET 请求 : http://127.0.0.1:9200/user/_mapping
,返回结果如下:
{"user": {"mappings": {"properties": {"name": {"type": "text"},"sex": {"type": "keyword"},"tel": {"type": "keyword","index": false}}}}
}
增加数据
在 Postman 中,向 ES 服务器发 PUT 请求 : http://127.0.0.1:9200/user/_create/1001
,附带JSON体如下:
{"name":"小米","sex":"男的","tel":"1111"
}
返回结果如下:
{"_index": "user","_type": "_doc","_id": "1001","_version": 1,"result": "created","_shards": {"total": 2,"successful": 1,"failed": 0},"_seq_no": 0,"_primary_term": 1
}
查找name含有”小“数据:
在 Postman 中,向 ES 服务器发 GET 请求 : http://127.0.0.1:9200/user/_search
,附带JSON体如下:
{"query":{"match":{"name" : "小"}}
}
返回结果如下:
{"took": 544,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 1,"relation": "eq"},"max_score": 0.2876821,"hits": [{"_index": "user","_type": "_doc","_id": "1001","_score": 0.2876821,"_source": {"name": "小米","sex": "男的","tel": "1111"}}]}
}
查找sex含有”男“数据:
在 Postman 中,向 ES 服务器发 GET 请求 : http://127.0.0.1:9200/user/_search
,附带JSON体如下:
{"query":{"match":{"sex" : "男"}}
}
返回结果如下:
{"took": 0,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 0,"relation": "eq"},"max_score": null,"hits": []}
}
找不想要的结果,只因创建映射时"sex"的类型为"keyword"。
"sex"只能完全为”男的“,才能得出原数据。
把附带JSON体改成如下:
{"query":{"match":{"sex" : "男的"}}
}
返回结果如下:
{"took": 0,"timed_out": false,"_shards": {"total": 1,"successful": 1,"skipped": 0,"failed": 0},"hits": {"total": {"value": 1,"relation": "eq"},"max_score": 0.2876821,"hits": [{"_index": "user","_type": "_doc","_id": "1001","_score": 0.2876821,"_source": {"name": "小米","sex": "男的","tel": "1111"}}]}
}
查询电话
在 Postman 中,向 ES 服务器发 GET 请求 : http://127.0.0.1:9200/user/_search
,附带JSON体如下:
{"query":{"match":{"tel" : "1111"}}
}
返回结果如下:
{"error": {"root_cause": [{"type": "query_shard_exception","reason": "failed to create query: Cannot search on field [tel] since it is not indexed.","index_uuid": "3YPoB0dzSI6zbOqrntMYDg","index": "user"}],"type": "search_phase_execution_exception","reason": "all shards failed","phase": "query","grouped": true,"failed_shards": [{"shard": 0,"index": "user","node": "qui0nCdjRquAhmvJcFSOcw","reason": {"type": "query_shard_exception","reason": "failed to create query: Cannot search on field [tel] since it is not indexed.","index_uuid": "3YPoB0dzSI6zbOqrntMYDg","index": "user","caused_by": {"type": "illegal_argument_exception","reason": "Cannot search on field [tel] since it is not indexed."}}}]},"status": 400
}
报错只因创建映射时"tel"的"index"为false。
2.2.4 Java API 操作
2.2.4.1 创建Maven项目
新建Maven工程。
添加依赖:
<dependencies><dependency><groupId>org.elasticsearch</groupId><artifactId>elasticsearch</artifactId><version>7.8.0</version></dependency><!-- elasticsearch 的客户端 --><dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId><version>7.8.0</version></dependency><!-- elasticsearch 依赖 2.x 的 log4j --><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-api</artifactId><version>2.8.2</version></dependency><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.8.2</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.9.9</version></dependency><!-- junit 单元测试 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version></dependency>
</dependencies>
2.2.4.2 客户端对象
创建 com.atguigu.es.test.Elasticsearch01_Client
类,代码中创建 Elasticsearch 客户端对象
因为早期版本的客户端对象已经不再推荐使用,且在未来版本中会被删除,所以这里我们采用高级 REST 客户端对象
public class ESTest_Client {public static void main(String[] args) throws IOException {// 创建ES客户端RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 关闭ES客户端esClient.close();}}
2.2.4.3 索引操作
ES服务器正常启动后,可以通过 Java API 客户端对象对 ES 索引进行操作
创建索引
public class ESTest_Index_Create {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 创建索引CreateIndexRequest request = new CreateIndexRequest("user");CreateIndexResponse response = esClient.indices().create(request, RequestOptions.DEFAULT);// 响应状态boolean acknowledged = response.isAcknowledged();System.out.println("索引操作 : " + acknowledged);esClient.close();}}
返回结果如下:
查询索引
public class ESTest_Index_Search {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 查询索引GetIndexRequest request = new GetIndexRequest("user");GetIndexResponse response = esClient.indices().get(request, RequestOptions.DEFAULT);// 响应状态System.out.println(response.getAliases());System.out.println(response.getMappings());System.out.println(response.getSettings());esClient.close();}}
返回结果如下:
与Postman中查询的结果对应:
删除索引
public class ESTest_Index_Delete {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 删除索引DeleteIndexRequest request = new DeleteIndexRequest("user");AcknowledgedResponse response = esClient.indices().delete(request, RequestOptions.DEFAULT);// 响应状态System.out.println(response.isAcknowledged());esClient.close();}}
返回结果如下:
再去Postman中查询所有索引,发现该索引已被删除:
2.2.4.4 文档操作
新增文档
创建数据模型
public class User {private String name;private String sex;private Integer age;public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getAge() {return age;}public void setAge(Integer age) {this.age = age;}public String getSex() {return sex;}public void setSex(String sex) {this.sex = sex;}
}
创建数据,添加到文档中
public class ESTest_Doc_Insert {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 插入数据IndexRequest request = new IndexRequest();request.index("user").id("1001");User user = new User();user.setName("zhangsan");user.setAge(30);user.setSex("男");// 向ES插入数据,必须将数据转换为JSON格式ObjectMapper mapper = new ObjectMapper();String userJson = mapper.writeValueAsString(user);request.source(userJson, XContentType.JSON);IndexResponse response = esClient.index(request, RequestOptions.DEFAULT);System.out.println(response.getResult());esClient.close();}}
返回结果如下:
在Postman中查询文档数据
修改文档
public class ESTest_Doc_Update {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 修改数据-局部修改UpdateRequest request = new UpdateRequest();request.index("user").id("1001");request.doc(XContentType.JSON, "sex", "女");UpdateResponse response = esClient.update(request, RequestOptions.DEFAULT);System.out.println(response.getResult());esClient.close();}}
在Postman中查询修改后的文档数据
查询文档
public class ESTest_Doc_Get {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 查询数据GetRequest request = new GetRequest();request.index("user").id("1001");GetResponse response = esClient.get(request, RequestOptions.DEFAULT);System.out.println(response.getSourceAsString());esClient.close();}}
返回结果如下:
删除文档
public class ESTest_Doc_Delete {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 删除数据DeleteRequest request = new DeleteRequest();request.index("user").id("1001");DeleteResponse response = esClient.delete(request, RequestOptions.DEFAULT);System.out.println(response.toString());esClient.close();}}
返回结果如下:
在Postman查询该文档,结果如下:
[外链图片转存中…(img-W68arR5G-1733750133974)]
批量新增文档
public class ESTest_Doc_Insert_Batch {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 批量插入数据BulkRequest request = new BulkRequest();request.add(new IndexRequest().index("user").id("1001").source(XContentType.JSON, "name", "zhangsan"));request.add(new IndexRequest().index("user").id("1002").source(XContentType.JSON, "name", "lisi"));request.add(new IndexRequest().index("user").id("1003").source(XContentType.JSON, "name", "wangwu"));BulkResponse response = esClient.bulk(request, RequestOptions.DEFAULT);System.out.println(response.getTook());System.out.println(response.getItems());esClient.close();}}
返回结果如下:
批量删除文档
public class ESTest_Doc_Delete_Batch {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 批量删除数据BulkRequest request = new BulkRequest();request.add(new DeleteRequest().index("user").id("1001"));request.add(new DeleteRequest().index("user").id("1002"));request.add(new DeleteRequest().index("user").id("1003"));BulkResponse responses = esClient.bulk(request, RequestOptions.DEFAULT);System.out.println(responses.getTook());System.out.println(responses.getItems());esClient.close();}}
返回结果如下:
在Postman中查询数据
全量查询
先批量增加数据
public class ESTest_Doc_Insert_Batch {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 批量插入数据BulkRequest request = new BulkRequest();request.add(new IndexRequest().index("user").id("1001").source(XContentType.JSON, "name", "zhangsan", "age", 30, "sex", "男"));request.add(new IndexRequest().index("user").id("1002").source(XContentType.JSON, "name", "lisi", "age", 30, "sex", "女"));request.add(new IndexRequest().index("user").id("1003").source(XContentType.JSON, "name", "wangwu", "age", 40, "sex", "男"));request.add(new IndexRequest().index("user").id("1004").source(XContentType.JSON, "name", "wangwu1", "age", 40, "sex", "女"));request.add(new IndexRequest().index("user").id("1005").source(XContentType.JSON, "name", "wangwu2", "age", 50, "sex", "男"));request.add(new IndexRequest().index("user").id("1006").source(XContentType.JSON, "name", "wangwu3", "age", 50, "sex", "男"));BulkResponse response = esClient.bulk(request, RequestOptions.DEFAULT);System.out.println(response.getTook());System.out.println(response.getItems());esClient.close();}}
查询所有索引数据
public class ESTest_Doc_Query {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 查询索引中全部数据SearchRequest request = new SearchRequest();request.indices("user");request.source(new SearchSourceBuilder().query(QueryBuilders.matchAllQuery()));SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);SearchHits hits = response.getHits();System.out.println(hits.getTotalHits());System.out.println(response.getTook());for (SearchHit hit : hits) {System.out.println(hit.getSourceAsString());}esClient.close();}}
返回结果如下:
条件查询
public class ESTest_Doc_Query {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 2. 条件查询SearchRequest request = new SearchRequest();request.indices("user");request.source(new SearchSourceBuilder().query(QueryBuilders.termQuery("age", 30)));SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);SearchHits hits = response.getHits();System.out.println(hits.getTotalHits());System.out.println(response.getTook());for (SearchHit hit : hits) {System.out.println(hit.getSourceAsString());}esClient.close();}}
返回结果如下:
分页查询
public class ESTest_Doc_Query {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 3. 分页查询SearchRequest request = new SearchRequest();request.indices("user");SearchSourceBuilder builder = new SearchSourceBuilder().query(QueryBuilders.matchAllQuery());builder.from(0);builder.size(2);request.source(builder);SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);SearchHits hits = response.getHits();System.out.println(hits.getTotalHits());System.out.println(response.getTook());for (SearchHit hit : hits) {System.out.println(hit.getSourceAsString());}esClient.close();}}
返回结果如下:
排序查询
public class ESTest_Doc_Query {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 4. 排序查询SearchRequest request = new SearchRequest();request.indices("user");SearchSourceBuilder builder = new SearchSourceBuilder().query(QueryBuilders.matchAllQuery());builder.sort("age", SortOrder.DESC);request.source(builder);SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);SearchHits hits = response.getHits();System.out.println(hits.getTotalHits());System.out.println(response.getTook());for (SearchHit hit : hits) {System.out.println(hit.getSourceAsString());}esClient.close();}}
返回结果如下:
过滤查询
public class ESTest_Doc_Query {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 5. 过滤字段SearchRequest request = new SearchRequest();request.indices("user");SearchSourceBuilder builder = new SearchSourceBuilder().query(QueryBuilders.matchAllQuery());String[] excludes = {};String[] includes = {"name"};builder.fetchSource(includes, excludes);request.source(builder);SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);SearchHits hits = response.getHits();System.out.println(hits.getTotalHits());System.out.println(response.getTook());for (SearchHit hit : hits) {System.out.println(hit.getSourceAsString());}esClient.close();}}
返回结果如下:
组合条件查询
public class ESTest_Doc_Query {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 6. 组合条件查询SearchRequest request = new SearchRequest();request.indices("user");SearchSourceBuilder builder = new SearchSourceBuilder();BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();// boolQueryBuilder.must(QueryBuilders.matchQuery("age", 30));
// boolQueryBuilder.must(QueryBuilders.matchQuery("sex", "男"));
// boolQueryBuilder.mustNot(QueryBuilders.matchQuery("sex", "男"));boolQueryBuilder.should(QueryBuilders.matchQuery("age", 30));boolQueryBuilder.should(QueryBuilders.matchQuery("age", 40));builder.query(boolQueryBuilder);request.source(builder);SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);SearchHits hits = response.getHits();System.out.println(hits.getTotalHits());System.out.println(response.getTook());for (SearchHit hit : hits) {System.out.println(hit.getSourceAsString());}esClient.close();}}
返回结果如下:
范围查询
public class ESTest_Doc_Query {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 7. 范围查询SearchRequest request = new SearchRequest();request.indices("user");SearchSourceBuilder builder = new SearchSourceBuilder();RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("age");rangeQueryBuilder.gte(30);rangeQueryBuilder.lte(40);builder.query(rangeQueryBuilder);request.source(builder);SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);SearchHits hits = response.getHits();System.out.println(hits.getTotalHits());System.out.println(response.getTook());for (SearchHit hit : hits) {System.out.println(hit.getSourceAsString());}esClient.close();}}
返回结果如下:
模糊查询
public class ESTest_Doc_Query {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 8. 模糊查询SearchRequest request = new SearchRequest();request.indices("user");SearchSourceBuilder builder = new SearchSourceBuilder();// 与wangwu相差一个字符builder.query(QueryBuilders.fuzzyQuery("name", "wangwu").fuzziness(Fuzziness.ONE));request.source(builder);SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);SearchHits hits = response.getHits();System.out.println(hits.getTotalHits());System.out.println(response.getTook());for (SearchHit hit : hits) {System.out.println(hit.getSourceAsString());}esClient.close();}}
返回结果如下:
高亮查询
public class ESTest_Doc_Query {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 9. 高亮查询SearchRequest request = new SearchRequest();request.indices("user");SearchSourceBuilder builder = new SearchSourceBuilder();TermsQueryBuilder termsQueryBuilder = QueryBuilders.termsQuery("name", "zhangsan");HighlightBuilder highlightBuilder = new HighlightBuilder();highlightBuilder.preTags("<font color='red>");highlightBuilder.postTags("</font>");highlightBuilder.field("name");builder.highlighter(highlightBuilder);builder.query(termsQueryBuilder);request.source(builder);SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);SearchHits hits = response.getHits();System.out.println(hits.getTotalHits());System.out.println(response.getTook());for (SearchHit hit : hits) {System.out.println(hit.getSourceAsString());}esClient.close();}}
返回结果如下:
聚合查询
public class ESTest_Doc_Query {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 10. 聚合查询SearchRequest request = new SearchRequest();request.indices("user");SearchSourceBuilder builder = new SearchSourceBuilder();AggregationBuilder aggregationBuilder = AggregationBuilders.max("maxAge").field("age");builder.aggregation(aggregationBuilder);request.source(builder);SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);System.out.println(new ObjectMapper().writeValueAsString(response.getAggregations()));SearchHits hits = response.getHits();System.out.println(hits.getTotalHits());System.out.println(response.getTook());for (SearchHit hit : hits) {System.out.println(hit.getSourceAsString());}esClient.close();}}
分组查询
public class ESTest_Doc_Query {public static void main(String[] args) throws IOException {RestHighLevelClient esClient = new RestHighLevelClient(RestClient.builder(new HttpHost("127.0.0.1", 9200, "http")));// 11. 分组查询SearchRequest request = new SearchRequest();request.indices("user");SearchSourceBuilder builder = new SearchSourceBuilder();AggregationBuilder aggregationBuilder = AggregationBuilders.terms("ageGroup").field("age");builder.aggregation(aggregationBuilder);request.source(builder);SearchResponse response = esClient.search(request, RequestOptions.DEFAULT);System.out.println(new ObjectMapper().writeValueAsString(response.getAggregations()));SearchHits hits = response.getHits();System.out.println(hits.getTotalHits());System.out.println(response.getTook());for (SearchHit hit : hits) {System.out.println(hit.getSourceAsString());}esClient.close();}}
返回结果如下
3. ElasticSearch环境
3.1 相关概念
3.1.1 单机 & 集群
单台Elasticsearch 服务器提供服务,往往都有最大的负载能力,超过这个阈值,服务器性能就会大大降低甚至不可用,所以生产环境中,一般都是运行在指定服务器集群中。
除了负载能力,单点服务器也存在其他问题:
- 单台机器存储容量有限
- 单服务器容易出现单点故障,无法实现高可用
- 单服务的并发处理能力有限
配置服务器集群时,集群中节点数量没有限制,大于等于2 个节点就可以看做是集群了。一般出于高性能及高可用方面来考虑集群中节点数量都是3 个以上。
3.1.2 集群Cluster
一个集群就是由一个或多个服务器节点组织在一起,共同持有整个的数据,并一起提供索引和搜索功能。一个Elasticsearch 集群有一个唯一的名字标识,这个名字默认就是”elasticsearch”。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。
3.1.3 节点Node
集群中包含很多服务器,一个节点就是其中的一个服务器。作为集群的一部分,它存储数据,参与集群的索引和搜索功能。
一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色的名字,这个名字会在启动的时候赋予节点。这个名字对于管理工作来说挺重要的,因为在这个管理过程中,你会去确定网络中的哪些服务器对应于Elasticsearch 集群中的哪些节点。
一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,每个节点都会被安排加入到一个叫做“elasticsearch”的集群中,这意味着,如果你在你的网络中启动了若干个节点,并假定它们能够相互发现彼此,它们将会自动地形成并加入到一个叫做“elasticsearch”的集群中。
在一个集群里,只要你想,可以拥有任意多个节点。而且,如果当前你的网络中没有运行任何Elasticsearch 节点,这时启动一个节点,会默认创建并加入一个叫做“elasticsearch”的集群。
3.2 docker部署ES集群
视频教程: https://www.bilibili.com/video/BV1f24y1c7Ej/?share_source=copy_web&vd_source=98c598715a575886b2911df326ba4084
4. ElasticSearch 进阶
4.1 核心概念
4.1.1 索引(Index)
一个索引就是一个拥有几分相似特征的文档的集合。比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母),并且当我们要对这个索引中的文档进行索引、搜索、更新和删除(CRUD)的时候,都要使用到这个名字。在一个集群中,可以定义任意多的索引。
能搜索的数据必须索引,这样的好处是可以提高查询速度,比如:新华字典前面的目录就是索引的意思,目录可以提高查询速度。
Elasticsearch 索引的精髓:一切设计都是为了提高搜索的性能。
4.1.2 类型(Type)
在一个索引中,你可以定义一种或多种类型。
一个类型是你的索引的一个逻辑上的分类/分区,其语义完全由你来定。通常,会为具
有一组共同字段的文档定义一个类型。不同的版本,类型发生了不同的变化。
版本 | Type |
---|---|
5.x | 支持多种 type |
6.x | 只能有一种 type |
7.x | 默认不再支持自定义索引类型(默认类型为: _doc) |
4.1.3 文档(Document)
一个文档是一个可被索引的基础信息单元,也就是一条数据。
比如:你可以拥有某一个客户的文档,某一个产品的一个文档,当然,也可以拥有某个订单的一个文档。文档以 JSON(Javascript Object Notation)格式来表示,而 JSON 是一个到处存在的互联网数据交互格式。
在一个 index/type 里面,你可以存储任意多的文档。
4.1.4 字段(Field)
相当于是数据表的字段,对文档数据根据不同属性进行的分类标识。
4.1.5 映射(Mapping)
mapping 是处理数据的方式和规则方面做一些限制,如:某个字段的数据类型、默认值、分析器、是否被索引等等。这些都是映射里面可以设置的,其它就是处理 ES 里面数据的一些使用规则设置也叫做映射,按着最优规则处理数据对性能提高很大,因此才需要建立映射,并且需要思考如何建立映射才能对性能更好。
4.1.6 分片(Shards)
一个索引可以存储超出单个节点硬件限制的大量数据。比如,一个具有 10 亿文档数据的索引占据 1TB 的磁盘空间,而任一节点都可能没有这样大的磁盘空间。 或者单个节点处理搜索请求,响应太慢。为了解决这个问题,**Elasticsearch 提供了将索引划分成多份的能力,每一份就称之为分片。**当你创建一个索引的时候,你可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。
分片很重要,主要有两方面的原因:
- 允许你水平分割 / 扩展你的内容容量。
- 允许你在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量。
至于一个分片怎样分布,它的文档怎样聚合和搜索请求,是完全由 Elasticsearch 管理的,对于作为用户的你来说,这些都是透明的,无需过分关心。
被混淆的概念是,一个 Lucene 索引 我们在 Elasticsearch 称作 分片 。 一个Elasticsearch 索引 是分片的集合。 当 Elasticsearch 在索引中搜索的时候, 他发送查询到每一个属于索引的分片(Lucene 索引),然后合并每个分片的结果到一个全局的结果集。
Lucene 是 Apache 软件基金会 Jakarta 项目组的一个子项目,提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在 Java 开发环境里 Lucene 是一个成熟的免费开源工具。就其本身而言, Lucene 是当前以及最近几年最受欢迎的免费 Java 信息检索程序库。但 Lucene 只是一个提供全文搜索功能类库的核心工具包,而真正使用它还需要一个完善的服务框架搭建起来进行应用。
目前市面上流行的搜索引擎软件,主流的就两款: Elasticsearch 和 Solr,这两款都是基于 Lucene 搭建的,可以独立部署启动的搜索引擎服务软件。由于内核相同,所以两者除了服务器安装、部署、管理、集群以外,对于数据的操作 修改、添加、保存、查询等等都十分类似。
4.1.7 副本(Replicas)
在一个网络 / 云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的, Elasticsearch 允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片(副本)。
复制分片之所以重要,有两个主要原因:
-
在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与原/主要(original/primary)分片置于同一节点上是非常重要的。
-
扩展你的搜索量/吞吐量,因为搜索可以在所有的副本上并行运行。
总之,每个索引可以被分成多个分片。一个索引也可以被复制 0 次(意思是没有复制)或多次。一旦复制了,每个索引就有了主分片(作为复制源的原来的分片)和复制分片(主分片的拷贝)之别。
分片和复制的数量可以在索引创建的时候指定。在索引创建之后,你可以在任何时候动态地改变复制的数量,但是你事后不能改变分片的数量。
默认情况下,Elasticsearch 中的每个索引被分片 1 个主分片和 1 个复制,这意味着,如果你的集群中至少有两个节点,你的索引将会有 1 个主分片和另外 1 个复制分片(1 个完全拷贝),这样的话每个索引总共就有 2 个分片, 我们需要根据索引需要确定分片个数。
4.1.8 分配(Allocation)
将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分片复制数据的过程。这个过程是由 master 节点完成的。
4.2 系统架构
一个运行中的 Elasticsearch 实例称为一个节点,而集群是由一个或者多个拥有相同cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。
当一个节点被选举成为主节点时, 它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。我们的示例集群就只有一个节点,所以它同时也成为了主节点。
作为用户,我们可以将请求发送到集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。 Elasticsearch 对这一切的管理都是透明的。
4.3 分布式集群
4.3.1 单节点集群
我们在包含一个空节点的集群内创建名为 users 的索引,为了演示目的,我们将分配 3个主分片和一份副本(每个主分片拥有一个副本分片)。
在Postman 中,向 ES 服务器发 PUT 请求 http://127.0.0.1:9200/users
,并附上JSON数据:
{"settings" : {"number_of_shards" : 3,"number_of_replicas" : 1}
}
返回结果如下:
在Postman 中,向 ES 服务器发 GET 请求 http://127.0.0.1:9200/users
,查看刚刚创建的索引数据:
集群现在是拥有一个索引的单节点集群。所有 3 个主分片都被分配在 node-1 。
通过 elasticsearch-head 插件(一个Chrome插件)查看集群情况 。
- 集群健康值:yellow( 3 of 6 ):表示当前集群的全部主分片都正常运行,但是副本分片没有全部处在正常状态。
- 3 个主分片正常。
- 3 个副本分片都是 Unassigned,它们都没有被分配到任何节点。 在同 一个节点上既保存原始数据又保存副本是没有意义的,因为一旦失去了那个节点,我们也将丢失该节点 上的所有副本数据。
4.3.2 故障转移
当集群中只有一个节点在运行时,意味着会有一个单点故障问题——没有冗余。 幸运的是,我们只需再启动一个节点即可防止数据丢失。当你在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的 cluster.name 配置,它就会自动发现集群并加入到其中。但是在不同机器上启动节点的时候,为了加入到同一集群,你需要配置一个可连接到的单播主机列表。之所以配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。
如果启动了第二个节点,集群将会拥有两个节点 : 所有主分片和副本分片都已被分配 。
[外链图片转存中…(img-F8vvuQ8t-1733750133975)]
通过 elasticsearch-head 插件查看集群情况
-
集群健康值:green( 6 of 6 ):表示所有 6 个分片(包括 3 个主分片和 3 个副本分片)都在正常运行。
-
:3 个主分片正常。
-
:第二个节点加入到集群后, 3 个副本分片将会分配到这个节点上——每 个主分片对应一个副本分片。这意味着当集群内任何一个节点出现问题时,我们的数据都完好无损。所 有新近被索引的文档都将会保存在主分片上,然后被并行的复制到对应的副本分片上。这就保证了我们 既可以从主分片又可以从副本分片上获得文档。
4.3.3 水平扩容
怎样为我们的正在增长中的应用程序按需扩容呢?当启动了第三个节点,我们的集群将会拥有三个节点的集群 : 为了分散负载而对分片进行重新分配 。
通过 elasticsearch-head 插件查看集群情况。
Node 1 和 Node 2 上各有一个分片被迁移到了新的 Node 3 节点,现在每个节点上都拥有 2 个分片, 而不是之前的 3 个。 这表示每个节点的硬件资源(CPU, RAM, I/O)将被更少的分片所共享,每个分片 的性能将会得到提升。
分片是一个功能完整的搜索引擎,它拥有使用一个节点上的所有资源的能力。 我们这个拥有 6 个分 片(3 个主分片和 3 个副本分片)的索引可以最大扩容到 6 个节点,每个节点上存在一个分片,并且每个 分片拥有所在节点的全部资源。
但是如果我们想要扩容超过 6 个节点怎么办呢?
主分片的数目在索引创建时就已经确定了下来。实际上,这个数目定义了这个索引能够存储 的最大数据量。(实际大小取决于你的数据、硬件和使用场景。) 但是,读操作——搜索和返回数据——可以同时被主分片 或 副本分片所处理,所以当你拥有越多的副本分片时,也将拥有越高的吞吐量。
在运行中的集群上是可以动态调整副本分片数目的,我们可以按需伸缩集群。让我们把副本数从默认的 1 增加到 2。
在Postman 中,向 ES 服务器发 PUT 请求 http://127.0.0.1:9200/users/_settings
,并附上JSON数据:
{"number_of_replicas" : 2
}
返回结果如下:
users 索引现在拥有 9 个分片: 3 个主分片和 6 个副本分片。 这意味着我们可以将集群扩容到 9 个节点,每个节点上一个分片。相比原来 3 个节点时,集群搜索性能可以提升 3 倍。
通过 elasticsearch-head 插件查看集群情况:
当然,如果只是在相同节点数目的集群上增加更多的副本分片并不能提高性能,因为每个分片从节点上获得的资源会变少。 你需要增加更多的硬件资源来提升吞吐量。
但是更多的副本分片数提高了数据冗余量:按照上面的节点配置,我们可以在失去 2 个节点的情况下不丢失任何数据。
4.3.4 应对故障
我们关闭第一个节点,这时集群的状态为:关闭了一个节点后的集群。
我们关闭的节点是一个主节点。而集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点: Node 2 。在我们关闭 Node 1 的同时也失去了主分片 1 和 2 ,并且在缺失主分片的时候索引也不能正常工作。 如果此时来检查集群的状况,我们看到的状态将会为 red :不是所有主分片都在正常工作。
幸运的是,在其它节点上存在着这两个主分片的完整副本, 所以新的主节点立即将这些分片在 Node 2 和 Node 3 上对应的副本分片提升为主分片, 此时集群的状态将会为yellow。这个提升主分片的过程是瞬间发生的,如同按下一个开关一般。
[外链图片转存中…(img-k93L6fnw-1733750133975)]
为什么我们集群状态是 yellow 而不是 green 呢?
虽然我们拥有所有的三个主分片,但是同时设置了每个主分片需要对应 2 份副本分片,而此时只存在一份副本分片。 所以集群不能为 green 的状态,不过我们不必过于担心:如果我们同样关闭了 Node 2 ,我们的程序 依然 可以保持在不丢任何数据的情况下运行,因为Node 3 为每一个分片都保留着一份副本。
如果想回复原来的样子,重启Node1。
集群可以将缺失的副本分片再次进行分配,那么集群的状态也将恢复成之前的状态。 如果 Node 1 依然拥有着之前的分片,它将尝试去重用它们,同时仅从主分片复制发生了修改的数据文件。和之前的集群相比,只是 Master 节点切换了。
4.4 路由计算
当索引一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?当我们创建文档时,它如何决定这个文档应当被存储在分片 1 还是分片 2 中呢?首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:
shard = hash(routing) % number_of_primary_shards
routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing 通过hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。
这就解释了为什么我们要在创建索引的时候就确定好主分片的数量并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。
所有的文档API ( get . index . delete 、 bulk , update以及 mget )都接受一个叫做routing 的路由参数,通过这个参数我们可以自定义文档到分片的映射。一个自定义的路由参数可以用来确保所有相关的文档—一例如所有属于同一个用户的文档——都被存储到同一个分片中。
4.5 分片控制
我们可以发送请求到集群中的任一节点。每个节点都有能力处理任意请求。每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。在下面的例子中,如果将所有的请求发送到Node 1001,我们将其称为协调节点coordinating node。
当发送请求的时候, 为了扩展负载,更好的做法是轮询集群中所有的节点。
4.5.1 数据写流程
新建、索引和删除请求都是写操作, 必须在主分片上面完成之后才能被复制到相关的副本分片。
新建,索引和删除文档所需要的步骤顺序:
- 客户端向 Node 1 发送新建、索引或者删除请求。
- 节点使用文档的 _id 确定文档属于分片 0 。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。
- Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1 和 Node 2的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。
在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。有一些可选的请求参数允许您影响这个过程,可能以数据安全为代价提升性能。这些选项很少使用,因为Elasticsearch 已经很快,但是为了完整起见,请参考下面表格:
参数 | 含义 |
---|---|
consistency | consistency,即一致性。在默认设置下,即使仅仅是在试图执行一个_写_操作之前,主分片都会要求 必须要有 规定数量(quorum)(或者换种说法,也即必须要有大多数)的分片副本处于活跃可用状态,才会去执行_写_操作(其中分片副本可以是主分片或者副本分片)。这是为了避免在发生网络分区故障(networkpartition)的时候进行写操作,进而导致数据不一致。规定数量即:int( (primary + number_of_replicas) / 2 ) + 1consistency 参数的值可以设为 one (只要主分片状态 ok 就允许执行_写_操作),all(必须要主分片和所有副本分片的状态没问题才允许执行写_操作), 或quorum 。默认值为 quorum , 即大多数的分片副本状态没问题就允许执行写操作。注意,规定数量 的计算公式中 number_of_replicas 指的是在索引设置中的设定副本分片数,而不是指当前处理活动状态的副本分片数。如果你的索引设置中指定了当前索引拥有三个副本分片,那规定数量的计算结果即:int( (primary + 3 replicas) / 2 ) + 1 = 3如果此时你只启动两个节点,那么处于活跃状态的分片副本数量就达不到规定数量,也因此您将无法索引和删除任何文档。 |
timeout | 如果没有足够的副本分片会发生什么? Elasticsearch 会等待,希望更多的分片出现。默认情况下,它最多等待1 分钟。 如果你需要,你可以使用 timeout 参数使它更早终止: 100 100 毫秒,30s 是30 秒。 |
新索引默认有 1 个副本分片,这意味着为满足规定数量应该需要两个活动的分片副本。 但是,这些默认的设置会阻止我们在单一节点上做任何事情。为了避免这个问题,要求只有当 number_of_replicas 大于1 的时候,规定数量才会执行。
4.5.2 读流程
在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。
4.5.3 更新流程
部分更新一个文档结合了先前说明的读取和写入流程:
部分更新一个文档的步骤如下:
- 客户端向Node 1发送更新请求。
- 它将请求转发到主分片所在的Node 3 。
- Node 3从主分片检索文档,修改_source字段中的JSON,并且尝试重新索引主分片的文档。如果文档已经被另一个进程修改,它会重试步骤3 ,超过retry_on_conflict次后放弃。
- 如果 Node 3成功地更新文档,它将新版本的文档并行转发到Node 1和 Node 2上的副本分片,重新建立索引。一旦所有副本分片都返回成功,Node 3向协调节点也返回成功,协调节点向客户端返回成功。
当主分片把更改转发到副本分片时, 它不会转发更新请求。 相反,它转发完整文档的新版本。请记住,这些更改将会异步转发到副本分片,并且不能保证它们以发送它们相同的顺序到达。 如果 Elasticsearch 仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档。
4.6 分片原理
分片是Elasticsearch最小的工作单元。但是究竟什么是一个分片,它是如何工作的?
传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值的能力。最好的支持是一个字段多个值需求的数据结构是倒排索引。
4.6.1 倒排索引
Elasticsearch使用一种称为倒排索引的结构,它适用于快速的全文搜索。
见其名,知其意,有倒排索引,肯定会对应有正向索引。正向索引(forward index),反向索引(inverted index)更熟悉的名字是倒排索引。
所谓的正向索引,就是搜索引擎会将待搜索的文件都对应一个文件ID,搜索时将这个ID和搜索关键字进行对应,形成K-V对,然后对关键字进行统计计数。
[外链图片转存中…(img-eKXB9FRj-1733750133975)]
但是互联网上收录在搜索引擎中的文档的数目是个天文数字,这样的索引结构根本无法满足实时返回排名结果的要求。所以,搜索引擎会将正向索引重新构建为倒排索引,即把文件ID 对应到关键词的映射转换为关键词到文件ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现这个关键词。
一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。例如,假设我们有两个文档,每个文档的 content 域包含如下内容:
- The quick brown fox jumped over the lazy dog
- Quick brown foxes leap over lazy dogs in summer
为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的 词(我们称它为 词条或 tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示:
现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:
两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单相似性算法,那么我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。
但是,我们目前的倒排索引有一些问题:
- Quick 和 quick 以独立的词条出现,然而用户可能认为它们是相同的词。
- fox 和 foxes 非常相似, 就像 dog 和 dogs ;他们有相同的词根。
- jumped 和 leap, 尽管没有相同的词根,但他们的意思很相近。他们是同义词。
使用前面的索引搜索 +Quick +fox 不会得到任何匹配文档。(记住,+ 前缀表明这个词必须存在。)只有同时出现 Quick 和 fox 的文档才满足这个查询条件,但是第一个文档包含quick fox ,第二个文档包含 Quick foxes 。
我们的用户可以合理的期望两个文档与查询匹配。我们可以做的更好。如果我们将词条规范为标准模式,那么我们可以找到与用户搜索的词条不完全一致,但具有
足够相关性的文档。例如:
- Quick 可以小写化为 quick 。
- foxes 可以 词干提取 --变为词根的格式-- 为 fox 。类似的, dogs 可以为提取为 dog 。
- jumped 和 leap 是同义词,可以索引为相同的单词 jump 。
现在索引看上去像这样:
这还远远不够。我们搜索 +Quick +fox 仍然 会失败,因为在我们的索引中,已经没有 Quick了。但是,如果我们对搜索的字符串使用与 content 域相同的标准化规则,会变成查询+quick +fox,这样两个文档都会匹配!分词和标准化的过程称为分析。
这非常重要。你只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式。
4.6.2 文档搜索
早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 一旦新的索引就绪,旧的就会被其替换,这样最近的变化便可以被检索到。
倒排索引被写入磁盘后是 不可改变 的:它永远不会修改。
不变性有重要的价值:
- 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
- 其它缓存(像filter 缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
- 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。
当然,一个不变的索引也有不好的地方。主要事实是它是不可变的! 你不能修改它。如果你需要让一个新的文档 可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。
4.7 文档分析
分析包含下面的过程:
- 将一块文本分成适合于倒排索引的独立的词条。
- 将这些词条统一化为标准格式以提高它们的“可搜索性”,或者recall。
分析器执行上面的工作。分析器实际上是将三个功能封装到了一个包里:
- 字符过滤器:首先,字符串按顺序通过每个 字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉 HTML,或者将 & 转化成 and。
- 分词器:其次,字符串被分词器分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。
- Token 过滤器:最后,词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,小写化Quick ),删除词条(例如, 像 a, and, the 等无用词),或者增加词条(例如,像jump和leap这种同义词)
4.7.1 内置分析器
Elasticsearch还附带了可以直接使用的预包装的分析器。接下来我们会列出最重要的分析器。为了证明它们的差异,我们看看每个分析器会从下面的字符串得到哪些词条:“Set the shape to semi-transparent by calling set_trans(5)”
-
标准分析器
标准分析器是Elasticsearch 默认使用的分析器。它是分析各种语言文本最常用的选择。它根据Unicode 联盟定义的单词边界划分文本。删除绝大部分标点。最后,将词条小写。它会产生:
set, the, shape, to, semi, transparent, by, calling, set_trans, 5
-
简单分析器
简单分析器在任何不是字母的地方分隔文本,将词条小写。它会产生:
set, the, shape, to, semi, transparent, by, calling, set, trans
-
空格分析器
空格分析器在空格的地方划分文本。它会产生:
Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
-
语言分析器
特定语言分析器可用于很多语言。它们可以考虑指定语言的特点。例如,英语分析器附带了一组英语无用词(常用单词,例如and或者the ,它们对相关性没有多少影响),它们会被删除。由于理解英语语法的规则,这个分词器可以提取英语单词的词干。
英语 分词器会产生下面的词条:
set, shape, semi, transpar, call, set_tran, 5
注意看 transparent、 calling 和 set_trans 已经变为词根格式
4.7.2 分析器使用场景
当我们索引一个文档,它的全文域被分析成词条以用来创建倒排索引。但是,当我们在全文域搜索的时候,我们需要将查询字符串通过相同的分析过程,以保证我们搜索的词条格式与索引中的词条格式一致。
全文查询,理解每个域是如何定义的,因此它们可以做正确的事:
- 当你查询一个全文域时,会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。
- 当你查询一个精确值域时,不会分析查询字符串,而是搜索你指定的精确值。
4.7.3 测试分析器
有些时候很难理解分词的过程和实际被存储到索引中的词条,特别是你刚接触Elasticsearch。为了理解发生了什么,你可以使用analyze API来看文本是如何被分析的。在消息体里,指定分析器和要分析的文本。
在Postman 中,向 ES 服务器发 GET 请求 http://localhost:9200/_analyze
,并附上JSON数据:
{"analyzer": "standard","text": "Text to analyze"
}
返回结果如下:
- token是实际存储到索引中的词条。
- start_ offset 和end_ offset指明字符在原始字符串中的位置。
- position指明词条在原始文本中出现的位置。
4.7.4 IK分词器
在Postman 中,向 ES 服务器发 GET 请求 http://localhost:9200/_analyze
,并附上JSON数据:
{"text":"测试单词"
}
返回结果如下:
{"tokens": [{"token": "测","start_offset": 0,"end_offset": 1,"type": "<IDEOGRAPHIC>","position": 0},{"token": "试","start_offset": 1,"end_offset": 2,"type": "<IDEOGRAPHIC>","position": 1},{"token": "单","start_offset": 2,"end_offset": 3,"type": "<IDEOGRAPHIC>","position": 2},{"token": "词","start_offset": 3,"end_offset": 4,"type": "<IDEOGRAPHIC>","position": 3}]
}
ES 的默认分词器无法识别中文中测试、 单词这样的词汇,而是简单的将每个字拆完分为一个词。
这样的结果显然不符合我们的使用要求,所以我们需要下载 ES 对应版本的中文分词器——IK分词器。
我们这次加入新的查询参数"analyzer":“ik_max_word”。
{"text":"测试单词","analyzer":"ik_max_word"
}
- ik_max_word:会将文本做最细粒度的拆分。
- ik_smart:会将文本做最粗粒度的拆分。
使用中文分词后的结果为:
{"tokens": [{"token": "测试","start_offset": 0,"end_offset": 2,"type": "CN_WORD","position": 0},{"token": "单词","start_offset": 2,"end_offset": 4,"type": "CN_WORD","position": 1}]
}
ES 中也可以进行扩展词汇,首先查询
{"text":"弗雷尔卓德","analyzer":"ik_max_word"
}
仅仅可以得到每个字的分词结果,我们需要做的就是使分词器识别到弗雷尔卓德也是一个词语。
{"tokens": [{"token": "弗","start_offset": 0,"end_offset": 1,"type": "CN_CHAR","position": 0},{"token": "雷","start_offset": 1,"end_offset": 2,"type": "CN_CHAR","position": 1},{"token": "尔","start_offset": 2,"end_offset": 3,"type": "CN_CHAR","position": 2},{"token": "卓","start_offset": 3,"end_offset": 4,"type": "CN_CHAR","position": 3},{"token": "德","start_offset": 4,"end_offset": 5,"type": "CN_CHAR","position": 4}]
}
- 首先进入 ES 根目录中的 plugins 文件夹下的 ik 文件夹,进入 config 目录,创建 custom.dic文件,写入“弗雷尔卓德”。
- 同时打开 IKAnalyzer.cfg.xml 文件,将新建的 custom.dic 配置其中。
- 重启 ES 服务器 。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties><comment>IK Analyzer 扩展配置</comment><!--用户可以在这里配置自己的扩展字典 --><entry key="ext_dict">custom.dic</entry><!--用户可以在这里配置自己的扩展停止词字典--><entry key="ext_stopwords"></entry><!--用户可以在这里配置远程扩展字典 --><!-- <entry key="remote_ext_dict">words_location</entry> --><!--用户可以在这里配置远程扩展停止词字典--><!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
扩展后再次查询,返回结果如下:
{"tokens": [{"token": "弗雷尔卓德","start_offset": 0,"end_offset": 5,"type": "CN_WORD","position": 0}]
}
4.8 文档处理
4.8.1 文档冲突
当我们使用index API更新文档,可以一次性读取原始文档,做我们的修改,然后重新索引整个文档。最近的索引请求将获胜:无论最后哪一个文档被索引,都将被唯一存储在 Elasticsearch 中。如果其他人同时更改这个文档,他们的更改将丢失。
很多时候这是没有问题的。也许我们的主数据存储是一个关系型数据库,我们只是将数据复制到Elasticsearch中并使其可被搜索。也许两个人同时更改相同的文档的几率很小。或者对于我们的业务来说偶尔丢失更改并不是很严重的问题。
但有时丢失了一个变更就是非常严重的。试想我们使用Elasticsearch 存储我们网上商城商品库存的数量,每次我们卖一个商品的时候,我们在 Elasticsearch 中将库存数量减少。有一天,管理层决定做一次促销。突然地,我们一秒要卖好几个商品。假设有两个web程序并行运行,每一个都同时处理所有商品的销售。
web_1 对stock_count所做的更改已经丢失,因为 web_2不知道它的 stock_count的拷贝已经过期。结果我们会认为有超过商品的实际数量的库存,因为卖给顾客的库存商品并不存在,我们将让他们非常失望。
变更越频繁,读数据和更新数据的间隙越长,也就越可能丢失变更。在数据库领域中,有两种方法通常被用来确保并发更新时变更不会丢失:
- 悲观并发控制:这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。
- 乐观并发控制:Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。
4.8.2 乐观并发控制
Elasticsearch是分布式的。当文档创建、更新或删除时,新版本的文档必须复制到集群中的其他节点。Elasticsearch也是异步和并发的,这意味着这些复制请求被并行发送,并且到达目的地时也许顺序是乱的。Elasticsearch需要一种方法确保文档的旧版本不会覆盖新的版本。
当我们之前讨论index , GET和DELETE请求时,我们指出每个文档都有一个_version(版本号),当文档被修改时版本号递增。Elasticsearch使用这个version号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达,它可以被简单的忽略。
我们可以利用version号来确保应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的 version号来达到这个目的。如果该版本不是当前版本号,我们的请求将会失败。
老的版本es使用version,但是新版本不支持了,会报下面的错误,提示我们用if_seq _no和if _primary_term
4.8.3 外部系统版本控制
一个常见的设置是使用其它数据库作为主要的数据存储,使用Elasticsearch做数据检索,这意味着主数据库的所有更改发生时都需要被复制到Elasticsearch,如果多个进程负责这一数据同步,你可能遇到类似于之前描述的并发问题。
如果你的主数据库已经有了版本号,或一个能作为版本号的字段值比如timestamp,那么你就可以在 Elasticsearch 中通过增加 version_type=extermal到查询字符串的方式重用这些相同的版本号,版本号必须是大于零的整数,且小于9.2E+18,一个Java中 long类型的正值。
外部版本号的处理方式和我们之前讨论的内部版本号的处理方式有些不同,Elasticsearch不是检查当前_version和请求中指定的版本号是否相同,而是检查当前_version是否小于指定的版本号。如果请求成功,外部的版本号作为文档的新_version进行存储。
4.9 Kibana
Kibana是一个免费且开放的用户界面,能够让你对Elasticsearch 数据进行可视化,并让你在Elastic Stack 中进行导航。你可以进行各种操作,从跟踪查询负载,到理解请求如何流经你的整个应用,都能轻松完成。
通过浏览器访问:http://localhost:5601。
[外链图片转存中…(img-x2B77KrQ-1733750133976)]
5. ElasticSearch集成
5.1 Spring Data框架集成
Spring Data是一个用于简化数据库、非关系型数据库、索引库访问,并支持云服务的开源框架。其主要目标是使得对数据的访问变得方便快捷,并支持 map-reduce框架和云计算数据服务。Spring Data可以极大的简化JPA(Elasticsearch…)的写法,可以在几乎不用写实现的情况下,实现对数据的访问和操作。除了CRUD 外,还包括如分页、排序等一些常用的功能。
5.1.2 Spring Data Elasticsearch介绍
Spring Data Elasticsearch基于Spring Data API简化 Elasticsearch 操作,将原始操作Elasticsearch 的客户端API进行封装。Spring Data为Elasticsearch 项目提供集成搜索引擎。Spring Data Elasticsearch POJO的关键功能区域为中心的模型与Elastichsearch交互文档和轻松地编写一个存储索引库数据访问层。
Spring Data Elasticsearch 官网
5.1.3 框架集成
一、创建Maven项目。
二、修改pom文件,增加依赖关系。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.6.RELEASE</version><relativePath/></parent><groupId>com.atguigu.es</groupId><artifactId>es-spring</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-test</artifactId></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId></dependency></dependencies></project>
三、增加配置文件。
在 resources 目录中增加application.properties文件
# es 服务地址
elasticsearch.host=127.0.0.1
# es 服务端口
elasticsearch.port=9200
# 配置日志级别,开启 debug 日志
logging.level.com.atguigu.es=debug
四、Spring Boot 主程序。
@SpringBootApplication
public class SpringDataElasticSearchMainApplication {public static void main(String[] args) {SpringApplication.run(SpringDataElasticSearchMainApplication.class, args);}
}
五、数据实体类。
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Document(indexName = "shopping", shards = 3, replicas = 1)
public class Product {//必须有 id,这里的 id 是全局唯一的标识,等同于 es 中的"_id"@Idprivate Long id;//商品唯一标识/*** type : 字段数据类型* analyzer : 分词器类型* index : 是否索引(默认:true)* Keyword : 短语,不进行分词*/@Field(type = FieldType.Text, analyzer = "ik_max_word")private String title;//商品名称@Field(type = FieldType.Keyword)private String category;//分类名称@Field(type = FieldType.Double)private Double price;//商品价格@Field(type = FieldType.Keyword, index = false)private String images;//图片地址
}
六、配置类
-
ElasticsearchRestTemplate是spring-data-elasticsearch项目中的一个类,和其他spring项目中的 template类似。
-
在新版的spring-data-elasticsearch 中,ElasticsearchRestTemplate 代替了原来的ElasticsearchTemplate。
-
原因是ElasticsearchTemplate基于TransportClient,TransportClient即将在8.x 以后的版本中移除。所以,我们推荐使用ElasticsearchRestTemplate。
-
ElasticsearchRestTemplate基于RestHighLevelClient客户端的。需要自定义配置类,继承AbstractElasticsearchConfiguration,并实现elasticsearchClient()抽象方法,创建RestHighLevelClient对象。
需要自定义配置类,继承AbstractElasticsearchConfiguration,并实现elasticsearchClient()抽象方法,创建RestHighLevelClient对象。
@ConfigurationProperties(prefix = "elasticsearch")
@Configuration
@Data
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {private String host ;private Integer port ;//重写父类方法@Overridepublic RestHighLevelClient elasticsearchClient() {RestClientBuilder builder = RestClient.builder(new HttpHost(host, port));RestHighLevelClient restHighLevelClient = newRestHighLevelClient(builder);return restHighLevelClient;}
}
七、DAO 数据访问对象
@Repository
public interface ProductDao extends ElasticsearchRepository<Product, Long> {
}
八、索引操作
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringDataESIndexTest {//注入 ElasticsearchRestTemplate@Resourceprivate ElasticsearchRestTemplate elasticsearchRestTemplate;//创建索引并增加映射配置@Testpublic void createIndex(){//创建索引,系统初始化会自动创建索引System.out.println("创建索引");}@Testpublic void deleteIndex(){//创建索引,系统初始化会自动创建索引boolean flg = elasticsearchRestTemplate.deleteIndex(Product.class);System.out.println("删除索引 = " + flg);}
}
十、文档操作
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringDataESProductDaoTest {@Resourceprivate ProductDao productDao;/*** 新增*/@Testpublic void save() {Product product = new Product();product.setId(2L);product.setTitle("华为手机");product.setCategory("手机");product.setPrice(2999.0);product.setImages("http://www.atguigu/hw.jpg");productDao.save(product);}/*** 修改*/@Testpublic void update() {Product product = new Product();product.setId(2L);product.setTitle("小米 2 手机");product.setCategory("手机");product.setPrice(9999.0);product.setImages("http://www.atguigu/xm.jpg");productDao.save(product);}/*** 根据id查询*/@Testpublic void findById() {Product product = productDao.findById(2L).get();System.out.println(product);}/*** 查询所有*/@Testpublic void findAll() {Iterable<Product> products = productDao.findAll();for (Product product : products) {System.out.println(product);}}/*** 删除*/@Testpublic void delete() {Product product = new Product();product.setId(2L);productDao.delete(product);}/*** 批量新增*/@Testpublic void saveAll() {List<Product> productList = new ArrayList<>();for (int i = 0; i < 10; i++) {Product product = new Product();product.setId(Long.valueOf(i));product.setTitle("[" + i + "]小米手机");product.setCategory("手机");product.setPrice(1999.0 + i);product.setImages("http://www.atguigu/xm.jpg");productList.add(product);}productDao.saveAll(productList);}/*** 分页查询*/@Testpublic void findByPageable() {//设置排序(排序方式,正序还是倒序,排序的 id)Sort sort = Sort.by(Sort.Direction.DESC, "id");int currentPage = 0;//当前页,第一页从 0 开始, 1 表示第二页int pageSize = 5;//每页显示多少条//设置查询分页PageRequest pageRequest = PageRequest.of(currentPage, pageSize, sort);//分页查询Page<Product> productPage = productDao.findAll(pageRequest);for (Product Product : productPage.getContent()) {System.out.println(Product);}}}
十一、文档搜索