ElasticSearch知识点总结

ElasticSearch简介

ElasticSearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful web 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可 条款下的开放源码发布,是当前流行的企业级搜索引擎。

ElasticSearch 的使用场景

(1)为用户提供按关键字查询的全文搜索功能。

(2)实现企业海量数据的处理分析的解决方案。大数据领域的重要一份子,如著名的 ELK 框架(ElasticSearch,Logstash,Kibana)。

(3)作为 OLAP 数据库,对数据进行统计分析。

Elasticsearch 的特点

天然的分布式数据库

ES 把数据分成多个 shard(分片),下图中的 P0-P2,多个 shard 可以组成一份完整的数 据,这些 shard 可以分布在集群中的各个机器节点中。随着数据的不断增加,集群可以增加 多个分片,把多个分片放到多个机子上,已达到负载均衡,横向扩展。

天然索引之倒排索引

ES 所有数据都是默认进行索引的,这点和 mysql 正好相反,mysql 是默认不加索引, 要加索引必须特别说明,ES 只有不加索引才需要说明。

传统的保存数据的方式是 记录 → 单词

倒排索引的保存数据的方式是 单词→记录

每个记录保存数据时,都不会直接存入数据库。系统先会对数据进行分词,然后以 倒排索引结构保存。然后等到用户搜索的时候,会把搜索的关键词也进行分词,会把“红海 行动”分词分成:红海和行动两个词。这样的话,先用红海进行匹配,得到 id 为 1 和 2 的 记录编号,再用行动匹配可以迅速定位 id 为 1,3 的记录。

通常,还会根据匹配程度进行打分,显然 1 号记录能匹配的次数更多。所 以显示的时候以评分进行排序的话,1 号记录会排到最前面。而 2、3 号记录也可以匹配到。

天然索引之正排索引( Doc Value 列式存储)

倒排索引在搜索包含指定词条的文档时非常高效,但是在相反的操作时表现很差:查询 一个文档中包含哪些词条。具体来说,倒排索引在搜索时最为高效,但在排序、聚合等与指 定字段相关的操作时效率低下,需要用 doc_values。

在 Elasticsearch 中,Doc Values 就是一种列式存储结构,默认情况下每个字段的 Doc Values 都是激活的。

列式存储结构非常适合排序、聚合以及字段相关的脚本操作。而且这种存储方式便于压缩,尤其是数字类型。压缩后能够大大减少磁盘空间,提升访问速度。

elasticsearch DSL

名词解释

cluster 整个 elasticsearch 默认就是集群状态,整个集群是一份完整、互备 的数据。

node 集群中的一个节点,一般指一个进程就是一个 node

shard 分片,即使是一个节点中的数据也会通过 hash 算法,分成多个片 存放,7.x 默认是 1 片,之前版本默认 5 片。

index index 相当于 table

type 一种逻辑分区,7.x 版本已经废除,用固定的_doc 占位替代。

document 类似于 rdbms 的 row、面向对象里的 object field 相当于字段、属性

服务状态查询

服务整体状态查询:GET /_cat/health?v

查询各个节点状态:GET /_cat/nodes?v

查询各个索引状态:GET _cat/indices?v

查询某个索引的分片情况:GET /_cat/shards/xxxx

显示各个节点分片情况:GET _cat/allocation?v

显示索引文档总数:GET _cat/count?v

参数说明

v 显示表头

help 显示命令返回的参数说明

h 指定要显示的列, GET _cat/count?h=epoch

format 设 置 要 返 回 的 内 容 格 式 json,yml,txt 等 , GET _cat/count?format=json

s 指定排序,GET _cat/indices?v&s=docs.count:desc

& 拼接多个参数,GET _cat/count?v&h=epoch

对数据的操作

es中保存的数据结构

在 elasticsearch 是用一个 json 来表示一个 document

{
  "id":"1",
  "name":"operation red sea",
  "doubanScore":"8.5",
  "actorList":[ 
    {"id":"1","name":"zhangyi"},
    {"id":"2","name":"haiqing"},
    {"id":"3","name":"zhanghanyu"}
  ] 
}

索引

创建索引

创建索引不指定字段,按照第一条数据自动推断

PUT /movie_index

查看索引结构

GET /movie_index/_mapping

删除索引

DELETE /movie_index

新增、修改、删除数据

新增文档(幂等)

指定doc_id

如果 index 不存在, 在新增文档时会自动创建 index。

