Elasticsearch
是一个基于Apache Lucene
的开源搜索引擎。无论在开源还是专有领域,Lucene
可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。但是,Lucene
只是一个库。想要使用它,必须使用Java来作为开发语言并将其直接集成到你的应用中,更糟糕的是,Lucene
非常复杂,需要深入了解检索的相关知识来理解它是如何工作的。
Elasticsearch
也使用Java开发并使用Lucene
作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的 RESTful API 来隐藏Lucene
的复杂性,从而让全文搜索变得简单。
Elasticsearch
的官方地址: Elastic
下载地址:https://www.elastic.co/cn/downloads/past-releases#elasticsearch
选择 Elasticsearch
的版本为Windows 。
解压即安装完毕,解压后的 Elasticsearch
的目录结构如下:
解压后,进入 bin 文件目录,点击 elasticsearch.bat
文件启动 ES 服务:
注意:9300 端口为 Elasticsearch
集群间组件的通信端口,9200 端口为浏览器访问的 http协议 RESTful 端口。
使用浏览器打开:http://localhost:9200/,出现下面内容,ES安装启动完毕。
Elasticsearch
是面向文档型数据库,一条数据在这里就是一个文档。为了理解,可以将 Elasticsearch
里存储的文档数据和关系型数据库 MySQL
存储数据的概念进行一个类比:
ES 里的 Index
(索引)可以看做一个库,而Types
(类型)相当于表,Documents
(文档)则相当于表的行。Types
的概念随着版本的更新已经被逐渐弱化,Elasticsearch 6.X 中,一个 index
下已经只能包含一个type
,Elasticsearch 7.X 中,Type
的概念已经被删除了。
创建索引使用PUT
向服务器发送请求:http://localhost:9200/test_index_02,test_index_02
就是对应的索引名称:
请求后,服务器返回响应:
返回结果字段说明:
{
"acknowledged"【响应结果】: true, // true 操作成功
"shards_acknowledged"【分片结果】: true, // 分片操作成功
"index"【索引名称】: "test_index_02"
}
// 注意:创建索引库的分片数默认 1 片,在 7.0.0 之前的 Elasticsearch 版本中,默认 5 片
重复添加索引,会返回错误信息:、
向ES服务器发送GET
请求:http://localhost:9200/_cat/indices?v
上面请求路径中的_cat
表示查看,indices
表示索引,服务器响应结果如下:
返回结果字段说明:
表头 | 含义 |
---|---|
health |
当前服务器健康状态:green(集群完整)、yellow(单节点正常,集群不完整)、red(单节点不正常) |
status |
索引打开(open)、关闭(close)状态 |
index |
索引名 |
uuid |
索引统一编号,服务器自动生成 |
pri |
主分片数量 |
rep |
副本数量 |
docs.count |
可用文档数量 |
docs.deleted |
文档删除数量(逻辑删除) |
store.size |
主分片和副分片整体占空间大小 |
pri.store.size |
主分片占空间大小 |
向ES服务器发送GET
请求:http://localhost:9200/test_index_01,test_index_01
为索引名称:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nLskbtyp-1637995216285)(Elasticsearch/image-20210904231348533.png)]
请求后,服务器返回响应:
返回结果字段说明:
{
"test_index_01"【索引名】: {
"aliases"【别名】: {},
"mappings"【映射】: {},
"settings"【设置】: {
"index": 【设置-索引】{
"creation_date"【设置-索引-创建时间】: "1630767552252",
"number_of_shards"【设置-索引-主分片数量】: "1",
"number_of_replicas"【设置-索引-副分片数量】: "1",
"uuid"【设置-索引-唯一标识】: "GJwBy-0nShG6LQsWJarDbQ",
"version"【设置-索引-版本号】: {
"created": "7080099"
},
"provided_name"【设置-索引-名称】: "test_index_01"
}
}
}
}
向ES服务器发送GET
请求:http://localhost:9200/movies/_count,movies
为索引名,_count
统计文档数。
向ES服务器发送DELETE
请求:http://localhost:9200/test_index_02,test_index_02
为索引名:
服务器响应结果:
删除索引后,再次访问索引,服务器会返回响应:索引不存在。
索引创建好之后,在索引上创建文档,并且添加数据。
向ES服务器发送POST
请求:http://localhost:9200/test_index_01/_doc。请求体内容为:
{
"brand": "小米",
"model": "MIX4",
"images": "https://cdn.cnbj1.fds.api.mi-img.com/product-images/mix4/specs_m.png",
"price": 3999.00,
"stock": 1000
}
服务器返回响应结果:
返回结果字段说明:
{
"_index"【索引名】: "test_index_01",
"_type"【类型-文档】: "_doc",
"_id"【唯一标识】: "LPp4sXsB7_Yk5DHNib04", // 类似主键,随机生成
"_version"【版本号】: 1,
"result"【结果】: "created", // created:表示创建成功,updated:表示更新成功
"_shards"【分片】: {
"total"【分片-总数】: 2,
"successful"【分片-成功】: 1,
"failed"【分片-失败】: 0
},
"_seq_no"【递增的序列号】: 0,
"_primary_term": 1
}
上面的文档创建成功之后,默认情况下,ES服务器会随机生成一个唯一标识。
也可以在创建文档时,指定唯一标识:http://localhost:9200/test_index_01/_doc/1,1即为指定的唯一标识。
如果增加文档时,明确唯一标识,请求方式也可以是PUT
。
查看文档时,需要指定文档的唯一标识,类似MySQL中数据的主键查询。向ES服务器发送GET
请求:http://localhost:9200/test_index_01/_doc/1:
查询成功,服务器返回结果:
返回结果字段说明:
{
"_index"【索引名】: "test_index_01",
"_type"【类型-文档】: "_doc",
"_id"【文档唯一标识】: "1",
"_version"【版本号】: 5,
"_seq_no"【递增序列号】: 6,
"_primary_term": 1,
"found"【查询结果】: true, // true:表示找到,false:表示没有找到
"_source"【文档源信息】: {
"brand": "小米",
"model": "MIX4",
"images": "https://cdn.cnbj1.fds.api.mi-img.com/product-images/mix4/specs_m.png",
"price": 3999.00,
"stock": 1001
}
}
修改文档同创建文档一样,请求路径一样,如果请求体发生了变化,会将原有的数据内容覆盖更新。请求体内容为:
{
"brand": "华为",
"model": "P50",
"images": "https://res.vmallres.com/pimages//product/6941487233519/78_78_C409A15DAE69B8B4E4A504FBDF5AB6FEB2C8F5868A7C84C4mp.png",
"price": 7488.00,
"stock": 100
}
修改成功,服务器返回结果:
返回结果字段说明:
{
"_index"【索引】: "test_index_01",
"_type"【类型-文档】: "_doc",
"_id"【唯一标识】: "1",
"_version"【版本】: 6,
"result"【结果】: "updated",
"_shards"【分片】: {
"total"【分片总数】: 2,
"successful"【分片成功】: 1,
"failed"【分片失败】: 0
},
"_seq_no": 8,
"_primary_term": 1
}
更新 API 还支持部分文档的更新。
向ES服务器发送POST
请求:http://localhost:9200/test_index_01/_update/1,_update
表示更新,请求体为:
{
"doc":{
"stock": 123
}
}
修改成功后,返回响应结果:
注意:如果要修改的字段在源数据不存在,则会在源数据中增加该字段。
删除一个文档,不会立即从磁盘上移除,只是会被标记为已删除(逻辑删除)。
向ES服务器发送DELETE
请求:http://localhost:9200/test_index_01/_doc/11
删除成功后,服务器返回响应结果:
返回结果字段说明:
{
"_index"【索引】: "test_index_01",
"_type"【类型-文档】: "_doc",
"_id"【唯一标识】: "11",
"_version"【版本号】: 2,
"result"【结果】: "deleted", // deleted:删除成功
"_shards"【分片】: {
"total"【分片总数】: 2,
"successful"【分片成功】: 1,
"failed"【分片失败】: 0
},
"_seq_no": 11,
"_primary_term": 1
}
删除文档之后,再查看文档:
删除一个已经删除了的文档或者不存在的文档:
除了根据文档的唯一性标识进行删除外,还可以根据条件删除文档。
向ES服务器发送POST
请求:http://localhost:9200/test_index_01/_delete_by_query,请求体为:
{
"query": {
"match": {
"price": 1999.00
}
}
}
删除成功后,服务器返回响应结果:
返回结果字段说明:
{
"took"【耗时】: 626,
"timed_out"【请求是否超时】: false,
"total"【文档总数】: 1,
"deleted"【删除文档数量】: 1,
"batches": 1,
"version_conflicts": 0,
"noops": 0,
"retries": {
"bulk": 0,
"search": 0
},
"throttled_millis": 0,
"requests_per_second": -1.0,
"throttled_until_millis": 0,
"failures": []
}
向ES服务器发送PUT
请求:http://localhost:9200/test_index_01/_mapping。请求体:
{
"properties": {
"brand": {
"type": "text",
"index": true
},
"price": {
"type": "float",
"index": true
},
"stock": {
"type": "long",
"index": true
}
}
}
映射创建成功,返回响应:
创建映射,参数说明:
字段名 | 描述 |
---|---|
properties |
创建映射属性集合 |
brand |
属性名,任意填写,可指定多种属性 |
type |
类型,ES中支持数据类型丰富,常见的有: 1、 String 类型,又可以分为两种:text ,表示可分词,keyword ,表示不可分词,数据会作为完整字段进行匹配。2、 Numerical 数值类型,分两类基本数据类型: long ,integer ,short ,byte ,double ,float ,half_float 高精度浮点数: scaled_float 3、 Date 日期类型4、 Array 数组类型5、 Object 对象 |
index |
是否索引,默认为true ,字段会被索引,可以进行搜索。 |
store |
是否将数据进行独立存储,默认为false 。原始文本会存储在_store 中,默认情况下,字段都不是独立存储的,都是从_store 提取出来的 |
analyzer |
分词器,例如ik_max_word |
映射一旦创建,已有的字段类型无法修改,但是可以继续添加新的字段,而且我们可以控制映射的动态性dynamic
。
向ES服务器发送PUT
请求:http://localhost:9200/student/_mapping,请求体如下:
{
"dynamic": "true" // true:开启动态模式, false:开启静态模式, strict:开启严格模式
}
向ES服务器发送GET
请求:http://localhost:9200/test_index_01/_mapping
服务器返回响应结果:
{
"test_index_01": {
"mappings": {
"properties": {
"123": {
"type": "long"
},
"brand": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"images": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"model": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"price": {
"type": "float"
},
"qwewq": {
"type": "text"
},
"stock": {
"type": "long"
}
}
}
}
}
Elasticsearch
提供了基于JSON的完整查询DSL来定义查询。
先创建索引student
,然后创建文档:
// 创建索引
// POST http://localhost:9200/student
// 创建文档
// POST http://localhost:9200/student/_doc/10001
{
"name": "怀勇",
"sex": "男",
"age": 24,
"level": 3,
"phone": "15071833125"
}
// POST http://localhost:9200/student/_doc/10002
{
"name": "朱浩",
"sex": "男",
"age": 28,
"level": 6,
"phone": "15072833125"
}
// POST http://localhost:9200/student/_doc/10003
{
"name": "菜头",
"sex": "男",
"age": 28,
"level": 5,
"phone": "178072833125"
}
// POST http://localhost:9200/student/_doc/10004
{
"name": "基地",
"sex": "男",
"age": 24,
"level": 3,
"phone": "15071833124"
}
// PSOT http://localhost:9200/student/_doc/10005
{
"name": "张雅",
"sex": "女",
"age": 26,
"level": 3,
"phone": "151833124"
}
向ES服务器发送GET
请求:http://localhost:9200/student/_search。请求体为:
{
"query": {
"match_all":{}
}
}
// query代表一个查询对象,match_all表示查询所有
服务器返回响应结果如下:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 5,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
......
]
}
}
hits
响应中最重要的部分是 hits
,它包含了 total
字段来表示匹配到的文档总数, hits
数组还包含了匹配到的前10条数据。
hits
数组中的每个结果都包含_index
、 _type
和文档的 _id
字段,文档源数据被加入到 _source
字段中这意味着在搜索结果中将可以直接使用全部文档。
每个节点都有一个 _score
字段,这是相关性得分(relevance score),它衡量了文档与查询的匹配程度。默认的,返回的结果中关联性最大的文档排在首位;这意味着,它是按照 _score
降序排列的。
max_score
指的是所有文档匹配查询中_score
的最大值。
total
表示搜索条件匹配的文档总数,其中value
表示总命中计数的值,relation
表示取值规则(eq
计数准确,gte
计数不准确)。
took
整个搜索请求花费的毫秒数。
_shards
_shards
节点表示参与查询的分片数( total
字段),其中有多少是成功的(successful
字段),有多少的是失败的( failed
字段)。如果遭受一些重大的故障导致主分片和复制分片都故障,那这个分片的数据将无法响应给搜索请求。这种情况下,Elasticsearch
将报告分片 failed ,但仍将继续返回剩余分片上的结果。
timed_out
time_out
值表示本次查询超时与否。一般的,搜索请求不会超时。如果响应速度比完整的结果更重要,可以定义 timeout
参数为 10 或者 10ms (10毫秒),或者 1s (1秒),那么Elasticsearch
将返回在请求超时前收集到的结果。 向ES服务器发送请求:http://localhost:9200/student/_search?timeout=1ms,?timeout=1ms
就表示返回 1ms 内顺利返回结果的节点数据。
注意:设置了 timeout
不会停止执行查询,它仅仅返回目前顺利执行查询的节点时刻,然后关闭连接。在后台,其他分片可能依旧执行查询,尽管结果已经被发送。
{
"took"【查询花费时间,单位毫秒】: 1,
"timed_out"【是否超时】: false,
"_shards"【分片信息】: {
"total"【分片总数】: 1,
"successful【成功分片数】": 1,
"skipped"【忽略分片数】: 0,
"failed"【失败分片数】: 0
},
"hits"【搜索条件命中结果信息】: {
"total"【搜索条件匹配的文档总数】: {
"value"【总命中计数的值】: 5,
"relation"【计数规则】: "eq" // eq:计数准确 gte:计数不准确
},
"max_score"【匹配度分值】: 1.0,
"hits"【搜索条件命中结果集合】: [
{
"_index【索引名】": "student",
"_type"【类型】: "_doc",
"_id"【文档id】: "10001",
"_score"【相关性得分】: 1.3862942,
"_source": {
"name": "怀勇",
"sex": "男",
"age": 24,
"level": 3,
"phone": "15071833125"
}
}
]
}
}
match
匹配类型的查询,会把查询条件进行分词,然后进行查询,多个词条之间是or
的关系。
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"query": {
"match": {
"name": "怀 勇",
"operator": "and"
}
}
}
服务器返回结果:
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1.3862942,
"hits": [
{
"_index": "student",
"_type": "_doc",
"_id": "10001",
"_score": 1.3862942,
"_source": {
"name": "怀勇",
"sex": "男",
"age": 24,
"level": 3,
"phone": "15071833125"
}
}
]
}
}
match
只能匹配一个字段,要匹配多个字段就得使用multi_match
。
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"query": {
"multi_match": {
"query": 24,
"fields":["age", "phone"]
}
}
}
服务器返回响应结果:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "student",
"_type": "_doc",
"_id": "10001",
"_score": 1.0,
"_source": {
"name": "怀勇",
"sex": "男",
"age": 24,
"level": 3,
"phone": "15071833125"
}
},
{
"_index": "student",
"_type": "_doc",
"_id": "10004",
"_score": 1.0,
"_source": {
"name": "基地",
"sex": "男",
"age": 24,
"level": 3,
"phone": "15071833124"
}
}
]
}
}
使用term
查询,精确的匹配关键字,不会对查询条件进行分词。
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"query": {
"term": {
"name.keyword": { // term查询,查询条件不会分词,加上.keyword才能正确匹配数据
"value": "基地"
}
}
}
}
服务器返回响应结果:
{
"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": "student",
"_type": "_doc",
"_id": "10004",
"_score": 1.3862942,
"_source": {
"name": "基地",
"sex": "男",
"age": 24,
"level": 3,
"phone": "15071833124"
}
}
]
}
}
terms
与term
效果一样,但是允许指定多个关键字,效果类似 in
查询。
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"query": {
"terms": {
"name.keyword": ["基地", "怀勇"]
}
}
}
服务器返回响应结果:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "student",
"_type": "_doc",
"_id": "10001",
"_score": 1.0,
"_source": {
"name": "怀勇",
"sex": "男",
"age": 24,
"level": 3,
"phone": "15071833125"
}
},
{
"_index": "student",
"_type": "_doc",
"_id": "10004",
"_score": 1.0,
"_source": {
"name": "基地",
"sex": "男",
"age": 24,
"level": 3,
"phone": "15071833124"
}
}
]
}
}
默认情况下,ES在搜索的结果中,会把文档保存在_source
的所有字段都返回。可以通过_source
指定要返回的字段。
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"_source": ["name", "sex"],
"query": {
"term": {
"name": {
"value": "基"
}
}
}
}
服务器返回响应结果:
{
"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": "student",
"_type": "_doc",
"_id": "10004",
"_score": 1.3862942,
"_source": {
"sex": "男",
"name": "基地"
}
}
]
}
}
还可以通过_includes
指定要显示的字段,_excludes
指定不想显示的字段。
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"_source": {
"includes": ["name", "sex"]
},
"query": {
"term": {
"name": {
"value": "基"
}
}
}
}
服务器返回响应结果为:
{
"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": "student",
"_type": "_doc",
"_id": "10004",
"_score": 1.3862942,
"_source": {
"sex": "男",
"name": "基地"
}
}
]
}
}
bool
可以用来合并多个过滤条件查询结果的布尔逻辑,它包含一下操作符:
1、must
:多个查询条件完全匹配,相当于and
,会计算相关性得分;
2、must_not
:多个查询条件的相反匹配,相当于not
,不会计算相关性得分;
3、should
:至少有一个查询条件匹配,相当于or
,会计算相关性得分;
4、filter
:相当于must
,但是不计算相关性。
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"_source":["name", "sex", "age", "level", "phone"],
"query":{
"bool": {
"must": {
"term": {
"level": 3
}
},
"must_not": {
"term": {
"name": {
"value": "怀"
}
}
},
"should": {
"match": {
"sex": "男"
}
}
}
}
}
服务器返回结果为:
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1.287682,
"hits": [
{
"_index": "student",
"_type": "_doc",
"_id": "10004",
"_score": 1.287682,
"_source": {
"level": 3,
"phone": "15071833124",
"sex": "男",
"name": "基地",
"age": 24
}
},
{
"_index": "student",
"_type": "_doc",
"_id": "10005",
"_score": 1.0,
"_source": {
"level": 3,
"phone": "151833124",
"sex": "女",
"name": "张雅",
"age": 26
}
}
]
}
}
通过range
可以找出在指定区间范围内的数字或者时间。range
支持以下字符:
操作符 | 说明 |
---|---|
gt |
大于 > |
gte |
大于等于 |
lt |
小于< |
lte |
小于等于 |
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"_source": ["name", "age", "level", "sex"],
"query": {
"bool": {
"must": [
{
"range": {
"age": {
"gt": 25,
"lt": 30
}
}
},
{
"match": {
"sex": "女"
}
}
]
}
}
}
服务器返回结果为:
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 2.3862944,
"hits": [
{
"_index": "student",
"_type": "_doc",
"_id": "10005",
"_score": 2.3862944,
"_source": {
"level": 3,
"sex": "女",
"name": "张雅",
"age": 26
}
}
]
}
}
返回包含与搜索字词相似的字词的文档。
编辑距离是将一个字词转换为另一个字词所需要的字符更改的次数,这些更改包括:
1、更改字符(box -> fox)
2、删除字符(black -> lack)
3、插入字符(sic -> sick)
4、转置两个相邻字符(act -> cat)
为了找到相似的术语,fuzzy
查询会在指定的编辑距离内创建一组搜索词的所有可能的变体或扩展。然后查询返回每个扩展的完全匹配。通过 fuzziness
修改编辑距离。一般使用默认值 AUTO
,根据术语的长度生成编辑距离。
向 ES 服务器发送 GET
请求 :http://127.0.0.1:9200/student/_search,请求体为:
{
"query": {
"fuzzy": {
"name": {
"value": "基",
"fuzziness": 0
}
}
}
}
服务器返回响应结果:
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 4,
"relation": "eq"
},
"max_score": 0.72615415,
"hits": [
{
"_index": "student",
"_type": "_doc",
"_id": "10004",
"_score": 0.72615415,
"_source": {
"name": "基地",
"sex": "男",
"age": 24,
"level": 3,
"phone": "15071833124"
}
},
{
"_index": "student",
"_type": "_doc",
"_id": "10008",
"_score": 0.72615415,
"_source": {
"name": "地基",
"sex": "男",
"age": 56,
"level": 7,
"phone": "15071833124"
}
},
{
"_index": "student",
"_type": "_doc",
"_id": "10006",
"_score": 0.60996956,
"_source": {
"name": "基地1",
"sex": "男",
"age": 21,
"level": 4,
"phone": "15071833124"
}
},
{
"_index": "student",
"_type": "_doc",
"_id": "10007",
"_score": 0.60996956,
"_source": {
"name": "1基地",
"sex": "1男",
"age": 25,
"level": 4,
"phone": "15071833124"
}
}
]
}
}
sort
可以按照不同的字段进行排序,并且通过 order
指定排序的方式:desc
降序,asc
升序。
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"query": {
"bool": {
"must": {
"match": {
"name": "基"
}
},
"must_not": {
"range": {
"level": {
"gte": 1,
"lte": 3
}
}
}
}
},
"sort": [
{
"age": {
"order": "desc"
}
}
]
}
服务器返回响应结果:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "student",
"_type": "_doc",
"_id": "10008",
"_score": null,
"_source": {
"name": "地基",
"sex": "男",
"age": 56,
"level": 7,
"phone": "15071833124"
},
"sort": [
56
]
},
{
"_index": "student",
"_type": "_doc",
"_id": "10007",
"_score": null,
"_source": {
"name": "1基地",
"sex": "1男",
"age": 25,
"level": 4,
"phone": "15071833124"
},
"sort": [
25
]
},
{
"_index": "student",
"_type": "_doc",
"_id": "10006",
"_score": null,
"_source": {
"name": "基地1",
"sex": "男",
"age": 21,
"level": 4,
"phone": "15071833124"
},
"sort": [
21
]
}
]
}
}
观察返回结果会发现:
1、_score
和max_score
字段没有进行相关性计算,这是由于计算 _score
是比较消耗性能的,而且通常主要用作排序,当不是用相关性进行排序的时候,就不需要统计其相关性。 如果想强制计算其相关性,可以设置 track_scores
为 true
,例如向ES服务器发送GET
请求:http://localhost:9200/student/_search?track_scores=true;
2、hits
数组中每个返回结果都多了sort
字段,它所包含的值是用来排序的。
注意:排序是对字段的原始内容进行的,倒排索引是无法发挥作用的。ES中可以通过fielddata
和doc_values
进行设置。
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"query": {
"bool": {
"must_not": {
"match": {
"name": "怀"
}
},
"must": {
"range": {
"age": {
"gte": 56,
"lte": 56
}
}
}
}
},
"sort": [
{
"age": {
"order": "desc"
}
},
{
"level": {
"order": "asc"
}
}
]
}
结果集会先用第一排序字段来排序,当用用作第一字段排序的值相同的时候, 然后再用第二字段对第一排序值相同的文档进行排序,以此类推。
ES支持对查询内容中的关键字部分,通过highlight
进行标签和样式的设置。
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"query": {
"match": {
"name": "朱"
}
},
"highlight": {
"pre_tags"【前置标签】: "",
"post_tags"【后置标签】: "",
"fields"【要高亮显示的字段】: {
"name"【字段名】: {}
}
}
}
服务器返回结果:
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 2.1382177,
"hits": [
{
"_index": "student",
"_type": "_doc",
"_id": "10002",
"_score": 2.1382177,
"_source": {
"name": "朱浩",
"sex": "男",
"age": 28,
"level": 6,
"phone": "15072833125"
},
"highlight": {
"name": [
"朱浩"
]
}
}
]
}
}
ES支持分页查询。通过size
设置当前页的大小,from
设置当前页的起始索引,默认从0开始,计算规则:
f r o m = ( p a g e N u m − 1 ) ∗ p a g e S i z e from = (pageNum - 1) * pageSize from=(pageNum−1)∗pageSize
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"query": {
"match": {
"name": "基"
}
},
"sort": [
{
"age": {
"order": "asc"
}
}
],
"from": 0,
"size": 1
}
服务器返回结果:
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 5,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "student",
"_type": "_doc",
"_id": "10006",
"_score": null,
"_source": {
"name": "基地1",
"sex": "男",
"age": 21,
"level": 4,
"phone": "15071833124"
},
"sort": [
21
]
}
]
}
}
ES 可以通过聚合对文档进行统计分析,类似关系型数据库中的group by
,max
,avg
等。
1、对某个字段取最大值max
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"query": {
"match": {
"name": "基"
}
},
"sort": [
{
"age": {
"order": "asc"
}
}
],
"size": 0, // 限制不返回源数据
"aggs": {
"max_age": {
"max": {
"field": "age"
}
}
}
}
服务器返回结果:
{
"took": 5,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 5,
"relation": "eq"
},
"max_score": null,
"hits": []
},
"aggregations": {
"max_age": {
"value": 56.0
}
}
}
**2、对某个字段取最小值min **
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"aggs": {
"min_level": {
"min": {
"field": "level"
}
}
},
"size": 0
}
服务器返回结果为:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 9,
"relation": "eq"
},
"max_score": null,
"hits": []
},
"aggregations": {
"min_levels": {
"value": null
}
}
}
3、对某个字段求和
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体为:
{
"aggs": {
"sum_age": {
"sum": {
"field": "age"
}
}
},
"size": 0
}
服务器返回结果为:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 9,
"relation": "eq"
},
"max_score": null,
"hits": []
},
"aggregations": {
"sum_age": {
"value": 288.0
}
}
}
4、对某个字段取平均值
平均值使用avg
,其它与max
一致。
5、对某个字段的值去重之后再取总数
请求体为:
{
"aggs": {
"distinct_age": {
"cardinality": {
"field": "age"
}
}
},
"size": 0
}
6、State聚合
stats
聚合,对某个字段一次性返回count
,max
,min
,avg
,sum
五个指标。
请求体为:
{
"aggs": {
"stats_age": {
"stats": {
"field": "age"
}
}
},
"size": 0
}
服务器返回结果为:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 9,
"relation": "eq"
},
"max_score": null,
"hits": []
},
"aggregations": {
"stats_age": {
"count": 9,
"min": 21.0,
"max": 56.0,
"avg": 32.0,
"sum": 288.0
}
}
}
7、桶聚合
桶聚合相当于sql中的group by
子句。
1、terms
聚合,分组统计
请求体为:
{
"aggs": {
"age_groupby": {
"terms": {
"field": "level"
}
}
},
"size": 0
}
服务器返回响应结果为:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 9,
"relation": "eq"
},
"max_score": null,
"hits": []
},
"aggregations": {
"age_groupby": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": 3,
"doc_count": 3
},
{
"key": 4,
"doc_count": 2
},
{
"key": 5,
"doc_count": 1
},
{
"key": 6,
"doc_count": 1
},
{
"key": 7,
"doc_count": 1
},
{
"key": 8,
"doc_count": 1
}
]
}
}
}
2、terms
分组下再进行聚合
请求体为:
{
"aggs": {
"age_groupby": {
"terms": {
"field": "age"
},
"aggs": {
"sum_age": {
"sum":{
"field": "age"
}
}
}
}
},
"size": 0
}
服务器返回结果为:
{
"took": 5,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 9,
"relation": "eq"
},
"max_score": null,
"hits": []
},
"aggregations": {
"age_groupby": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": 24,
"doc_count": 2,
"sum_age": {
"value": 48.0
}
},
{
"key": 28,
"doc_count": 2,
"sum_age": {
"value": 56.0
}
},
{
"key": 56,
"doc_count": 2,
"sum_age": {
"value": 112.0
}
},
{
"key": 21,
"doc_count": 1,
"sum_age": {
"value": 21.0
}
},
{
"key": 25,
"doc_count": 1,
"sum_age": {
"value": 25.0
}
},
{
"key": 26,
"doc_count": 1,
"sum_age": {
"value": 26.0
}
}
]
}
}
}
使用filter
,不会计算相关性得分,相关查询还会被缓存,可以提高服务器响应性能。
向ES服务器发送GET
请求:http://localhost:9200/student/_search,请求体如下:
{
"query": {
"constant_score": {
"filter": { // 过滤
"term": {
"name.keyword": "基地"
}
}
}
}
}
validate
API 可以验证一条查询语句是否合法。向ES服务器发送GET
请求:http://localhost:9200/student/_validate/query?explain,请求体如下:
{
"query": {
"multi_match": {
"query": 24,
"fields":["age","phone"]
}
}
}
服务器返回响应:
{
"_shards"【分片信息】: {
"total": 1,
"successful": 1,
"failed": 0
},
"valid"【验证结果】: true,
"explanations"【索引描述信息】: [
{
"index": "student",
"valid": true,
"explanation": "(phone:24 | age:[24 TO 24])"
}
]
}
在IDEA中创建项目,修改pom
文件,添加ES相关依赖:
<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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
</dependencies>
public class ConnectionTest {
/**
* ES 客户端
*/
private static RestHighLevelClient client;
/**
* 客户端与服务器建立连接
*/
@Before
public void connect(){
client = new RestHighLevelClient(RestClient.builder(new HttpHost("localhost", 9200, "http")));
}
/**
* 关闭客户端与服务器的连接
*/
@After
public void close(){
if(Objects.nonNull(client)){
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注意:9200是ES的Web通信接口。
/**
* 创建索引
*/
@Test
public void createIndex(){
// 创建索引--请求对象
CreateIndexRequest request = new CreateIndexRequest("user1");
try {
// 发送请求
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
// 服务器返回响应
boolean acknowledged = response.isAcknowledged();
System.out.println("创建索引,服务器响应:" + acknowledged);
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void getIndex() throws IOException {
// 查询索引--请求对象
GetIndexRequest request = new GetIndexRequest("user");
// 服务器响应
GetIndexResponse response = client.indices().get(request, RequestOptions.DEFAULT);
System.out.println(response.getSettings());
}
服务器返回结果:
/**
* 删除索引
*/
@Test
public void deleteIndex() throws IOException {
// 删除索引--请求对象
DeleteIndexRequest request = new DeleteIndexRequest("user1");
// 服务器返回响应
AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
System.out.println(response.isAcknowledged());
}
先创建数据模型:
package com.jidi.elastic.search.test;
import lombok.Data;
/**
* @Description 用户实体
* @Author jidi
* @Email [email protected]
* @Date 2021/9/6
*/
@Data
public class UserDto {
/**
* 主键id
*/
private Integer id;
/**
* 名字
*/
private String name;
/**
* 昵称
*/
private String nickName;
/**
* 年龄
*/
private Integer age;
/**
* 性别 1:男 2:女
*/
private byte sex;
/**
* 级别
*/
private Integer level;
/**
* 手机号码
*/
private String phone;
@Override
public String toString() {
return "UserDto{" +
"id=" + id +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
", age=" + age +
", sex=" + sex +
", level=" + level +
", phone='" + phone + '\'' +
'}';
}
}
/**
* 创建文档(文档存在,则整个都修改)
*/
@Test
public void createDocument() throws IOException {
// 创建文档--请求对象
IndexRequest request = new IndexRequest();
// 设置索引和唯一标识
request.index("user").id("10001");
// 创建数据对象
UserDto user = new UserDto();
user.setId(10001);
user.setName("基地");
user.setAge(24);
user.setLevel(3);
user.setSex((byte)1);
user.setNickName("鸡子哥");
user.setPhone("15071833124");
// 添加文档数据
String userJson = new ObjectMapper().writeValueAsString(user);
request.source(userJson, XContentType.JSON);
// 服务器返回响应
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
// 打印结果信息
System.out.println("_index:" + response.getIndex());
System.out.println("_id:" + response.getId());
System.out.println("result:" + response.getResult());
System.out.println("_version:" + response.getVersion());
System.out.println("_seqNo:" + response.getSeqNo());
System.out.println("_shards:" + response.getShardInfo());
}
执行结果:
#### 1.3.4.3 修改文档
/**
* 修改文档
*/
@Test
public void updateDocument() throws IOException {
// 修改文档--请求对象
UpdateRequest request = new UpdateRequest();
// 配置修改参数
request.index("user").id("10001");
// 设置请求体
request.doc(XContentType.JSON, "sex", 1, "age", 24, "phone", "15071833124");
// 发送请求,获取响应
UpdateResponse response = client.update(request, RequestOptions.DEFAULT);
System.out.println("_index:" + response.getIndex());
System.out.println("_id:" + response.getId());
System.out.println("result:" + response.getResult());
}
执行结果:
/**
* 查询文档
*/
@Test
public void searchDocument() throws IOException {
// 创建请求对象
GetRequest request = new GetRequest().id("10001").index("user");
// 返回响应体
GetResponse response = client.get(request, RequestOptions.DEFAULT);
System.out.println(response.getIndex());
System.out.println(response.getType());
System.out.println(response.getId());
System.out.println(response.getSourceAsString());
}
执行结果:
/**
* 删除文档
*/
@Test
public void deleteDocument() throws IOException {
// 创建请求对象
DeleteRequest request = new DeleteRequest();
// 构建请求体
request.index("user");
request.id("10001");
// 发送请求,返回响应
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
System.out.println(response.toString());
}
执行结果:
/**
* 批量创建文档
*/
@Test
public void batchCreateDocument() throws IOException {
// 创建请求对象
BulkRequest request = new BulkRequest();
request.add(
new IndexRequest()
.index("user")
.id("10001")
.source(
XContentType.JSON,
"id", 10001, "name", "基地", "nickName", "鸡子哥", "age", 24, "sex", 1, "level", 3, "phone", "15071833124"));
request.add(
new IndexRequest()
.index("user")
.id("10002")
.source(
XContentType.JSON,
"id", 10002, "name", "怀经", "nickName", "勇子哥", "age", 23, "sex", 1, "level", 3, "phone", "15071831234"));
// 发送请求,返回响应
BulkResponse responses = client.bulk(request, RequestOptions.DEFAULT);
System.out.println(responses.getTook());
System.out.println(responses.getItems());
}
执行结果为:
/**
* 批量删除文档
*/
@Test
public void batchDeleteDocument() throws IOException {
// 创建请求对象
BulkRequest request = new BulkRequest();
request.add(new DeleteRequest("user").id("10001"));
request.add(new DeleteRequest("user").id("10002"));
// 发送请求,返回响应
BulkResponse responses = client.bulk(request, RequestOptions.DEFAULT);
System.out.println(responses.getTook());
System.out.println(responses.getItems());
}
执行结果为:
/**
* 查询所有文档数据
*/
@Test
public void getAllDocument() throws IOException {
// 创建请求对象
SearchRequest request = new SearchRequest();
request.indices("user");
// 构建查询请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 查询所有数据
sourceBuilder.query(QueryBuilders.matchAllQuery());
request.source(sourceBuilder);
// 发送请求,返回响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit: hits) {
System.out.println(hit.getSourceAsString());
}
}
执行结果:
/**
* 单字段匹配查询
*/
@Test
public void getDocumentByMatch() throws IOException {
// 创建请求对象
SearchRequest request = new SearchRequest();
request.indices("user");
// 构建查询请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 单字段匹配数据
sourceBuilder.query(new MatchQueryBuilder("name", "基地"));
request.source(sourceBuilder);
// 发送请求,返回响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit: hits) {
System.out.println(hit.getSourceAsString());
}
}
执行结果:
/**
* 单字段匹配查询
*/
@Test
public void getDocumentByMatchMultiField() throws IOException {
// 创建请求对象
SearchRequest request = new SearchRequest();
request.indices("user");
// 构建查询请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 单字段匹配数据
sourceBuilder.query(new MultiMatchQueryBuilder("基地", "name", "nickName"));
request.source(sourceBuilder);
// 发送请求,返回响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit: hits) {
System.out.println(hit.getSourceAsString());
}
}
执行结果:
/**
* 关键字精确查询
*/
@Test
public void getDocumentByKeWorld() throws IOException {
// 创建请求对象
SearchRequest request = new SearchRequest();
request.indices("user");
// 构建请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(new TermQueryBuilder("name", "基地"));
sourceBuilder.query(new TermQueryBuilder("age", "26"));
sourceBuilder.query(new TermQueryBuilder("level", "7"));
request.source(sourceBuilder);
// 发送请求,返回响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
执行结果:
/**
* 多关键字精确查询
*/
@Test
public void getDocumentByMultiKeyWorld() throws IOException {
// 创建请求对象
SearchRequest request = new SearchRequest();
request.indices("user");
// 创建请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(new TermsQueryBuilder("name", "2", "123"));
request.source(sourceBuilder);
// 发送请求,返回响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
执行结果:
/**
* 过滤字段
*/
@Test
public void getDocumentByFetchField() throws IOException {
// 创建请求对象
SearchRequest request = new SearchRequest();
request.indices("user");
// 构建请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(new MatchAllQueryBuilder());
// 指定查询字段
sourceBuilder.fetchSource(new String[]{"id", "name"}, null);
request.source(sourceBuilder);
// 发送请求,返回响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
执行结果:
/**
* 组合查询
*/
@Test
public void getDocumentByBool() throws IOException {
// 创建请求对象
SearchRequest request = new SearchRequest();
request.indices("user");
// 构建请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 组合查询
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
// 必须包含
boolQueryBuilder.must(new MatchQueryBuilder("name", "基地"));
boolQueryBuilder.must(new TermQueryBuilder("nickName", "哥"));
// 必须不包含
boolQueryBuilder.mustNot(new TermQueryBuilder("level", 7));
// 可能包含
boolQueryBuilder.should(new MatchQueryBuilder("sex", 1));
sourceBuilder.query(boolQueryBuilder);
request.source(sourceBuilder);
// 发送请求,返回响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
执行结果:
/**
* 范围查询
*/
@Test
public void getDocumentByRange() throws IOException {
// 创建请求对象
SearchRequest request = new SearchRequest();
request.indices("user");
// 构建请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 范围查询
RangeQueryBuilder rangeQueryBuilder = new RangeQueryBuilder("age");
// 大于等于
rangeQueryBuilder.gte(24);
// 小于等于
rangeQueryBuilder.lte(35);
sourceBuilder.query(rangeQueryBuilder);
sourceBuilder.from(0);
sourceBuilder.size(10);
request.source(sourceBuilder);
// 发送请求,返回响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
执行结果:
/**
* 模糊查询
*/
@Test
public void getDocumentByLike() throws IOException {
// 创建请求对象
SearchRequest request = new SearchRequest();
request.indices("user");
// 构建请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 模糊查询
sourceBuilder.query(new FuzzyQueryBuilder("name", "基").fuzziness(Fuzziness.AUTO));
request.source(sourceBuilder);
// 发送请求,返回响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
/**
* 排序查询
*/
@Test
public void getDocumentByOrder() throws IOException {
// 创建请求对象
SearchRequest request = new SearchRequest();
request.indices("user");
// 构建请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(new MatchAllQueryBuilder());
// 升序
sourceBuilder.sort("age", SortOrder.ASC);
request.source(sourceBuilder);
// 发送请求,返回响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
执行结果:
/**
* 高亮查询
*/
@Test
public void getDocumentByHighLight() throws IOException {
// 创建请求对象
SearchRequest request = new SearchRequest();
request.indices("user");
// 构建请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(new TermQueryBuilder("name", "基"));
// 高亮查询
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("name");
highlightBuilder.preTags("");
highlightBuilder.postTags("");
sourceBuilder.highlighter(highlightBuilder);
request.source(sourceBuilder);
// 发送请求,返回响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
// 获取高亮结果
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
System.out.println(highlightFields);
}
}
执行结果:
/**
* 分页查询
*/
@Test
public void getDocumentByPage() throws IOException {
// 创建请求对象
SearchRequest request = new SearchRequest();
request.indices("user");
// 构建请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(new MatchAllQueryBuilder());
// 分页
sourceBuilder.from(0);
sourceBuilder.size(2);
request.source(sourceBuilder);
// 发送请求,返回响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits hits = response.getHits();
for (SearchHit hit : hits) {
System.out.println(hit.getSourceAsString());
}
}
执行结果:
/**
* 聚合查询
*/
@Test
public void getDocumentByAggregation() throws IOException {
// 创建请求对象
SearchRequest request = new SearchRequest();
request.indices("user");
// 构建请求体
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 年龄最大
sourceBuilder.aggregation(AggregationBuilders.max("maxAge").field("age"));
sourceBuilder.size(0);
request.source(sourceBuilder);
// 发送请求,返回响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
System.out.println(new ObjectMapper().writeValueAsString(response.getAggregations().getAsMap().values()));
}
执行结果:
单台 Elasticsearch
服务器提供服务,往往都有最大的负载能力,超过这个阈值,服务器性能就会大大降低甚至不可用,所以生产环境中,一般都是运行在指定服务器集群中。除了负载能力,单点服务器也存在其他问题:
1、单台机器存储容量有限;
2、单台服务器容易出现单点故障,无法实现高可用;
3、单服务器并发处理能力有限。
配置服务器集群时,集群中节点数量没有限制,大于等于 2 个节点就可以看做是集群了。一般出于高性能及高可用方面来考虑集群中节点数量都是 3 个以上。
一个集群就是由多个服务器节点组织在一起,共同持有整个的数据,并一起提供索引和搜索功能。一个 Elasticsearch
集群有一个唯一的名字标识,这个名字默认就是elasticsearch
。节点只能通过指定某个集群的名字,来加入这个集群。
在Elasticsearch
集群中可以监控统计很多信息,但是只有一个是最重要的:集群健康(cluster health
)。集群健康有三种状态:
1、 green
:所有主分片和复制分片都可用;
2、 yellow
:所有主分片可用,但不是所有复制分片都可用;
3、 red
:不是所有主分片都可用。
一个节点(node
)就是一个Elasticsearch
实例,而一个集群(cluster
)由一个或多个节点组成,它们具有相同的 cluster.name
,它们协同工作,分享数据和负载。当加入新的节点或者删除一个节点时,集群就会感知到并平衡数据。作为集群的一部分,它存储数据,参与集群的索引和搜索功能。
一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色的名字,这个名字会在启动的时候赋予节点。一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,每个节点都会被安排加入到一个叫做elasticsearch
的集群中。
每个节点都保存了集群的状态,但是只有Master
节点才能修改集群的状态信息(所有节点信息,所有的索引和相关的Mapping
与Setting
信息,分片路由信息)。主节点不参与文档级别的变更或搜索,这意味着在流量增长的时候,主节点不会成为集群的瓶颈。任何节点都可以成为主节点。
每个节点启动后,默认就是一个 Master eligible
节点,Master eligible
节点可以参加选主流程,成为Master
节点,可以通过设置node.master:false
禁止。
节点分类
节点根据功能可以分为:
1、Master Node
:主节点,负责修改集群状态信息;
2、Data Node
:数据节点,负责保存分片数据;
3、Ingest Node
:预处理节点,负责在真正对文档进行索引之前对文件进行预处理;
4、Coordinating Node
:协调节点,负责接受客户端请求,将请求分发到合适的节点,最终把结果汇集在一起,返回给客户端。每个节点默认都起到了协调节点的职责。
其它的节点类型
1、Hot & Warm Node
:冷热节点,不同硬件配置的Data Node
,用于实现Hot & Warm
架构,降低集群部署的成本;
2、Machine Learing Node
:负责跑机器学习任务的节点;
3、Tribe Node
: Tribe Node
(已被Deprecated)可以连接到不同的elasticsearch
集群,并且支持将这些集群当做一个单独的集群处理。
通常情况下:开发环境一个节点可以承担多种角色;生产环境中,为了提高性能,节点应该设置成单一的角色。
节点类型 | 配置参数 | 默认值 |
---|---|---|
master eligible |
node.master |
true |
data |
node.data |
true |
ingest |
node.ingest |
true |
coordinating only |
无 | 每个节点默认都是协调节点。 |
machine learning |
node.ml |
true (需要enable x-pack ,X-Pack 将安全,警报,监视,报告和图形功能包含在一个易于安装的软件包中) |
1、创建elasticsearch-7.8.0-cluster文件夹,内部复制三个elasticsearch服务
2、修改每个节点的配置信息(config/elasticsearch.yml
)
node-001
节点配置:
# ---------------------------------- Cluster -----------------------------------
# 集群名称
cluster.name: my-application
# ------------------------------------ Node ------------------------------------
# 节点名称
node.name: node-001
node.master: true
node.data: true
# ---------------------------------- Network -----------------------------------
# ip地址
network.host: localhost
#
# Set a custom port for HTTP:
# http端口
http.port: 9201
# tcp监听端口
transport.tcp.port: 9301
# 跨域配置
http.cors.enabled: true
http.cors.allow-origin: "*"
# --------------------------------- Discovery ----------------------------------
# 候选主节点的地址,在开启服务后可以被选为主节点
discovery.seed_hosts: ["localhost:9301", "localhost:9302", "localhost:9303"]
discovery.zen.fd.ping_timeout: 1m
discovery.zen.fd.ping_retries: 5
# 集群内可以被选为主节点的节点列表
cluster.initial_master_nodes: ["node-001", "node-002", "node-003"]
node-002
节点配置:
# ---------------------------------- Cluster -----------------------------------
# 集群名称
cluster.name: my-application
# ------------------------------------ Node ------------------------------------
# 节点名称
node.name: node-002
node.master: true
node.data: true
# ---------------------------------- Network -----------------------------------
# ip地址
network.host: localhost
#
# Set a custom port for HTTP:
# http端口
http.port: 9202
# tcp监听端口
transport.tcp.port: 9302
# 跨域配置
http.cors.enabled: true
http.cors.allow-origin: "*"
# --------------------------------- Discovery ----------------------------------
# 候选主节点的地址,在开启服务后可以被选为主节点
discovery.seed_hosts: ["localhost:9301", "localhost:9302", "localhost:9303"]
discovery.zen.fd.ping_timeout: 1m
discovery.zen.fd.ping_retries: 5
# 集群内可以被选为主节点的节点列表
cluster.initial_master_nodes: ["node-001", "node-002", "node-003"]
node-003
节点配置:
# ---------------------------------- Cluster -----------------------------------
# 集群名称
cluster.name: my-application
# ------------------------------------ Node ------------------------------------
# 节点名称
node.name: node-003
node.master: true
node.data: true
# ---------------------------------- Network -----------------------------------
# ip地址
network.host: localhost
#
# Set a custom port for HTTP:
# http端口
http.port: 9203
# tcp监听端口
transport.tcp.port: 9303
# 跨域配置
http.cors.enabled: true
http.cors.allow-origin: "*"
# --------------------------------- Discovery ----------------------------------
# 候选主节点的地址,在开启服务后可以被选为主节点
discovery.seed_hosts: ["localhost:9301", "localhost:9302", "localhost:9303"]
discovery.zen.fd.ping_timeout: 1m
discovery.zen.fd.ping_retries: 5
# 集群内可以被选为主节点的节点列表
cluster.initial_master_nodes: ["node-001", "node-002", "node-003"]
3、启动集群(需要先删除data
目录下面的所有数据),分别进入bin
目录点击elasticsearch.bat
脚本
4、测试集群,分别向节点发送GET
请求:http://localhost:9201/_cluster/health,9201为访问节点的端口
一个索引就是一个拥有相似特征的文档的集合。实际上,索引只是一个用来指向一个或多个分片(shards)的“逻辑命名空间(logical namespace)”。一个索引由一个名字来标识(必须全部是小写字母),并且当我们要对这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。
在一个集群中,可以定义任意多的索引,能搜索的数据必须索引。
Elasticsearch 索引的精髓:一切设计都是为了提高搜索的性能。
在一个索引中,可以定义一种或多种类型。一个类型是:索引的一个逻辑上的分类/分区,其语义完全由用户定义。通常,会为具有一组共同字段的文档定义一个类型。不同的版本,类型发生了不同的变化:
elasticsearch版本 | 类型支持情况 |
---|---|
5.x |
支持多种type |
6.x |
只能有一种type |
7.x |
默认不再支持自定投索引类型(默认类型为_doc ) |
程序中大多的实体或对象能够被序列化为包含键值对的JSON对象,键(key)是字段(field)或属性(property)的名字,值(value)可以是字符串、数字、布尔类型、另一个对象、值数组或者其他特殊类型,比如表示日期的字符串或者表示地理位置的对象。
{
"name": "John Smith",
"age": 42,
"confirmed": true,
"join_date": "2014-06-01",
"home": {
"lat": 51.5,
"lon": 0.1
},
"accounts": [
{
"type": "facebook",
"id": "johnsmith"
},
{
"type": "twitter",
"id": "johnsmith"
}
]
}
通常,可以认为对象(object)和文档(document)是等价相通的。不过,他们还是有所差别:对象(Object)是一个JSON结构体;对象(Object)中还可能包含其他对象(Object)。
在Elasticsearch
中,文档(document)这个术语有着特殊含义。它特指最顶层结构或者根对象(root object)序列化成的JSON数据(以唯一ID标识并存储于Elasticsearch中)。
一个文档不只有数据。它还包含了元数据(metadata
)——关于文档的信息。三个必须的元数据节点是:
1、_index
:文档存储的地方;
2、_type
:文档代表的对象的类;
3、_id
:文档的唯一标识。仅仅是一个字符串,它与 _index
和_type
组合时,就可以在ELasticsearch
中唯一标识一个文档。
相当于数据表的字段,对文档数据根据不同属性进行的分类标识。一个索引的字段数量有上限的,超过上限就会报错。
映射(mapping
)机制用于进行字段类型确认,将每个字段匹配为一种确定的数据类型( string
、number
、booleans
、date
等)。
mapping
是处理数据的方式和规则方面做的一些限制,如:某个字段的数据类型、默认值、分析器、是否被索引等等。这些都是映射里面可以设置的,一个映射定义了字段类型,每个字段的数据类型,以及字段被Elasticsearch
处理的方式。映射还用于设置关联到类型上的元数据。
mapping
又可以分为以下三种(具体由dynamic
属性控制):
1、动态映射(dynamic:true
):能够根据文档信息推断出字段的数据类型然后动态添加新的字段;
2、静态映射(dynamic:false
):在原有的映射基础上,当有新的字段时,不会主动的添加新的映射关系,只作为查询结果出现在查询中;
3、严格映射(dynamic:strict
):如果遇到新的字段,就抛出异常。
更改mapping
mapping
中字段类型的修改分为两种情况:
1、新增加字段:根据映射类型不同,采取不同的处理方式;
2、已存在字段:一旦已有数据写入,不再支持修改字段定义,因为Lucene
实现的倒排索引,一旦生成之后,就不允许修改。若希望改变字段类型,必须Reindex
重建索引。
索引模板,是ES提供的一种复用机制。当新建一个索引时,可以自动匹配模板,完成索引的基础部分搭建。
下面是一个典型例子:
// PUT http://localhost:9200/_template/test_template
{
"order": 1 ,
"index_patterns" : "tes*",
"settings" : {
"index": {
"number_of_shards" : 2,
"analysis": {
"char_filter": {
"&_to_and": {
"type": "mapping",
"mappings": ["&=> and"]
},
"|_to_or": {
"type": "mapping",
"mappings": ["|=> or"]
},
"replace_dot": {
"pattern": "\\.",
"type": "pattern_replace",
"replacement": " "
},
"html": {
"type": "html_strip"
}
},
"filter": {
"my_stop": {
"type": "stop",
"stopwords": ["的"]
}
},
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": ["&_to_and", "|_to_or", "replace_dot"],
"tokenizer": "ik_max_word",
"filter": ["lowercase", "my_stop"]
}
}
}
}
},
"mappings" : {
"date_detection": true,
"numeric_detection": true,
"dynamic_templates": [
{
"string_fields": {
"match": "*",
"match_mapping_type": "string",
"mapping": {
"fielddata": {
"fromat": "disabled"
},
"analyzer": "my_analyzer",
"index": "analyzed",
"omit_norms": true,
"type": "string",
"fields": {
"raw": {
"ignore_above": 256,
"index": "not_analyzed",
"type": "string",
"doc_values": true
}
}
}
}
}
],
"properties": {
"money": {
"type": "double",
"doc_values": true
}
}
},
"aliases": {}
}
上面的例子乍一看很复杂,拆分来看,主要是以下几部分:
{
"order": 0, // 模板优先级
"index_patterns": [], // 模板匹配的名称方式
"settings": {...}, // 索引设置
"mappings": {...}, // 索引映射
"aliases": {...} // 索引别名
}
模板的优先级是通过模板中的 order
字段定义的,数字越大,优先级越高。优先级高的模板可以覆盖优先级低的模板。
下面是一个匹配te
开头的索引的模板:
// PUT http://localhost:9200/_template/test_template_1
{
"order": 0 ,
"index_patterns" : "te*",
"settings" : {
"number_of_shards" : 1
},
"mappings" : {
"_source" : { "enabled" : false }
}
}
索引模板是有序合并的,如果要修改索引的某些设置,可以在添加一个优先级更高的模板:
// PUT http://localhost:9200/_template/test_template_2
{
"order": 1 ,
"index_patterns" : "tes*",
"settings" : {
"number_of_shards" : 2
},
"mappings" : {
"date_detection": true,
"numeric_detection": true
}
}
索引模板中的 index_patterns
字段定义的是该索引模板所应用的索引情况。如 "index_patterns": "teS*"
所表示的含义是,当新建索引时,所有以 tes
开头的索引都会自动匹配到该索引模板。利用该模板进行相应的设置和字段添加等。、、
以上面创建的两个模板为基础,此时创建名为test1
的索引,就会自动匹配到test_template_1
和test_template_2
两个事先定义好的索引模板:
// 创建索引test1
// PUT http://localhost:9200/test1
// 查看索引test1
// GET http://localhost:9200/test1
{
"test1": {
"aliases": {},
"mappings": {
"_source": {
"enabled": false
},
"date_detection": true,
"numeric_detection": true
},
"settings": {
"index": {
"creation_date": "1633230450431",
"number_of_shards": "2",
"number_of_replicas": "1",
"uuid": "Ucg4fPEaTv6pPtS6-twK6g",
"version": {
"created": "7080099"
},
"provided_name": "test1"
}
}
}
}
索引模板中的 setting
部分一般定义的是索引的主分片、拷贝分片、刷新时间、自定义分析器等设置信息。常见的 setting
部分结构如下:
"settings": {
"index": {
"analysis": {...}, // 自定义的分析器
"number_of_shards": "32", // 主分片的个数
"number_of_replicas": "1", // 主分片的拷贝分片个数
"refresh_interval": "5s" // 刷新时间
}
}
setting
的设置中,重点是自定义分析器的设置。分析器是三个顺序执行的组件的结合。他们分别是字符过滤器、分词器、标记过滤器。下面是一个自定义分析器的结构:
"settings": {
"index": {
"analysis": {
"char_filter": { ... }, // 用户自定义字符过滤器
"tokenizer": { ... }, // 用户自定义分词器
"filter": { ... }, // 用户自定义标记过滤器
"analyzer": { ... } // 用户自定义分析器
},
...
}
}
1、字符过滤器
目前字符过滤器有三种:映射字符过滤器(Mapping charfilter
)、HTML过滤器(HTML Strip char filter
)和格式替换过滤器(Pattern Replace char filter
)。HTML过滤器去除所有的HTML标签。
如下定义一个映射字符过滤器,将&
替换成and
:
"char_filter": {
"&_to_and": {
"type": "mapping", // 过滤器类型为字符映射过滤器
"mappings": [ "&=> and"] // 要替换的字符
}
}
如下在定义一个格式替换过滤器,将点 .
替换成空格:
"char_filter": {
"replace_dot": {
"pattern": "\\.", // 匹配被替换的字符
"type": "pattern_replace", // 过滤器类型为格式替换过滤器
"replacement": " " // 替换后的字符
}
}
2、分词器
常用的分词器有 standard
、keyword
、whitespace
、pattern
等。对于中文的分词可以使用IK
分词器。
下面是一个使用IK
分词器的例子:
"tokenizer": "ik_max_word" // 使用IK分词器
3、标记过滤器
常用的标记过滤器有lowercase
和 stop
。lowercase
标记过滤器将词转换为小写,stop
标记过滤器去除一些用户自定义停用词或者是语言内定义的停用词。
stop
标记过滤器常用结构如下:
"filter": {
"my_stopwords": {
"type": "stop", // 类型为stop过滤器
"stopwords": [ "the", "a" ] // 要过滤的字符
}
}
4、分析器组合
将自定义的字符过滤器,分词器和标记过滤器按顺序组合起来,就是用户自定义的分析器。
"analyzer": {
"my_analyzer": { // 自定义分析器名字
"type": "custom", // 类型为自定义
"char_filter": ["&_to_and", "|_to_or", "replace_dot"], // 字符过滤器
"tokenizer": "ik_max_word", // 分词器
"filter": ["lowercase", "my_stop"] // 标记过滤器
}
}
将自定义分析器各部分完整表示如下:
"settings" : {
"index": {
"number_of_shards" : 2,
"analysis": {
"char_filter": {
"&_to_and": {
"type": "mapping",
"mappings": ["&=> and"]
},
"|_to_or": {
"type": "mapping",
"mappings": ["|=> or"]
},
"replace_dot": {
"pattern": "\\.",
"type": "pattern_replace",
"replacement": " "
},
"html": {
"type": "html_strip"
}
},
"filter": {
"my_stop": {
"type": "stop",
"stopwords": ["的"]
}
},
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": ["&_to_and", "|_to_or", "replace_dot"],
"tokenizer": "ik_max_word",
"filter": ["lowercase", "my_stop"]
}
}
}
}
}
索引模板中,映射字段所对应的常用结构是:
"mappings": {
"dynamic_templates": [ ... ], // 动态映射部分,用于未定义的 my_type 下字段
"properties": { ... } // 自定义字段的映射
}
1、动态映射
动态映射 dynamic_templates
字段对应的是一个数组,数组中的元素是一个个字段的映射模板。每个字段的映射模板都有一个名字描述这个模板的用途,一个 mapping
字段指明这个映射如何使用,和至少一个参数(例如 match)来定义这个模板适用于哪个字段。
dynamic_templates
字段对应的字段模板结构如下:
{
"string_fields": { // 字段映射模板的名称,一般为"类型_fields"的命名方式
"match": "*", // 匹配的字段名为所有
"match_mapping_type": "string", // 限制匹配的字段类型,只能是 string 类型
"mapping": { ... } // 字段的处理方式
}
如下是一个实例:
"mappings": {
"dynamic_templates": [
{
"string_fields": { // 字段映射模板的名称,一般为"类型_fields"的命名方式
"match": "*", // 匹配的字段名为所有
"match_mapping_type": "string", // 限制匹配的字段类型,只能是 string 类型
"mapping": {
"fielddata": { "format": "disabled" }, // fielddata 不可用,对于分析字段,其默认值是可用
"analyzer": "only_words_analyzer", // 字段采用的分析器名,默认值为 standard 分析器
"index": "true", // 索引方式定义为索引,默认值是true
"omit_norms": true, // omit_norms 为真表示考虑字段的加权,可分析字段默认值 false
"type": "string", // 字段类型限定为 string
"fields": { // 定义一个嵌套字段,将该字段应用于不分析的场景
"raw": {
"ignore_above": 256, // 忽略字段对应的值长度大于256的字段
"index": "false",
"type": "string", // 字段的类型为 string
"doc_values": true // 对于不分析字段,doc_values 对应的是一种列式存储结构,默认false
}
}
}
}
},
"float_fields": {
"match": "*",
"match_mapping_type": "flaot",
"mapping": {
"type": "flaot",
"doc_values": true
}
}
],
"properties": { ... }
}
2、自定义字段映射
针对索引类型中存在的字段,除了可以采用动态模板的方式,还可以采用定义定义的方式,常见的自定义结构如下:
"mappings": {
"dynamic_templates": [ ... ],
"properties": {
"user_city": { // 字段名
"analyzer": "lowercase_analyzer", // 字段分析器
"index": "analyzed", // 字段索引方式定义索引
"type": "string", // 字段数据类型定义为 string
"fields": { // 定义一个名为 user_city.raw 的嵌入的不分析字段
"raw": {
"ignore_above": 512,
"index": "not_analyzed",
"type": "string"
}
}
},
"money":{
"type": "double",
"doc_values": true
}
...
}
}
用以解决数据水平扩展的问题,Elasticsearch
提供了将索引划分成多份的能力,每一份就称之为分片。
一个分片(shard
)是一个最小级别工作单元(worker unit
),它只是保存了索引中所有数据的一部分。分片就是一个Lucene
实例,并且它本身就是一个完整的搜索引擎。文档存储在分片中,并且在分片中被索引,但是应用程序不会直接与它们通信,取而代之的是,直接与索引通信。
当集群扩容或缩小,Elasticsearch
将会自动在节点间迁移分片,以使集群保持平衡。
当创建一个索引的候,可以指定分片的数量。分片可以是主分片(primary shard
)或者是复制分片(replica shard
)。索引中的每个文档属于一个单独的主分片,所以主分片的数量决定了索引最多能存储多少数据。理论上主分片能存储的数据大小是没有限制的,限制取决于实际的使用情况:硬件存储的大小、文档的大小和复杂度、如何索引和查询文档,以及期望的响应时间。
分片的优点:
1、允许水平分割 / 扩展内存容量;
2、允许在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量。
至于一个分片怎样分布,它的文档怎样聚合和搜索请求,完全由Elasticsearch
进行管理的,对于用户来说,这些都是透明的,无需过分关心。
一个Elasticsearch
索引是分片的集合。 当 Elasticsearch
在索引中搜索的时候, 他发送查询到每一个属于索引的分片,然后合并每个分片的结果到一个全局的结果集。
分片的设定
对于生产环境中分片的设定,需要提前规划好容量:
1、分片数设置过小:后续无法增加节点实现水平扩展,单个分片数据量太大,数据重新分配耗时;
2、分片数设置过大:影响搜索结果的相关性打分和准确性,同时单个节点上过多的分片,会导致资源浪费,影响性能。
Elasticsearch
允许创建分片的一份或多份拷贝,这些拷贝叫做复制分片(副本)。复制分片只是主分片的一个副本,用以解决数据高可用问题。
复制分片的优点:
1、在分片/节点失败的情况下,提供了高可用性;
2、扩展搜索量/吞吐量,因为搜索可以在所有的副本上并行运行。
总之,每个索引可以被分成多个分片。一个分片也可以被复制 0 次或多次。一旦复制了,每个索引就有了主分片(作为复制源的分片)和复制分片(主
分片的拷贝)之别。分片和副本的数量可以在索引创建的时候指定。在索引创建之后,可以在任何时候动态地改变复制副本的数量,但是分片的数量无法改变。默认情况下,Elasticsearch
中的每个索引被分片 1 个主分片和 1 个副本,
默认情况下,返回结果是按相关性倒序排列的。 但是什么是相关性? 相关性如何计算?
每个文档都有相关性评分,用一个相对的浮点数字段 _score
来表示 ,_score
的评分越高,相关性越高。
查询语句会为每个文档添加一个_score
字段。评分的计算方式取决于不同的查询类型 ,不同的查询语句用于不同的目的: fuzzy
查询会计算与关键词的拼写相似程度, terms
查询会计算找到的内容与关键词组成部分匹配的百分比,但是一般意义上说的全文本搜索是指计算内容与关键词的类似程度。
ElasticSearch
的相似度算法被定义为 TF/IDF
,即检索词频率/反向文档频率,包括:
1、检索词频率(TF
):检索词在一个文档中出现的频率,出现频率越高,相关性也越高;
2、反向文档频率(IDF
):每个检索词在所有文档中出现的频率,频率越高,相关性越低。 检索词出现在多数文档中会比出现在少数文档中的权重更低, 即
检验一个检索词在文档中的普遍重要性;
3、字段长度准则:字段的长度越长,相关性越低。 检索词出现在一个短的 title
要比同样的词出现在一个长的 content
字段相关性高。
如果多条查询子句被合并为一条复合查询语句,比如 bool
查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中。
Lucene
中的TF/IDF
评分公式:
从ES5.X开始,默认的 相似度算法修改为BM25
,跟经典的TF/IDF
相比,当TF
无限增加时,BM25
的算分会趋于一个稳定的数值。
BM25
算法的评分公式如下:
注意:k
的默认值时1.2,数值越小,饱和度越高,b
的默认值时0.75。
一个运行中的 Elasticsearch
实例称为一个节点,而集群是由一个或者多个拥有相同cluster.name
配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。
当一个节点被选举成为主节点时, 它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。
用户可以将请求发送到集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。 Elasticsearch
对这一切的管理都是透明的。
在包含一个空节点的集群内创建名为 users
的索引,该索引将分配 3个主分片和1份副本。向ES服务器发送GET
请求:http://localhost:9200/users,请求体如下:
{
"settings": {
"number_of_shards": 3, # 主分片数 3
"number_of_replicas": 1 # 副本数 1
}
}
当前单节点集群拥有一个索引users
,所有的3个主分片都会分配在node1
上。通过elasticsearch-head
插件可以查看集群情况:
集群健康值:yellow(9 of 18):表示当前集群的全部主分片都正常运行,但是副本分片没有全部处在正常状态。
:3个主分片正常运行。
:3个副本分片都是Unassigned
,它们没有被分配到任何节点。同一个节点上即保存元数据又保存副本是没有意义的,一旦节点异常,该节点存放的数据全部都会丢失。
当集群中只有一个节点在运行时,意味着会有一个单点故障问题——没有冗余。 可以再启动一个节点即可防止数据丢失。当在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的 cluster.name
配置,它就会自动发现集群并加入到其中。但是在不同机器上启动节点的时候,为了加入到同一集群,需要配置一个可连接到的单播主机列表。之所以配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。
文档的索引将首先被存储在主分片中,然后并发复制到对应的复制节点上。这可以确保数据在主节点和复制节点上都可以被检索。
如果启动了多个节点,所有主分片和副本分片都将被分配:
:表示所有6个分片(3个主分片,3个副本分片)都正常运行。
:三个主分片正常。
:3个副本分片和主分片会分配在三个节点上,且同一个主分片和它的副本分片不会同时分配给同一个节点。
主分片的数目在索引创建的时候就已经确定了。实际上,这个数目定义了这个索引能够存储的最大数据量(实际大小还取决于硬件和使用场景)。
但是,读操作——搜索和返回数据——可以同时被主分片或副本分片所处理,所以当拥有越多的副本分片时,也将拥有越高的吞吐量。在运行中的集群上是可以动态调整副本分片数目的,我们可以按需伸缩集群。
向ES服务器发送PUT
请求:http://localhost:9201/users/_settings,请求体如下:
{
"number_of_replicas": 2
}
users
索引现在拥有 9 个分片:3 个主分片和 6 个副本分片。 这意味着我们可以将集群扩容到 9 个节点,每个节点上一个分片。相比原来 3 个节点时,集群搜索性能可以提升 3 倍。
当然,如果只是在相同节点数目的集群上增加更多的副本分片并不能提高性能,因为每个分片从节点上获得的资源会变少。 需要增加更多的硬件资源来提升吞吐量。但是更多的副本分片数提高了数据冗余量:按照上面的节点配置,我们可以在失去 2 个节点的情况下不丢失任何数据。
将现在的ES集群,关闭一个节点。集群要做的第一件事情就是选举新的主节点:node-002
。关闭了一个节点后,该节点分配的主分片和副本也会缺失,但是其它两个节点存在着该节点主分片的副本,新的主节点会将node-001
和node-002
上对应的副本提升为主分片,这个过程是瞬间发生的。
为什么集群状态是yellow
而不是green
呢?
虽然节点node-003
不可用,但是集群还是拥有所有的3个主分片,不过集群同时设置了每个主分片需要对应 2 份副本分片,而此时只存在1份副本分片。 所以集群不能为 green
的状态。如果再关闭节点node-002
,程序依然可以保持在不丢任何数据的情况下运行,因为节点node-001
为每一个分片都保留着一份副本。如果重新启动节点node-003
,集群可以将缺失的副本分片再次进行分配,那么集群的状态也将恢复成之前的状态。 如果节点node-003
依然拥有着之前的分片,它将尝试去重用它们,同时仅从主分片复制发生了修改的数据文件。和之前的集群相比,只是Master
节点切换了。
当索引一个文档的时候,文档会被存储在一个主分片中,ES是根据路由计算的结果 决定文档应该存储在哪个分片上的。路由计算公式:
s h a r d = h a s h ( r o u t i n g ) % n u m b e r _ o f _ p r i m a r y _ s h a r d s shard = hash(routing) \% number\_of\_primary\_shards shard=hash(routing)%number_of_primary_shards
routing
是一个可变值,默认是文档的id
,也可以设置成一个自定义的值。这也是创建索引时就确定主分片的数量,后面不能更改主分片数量的原因,一旦后面主分片数量更改了,之前路由的值就会无效,文档无法正确获取了。
所有的文档API(get
,index
,delete
,bulk
)都接收一个routing
的路由参数,通过这个参数可以自定义文档到分片的映射。一个自定义的路由参数可以确保所有相关的文档都被存储再同一个分片中。
假设一个ES集群由三个节点组成,包含一个user
的索引,有2个主分片,每个主分片有两个副本。向ES服务器发送PUT
请求:http://localhost:9201/user,请求体如下:
{
"settings": {
"number_of_shards": 2, # 主分片为2
"number_of_replicas": 2 # 副本为2
}
}
通过elasticsearch-head
查看集群状态:
可以发送请求到集群中的任一节点, 每个节点都有能力处理任意请求。 每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。 在下面的例子中,将所有的请求发送到节点 node-001
,我们将其称为协调节点(coordinating node) 。
当发送请求的时候, 为了扩展负载,更好的做法是轮询集群中所有的节点。
写操作, 必须在主分片上面完成之后才能被复制到相关的副本分片。
步骤:
1、客户端请求ES集群节点(任意节点,该节点即为协调节点),此处假定为节点node-002
;
2、协调节点计算出请求属于分片0,请求会被转发到节点node-001
(分片0位于节点node-001
);
3、节点node-001
执行请求。如果成功,它将请求并行转发到节点node-002
和节点node-003
的副本上,一旦所有副本分片报告成功,节点node-001
将向协调节点返回响应,协调节点向客户端返回响应。
在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。有一些可选的请求参数允许影响这个过程,可能以数据安全为代价提升性能。这些选项很少使用,因为 Elasticsearch
已经很快,但是为了完整起见,请参考下面表格:
参数 | 含义 |
---|---|
consistency |
consistency ,即一致性。在默认设置下,即使仅仅是在试图执行一个写操作之前,主分片都会要求必须要有规定数量(quorum )(或者换种说法,也即必须要有大多数)的分片副本处于活跃可用状态,才会去执行写操作(其中分片副本可以是主分片或者副本分片)。这是为了避免在发生网络分区故障(network partition )的时候进行写操作,进而导致数据不一致。规定数量:int( (primary + number_of_replicas) / 2 ) + 1 。consistency 参数的值可以设为one (只要主分片状态 ok 就允许执行_写_操作),all (必须要主分片和所有副本分片的状态没问题才允许执行_写_操作),或quorum 。注意,规定数量的计算公式中 number_of_replicas 指的是在索引设置中的副本分片数,而不是指当前处理活动状态的副本分片数。如果创建索引时指定了当前索引拥有3个副本分片,那规定数量的计算结果即: (primary + 3 replicas) / 2 ) + 1 = 3 ,如果此时只启动两个节点,那么处于活跃状态的分片副本数量就达不到规定数量,也因此将无法索引和删除任何文档。 |
timeout |
如果没有足够的副本分片Elasticsearch 会等待,希望更多的分片出现。默认情况下,最多等待 1分钟。 如果需要,可以使用 timeout 参数使它更早终止。 |
replication |
复制默认的值是 sync ,这将导致主分片得到复制分片的成功响应后才返回。设置 replication 为 async (不建议使用),请求在主分片上被执行后就会返回给客户端。它依旧会转发请求给复制节点,但将不知道复制节点成功与否。默认的 sync 复制允许Elasticsearch 强制反馈传输。 async 复制可能会因为在不等待其它分片就绪的情况下发送过多的请求而使Elasticsearch 过载。 |
可以从主分片或者其它任意副本检索文档。
步骤:
1、客户端请求ES集群节点(任意节点,该节点即为协调节点),此处假定为节点node-002
;
2、协调节点计算出请求属于分片0,分片0的副本存在于三个节点上,这种情况下会将请求直接返回给客户端。
在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。
update
API 结合了之前提到的读和写的模式。
执行局部更新必要的顺序步骤:
1、客户端给 Node 1
发送更新请求;
2、它转发请求到主分片所在节点 Node 3
;
3、Node 3
从主分片检索出文档,修改 _source
字段的JSON,然后在主分片上重建索引。如果有其他进程修改了文档,它以 retry_on_conflict
设置的次数重复步骤3,都未成功则放弃;
4、如果 Node 3
成功更新文档,它同时转发文档的新版本到 Node 1
和 Node 2
上的复制节点以重建索引。当所有复制节点报告成功, Node 3
返回成功给请求节点,然后返回给客户端。
注意:当主分片转发更改给复制分片时,并不是转发更新请求,而是转发整个文档的新版本。因为这些修改转发到复制节点是异步的,它们并不能保证到达的顺序与发送相同。如果Elasticsearch
转发的仅仅是修改请求,修改的顺序可能是错误的,那得到的就是个损坏的文档。
分片是 Elasticsearch
最小的工作单元。传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值的能力。最好的支持一个字段多个值需求的数据结构是倒排索引。
Elasticsearch
使用一种称为倒排索引(inverted index
)的结构,它适用于快速的全文搜索。有倒排索引,肯定会对应有正向索引(forward index
),所谓的正向索引,就是搜索引擎会将待搜索的文件都对应一个文件 ID,搜索时将这个ID 和搜索关键字进行对应,形成 K-V 对,然后对关键字进行统计计数。
但是互联网上收录在搜索引擎中的文档的数目是个天文数字,这样的索引结构根本无法满足实时返回排名结果的要求。所以,搜索引擎会将正向索引重新构建为倒排索引,即把文件ID对应到关键词的映射转换为关键词到文件ID的映射,每个关键词都对应着一系列的文件,这些文件中都出现这个关键词。
一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。例如,假设有两个文档,每个文档的 content
域包含如下内容:
The quick brown fox jumped over the lazy dog
Quick brown foxes leap over lazy dogs in summer
为了创建倒排索引,首先将每个文档的 content
域拆分成单独的词(称为词条或 tokens
),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YR4bT846-1637995216351)(Elasticsearch/image-20210913230737835.png)]
现在,如果想搜索 quick
和brown
,只需要查找包含每个词条的文档:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-82Dkef36-1637995216352)(Elasticsearch/image-20210913230819837.png)]
两个文档都匹配,但是第一个文档比第二个匹配度更高。如果使用仅计算匹配词条数量的简单相似性算法,那么可以说,对于当前查询的相关性来讲,第一个文档比第二个文档更佳。但是,目前的倒排索引有一些问题:
1、Quick
和quick
以独立的词条出现,然而用户可能认为它们是相同的词;
2、fox
和 foxes
非常相似,就像 dog
和 dogs
,他们有相同的词根;
3、jumped
和leap
, 尽管没有相同的词根,但他们是同义词。
使用前面的索引搜索 +Quick
和 +fox
不会得到任何匹配文档。(+
前缀表明这个词必须存在)只有同时出现 Quick
和 fox
的文档才满足这个查询条件,但是第一个文档包含quick
、fox
,第二个文档包含 Quick
、 foxes
。用户可以合理的期望两个文档与查询匹配。如果将词条规范为标准模式,那么就可以找到与用户搜索的词条不完全一致,但具有足够相关性的文档。例如:
1、Quick
可以小写化为 quick
;
2、foxes
可以词干提取 ,变为词根的格式fox
。类似的, dogs
可以为提取为 dog
;
3、jumped
和 leap
是同义词,可以索引为相同的单词 jump
。
经过处理后的倒排索引看上去像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nRoonWrp-1637995216354)(Elasticsearch/image-20210913231947313.png)]
这还远远不够。我们搜索 +Quick
、 +fox
仍然会失败,因为在我们的索引中,已经没有 Quick
了。但是,如果我们对搜索的字符串使用与 content
域相同的标准化规则,会变成查询+quick
、+fox
,这样两个文档都会匹配!分词和标准化的过程称为分析。
只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式。
倒排索引的核心组成
倒排索引包含两个部分:
1、单词词典(Term Dictionary
):记录所有文档的单词和单词到倒排列表的关联关系;
2、倒排列表(Posting List
):记录了单词对应的文档结合,由倒排索引项组成。倒排索引项由以下部分组成:
a、文档id;
b、词频TF,该单词在文档中出现的次数,用于相关性评分;
c、位置(position
),单词在文档中分词的位置;
d、偏移(offset
),记录单词的开始结束位置。
早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 一旦新的索引就绪,旧的就会被其替换,这样最近的变化便可以被检索到。倒排索引被写入磁盘后是不可改变的:它永远不会修改。
不变性有重要的价值:
1、不需要锁。如果从来不更新索引,就不需要担心多进程同时修改数据的问题;
2、一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘;
3、其它缓存(像 filter
缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化;
4、写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。
当然,一个不变的索引也有不好的地方。主要是它是不可变的!不能修改它。如果需要让一个新的文档可被搜索,需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。
如何在保留不变性的前提下实现倒排索引的更新?
答案就是:用更多的索引。通过增加新的补充索引来反映最近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到,从最早的开始查询完后再对结果进行合并。Elasticsearch
基于 Lucene
, 这个 java 库引入了按段搜索的概念。 每一 段本身都是一个倒排索引, 但索引在 Lucene
中除表示所有段的集合外, 还增加了提交点的概念 —— 一个列出了所有已知段的文件:
按段搜索会按照以下流程执行:
1、新文档被收集到内存索引缓存;
2、缓存不定时被提交:
a、一个新的段被写入磁盘;
b、一个新的包含新段名字的提交点被写入磁盘;
c、磁盘进行同步,所有在文件系统缓存中等待的写入都刷新到磁盘。
3、新的段被开启,它包含的文档可见以被搜索;
4、内存缓存被清空,等待接收新的文档。
当一个查询被触发,所有已知的段按顺序被查询。词项统计会对所有段的结果进行聚合,以保证每个词和每个文档的关联都被准确计算。 这种方式可以用相对较低的成本将新文档添加到索引。
段是不可改变的,所以既不能把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每个提交点会包含一个.del
文件,文件中会列出这些被删除文档的段信息。
当一个文档被 “删除” 时,它实际上只是在.del
文件中被标记删除。一个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。
文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。
随着按段(per-segment
)搜索的发展,一个新的文档从索引到可被搜索的延迟显著降低了。新文档在几分钟之内即可被检索,但这样还是不够快。磁盘在这里成为了瓶颈。提交(Commiting
)一个新的段到磁盘需要一个 fsync
来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。 但是 fsync
操作代价很大;如果每次索引一个文档都去执行一次的话会造成很大的性能问题。
在 Elasticsearch
和磁盘之间是文件系统缓存。 在内存索引缓冲区中的文档会被写入到一个新的段中。 但是这里新段会被先写入到文件系统缓存 —— 这一步代价会比较低,稍后再被刷新到磁盘 —— 这一步代价比较高。不过只要文件已经在缓存中,就可以像其它文件一样被打开和读取了。
Lucene
允许新段被写入和打开 —— 使其包含的文档在未进行一次完整提交时便对搜索可见。这种方式比进行一次提交代价要小得多,并且在不影响性能的前提下可以被频繁地执行。
在 Elasticsearch
中,写入和打开一个新段的轻量的过程叫做 refresh
。 默认情况下每个分片会每秒自动刷新一次。这就是为什么Elasticsearch
是近 实时搜索。 文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。
这些行为可能会对新用户造成困惑:他们索引了一个文档然后尝试搜索它,但却没有搜到。这个问题的解决办法是用 refresh
API 执行一次手动刷新: /users/_refresh
尽管刷新是比提交轻量很多的操作,它还是会有性能开销。当写测试的时候, 手动刷新很有用,但是不要在生产环境下每次索引一个文档都去手动刷新。
并不是所有的情况都需要每秒刷新。 可以通过设置 refresh_interval
, 降低每个索引的刷新频率,向ES服务器发送PUT
请求:http://localhost:9200/users/_settings,请求体如下:
{
"settings": {
"refresh_interval": "30s"
}
}
// 另一种写法
{
"refresh_interval": "30s"
}
refresh_interval
可以在既存索引上进行动态更新。 在生产环境中,当正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来:
// 关闭自动刷新
PUT http://localhost:9200/users/_settings
{
"settings": {
"refresh_interval": -1
}
}
// 每一秒刷新
PUT http://localhost:9200/users/_settings
{
"refresh_interval": "1s"
}
如果没有用 fsync
把数据从文件系统缓存刷(flush
)到硬盘,不能保证数据在断电甚至是程序正常退出之后依然存在。为了保证 Elasticsearch
的可靠性,需要确保数据变化被持久化到磁盘。在动态更新索引,我们说一次完整的提交会将段刷到磁盘,并写入一个包含所有段列表的提交点。Elasticsearch
在启动或重新打开一个索引的过程中使用这个提交点来判断哪些段隶属于当前分片。
即使通过每秒刷新(refresh
)实现了近实时搜索,仍然需要经常进行完整提交来确保能从失败中恢复。但在两次提交之间发生变化的文档怎么办?Elasticsearch
增加了一个 translog
,或者叫事务日志,在每一次对 Elasticsearch
进行操作时均进行了日志记录。
整个流程如下:
1、一个文档被索引之后,就会被添加到内存缓冲区,并且追加到了 translog
;
2、刷新(refresh
)使分片每秒被刷新(refresh
)一次:
a、在内存缓冲区的文档被写入到一个新的段中,且没有进行 fsync
操作;
b、这个段被打开,使其可被搜索;
c、内存缓冲区被清空。
3、进程继续工作,更多的文档被添加到内存缓冲区和追加到事务日志;
4、每隔一段时间,例如 translog
变得越来越大;索引被刷新(flush
);一个新的 translog
被创建,并且一个全量提交被执行:
a、所有在内存缓冲区的文档都被写入一个新的段;
b、缓冲区被清空;
c、一个提交点被写入硬盘;
d、文件系统缓存通过fsync
被刷新(flush
);
e、老的 translog
被删除。
translog
提供所有还没有被刷到磁盘的操作的一个持久化纪录。当 Elasticsearch
启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog
中所有在最后一次提交后发生的变更操作。
translog
也被用来提供实时 CRUD 。当你试着通过 ID 查询、更新、删除一个文档,它会在尝试从相应的段中检索之前, 首先检查 translog
任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。
执行一个提交并且截断 translog
的行为在 Elasticsearch
被称作一次 flush
,分片每 30 分钟被自动刷新(flush
),或者在 translog
太大的时候也会刷新。
translog
的目的是保证操作不会丢失,在文件被 fsync
到磁盘前,被写入的文件在重启之后就会丢失。默认 translog
是每 5 秒被fsync
刷新到硬盘, 或者在每次写请求完成之后执行(e.g. index
, delete
,update
,bulk
)。这个过程在主分片和复制分片都会发生。这意味着在整个请求被 fsync
到主分片和复制分片的 translog
之前,客户端不会得到一个 200 OK 响应。
在每次请求后都执行一个 fsync
会带来一些性能损失,尽管实践表明这种损失相对较小(特别是 bulk
导入,它在一次请求中平摊了大量文档的开销)。但是对于一些大容量的偶尔丢失几秒数据问题也并不严重的集群,使用异步的 fsync
还是比较有益的。比如,写入的数据被缓存到内存中,再每5秒执行一次 fsync
。
由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 每一个段都会消耗文件句柄、内存和 cpu 运行周期。更重要的是,每个搜索请求都必须轮流检查每个段:所以段越多,搜索也就越慢。
Elasticsearch
通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。
进行索引和搜索时启动段合并会自动进行:
1、当索引的时候,刷新(refresh
)操作会创建新的段并将段打开以供搜索使用;
2、合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和搜索;
3、一旦合并结束,老的段被删除:
a、新的段被刷新(flush
)到了磁盘。 写入一个包含新段且排除旧的和较小的段的新提交点;
b、新的段被打开用来搜索;
c、老的段被删除。
合并大的段需要消耗大量的 I/O 和 CPU 资源,如果任其发展会影响搜索性能。Elasticsearch
在默认情况下会对合并流程进行资源限制,所以搜索仍然有足够的资源很好地执行。
搜索是如何在分布式环境中执行的。 它比之前讲的基础的增删改查请求要复杂一些。
一个CRUD操作只处理一个单独的文档。文档的唯一性由 _index
,_type
和 routing-value
(通常默认是该文档的_id
)的组合来确定。这意味着我们可以准确知道集群中的哪个分片持有这个文档。
由于不知道哪个文档会匹配查询(文档可能存放在集群中的任意分片上),所以搜索需要一个更复杂的模型。一个搜索不得不通过查询每一个索引的分片副本,来看是否含有任何匹配的文档。
但是,找到所有匹配的文档只完成了这件事的一半。在搜索( search
)API返回一页结果前,来自多个分片的结果必须被组合放到一个有序列表中。因此,搜索的执行过程分两个阶段,称为查询然后取回(query then fetch
)。
在初始化查询阶段,查询被向索引中的每个分片副本(原本或副本)广播。每个分片在本地执行搜索并且建立了匹配文档的优先队列,一个优先队列只是一个存有前n个(top-n)匹配文档的有序列表。这个优先队列的大小由分页参数from
和size
决定。
整个查询阶段分为三步:
1、客户端发送一个 search
(搜索) 请求给 Node 3
, Node 3
创建了一个长度为 from
+ size
的空优先级队列;
2、Node 3
转发这个搜索请求到索引中每个分片的原本或副本。每个分片在本地执行这个查询并且将结果(个轻量级的结果列表,只包含document
ID值和排序需要用到的值)放到一个大小为 from
+ size
的有序本地优先队列里去;
3、每个分片返回文档的ID
和它优先队列里的所有文档的排序值给协调节点Node 3
。 Node 3
把这些值合并到自己的优先队列里产生全局排序结果,这个就代表了最终的全局有序结果集,至此,查询阶段结束。
当一个搜索请求被发送到一个节点,这个节点就变成了协调节点。这个节点的工作是向所有相关的分片广播搜索请求并且把它们的响应整合成一个全局的有序结果集。这个结果集会被返回给客户端。
查询阶段辨别出那些满足搜索请求的文档,但仍然需要取回那些文档本身。这就是取回阶段的工作,分布式搜索的取回阶段如图所示:
整个取回阶段分为三步:
1、协调节点辨别出哪个document
需要取回,并且向相关分片发出 GET
请求;
2、每个分片加载document
并且根据需要丰富(enrich
)它们,然后再将document
返回协调节点;
3、一旦所有的document
都被取回,协调节点会将结果返回给客户端。
协调节点先决定哪些document
是实际(actually)需要取回的,然后为每个持有相关document
的分片建立多点get
请求然后发送请求到处理查询阶段的分片副本,最后分片加载document
主体—— _source
。如果需要,还会根据元数据丰富结果和高亮搜索片断。一旦协调节点收到所有结果,会将它们汇集到单一回答响应里,这个响应将会返回给客户端。
根据document
的数量,分片的数量以及所使用的硬件,对10,000到50,000条结果深分页是可行的。但是对于足够大的 from
值,排序过程将会变得非常繁重,会使用巨大量的CPU,内存和带宽。因此,强烈不建议使用深分页。
ES支持通过一些可选的查询参数影响搜索过程。
preference
preference
参数允许控制使用哪个分片或节点来处理搜索请求。她接受如下参数:
1、_primary
:只查询主分片,不管有多少个副本;
2、 _primary_first
:优先读取主分片,如果主分片无效或者失败,则会读取其他分片;
3、 _replica
:只查询副本;
4、_replica_first
:优先查询副本,副本无效,才会查询主分片;
5、_local
:尽可能在本地执行查询,不跨网络;
6、_prefer_nodes:abc,xyz
:在指定的节点id上执行查询;
7、_shards:2,3
:查询指定分片上的数据;
8、 _only_nodes:1
:限制在特定的node上执行操作 。
然而通常最有用的值是一些随机字符串,它们可以避免结果震荡问题。结果震荡:搜索请求在有效的分片副本之间轮询,可能出现在主分片中的顺序与副本中的顺序不一致的情况,每次搜素请求,结果的顺序都会发生变化。
search_type
虽然 query_then_fetch
是默认的搜索类型,但也可以根据特定目的指定其它的搜索类型。
1、count
:count
(计数) 搜索类型只有一个 query
(查询) 的阶段。当不需要搜索结果只需要知道满足查询的document
的数量时,可以使用这个查询类型;
2、query_and_fetch
:query_and_fetch
(查询并且取回) 搜索类型将查询和取回阶段合并成一个步骤。这是一个内部优化选项,当搜索请求的目标只是一个分片时可以使用,但是这么做基本上不会有什么效果;
3、dfs_query_then_fetch
和 dfs_query_and_fetch
:dfs
搜索类型有一个预查询的阶段,它会从全部相关的分片里取回项目频数来计算全局的项目频数;
4、scan
:scan
(扫描) 搜索类型是和 scroll
(滚屏) API连在一起使用的,可以高效地取回巨大数量的结果。它是通过禁用排序来实现的。
scan
(扫描) 搜索类型是和 scroll
(滚屏) API一起使用来从Elasticsearch
里高效地取回巨大数量的结果而不需要付出深分页的代价。
文本分析(analysis
)机制用于进行全文文本(Full Text
)的分词,以建立供搜索用的倒排索引。
文本分析包含下面的过程:
1、首先,表征化一个文本块为适用于倒排索引单独的词;
2、然后把标准化这些词为标准形式,提高它们的“可搜索性”或“查全率。
文本分析(analysis
)是通过分析器(Analyzer
)来实现的。分析器由三部分组成:
1、字符过滤器(Character Filters
):首先,字符串按顺序通过字符过滤器 ,它们的工作是在表征化前处理字符串。字符过滤器能够去除HTML标记,或者转换 “&” 为 “and” ;
2、分词器(Tokenizer
):字符串被分词器分为单个的词条,一个简单的分词器可以根据空格或逗号将单词分开;
3、Token 过滤器(Token Filters
):词条按顺序通过每个 token
过滤器 。这个过程可能会改变词条(例如,小写化Quick
),删除词条(例如, 像 a
, and
, the
等无用词),或者增加词条(例如,像 jump
和 leap
这种同义词)。
Elasticsearch
还附带了可以直接使用的预包装的分析器:
1、标准分析器(Standard Analyzer
): Elasticsearch
默认使用的分析器。它是分析各种语言文本最常用的选择。它根据 Unicode 联盟定义的单词边界划分文本。删除绝大部分标点,最后,将词条小写;
2、简单分析器(Simple Analyzer
):简单分析器在任何不是字母的地方分隔文本,将词条小写;
3、空格分析器(Whitespace Analyzer
):空格分析器在空格的地方划分文本;
4、关键词分析器(Keyword Analyzer
):不分词,直接将输入当作输出;
5、正则表达式分析器(Patter Analyzer
):默认非字符分割;
6、语言分析器(Language
):特定语言分析器可用于很多语言。它们可以考虑指定语言的特点。例如, 英语分析器附带了一组英语无用词(例如 and
或者 the
,它们对相关性没有多少影响),它们会被删除。 由于理解英语语法的规则,这个分词器可以提取英语单词的词干。
当索引一个文档,全文字段会被分析为单独的词来创建倒排索引。 但是,当在全文域搜索的时候,需要将查询字符串通过相同的分析过程 ,以保证搜索的词条格式与索引中的词条格式一致。
全文查询,理解每个域是如何定义的,因此它们可以做正确的事:
1、当查询一个全文域时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表;
2、当查询一个精确值域时,不会分析查询字符串,而是搜索指定的精确值。
可以使 用 analyze
API 来看文本是如何被分析的。向ES服务器发送GET
请求:http://localhost:9200/_analyze,请求体如下:
{
"analyzer": "standard", // 要使用的分词器类型为标准分析器
"text": "This is a example for use analyzer" // 要分析的文本
}
服务器返回结果:
{
"tokens": [
{
"token": "this",
"start_offset": 0,
"end_offset": 4,
"type": "" ,
"position": 0
},
{
"token": "is",
"start_offset": 5,
"end_offset": 7,
"type": "" ,
"position": 1
},
{
"token": "a",
"start_offset": 8,
"end_offset": 9,
"type": "" ,
"position": 2
},
{
"token": "example",
"start_offset": 10,
"end_offset": 17,
"type": "" ,
"position": 3
},
{
"token": "for",
"start_offset": 18,
"end_offset": 21,
"type": "" ,
"position": 4
},
{
"token": "use",
"start_offset": 22,
"end_offset": 25,
"type": "" ,
"position": 5
},
{
"token": "analyzer",
"start_offset": 26,
"end_offset": 34,
"type": "" ,
"position": 6
}
]
}
token
是实际存储到索引中的词条。 position
指明词条在原始文本中出现的位置。start_offset
和 end_offset
指明字符在原始字符串中的位置。
Elasticsearch
通过插件的形式支持指定分析器。常用的中文分词器有:HanLp
,IK
,pinyin
。此处演示IK
中文分词器对中文进行分析,下载地址为:https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.8.0,将解压后的后的文件夹放入 ES 根目录下的 plugins
目录下,重启 ES 即可使用。
IK
分词器支持两种级别的拆分:
1、ik_max_word
:将文本做最细粒度的拆分;
2、ik_smart
:将文本做最粗粒度的拆分。
向ES服务器发送GET
请求:http://localhost:9200/_analyze,使用ik_max_word
级别进行拆分,请求体如下:
{
"analyzer": "ik_max_word", # 指定ik分词器拆分级别
"text": "中国人" # 要分析的文本
}
服务器返回响应:
{
"tokens": [
{
"token": "中国人",
"start_offset": 0,
"end_offset": 3,
"type": "CN_WORD",
"position": 0
},
{
"token": "中国",
"start_offset": 0,
"end_offset": 2,
"type": "CN_WORD",
"position": 1
},
{
"token": "国人",
"start_offset": 1,
"end_offset": 3,
"type": "CN_WORD",
"position": 2
}
]
}
ES 中也可以进行扩展词汇,进入 ES 根目录中的 plugins
文件夹下的 ik 文件夹,进入 config
目录,创建 custom.dic
文件,写入要扩展的词汇,例如:弗雷尔卓德。同时打开 IKAnalyzer.cfg.xml
文件,将新建的custom.dic
配置其中,重启 ES 服务器,扩展词汇就会生效。
向ES服务器发送请求,分析自定义词汇,结果如下:
虽然 Elasticsearch
带有一些现成的分析器,然而在分析器上Elasticsearch
真正的强大之处在于,可以通过在一个特定数据的设置之中组合字符过滤器、分词器、词汇单元过滤器来创建自定义的分析器。
向ES服务器发送POST
请求:http://localhost:9200/my_custom,请求体如下:
{
"settings": {
"analysis": {
"char_filter": {
"&_to_and": {
"type": "mapping",
"mappings": ["&=> and "]
}
},
"filter": {
"my_stopwords": {
"type": "stop",
"stopwords": ["the", "a", "an"]
}
},
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": ["&_to_and"],
"tokenizer": "standard",
"filter": ["my_stopwords"]
}
}
}
}
}
向ES服务器发送GET
请求:http://localhost:9200/my_custom,查看自定义的分析器:
当我们使用 index API 更新文档 ,可以一次性读取原始文档,做我们的修改,然后重新索引整个文档 。 最近的索引请求将获胜:无论最后哪一个文档被索引,都将被唯一存储在 Elasticsearch
中。如果其他人同时更改这个文档,他们的更改将丢失。
很多时候这是没有问题的。也许我们的主数据存储是一个关系型数据库,我们只是将数据复制到 Elasticsearch
中并使其可被搜索。 也许两个人同时更改相同的文档的几率很小。或者对于我们的业务来说偶尔丢失更改并不是很严重的问题。
但有时丢失了一个变更就是非常严重的 。试想我们使用 Elasticsearch
存储我们网上商城商品库存的数量, 每次我们卖一个商品的时候,我们在 Elasticsearch
中将库存数量减少。有一天,要做一次促销。突然地,我们一秒要卖好几个商品。 假设有两个 web程序并行运行,每一个都同时处理所有商品的销售:
web_1 对 stock_count 所做的更改已经丢失,因为 web_2 不知道它的 stock_count 的拷贝已经过期。 结果就会出现超卖,变更越频繁,读数据和更新数据的间隙越长,也就越可能丢失变更。
在数据库领域中,有两种方法通常被用来确保并发更新时变更不会丢失:
1、悲观并发控制:这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。 一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改;
2、乐观并发控制:这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。
Elasticsearch
是分布式的。当文档创建、更新或删除时, 新版本的文档必须复制到集群中的其他节点。Elasticsearch
也是异步和并发的,这意味着这些复制请求被并行发送,并且到达目的地时也许乱序的 。 Elasticsearch
需要一种方法确保文档的旧版本不会覆盖新的版本。
每个文档都有一个 _version
(版本)号,当文档被修改时版本号递增。 Elasticsearch
使用这个 version
号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达,它可以被简单的忽略。
还可以利用 version
号来确保应用中相互冲突的变更不会导致数据丢失。通过指定想要修改文档的 version
号来达到这个目的。 如果该版本不是当前版本号,请求将会失败。
向ES服务器发送PUT
请求:http://localhost:9200/test_index_01/_doc/1?version=12,通过指定version
版本更新版本,但是新版本的ES不再支持该用法,会报错:
新版本的ES使用if_seq_no
和if_primary_term
代替version
,向ES服务器发送PUT
请求:http://localhost:9200/test_index_01/_doc/1?if_seq_no=21&if_primary_term=16,只要if_seq_no
和if_primary_term
与要更新的文档匹配,文档将会更新成功,否则不会进行更新。
一个常见的设置是使用其它数据库作为主要的数据存储,使用 Elasticsearch
做数据检索, 这意味着主数据库的所有更改发生时都需要被复制到 Elasticsearch
,如果多个进程负责这一数据同步,可能遇到更新丢失等并发问题。
如果主数据库已经有了版本号或一个能作为版本号的字段值比如 timestamp
, 那么就可以在 Elasticsearch
中通过增加 version_type=external
到查询字符串的方式重用这些相同的版本号, 版本号必须是大于零的整数, 且小于 9.2E+18。
外部版本号的处理方式和之前内部版本号的处理方式有些不同,Elasticsearch
不是检查当前 _version
和请求中指定的版本号是否相同, 而是检查当前
_version
是否小于指定的版本号。 如果请求成功,外部的版本号作为文档的新_version
进行存储。
向ES服务器发送PUT
请求:http://localhost:9200/test_index_01/_doc/1?version=12&version_type=external,只要外部版本号大于ES的版本号_version
,文档就会更新,且外部版本号将作为文档最新的_version
。
Spring Data for Elasticsearch 是 Spring Data 项目的一部分,该项目旨在为新数据存储提供熟悉且一致的基于 Spring 的编程模型,同时保留特定于存储的特性和功能。
Spring Data Elasticsearch 项目提供了与 Elasticsearch 搜索引擎的集成。Spring Data Elasticsearch 的关键功能领域是以 POJO 为中心的模型,用于与 Elastichsearch 文档交互并轻松编写 Repository 样式的数据访问层。
1、创建项目
创建maven项目,pom文件引入相关依赖:
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.6.RELEASEversion>
<relativePath/>
parent>
<groupId>com.jidi.testgroupId>
<artifactId>spring-data-elasticsearchartifactId>
<version>1.0.0-SNAPSHOTversion>
<name>spring-data-elasticsearchname>
<properties>
<java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-elasticsearchartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>RELEASEversion>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
<plugin>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-maven-pluginartifactId>
<version>1.3.2version>
<dependencies>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.23version>
dependency>
dependencies>
<configuration>
<configurationFile>${basedir}/src/main/resources/generatorConfig/generatorConfig.xmlconfigurationFile>
<overwrite>trueoverwrite>
<verbose>trueverbose>
configuration>
plugin>
plugins>
build>
project>
2、增加配置文件
application.yml
配置如下:
server:
port: 8081
# 日志相关配置
logging:
level:
com:
jidi:
test:
elasticsearch:
domain:
mapper: debug
level.root: INFO
# 日志配置文件
config: classpath:logback-spring.xml
file:
max-size: 256MB
name: ${user.home}/work/logs/spring-data-elasticsearch.log
spring:
profiles:
active: dev #配置环境为开发环境
datasource:
name: db
url: jdbc:mysql://localhost:3306/mlxg?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: 123456
hikari:
connection-timeout: 60000
validation-timeout: 3000
idle-timeout: 60000
login-timeout: 5
max-lifetime: 60000
maximum-pool-size: 10
minimum-idle: 10
read-only: false
# elasticsearch 相关配置
elasticsearch:
rest:
uris: localhost:9200
read-timeout: 30s
connection-timeout: 5s
# mybatis 相关配置
mybatis:
mapper-locations: classpath*:mapper/*Mapper.xml
type-aliases-package: com.jidi.test.elasticsearch.domain.model
configuration:
call-setters-on-nulls: true
# 开启驼峰映射
map-underscore-to-camel-case: true
日志文件logback-spring.xml
配置如下:
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread][%X{requestId}][%X{traceId}] %-5level %logger{36} - %msg%npattern>
encoder>
<file>${LOG_FILE}file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}fileNamePattern>
<maxHistory>7maxHistory>
rollingPolicy>
appender>
<springProfile name="dev,uat,docker">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
root>
springProfile>
<springProfile name="test, prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
root>
springProfile>
configuration>
mybatis-generator配置文件generatorConfig.xml
配置:
DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="MysqlTables" targetRuntime="MyBatis3">
<commentGenerator>
<property name="suppressDate" value="true"/>
<property name="suppressAllComments" value="true"/>
commentGenerator>
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mlxg?serverTimezone=GMT%2B8"
userId="root" password="123456">
jdbcConnection>
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
javaTypeResolver>
<javaModelGenerator targetPackage="com.jidi.test.elasticsearch.domain.model" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
javaModelGenerator>
<sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
<property name="enableSubPackages" value="true"/>
sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER" targetPackage="com.jidi.test.elasticsearch.domain.mapper" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
javaClientGenerator>
<table tableName="item" domainObjectName="Item" enableCountByExample="false" enableUpdateByExample="false"
enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false">
<property name="useActualColumnNames" value="false"/>
<generatedKey column="id" sqlStatement="Mysql" identity="true"/>
table>
context>
generatorConfiguration>
如果mybatis-generator-config_1_0.dtd
文件加载失败,可以直接在手动创建,文件内容如下:
整个项目配置文件结构如下:
3、使用mybatis-generator插件自动生成dao、mapper和model。
4、创建ES交互实体
package com.jidi.test.elasticsearch.domain.model;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import java.util.Date;
/**
* @Description
* @Author jidi
* @Email [email protected]
* @Date 2021/9/20
*/
@Data
@Document(indexName = "product", shards = 3, replicas = 1, createIndex = true)
public class Product {
@Id
private Long id;
@MultiField(mainField = @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word" ),
otherFields = @InnerField(type = FieldType.Keyword, suffix = "keyword"))
private String name;
private Long brandId;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String type;
private String measurementUnit;
private String purchaseCondition;
@MultiField(mainField = @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word" ),
otherFields = @InnerField(type = FieldType.Keyword, suffix = "keyword"))
private String tagsJson;
private Boolean check;
@MultiField(mainField = @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word" ),
otherFields = @InnerField(type = FieldType.Keyword, suffix = "keyword"))
private String desc;
private String photoJson;
private Long categoryId;
private Long companyId;
private Long enterpriseId;
private Date upShelfTime;
private Byte status;
private String refusedReason;
private String refusedAttach;
private String commonAttributesJson;
private String customAttributesJson;
private Date createdAt;
private Date updatedAt;
private String customSalesAttributeJson;
private String salesAttributeJson;
@Override
public String toString() {
return "Product{" +
"id=" + id +
", name='" + name + '\'' +
", brandId=" + brandId +
", type='" + type + '\'' +
", measurementUnit='" + measurementUnit + '\'' +
", purchaseCondition='" + purchaseCondition + '\'' +
", tagsJson='" + tagsJson + '\'' +
", check=" + check +
", desc='" + desc + '\'' +
", photoJson='" + photoJson + '\'' +
", categoryId=" + categoryId +
", companyId=" + companyId +
", enterpriseId=" + enterpriseId +
", upShelfTime=" + upShelfTime +
", status=" + status +
", refusedReason='" + refusedReason + '\'' +
", refusedAttach='" + refusedAttach + '\'' +
", commonAttributesJson='" + commonAttributesJson + '\'' +
", customAttributesJson='" + customAttributesJson + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
", customSalesAttributeJson='" + customSalesAttributeJson + '\'' +
", salesAttributeJson='" + salesAttributeJson + '\'' +
'}';
}
}
5、创建Repository交互对象
Spring Data 的强大之处,就在于你不用写任何DAO处理,自动根据方法名或类的信息进行CRUD操作。只要你定义一个接口,然后继承Repository提供的一些子接口,就能具备各种基本的CRUD功能。
package com.jidi.test.elasticsearch.domain.repository;
import com.jidi.test.elasticsearch.domain.model.Product;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
/**
* @Description
* @Author jidi
* @Email [email protected]
* @Date 2021/9/20
*/
@Repository
public interface ProductRepository extends ElasticsearchRepository<Product, Long> {
}
6、测试
package com.jidi.test.elasticsearch;
import com.jidi.test.elasticsearch.domain.mapper.ItemMapper;
import com.jidi.test.elasticsearch.domain.model.Item;
import com.jidi.test.elasticsearch.domain.model.Product;
import com.jidi.test.elasticsearch.domain.repository.ProductRepository;
import org.apache.ibatis.session.SqlSessionFactory;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.junit.jupiter.api.Test;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@SpringBootTest
class SpringDataElasticsearchApplicationTests {
@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;
@Autowired
private ProductRepository productRepository;
@Autowired
private ItemMapper itemMapper;
/**
* 创建索引
*/
@Test
void createIndex(){
boolean success = elasticsearchRestTemplate.createIndex(Product.class);
if(success){
System.out.println("创建索引成功!");
}
}
/**
* 删除索引
*/
@Test
void deleteIndex(){
boolean success = elasticsearchRestTemplate.deleteIndex(Product.class);
if(success){
System.out.println("索引删除成功!");
}
}
/**
* 新增
*/
@Test
void insert(){
Product product = new Product();
product.setId(1L);
product.setName("商品信息");
product.setType("商品类型");
product.setTagsJson("商品标签");
product.setDesc("商品描述信息");
productRepository.save(product);
}
/**
* 修改(存在修改,不存在新增)
*/
@Test
void update(){
Product product = new Product();
product.setId(1L);
product.setName("商品信息12");
product.setType("商品类型12");
product.setTagsJson("商品标签12");
product.setDesc("商品描述信息12");
productRepository.save(product);
}
/**
* 批量新增
*/
@Test
void batchInsert() {
List<Item> itemList = itemMapper.select();
List<Product> productList = new ArrayList<>(itemList.size());
itemList.forEach(item -> {
Product product = new Product();
product.setId(item.getId());
product.setName(item.getName());
product.setType(item.getType());
product.setTagsJson(item.getTagsJson());
product.setDesc(item.getDesc());
productList.add(product);
});
productRepository.saveAll(productList);
}
/**
* 删除数据
*/
@Test
void delete(){
productRepository.deleteById(1L);
}
/**
* 查询全部
*/
@Test
void selectAll(){
// 根据id降序排序
Iterable<Product> products = productRepository.findAll(Sort.by(Sort.Direction.DESC, "id"));
products.forEach(product -> System.out.println(product));
}
/**
* 根据id查询
*/
@Test
void selectById(){
Optional<Product> productOptional = productRepository.findById(1L);
System.out.println(productOptional.get());
}
/**
* 根据条件查询
*/
@Test
void selectByCondition(){
Pageable pageable = PageRequest.of(0, 100);
MatchQueryBuilder matchQueryBuilder = new MatchQueryBuilder("name", "小花");
Page<Product> productPage = productRepository.search(matchQueryBuilder, pageable);
productPage.forEach(System.out::println);
}
}
Elasticsearch
的基础是 Lucene
,所有的索引和文档数据是存储在本地的磁盘中,具体的路径可在 ES 的配置文件../config/elasticsearch.yml
中配置,如下:
Elasticsearch
重度使用磁盘,磁盘能处理的吞吐量越大,节点就越稳定。可以通过以下手段优化磁盘 I/O:
1、使用SSD(固态硬盘),相较于机械硬盘,固态硬盘的读写速度更快;
2、使用多块硬盘,并允许 Elasticsearch
通过多个 path.data
目录配置把数据条带化分配到它们上面;
合理设置分片数
分片和副本的设计为 ES 提供了支持分布式和故障转移的特性,但并不意味着分片和副本是可以无限分配的。而且索引的分片完成分配后由于索引的路由机制,是不能重新修改分片数的。
在Elasticsearch
中分片是有代价的:
1、一个分片的底层即为一个Lucene
索引,会消耗一定的文件句柄、内存以及CPU;
2、每一个搜索请求都需要命中索引的每一个分片,如果多个分片都需要在同一个节点上竞争使用,会影响性能;
3、用于计算相关度的词项统计信息是基于分片的,如果有许多分片,每个分片都只有很少的数据,响应的相关度也会较低;
4、需要考虑节点数量,如果分片数太多,大大超过了节点数,可能导致一个节点上存在多个分片,一旦该节点故障,有可能到质变数据丢失,集群无法恢复。
延迟分配分片
对于节点瞬时中断的问题,默认情况,集群会等待一分钟来查看节点是否会重新加入,如果这个节点在此期间重新加入,重新加入的节点会保持其现有的分片数据,不会触发新的分片分配。这样就可以减少 ES 在自动再平衡可用分片时所带来的极大开销。
通过修改参数dalayed_timeout
,可以延长再均衡的时间,可以全局设置也可以在索引级别进行设置。向ES服务器发送PUT
请求:http://localhost:9200/_all/_settings,请求体如下:
{
"settings": {
"index.unassigned.node_left.delayed_timeout": "3m"
}
}
ES是根据路由计算的结果 决定文档应该存储在哪个分片上的。路由计算公式:
s h a r d = h a s h ( r o u t i n g ) % n u m b e r _ o f _ p r i m a r y _ s h a r d s shard = hash(routing) \% number\_of\_primary\_shards shard=hash(routing)%number_of_primary_shards
routing
是一个可变值,默认是文档的id
,也可以设置成一个自定义的值。
根据查询是否带有routing
,可以分为两种:
1、不带routing
查询:查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为 2 个步骤:
a、分发:请求到达协调节点后,协调节点将查询请求分发到每个分片上;
b、聚合:协调节点搜集到每个分片上查询结果,在将查询的结果进行排序,之后给用户返回结果。
2、带routing
查询:查询的时候,可以直接根据 routing
信息定位到某个分配查询,不需要查询所有的分配,经过协调节点排序。
ES 的默认配置,是综合了数据可靠性、写入速度、搜索实时性等因素。实际使用时,可以进行偏向性的优化。
针对于搜索性能要求不高,但是对写入要求较高的场景,需要尽可能的选择恰当写优化策略:
1、加大 Translog Flush
;
2、增加索引Refresh
间隔,减少段合并的次数;
3、调整Bulk
线程池和队列
ES 提供了 Bulk
API 支持批量操作,当有大量的写任务时,可以使用 Bulk
来进行批量写入。
通用的策略如下:Bulk
默认设置批量提交的数据量不能超过 100M。数据条数一般是根据文档的大小和服务器性能而定的,但是单次批处理的数据大小应从 5MB~15MB 逐渐增加,当性能没有提升时,把这个数据量作为最大值。
Lucene
以段的形式存储数据。当有新的数据写入索引时,Lucene
就会自动创建一个新的段。随着数据量的变化,段的数量会越来越多,消耗的文件句柄数及 CPU 就越多,查询效率就会下降。
由于Lucene
段合并的计算量庞大,会消耗大量的 I/O,所以 ES 默认采用较保守的策略,让后台定期进行段合并
Lucene
在新增数据时,采用了延迟写入的策略,默认情况下索引的 refresh_interval
为1 秒。Lucene
将待写入的数据先写到内存中,超过 1 秒(默认)时就会触发一次 Refresh
,然后 Refresh
会把内存中的的数据刷新到操作系统的文件缓存系统中。
如果对搜索的实效性要求不高,可以将 Refresh
周期延长,例如 30 秒。这样可以有效地减少段刷新次数,但这同时意味着需要消耗更多的 Heap
内存。
Flush
的主要目的是把文件缓存系统中的段持久化到硬盘,当 Translog
的数据量达到512MB 或者 30 分钟时,会触发一次 Flush
。index.translog.flush_threshold_size
参数的默认值是 512MB。增加参数值意味着文件缓存系统中可能需要存储更多的数据,所以需要为操作系统的文件缓存系统留下足够的空间。
ES 为了保证集群的可用性,提供了 Replicas
(副本)支持,然而每个副本也会执行分析、索引及可能的合并过程,所以 Replicas
的数量会严重影响写索引的效率。当写索引时,需要把写入的数据都同步到副本节点,副本节点越多,写索引的效率就越慢。
ES 默认安装后设置的内存是 1GB。如果是通过解压安装的 ES,则在 ES 安装文件中包含一个jvm.option
文件,添加如下命令来设置 ES 的堆大小:Xms
表示堆的初始大小,Xmx
表示可分配的最大内存,都是 1GB。
确保 Xmx
和 Xms
的大小是相同的,其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源,可以减轻伸缩堆大小带来的压力。
ES 堆内存的分配需要满足以下两个原则:
1、不要超过物理内存的 50%:Lucene
的设计目的是把底层 OS 里的数据缓存到内存中。Lucene
的段是分别存储到单个文件中的,这些文件都是不会变化的,所以很利于缓存,同时操作系统也会把这些段文件缓存起来,以便更快的访问。如果设置的堆内存过大,Lucene
可用的内存将会减少,就会严重影响降低Lucene
的全文本查询性能。
2、堆内存的大小最好不要超过 32GB。
一个节点在默认情况下会同时扮演:eligaible master node
、coordinate node
、data node
和ingest node
。为了提高程序性能,一个节点只承当一个角色。
节点类型 | 配置 |
---|---|
master node |
node.master:true node.ingest:false node.data:false |
data node |
node.master:false node.ingest:false node.data:true |
ingest node |
node.master:false node.ingest:true node.data:false |
coordinate node |
node.master:false node.ingest:false node.data:false |
参数名 | 参数值 | 说明 |
---|---|---|
cluster.name |
elasticsearch |
es集群的名称,默认为elasticsearch ,es会自动发现在同一网段下的具有相同集群名的节点。 |
node.name |
node_001 |
节点名称,同一个集群节点名不能重复,节点名一旦设置,不能改变。 |
node.master |
true |
指定该节点是否有资格被选举为Master ,默认为true 。 |
node.data |
true |
指定该节点是否存储索引数据,默认为true ,数据的增、删、改和查都是在Data节点完成的。 |
index.number_of_shards |
1 | 索引分片个数。 |
index.number_of_replcas |
1 | 索引副本数。 |
transport.tcp.compress |
true |
在节点间数据传输时是否压缩,默认不压缩。 |
discovery.zen.minimum_master_nodes |
1 | 设置在选举Master节点时需要参与的最少候选主节点数,默认为1,当使用默认值,在网络不稳定时可能出现脑裂。 合理的数值为:候选主节点数 / 2 + 1 |
discovery.zen.ping.timeout |
3s | 设置在集群中自动发现其它节点时Ping连接的超时时间,默认为3秒。 |
https://blog.csdn.net/slj821/article/details/114878734
https://doc.codingdict.com/elasticsearch/125/
){
Pageable pageable = PageRequest.of(0, 100);
MatchQueryBuilder matchQueryBuilder = new MatchQueryBuilder(“name”, “小花”);
Page productPage = productRepository.search(matchQueryBuilder, pageable);
productPage.forEach(System.out::println);
}
}
# 4. Elasticsearch优化
## 4.1 硬件选择
`Elasticsearch` 的基础是 `Lucene`,所有的索引和文档数据是存储在本地的磁盘中,具体的路径可在 ES 的配置文件`../config/elasticsearch.yml` 中配置,如下:
[外链图片转存中...(img-cr4hgxRQ-1637995216381)]
`Elasticsearch` 重度使用磁盘,磁盘能处理的吞吐量越大,节点就越稳定。可以通过以下手段优化磁盘 I/O:
1、使用SSD(固态硬盘),相较于机械硬盘,固态硬盘的读写速度更快;
2、使用多块硬盘,并允许 `Elasticsearch` 通过多个 `path.data` 目录配置把数据条带化分配到它们上面;
## 4.2 分片策略
**合理设置分片数**
分片和副本的设计为 ES 提供了支持分布式和故障转移的特性,但并不意味着分片和副本是可以无限分配的。而且索引的分片完成分配后由于索引的路由机制,是不能重新修改分片数的。
在`Elasticsearch` 中分片是有代价的:
1、一个分片的底层即为一个`Lucene`索引,会消耗一定的文件句柄、内存以及CPU;
2、每一个搜索请求都需要命中索引的每一个分片,如果多个分片都需要在同一个节点上竞争使用,会影响性能;
3、用于计算相关度的词项统计信息是基于分片的,如果有许多分片,每个分片都只有很少的数据,响应的相关度也会较低;
4、需要考虑节点数量,如果分片数太多,大大超过了节点数,可能导致一个节点上存在多个分片,一旦该节点故障,有可能到质变数据丢失,集群无法恢复。
**延迟分配分片**
对于节点瞬时中断的问题,默认情况,集群会等待一分钟来查看节点是否会重新加入,如果这个节点在此期间重新加入,重新加入的节点会保持其现有的分片数据,不会触发新的分片分配。这样就可以减少 ES 在自动再平衡可用分片时所带来的极大开销。
通过修改参数`dalayed_timeout`,可以延长再均衡的时间,可以全局设置也可以在索引级别进行设置。向ES服务器发送`PUT`请求:http://localhost:9200/_all/_settings,请求体如下:
```json
{
"settings": {
"index.unassigned.node_left.delayed_timeout": "3m"
}
}
ES是根据路由计算的结果 决定文档应该存储在哪个分片上的。路由计算公式:
s h a r d = h a s h ( r o u t i n g ) % n u m b e r _ o f _ p r i m a r y _ s h a r d s shard = hash(routing) \% number\_of\_primary\_shards shard=hash(routing)%number_of_primary_shards
routing
是一个可变值,默认是文档的id
,也可以设置成一个自定义的值。
根据查询是否带有routing
,可以分为两种:
1、不带routing
查询:查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为 2 个步骤:
a、分发:请求到达协调节点后,协调节点将查询请求分发到每个分片上;
b、聚合:协调节点搜集到每个分片上查询结果,在将查询的结果进行排序,之后给用户返回结果。
2、带routing
查询:查询的时候,可以直接根据 routing
信息定位到某个分配查询,不需要查询所有的分配,经过协调节点排序。
ES 的默认配置,是综合了数据可靠性、写入速度、搜索实时性等因素。实际使用时,可以进行偏向性的优化。
针对于搜索性能要求不高,但是对写入要求较高的场景,需要尽可能的选择恰当写优化策略:
1、加大 Translog Flush
;
2、增加索引Refresh
间隔,减少段合并的次数;
3、调整Bulk
线程池和队列
ES 提供了 Bulk
API 支持批量操作,当有大量的写任务时,可以使用 Bulk
来进行批量写入。
通用的策略如下:Bulk
默认设置批量提交的数据量不能超过 100M。数据条数一般是根据文档的大小和服务器性能而定的,但是单次批处理的数据大小应从 5MB~15MB 逐渐增加,当性能没有提升时,把这个数据量作为最大值。
Lucene
以段的形式存储数据。当有新的数据写入索引时,Lucene
就会自动创建一个新的段。随着数据量的变化,段的数量会越来越多,消耗的文件句柄数及 CPU 就越多,查询效率就会下降。
由于Lucene
段合并的计算量庞大,会消耗大量的 I/O,所以 ES 默认采用较保守的策略,让后台定期进行段合并
Lucene
在新增数据时,采用了延迟写入的策略,默认情况下索引的 refresh_interval
为1 秒。Lucene
将待写入的数据先写到内存中,超过 1 秒(默认)时就会触发一次 Refresh
,然后 Refresh
会把内存中的的数据刷新到操作系统的文件缓存系统中。
如果对搜索的实效性要求不高,可以将 Refresh
周期延长,例如 30 秒。这样可以有效地减少段刷新次数,但这同时意味着需要消耗更多的 Heap
内存。
Flush
的主要目的是把文件缓存系统中的段持久化到硬盘,当 Translog
的数据量达到512MB 或者 30 分钟时,会触发一次 Flush
。index.translog.flush_threshold_size
参数的默认值是 512MB。增加参数值意味着文件缓存系统中可能需要存储更多的数据,所以需要为操作系统的文件缓存系统留下足够的空间。
ES 为了保证集群的可用性,提供了 Replicas
(副本)支持,然而每个副本也会执行分析、索引及可能的合并过程,所以 Replicas
的数量会严重影响写索引的效率。当写索引时,需要把写入的数据都同步到副本节点,副本节点越多,写索引的效率就越慢。
ES 默认安装后设置的内存是 1GB。如果是通过解压安装的 ES,则在 ES 安装文件中包含一个jvm.option
文件,添加如下命令来设置 ES 的堆大小:Xms
表示堆的初始大小,Xmx
表示可分配的最大内存,都是 1GB。
确保 Xmx
和 Xms
的大小是相同的,其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源,可以减轻伸缩堆大小带来的压力。
ES 堆内存的分配需要满足以下两个原则:
1、不要超过物理内存的 50%:Lucene
的设计目的是把底层 OS 里的数据缓存到内存中。Lucene
的段是分别存储到单个文件中的,这些文件都是不会变化的,所以很利于缓存,同时操作系统也会把这些段文件缓存起来,以便更快的访问。如果设置的堆内存过大,Lucene
可用的内存将会减少,就会严重影响降低Lucene
的全文本查询性能。
2、堆内存的大小最好不要超过 32GB。
一个节点在默认情况下会同时扮演:eligaible master node
、coordinate node
、data node
和ingest node
。为了提高程序性能,一个节点只承当一个角色。
节点类型 | 配置 |
---|---|
master node |
node.master:true node.ingest:false node.data:false |
data node |
node.master:false node.ingest:false node.data:true |
ingest node |
node.master:false node.ingest:true node.data:false |
coordinate node |
node.master:false node.ingest:false node.data:false |
参数名 | 参数值 | 说明 |
---|---|---|
cluster.name |
elasticsearch |
es集群的名称,默认为elasticsearch ,es会自动发现在同一网段下的具有相同集群名的节点。 |
node.name |
node_001 |
节点名称,同一个集群节点名不能重复,节点名一旦设置,不能改变。 |
node.master |
true |
指定该节点是否有资格被选举为Master ,默认为true 。 |
node.data |
true |
指定该节点是否存储索引数据,默认为true ,数据的增、删、改和查都是在Data节点完成的。 |
index.number_of_shards |
1 | 索引分片个数。 |
index.number_of_replcas |
1 | 索引副本数。 |
transport.tcp.compress |
true |
在节点间数据传输时是否压缩,默认不压缩。 |
discovery.zen.minimum_master_nodes |
1 | 设置在选举Master节点时需要参与的最少候选主节点数,默认为1,当使用默认值,在网络不稳定时可能出现脑裂。 合理的数值为:候选主节点数 / 2 + 1 |
discovery.zen.ping.timeout |
3s | 设置在集群中自动发现其它节点时Ping连接的超时时间,默认为3秒。 |
https://doc.codingdict.com/elasticsearch/125/