赞
踩
ES使用模糊查询加分词匹配的方式进行搜索,不同于传统Mysql%关键词%的模糊查询(查询结果中关键词需要相连),ES能够对关键词进行分词完成全文搜索。
(1)为用户提供按关键字查询的全文搜索功能。(JavaEE中使用较广泛)
(2)实现企业海量数据的处理分析的解决方案。大数据领域的重要一份子,
如著名的ELK 框架(ElasticSearch(存储分析),Logstash(采集),Kibana(可视化))。
(3)作为 OLAP (联机分析处理)数据库,对数据进行统计分析。
redis | mysql | elasticsearch | hbase | hadoop/hive | |
---|---|---|---|---|---|
容量/容量扩展 | 低 | 中 | 较大(理论上能够横向扩容,但全文搜索索引成本较高) | 海量 | 海量 |
查询时效性 | 极高(内存) | 较高( 索引优化) | 较高 | 较 高 ( rowkey方式)较低(scan方式) | 低 |
查询灵活性 | 最 差k-v 模式 | 非 常好,支持 sql | 较好,关联查询较弱,但是可以全文检索,DSL 语言可以处理过滤、匹配、排序、聚合 | 较差,主要靠 rowkey,scan 的话性能不行,或者安装 phoenix 插件来实现 sql 及二级索引等各种操作 | 非常好,支持 sql |
写入速度 | 极快 | 中等(同步写入) | 较快(异步写入) | 较快(异步写入) | 慢 |
一致性、事务 | 弱 | 强 | 弱 | 弱 | 弱 |
同步写入:数据写入磁盘才算一次写入操作。
异步写入:数据写入内存后就算是写入完成,后续自动完成磁盘写入。
ES 把数据分成多个 shard(分片),下图中的 P0-P2,多个 shard 可以组成一份完整的数据文档,这些 shard 可以分布在集群中的各个机器节点中。随着数据的不断增加,集群可以增加多个分片,把多个分片放到多个机子上,已达到负载均衡,横向扩展。(类似HDFS数据块的存储特点)
在 Elasticsearch 中,每个索引(index)(表)都会被分配 5 个主分片和 1 个副本分片。主分片和副本分片都是独立的 Lucene 索引,它们分别存储了相同索引的不同部分。主分片负责处理所有的写入请求,而副本分片则用来提高查询性能和集群的可用性。当主分片发生故障或不可用时,副本分片会自动接管主分片的工作,从而保证集群的高可用性。
ES 所有数据都是默认进行索引的,这点和 mysql 正好相反,mysql 是默认不加索引,要加索引必须特别说明,ES 只有不加索引才需要说明。而 ES 使用的是倒排索引和 Mysql 的 B+Tree 索引不同。
传统关系性数据库索引
(1)传统的保存数据的方式是 记录→单词
(2)弊端:
对于传统的关系性数据库对于关键词的查询,只能逐字逐行的匹配,性能非常差。匹配方式不合理,比如搜索“小密手机” ,如果用 like 进行匹配, 根本匹配不到。但是考虑使用者的用户体验的话,除了完全匹配的记录,还应该显示一部分近似匹配的记录,至少应该匹配到“手机”。
倒排索引
全文搜索引擎目前主流的索引技术就是倒排索引的方式。
(1)传统的保存数据的方式是 记录 → 单词
(2)倒排索引的保存数据的方式是 单词→记录
(3)基于分词技术构建倒排索引:
首先每个记录保存数据时,都不会直接存入数据库。系统先会对数据进行分词,然后以倒排索引结构保存。然后等到用户搜索的时候,会把搜索的关键词也进行分词,会把“红海行动”分词分成:红海和行动两个词。这样的话,先用红海进行匹配,得到 id 为 1 和 2 的记录编号,再用行动匹配可以迅速定位 id 为 1,3 的记录。那么全文索引通常,还会根据匹配程度进行打分,显然 1 号记录能匹配的次数更多。所以显示的时候以评分进行排序的话,1 号记录会排到最前面。而 2、3 号记录也可以匹配到。
索引结构对比
(1)Mysql底层索引结构
(2)lucene 倒排索引结构
可以看到 lucene 为倒排索引(Term Dictionary)部分又增加一层 Term Index 结构,用于快速定位,而这 Term Index 是缓存在内存中的,但 mysql 的 B+tree 不在内存中,所以整体来看 ES 速度更快,但同时也更消耗资源(内存、磁盘)。
ES使用TermIndex对关键词索引,TermDictionary存储关键词,Postinglist是关键词对应的文档
大致查找过程: 对输入的搜索值进行分词,通过TermIndex查找到关键词,拿到关键词对应的所有文档元数据,紧接着就能够从磁盘中查找出对应的文档内容。
倒排索引在搜索包含指定词条的文档时非常高效,但是在相反的操作时表现很差:查询一个文档中包含哪些词条。具体来说,倒排索引在搜索时最为高效,但在排序、聚合等与指定字段相关的操作时效率低下,需要用 doc_values。
在 Elasticsearch 中,Doc Values 就是一种列式存储结构,默认情况下每个字段的 DocValues 都是激活的。
列式存储结构非常适合排序、聚合以及字段相关的脚本操作。而且这种存储方式便于压缩,尤其是数字类型。压缩后能够大大减少磁盘空间,提升访问速度。
DSL 全称 Domain Specific language,即特定领域专用语言。
名词 | 解释 |
---|---|
cluster | 整个 elasticsearch 默认就是集群状态,整个集群是一份完整、互备的数据。 |
node | 集群中的一个节点,一般指一个进程就是一个 node |
shard | 分片,即使是一个节点中的数据也会通过 hash 算法,分成多个片存放,7.x 默认是 1 片,之前版本默认 5 片。 |
index | index 相当于 table |
type | 一种逻辑分区,7.x 版本已经废除,用固定的_doc 占位替代。 |
document | 类似于 rdbms 的 row、面向对象里的 object |
field | 相当于字段、属性 |
传统关系型数据库中存储以下数据,因为范式需求需要使用3张表,而ES中使用一个json表示一个document
public class Movie {
String id;
String name;
Double doubanScore;
List<Actor> actorList;
}
public class Actor{
String id;
String name;
}
ES存储格式
{
"id":"1",
"name":"operation red sea",
"doubanScore":"8.5",
"actorList":[
{"id":"1","name":"zhangyi"},
{"id":"2","name":"haiqing"},
{"id":"3","name":"zhanghanyu"}]
}
ES中一个索引相当于一张表,创建索引允许不指定字段,当数据到来的时会根据第一条数据自动推断
ES能够对写入的JSON数据中的字段进行类型推断,字符串有两种格式,text代表倒排索引,keyword代表列式存储
命令 | 解释 |
---|---|
GET _cat/indices?v | 查看所有索引 |
PUT movie_index | 创建索引(建表) |
DELETE movie_index | 删除索引 |
PUT /movie_index/_doc/1 {JSON数据} | 幂等写入(需要指定docid) |
POST /movie_index/_doc {JSON数据} | 非幂等写入(ES自动生成唯一的docid) |
GET movie_index/_mapping | 查看索引mapping(desc table) |
DELETE /movie_index/_doc docid | 删除一个doc数据 |
删除指定字段,其余数据保持原样
post /movie_indx/_update/1
{
"doc":{
"name":operation red sea
}
}
幂等写入:幂等写入(PUT/movie_indx/_doc/docid)需要指定docid,该id能够唯一标识一份doc,当具有相同docid的数据再次写入时,会对之前的数据进行覆盖。
非幂等写入:非幂等写入不需要指定docid,ES能够为数据自动生成唯一的docid,因此能写入多次相同的数据。
(1) 向movie_index导入三条doc
PUT /movie_index/_doc/1 { "id":1, "name":"operation red sea", "doubanScore":8.5, "actorList":[ {"id":1,"name":"zhang yi"}, {"id":2,"name":"hai qing"}, {"id":3,"name":"zhang han yu"}] } PUT /movie_index/_doc/2 { "id":2, "name":"operation meigong river", "doubanScore":8.0, "actorList":[ {"id":3,"name":"zhang han yu"}] } PUT /movie_index/_doc/3 { "id":3, "name":"incident red sea", "doubanScore":5.0, "actorList":[ {"id":4,"name":"atguigu"}] }
(2) 使用分词查询
GET movie_index/_search { "query": { "match": { "name": "operation rea sea" } } } ========================查询结果:============================ { "took" : 30, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 3, "relation" : "eq" }, "max_score" : 1.0498221, "hits" : [ { "_index" : "movie_index", "_type" : "_doc", "_id" : "1", "_score" : 1.0498221, "_source" : { "id" : 1, "name" : "operation red sea", "doubanScore" : 8.5, "actorList" : [ { "id" : 1, "name" : "zhang yi" }, { "id" : 2, "name" : "hai qing" }, { "id" : 3, "name" : "zhang han yu" } ] } }, { "_index" : "movie_index", "_type" : "_doc", "_id" : "2", "_score" : 0.6931471, "_source" : { "id" : 2, "name" : "operation meigong river", "doubanScore" : 8.0, "actorList" : [ { "id" : 3, "name" : "zhang han yu" } ] } }, { "_index" : "movie_index", "_type" : "_doc", "_id" : "3", "_score" : 0.35667494, "_source" : { "id" : 3, "name" : "incident red sea", "doubanScore" : 5.0, "actorList" : [ { "id" : 4, "name" : "atguigu" } ] } } ] } }
倒排索引:
operation 1 2
red 13
sea 1 3
meigong 2
river 2
incident 3
搜索关键词operation red sea
搜索出来的数据顺序是经过打分和加权的:
根据匹配的程度,会影响数据的相关度评分,而相关度评分会影响默认排名。
正向因素 :命中次数、 命中长度比例。
负面因素: 关键词在该字段的其他词条中出现的次数。
GET movie_index/_search
{
"query": {
"match": {
"actorList.name": "zhang yi"
}
}
}
}
该种查询会因为字符串默认使用了倒排索引,而查询出多条数据,会把actorList.name中带有zhang或yi的数据一并查询出来,需要将actorList.name改为actorList.name.keyword使用列式存储的方式进行查询。
因此一些字段在存储时需要指定使用列式存储格式。
将"match"改为短语查询(“match_phrase”)也能够准确匹配出想要的数据
es 在存储字符串时,都会保留两种方式存储: 一种是倒排索引方式(text 类型),用于分词匹配。 一种是标准列式存储(keyword类型),用于过滤 ,分组,聚合,排序…. ,需要加 keyword。
1)条件过滤 name=”operation red sea” (值等判断)
GET movie_index/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"name.keyword": "operation red sea" //使用keyword列式存储整词匹配
}
}
]
}
}
}
2)分词匹配”red sea” , 条件过滤 actorList.name=”zhang han yu”
GET movie_index/_search { "query": { //使用must的结果为1号文档,使用should的结果为1,2号文档 "bool": { "must(should)": [ { "match": {//分词匹配:假设结果为1,3号文档 "name": "red sea" } } ], "filter": [ { "term": { //条件过滤:假设结果为 1,2号文档 "actorList.name.keyword": "zhang han yu" } } ] } } }
filter:过滤结果为1,2号文档
match:分词匹配结果为1,3号文档
should:使用should会展示filter过滤出的所有文档,但是只有match和filter的交集才会被打分
must:使用must只会会展示filter和match的交集
3) 过滤 - 范围过滤
GET movie_index/_search
{
"query": {
"range": {
"FIELD": { //FIELD 指定字段名
"gte": 10, //great then 大于等于
"lte": 20 //less then 小于等于
}
}
}
}
POST movie_index/_update_by_query { "query": { //使用等值匹配查询出文档 "term": { "actorList.name.keyword": { "value": "zhang han yu" } } }, "script": {//将结果文档中的[actorList]数组中的第一个元素的name修改 "source": "ctx._source['actorList'][0]['name']=params.newname", "params": { "newname":"aimyon" }, "lang": "painless" } }
过滤查询中如果查询结果中要修改的属性中包含多个字段需要使用下标指定对应的字段,当结果属性只有一个字段的时候可以省略下标。
POST movie_index/_delete_by_query
{
//将对应查询结果的文档删除
"query": {
"bool": {
"filter": [
{
"term": {
"actorList.name.keyword":"zhy"
}
}
]
}
}
}
GET movie_index/_search { "query": { "bool": { "filter": [ { "term": { "actorList.name.keyword": "aimyon" } } ] } }, //将查询结果按照doubanScore进行升序排序 "sort": [ { "doubanScore": { "order": "asc" } } ] }
GET movie_index/_search { "query": { "match": { "name": "red sea" } }, "highlight": { "fields": { "name": {} } } } ===============结果================== "highlight" : { "name" : [ "operation <em>red</em> sea" ] }
使用高亮后查询结果会新增highlight属性,< em >为前端标签,前端可以根据em标签给数据添加样式。
GET movie_index/_search { "aggs": { "groupbyactorname":{ //自定义字段名 "terms": { // group by name 根据名字分组 "field": "actorList.name.keyword", "size": 10 ,"order":{//排序 "doubanscoreavf":"desc" } } }, "aggs":{ //组内求平均分 "doubanscoreavg":{ "avg":{ "field":"doubanScore" } } } },"size":0 //查询结果默认显示20条明细数据 }
ElasticSearch SQL 是 6.3 版本以后的功能,能够支持一些最基本的 SQL 查询语句。
目前的一些情况:
只支持 select 操作 ,insert, update, delete 一律不支持;6.3 以前的版本无法支持;
SQL 比 DSL 有丰富的函数;不支持窗口函数;SQL 少一些特殊功能,比如高亮。
GET _sql?format=txt //format=使用dataframe展示 { "query": """ SELECT actorList.name.keyword, avg(doubanScore) ads FROM movie_index where match(name,'red') //虽然ES只支持了select,但提供了分词匹配 group by actorList.name.keyword order by ads desc limit 10 """ }
ES 不允许对索引结构进行修改,如果业务发生变化,字段类型需要进行修改,ES只能够再次创建一个新的索引对新的业务数据进行保留,这样既能够实现负载均衡提高查询效率,还能应对业务数据字段的变更。
因此分割索引是企业中常用的一种应对策略.实际就是根据时间间隔把一个业务索引切分成多个索引(类似Hive中的分区表)。
例如将某个业务存储数据使用到的索引,设计成以小时、天、周等分割后的多个索引。这样,每次分割都可以应对一次字段的变更。
原索引 | 分割索引 |
---|---|
order_info | order_info_20210101 |
order_info_20210102 | |
order_info_20210103 |
优点:
1)查询范围优化
因为一般情况并不会查询全部时间周期的数据,那么通过切分索引,物理上减少了扫描数据的范围,也是对性能的优化。
2)结构变化的灵活性
因为 elasticsearch 不允许对数据结构进行修改。但是实际使用中索引的结构和配置难免变化,那么只要对下一个间隔的索引进行修改,原来的索引位置原状。这样就有了一定的灵活性。
索引别名就像一个快捷方式或软连接,可以指向一个或多个索引,也可以给任何一个需要索引名的 API 来使用。别名 带给我们极大的灵活性,允许我们做下面这些:
1)给多个索引分组
分割索引可以解决数据结构变更的场景, 但是分割的频繁,如果想要统计一个大周期的数据(例如季度、年),数据是分散到不同的索引中的,统计比较麻烦。我们可以将分割的索引取相同的别名,这样,我们在统计时直接指定别名即可。
2)给索引的一个子集创建视图
将一个索引中的部分数据(基于某个条件)创建别名,查询此部分数据时,可以直接使用别名
3)在运行的集群中可以无缝的从一个索引切换到另一个索引
如果涉及到索引的切换,我们可以在程序中指定别名,而不是索引。当需要切换索引时,我们可以直接从一个索引上减去别名,在另一个索引上加上别名(减与加为原子操作)实现无缝切换。
给已有的索引添加别名
POST _aliases
{
"actions": [
{ "add": { "index": "movie_index", "alias":
"movie_index_2021" }}
]
}
POST _aliases
{
"actions": [
{ "add": { "index": "movie_index_cn", "alias":
"movie_index_2021" }}
]
}
创建索引的时候创建别名
PUT movie_index_1 { "aliases": { "movie_index_1_20210101": {} }, "mappings": { "properties": { "id":{ "type": "long" }, "name":{ "type": "text" , "analyzer": "ik_smart" }, "doubanScore":{ "type": "double" }, "actorList":{ "properties": { "id":{ "type":"long" }, "name":{ "type":"keyword" } } } } } }
通过加过滤条件缩小查询范围,建立一个子集视图
POST _aliases { "actions": [ { "add": { "index": "movie_index", "alias": "别名", "filter": { "range": { "字段": { "gte": 10, "lte": 20 } } } } } ] }
删除某个索引的别名
POST _aliases
{
"actions": [
{ "remove": {
"index": "movie_index",
"alias": "movie_index_2021" }}
]
}
为某个别名进行无缝切换(原子操作,要么都成功,要么都失败)
POST /_aliases
{
"actions": [
{ "remove": { //移除现有的
"index": "movie_index",
"alias": "movie_index_2021" }},
{ "add": { //添加新的别名
"index": "movie_index_cn",
"alias": "movie_index_2021" }}
]
}
Index Template 索引模板,顾名思义,就是创建索引的模具,其中可以定义一系列规则来帮助我们构建符合特定业务需求的索引的 mappings 和 settings,通过使用 IndexTemplate 可以让我们的索引具备可预知的一致性。
PUT _template/template_movie2023 { "index_patterns": ["movie_test*"], //如果索引的名字能匹配movie_test*就会使用这个模板 "settings": { "number_of_shards": 1 //指定副本 }, "aliases": { "{index}-query": {}, //使用这个模板创建的索引都会使用 索引名-query 作为名字 "movie_test-query":{} //使用该模板创建的别名都会有这个别名 }, "mappings": { //数据类型 "id":{ "type":"keyword" }, "movie_name":{ "type":"text", "analyzer":"ik_smart" } } }
一个索引(Index)(表)由一个或多个分片(Shard)组成,每个分片都是一个独立的Lucene索引。当一个文档被索引时,它会被分配到一个或多个分片中,Elasticsearch会自动处理文档的切分和分配。
一个分片还可以有多个副本(Replica),每个副本都是一个分片的完整拷贝。副本的作用是提高索引的可用性和容错能力,当一个分片不可用时,副本可以接管它的工作。副本的数量可以在创建索引时进行配置,默认为1个。
一个索引的组成包括:
分片(Shard):独立的Lucene索引,用于存储文档数据和倒排索引;
副本(Replica):分片的完整拷贝,用于提高索引的可用性和容错能力;
元数据(Metadata):索引的名称、设置、映射等元信息。
(1)写操作必须在主分片上面完成之后才能被复制到相关的副本分片。
(2)客户端向 Node 1 发送写操作请求,此时 Node1 为协调节点(接收客户端请求的节点)。
(3)Node1 节点使用文档的_id 通过公式计算确定文档属于分片 0 。请求会被转发到Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。
shard = hash(routing) % number_of_primary_shards
routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards(主分片的数量)后得到余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。
(4)Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1 和Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。
(5)那么如果 shard 的数量变化,是不是数据就要重新 rehash 呢?不会,因为一个 index 的 shards 数量是不能改变的。
(1)读操作可以从主分片或者从其它任意副本分片检索文档
(2)客户端向 Node 1 发送读请求,Node1 为协调节点
(3)节点使用文档的 _id 来确定文档属于分片 0 。分片 0 的主副分片存在于所有的三个节点上。协调节点在每次请求的时候都会通过轮询的方式将请求打到不同的节点上来达到负载均衡,假设本次它将请求转发到 Node 2 。
(4)Node 2 将文档返回给 Node 1 ,Node1 然后将文档返回给客户端。
(1)搜索被执行成一个两阶段过程,我们称之为 Query Then Fetch
(2)在初始查询阶段时,查询会广播到索引中每一个分片(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。(该队列只存储文档元数据(id))
PS:在搜索的时候是会查询 Filesystem Cache 的,但是有部分数据还在 Memory Buffer,所以搜索是近实时的。
(3)每个分片返回各自优先队列中 所有文档的 ID 和排序值 给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。
(4)接下来就是取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。每个分片加载并丰富文档,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。
ElasticSearch 中的全部文档数据都是不可变的,数据不能修改,只能通过版本号的方式不断增加。这样做的主要目的是解决更新过程中的并发冲突问题。
1)悲观并发控制
这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。 一个典型的例子是读取一行数据之前先将其锁住,确保只有获取锁的线程才能够对这行数据进行修改。
2)乐观并发控制
Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。
乐观锁使用版本信息对并发操作进行控制,当一个操作读取数据时,需要连同版本信息一并读取,在对数据提交更改操作时,需要提前确认当前数据的版本和读取到的版本信息一致,才能够进行更改操作的提交。
如果进行删除文档操作,也不会直接物理删除,而是通过给文档打删除标记,进行逻辑删除,至到该索引触发段合并时,才物理删除,释放存储空间。
由于索引一般是以天为单位进行建立,如果业务线很多,每个索引又不注意控制分片,日积月累下来一个集群几万到几十万个分片也是不难见到的。
shard 太多带来的危害
每个分片都有 Lucene 索引,这些索引都会消耗 cpu和内存。同样的数据,分片越多,额外消耗的 cpu 和内存就越多,会出现“1+1”>2 的情况。
shard 的目的是为了负载均衡让每个节点的硬件充分发挥,但是如果分片多,在单个节点上的多个 shard 同时接受请求,并对本节点的资源形成了竞争,实际上反而造成了内耗。
shard 优化
(1)及时归档冷数据(不需要使用的数据)将数据放入存储成本低的组件中。
(2)降低单个分片占用的资源消耗,具体方法就是:合并分片中多个 segment(段)。
(1)首先由请求端提交到 buffer 中,此时数据不可写。同时写入 translog(内存中),然后translog 根据默认设置同步磁盘文件。此时会返回给请求端处理成功。
(2)1-2 秒后执行 refresh 操作把 buffer 中的数据写入整理成为一个段,并提交到可读缓存。
(3)可读缓存会存在相当一段时间直到达到 flush 条件写入磁盘(flush 条件:默认 30分钟或者translog 达到默认上限 512M )。
(4)落盘到磁盘文件后多个段以文件形式保存,后台周期性或手动进行段合并,把段中的数据合并到 shard 文件中。
段越多,数据量不一定越多,但是占用的内存会增多。
segment (段)上的数据有着独立的 Lucene 索引。日积月累,如果一个 shard 是由成千上万的segment 组成,那么性能一定非常不好,而且消耗内存也大。
Segment(段)优化
由于 es 的异步写入机制,后台每一次把写入到内存的数据 refresh(默认每秒)到磁盘,都会在对应的 shard 上产生一个 segment。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。