PUT /movie_index/_doc/1
{
  "id":"1",
  "name":"operation red sea",
  "doubanScore":"8.5",
  "actorList":[ 
    {"id":"1","name":"zhangyi"},
    {"id":"2","name":"haiqing"},
    {"id":"3","name":"zhanghanyu"}
  ] 
}

新增文档 (非幂等)

重复执行会新增重复数据,doc_id 会随机生成。

POST /movie_index/_doc
{
  "id":"1",
  "name":"operation red sea",
  "doubanScore":"8.5",
  "actorList":[ 
    {"id":"1","name":"zhangyi"},
    {"id":"2","name":"haiqing"},
    {"id":"3","name":"zhanghanyu"}
  ] 
}

修改文档

整体替换,和新增没有区别

PUT /movie_index/_doc/1
{
  "id":"1",
  "name":"operation red sea",
  "doubanScore":"8.5",
  "actorList":[ 
    {"id":"1","name":"zhangyi"},
    {"id":"2","name":"haiqing"},
    {"id":"3","name":"zhanghanyu"}
  ] 
}

仅修改指定字段

POST movie_index/_update/3
{ 
  "doc": {
    "doubanScore":7.0
  } 
}

删除文档

DELETE movie_index/_doc/3

查询数据

查询一个 document

GET movie_index/_doc/3

按条件查询(全部)

GET movie_index/_search
{
  "query": {
    "match_all": {}
  }
}

按分词查询

根据匹配的程度,会影响数据的相关度评分,而相关度评分会影响默认排名。

正向因素 : 命中次数、 命中长度比例。

负面因素: 关键词在该字段的其他词条中出现的次数。

GET movie_index/_search
{
    "query": {
        "match": {
            "name": "operation red sea"
        }
    }
}    

按分词子属性查询

倒排索引查询

GET movie_index/_search
{
    "query": {
        "match": {
            "actorList.name": "zhang yi"
        }
    }
}  

列式存储查询

GET movie_index/_search
{
    "query": {
        "match": {
            "actorList.name.keyword": "zhang yi"
        }
    }
}  

按 match_phrase(短语)查询

按短语查询,不再利用分词技术,直接用短语在原始数据中匹配。

GET movie_index/_search
{
    "query": {
        "match_phrase": {
            "name": "operation red"
        }
    }
}  

过滤数据

过滤 - 值等判断

es 在存储字符串时,都会保留两种方式存储:

一种是倒排索引方式(text 类型),用于分词匹配。

一种是标准列式存储(keyword 类型),用于过滤 ,分组,聚合,排序…. ,需要加 keyword。

GET movie_index/_search
{
    "query": {
        "bool": {
            "filter": [
                {   //值等判断
                    "term":{"actorList.name.keyword": "zhang han yu" }
                }
            ]
        }
    }
}     

过滤 - 范围过滤

GET movie_index/_search
{
    "query": {
        "bool": {
            "filter": [
                {  
                    "range":{"doubanScore": {"gte":7,"lte:8"} }
                }
            ]
        }
    }
}   

过滤 - 符合查询

must 和 should 的区别: must 是必须有,should 在有其他条件命中的情况下,should 命 中给分并显示,如果 should 未命中则只显示不给分。

GET movie_index/_search
{
    "query": {
        "bool": {
            "mush": [ //过滤后,必须满足的条件  should不是必须满足,但满足的会打分
                { 
                    "match":{"name": "red sea" }
                }
            ]
            "filter": [
                {   //值等判断
                    "term":{"actorList.name.keyword": "zhang han yu" }
                }
            ]
        }
    }
}  

过滤 – 修改

bool、filter可以省略

POST movie_index/_update_by_query
{
    "query": {
        "bool": {
            "filter":[
        "trem":{"actorList.name.keyword": "zhang han yu"}
       ] } }, "script": { "source": "ctx._source['actorList'][0]['name']=params.newName", "params": { "newName":"shangguigu" }, "lang": "painless" }

过滤 – 删除

bool、filter可以省略

POST movie_index/_delete_by_query
{
    "query": {
        "bool": {
            "filter":[
        "trem":{"actorList.name.keyword": "zhang han yu"}
       ]           
         }
     }
}

其他

排序

GET movie_index/_search
{
    "sort": [
         {
             "doubanScore": {"order": "desc"}
         }
     ]
}

分页查询

GET movie_index/_search
{
    "query": {
        "match_all": {}
    }
    , 
    "from": 2 // 从哪行开始,需要通过页码进行计算 (pageNum - 1) * size 
     , 
    "size": 2
}

高亮

GET movie_index/_search
{
     "query": {
     "match": {
         "name": "red"
     }, 
    "highlight":
    {
        "fields": {
            "name": {} //默认标签
         }
    }
}

聚合

默认会送 count 结果

GET movie_index/_search
{
  "aggs": {
    "groupby_actor": {
      "terms": {
        "field": "actorList.name.keyword",
         size": 10
      }
    }
  }
}

平均数

GET movie_index/_search
{
  "aggs": {
    "groupby_actor": {
      "terms": {
        "field": "actorList.name.keyword",
        "size": 10,
        "order":{"avg_score": "desc"}       },       "aggs":{
        "avg_score":{
          "avg":{"field": "doubanScore"}
        }
      }     }   } }

中文分词

中文分词默认按照字拆分

使用 ik 分词器:

1)查看 mapping

GET movie_index/_mapping

2)创建 index,手动指定 type

PUT movie_index_cn
{
  "mappings": {
    "properties": {
      "name":{"type": "text" , "analyzer": "ik_smart" }
    }
  }
}

索引优化

分割索引

分割索引是企业中常用的一种应对策略.实际就是根据时间间隔把一个业务索引切分成 多个索引。

好处

1)查询范围优化

因为一般情况并不会查询全部时间周期的数据,那么通过切分索引,物理上减少了扫描 数据的范围,也是对性能的优化。类似于大家学过的 Hive 中的分区表。

2)结构变化的灵活性

因为 elasticsearch 不允许对数据结构进行修改。但是实际使用中索引的结构和配置难免变化,那么只要对下一个间隔的索引进行修改,原来的索引位置原状。这样就有了一定的灵活性。

索引别名

索引别名就像一个快捷方式或软连接,可以指向一个或多个索引,也可以给任何一个需 要索引名的 API 来使用。别名 带给我们极大的灵活性,允许我们做下面这些:

1)给多个索引分组

分割索引可以解决数据结构变更的场景, 但是分割的频繁,如果想要统计一个大周期 的数据(例如季度、年),数据是分散到不同的索引中的,统计比较麻烦。我们可以将分割的 索引取相同的别名,这样,我们在统计时直接指定别名即可。

2)给索引的一个子集创建视图

将一个索引中的部分数据(基于某个条件)创建别名,查询此部分数据时,可以直接使用 别名

3)在运行的集群中可以无缝的从一个索引切换到另一个索引

如果涉及到索引的切换,我们可以在程序中指定别名,而不是索引。当需要切换索引时, 我们可以直接从一个索引上减去别名,在另一个索引上加上别名(减与加为原子操作)实现 无缝切换。

建表时直接声明

PUT movie_index_1 { 
  "aliases": { "movie_index_1_20210101": {} },
  "mappings": {}
}

为已存在的索引增加别名

POST _aliases { 
  "actions": [
    {"add": {"index": "movie_index", "alias": "movie_index_query_zhy"},
     "filter": { "term": { "actorList.name.keyword": "zhang han yu" } }
    }
  ]
}

删除某个索引的别名

POST _aliases { 
  "actions": [ 
    {"remove": {"index": "movie_index", "alias": "movie_index_query_zhy"}}
  ] 
}

为某个别名进行无缝切换

POST _aliases
{
  "actions": [
     { "remove": { 
     "index": "movie_index", 
     "alias": "movie_index_2021" }},
 
     { "add": { 
    "index": "movie_index_cn",
    "alias": "movie_index_2021" }}
  ]
}

索引模板

Index Template 索引模板,顾名思义,就是创建索引的模具,其中可以定义一系列规则 来帮助我们构建符合特定业务需求的索引的 mappings 和 settings,通过使用 Index Template 可以让我们的索引具备可预知的一致性。

PUT _template/template_movie2021
{
   "index_patterns": ["movie_test*"], 
   "settings": { 
     "number_of_shards": 1
   },
   "aliases" : { 
     "{index}-query": {},
     "movie_test-query":{}
   },
   "mappings": { 
     "properties": {
       "id": {"type": "keyword"},
       "movie_name": {
         "type": "text",
         "analyzer": "ik_smart"
       }
     }
   }
}

其中 "index_patterns": ["movie_test*"], 的含义就是凡是往 movie_test 开头的索引写入 数据时,如果索引不存在,那么 es 会根据此模板自动建立索引。 在 "aliases" 中用{index}表示,获得真正的创建的索引名。

分布式读写原理

写流程(基于_id)

(1)写操作必须在主分片上面完成之后才能被复制到相关的副本分片

(2)客户端向 Node 1 发送写操作请求,此时 Node1 为协调节点(接收客户端请求的 节点)。

(3)Node1 节点使用文档的_id 通过公式计算(hash取余)确定文档属于分片 0 。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。

(4)Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1 和 Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功, 协调节点向客户端报告成功。

读流程(基于_id)

(1)读操作可以从主分片或者从其它任意副本分片检索文档

(2)客户端向 Node 1 发送读请求,Node1 为协调节点

(3)节点使用文档的 _id 来确定文档属于分片 0 。分片 0 的主副分片存在于所有的 三个节点上。协调节点在每次请求的时候都会通过轮询的方式将请求打到不同的节点上来达 到负载均衡,假设本次它将请求转发到 Node 2 。

(4)Node 2 将文档返回给 Node 1 ,Node1 然后将文档返回给客户端。

搜索流程(_search)

(1)搜索被执行成一个两阶段过程,我们称之为 Query Then Fetch

(2)在初始查询阶段时,查询会广播到索引中每一个分片(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。

(3)每个分片返回各自优先队列中 所有文档的 ID 和排序值 给协调节点,它合并这 些值到自己的优先队列中来产生一个全局排序后的结果列表。

(4)接下来就是取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交 多个 GET 请求。每个分片加载并丰富文档,接着返回文档给协调节点。一旦所有的文档都 被取回了,协调节点返回结果给客户端。

文档的修改和并发控制

ElasticSearch 中的全部文档数据都是不可变的,数据不能修改,只能通过版本号的方式 不断增加。这样做的主要目的是解决更新过程中的并发冲突问题(乐观并发控制)。

删除方式

如果进行删除文档操作,也不会直接物理删除,而是通过给文档打删除标记,进行逻辑 删除,至到该索引触发段合并时,才物理删除,释放存储空间。

关于 master

master 并不负责实际的数据处理,主要是负责对如下信息的维护工作。 集群状态;用户参数配置;数据的索引、别名、分析器的相关设置;分片所在节点位置等。 虽然这些信息每个节点都有,但是只有 master 节点可以发起变更,然后同步给其他节点

节点角色详细介绍

shard

shard 太多带来的危害

每个分片都有 Lucene 索引,这些索引都会消耗 cpu 和内存。同样的数据,分片越多, 额外消耗的 cpu 和内存就越多,会出现“1+1”>2 的情况。

shard 的目的是为了负载均衡让每个节点的硬件充分发挥,但是如果分片多,在单个节 点上的多个 shard 同时接受请求,并对本节点的资源形成了竞争,实际上反而造成了内耗。

如何规划 shard 数量

1)根据每日数据量规划 shard 数量

以按天划分索引情况为例,单日数据评估低于 10G 的情况可以只使用一个分片,高于 10G 的情况,单一分片也不能太大不能超过 30G。所以一个 200G 的单日索引大致划分 7-10 个分片。

2)根据堆内存规划 shard 数量

另外从堆内存角度,一个 Elasticsearch 服务官方推荐的最大堆内存是 32G。一个 10G 分 片,大致会占用 30-80M 堆内存,那么对于这个 32G 堆内存的节点,最好不要超过 1000 个 分片。

shard 优化

(1)及时归档冷数据。

(2)降低单个分片占用的资源消耗,具体方法就是:合并分片中多个 segment(段)

segment:由于 es 的异步写入机制,后台每一次把写入到内存的数据 refresh(默认每秒)到磁盘, 都会在对应的 shard 上产生一个 segment,大小不一且segment 上的数据有着独立的 Lucene 索引。

手动合并段:POST movie_index/_forcemerge?max_num_segments=1

ES 开发

//创建 es 客户端对象
val builder: RestClientBuilder = RestClient.builder(new HttpHost("hadoop102",9200))
val esClient = new RestHighLevelClient(builder)

//单条数据写入
val indexRequest = new IndexRequest()
indexRequest.index("indexName")
val movieJsonStr: String = JSON.toJSONString(Movie("1001","速度与激情"), new SerializeConfig(true))
indexRequest.source(movieJsonStr,XContentType.JSON)
indexRequest.id("doc_id")//指定id为幂等写
esClient.index(indexRequest,RequestOptions.DEFAULT)

//批量数据写入
val bulkRequest = new BulkRequest()
// BulkRequest 实际上就是由多个单条 IndexRequest 的组合
bulkRequest.add(indexRequest)
esClient.bulk(bulkRequest,RequestOptions.DEFAULT)

//根据 docid 更新字段
val updateRequest = new UpdateRequest()
updateRequest.index("indexName")
updateRequest.id("doc_id")
updateRequest.doc("move_name", "功夫")
esClient.update(updateRequest,RequestOptions.DEFAULT)

//根据条件修改
val updateByQueryRequest = new UpdateByQueryRequest()
updateByQueryRequest.indices("indexName")
//    query
val boolQueryBuilder: BoolQueryBuilder = QueryBUilder.boolQuery()
val tremQueryBuilder: TermQueryBuilder = QueryBUilder.termQuery("movice_name","红海行动")
boolQueryBuilder.filter(tremQueryBuilder)
updateByQueryRequest.setQuery(queryBuilder)
//    update
val map = new util.HashMap[String,AnyRef]()
map.put("newName",newValue)
val script =new Script(ScriptType.INLINE, Script.DEFAULT_SCRIPT_LANG, 
"ctx._source['movie_name']=params.newName", map)
updateByQueryRequest.setScript(script)
esClient.updateByQuery(updateByQueryRequest, RequestOptions.DEFAULT)

//删除数据
val deleteRequest = new DeleteRequest()
deleteRequest.index("indexName")
deleteRequest.id("doc_id")
esClient.delete(deleteRequest,RequestOptions.DEFAULT)

//单条查询
val getRequest: GetRequest = new GetRequest()
GetRequest.index("indexName")
GetRequest.id("doc_id")
val getResponse: GetResponse = client.get(getRequest,RequestOptions.DEFAULT)
println(getResponse.getSourceAsString)

//条件查询
val searchRequest = new SearchRequest()
GetRequest.index("indexName")
val searchSourceBuilder = new SearchSourceBuilder()
//        过滤匹配
val boolQueryBuilder = new BoolQueryBuilder()
val rangeQueryBuilder = new RangeQueryBuilder("doubanScore").gte("5.0")
boolQueryBuilder.filter(rangeQueryBuilder)
val matchQueryBuilder = new MatchQueryBuilder("name","red sea")
boolQueryBuilder.must(matchQueryBuilder)
searchSourceBuilder.query(boolQueryBuilder)
//        高亮
val highlightBuilder: HighlightBuilder = new HighlightBuilder().field("name")
searchSourceBuilder.highlighter(highlightBuilder)
//        分页
searchSourceBuilder.from(0)
searchSourceBuilder.size(20)
//        排序
searchSourceBuilder.sort("doubanScore",SortOrder.DESC)
searchRequest.source(searchSourceBuilder)
val searchResponse: SearchResponse = esClient.search(searchRequest, RequestOptions.DEFAULT)
val hits: Array[SearchHit] = searchResponse.getHits.getHits
for (searchHit <- hits) {
   println(searchHit.getSourceAsString)
}

//聚合查询
val searchRequest = new SearchRequest()
GetRequest.index("indexName")
val searchSourceBuilder = new SearchSourceBuilder()
searchSourceBuilder.size(0)
//        分组
val termsAggregationBuilder: TermsAggregationBuilder = AggregationBuilders.
 terms("groupByActor").
 field("actorList.name.keyword").
 size(100).
 order(BucketOrder.aggregation("avg_score", false))
//        聚合
val avgAggregationBuilder: AvgAggregationBuilder = AggregationBuilders.
 avg("avg_score").
 field("doubanScore")
termsAggregationBuilder.subAggregation(avgAggregationBuilder)
searchSourceBuilder.aggregation(termsAggregationBuilder)
searchRequest.source(searchSourceBuilder)
val searchResponse: SearchResponse = esClient.search(searchRequest,RequestOptions.DEFAULT)
//        处理结果
val groupByActorTerms: ParsedStringTerms = searchResponse.getAggregations.get[ParsedStringTerms]("groupByAct
or")
val buckets: util.List[_ <: Terms.Bucket] = groupByActorTerms.getBuckets
import scala.collection.JavaConverters._
for (bucket <- buckets.asScala) {
    val actorName: AnyRef = bucket.getKey
    val movieCount: Long = bucket.getDocCount
    val aggregations: Aggregations = bucket.getAggregations
    val parsedAvg: ParsedAvg = aggregations.get[ParsedAvg]("avg_score")
    val avgScore: Double = parsedAvg.getValue
}

//关闭 es 客户端对象
esClient.close()

 

posted @ 2022-05-23 22:03  1243741754  阅读(281)  评论(0编辑  收藏  举报