公号:码农充电站pro

主页:https://codeshellme.github.io

ES 中的聚合分析(Aggregations)是对数据的统计分析功能,它的优点是实时性较高,相比于 Hadoop 速度更快。

1,聚合的分类

ES 中的聚合分析主要有以下 3 大类,每一类都提供了多种统计方法:

一般使用聚合分析时,通常将 size 设置为 0,表示不需要返回查询结果,只需要返回聚合结果。

一个示例:

# 多个 Metric 聚合,找到最低最高和平均工资
POST index_name/_search
{
"size": 0, # size 为 0
"aggs": {
"max_salary": { # 自定义聚合名称
"max": { # 聚合类型
"field": "salary" # 聚合字段
}
},
"min_salary": { # 自定义聚合名称
"min": { # 聚合类型
"field": "salary" # 聚合字段
}
},
"avg_salary": { # 自定义聚合名称
"avg": { # 聚合类型
"field": "salary" # 聚合字段
}
}
}
}

2,Metrics 聚合

Metrics 聚合可以分为单值分析和多值分析:

  • 单值分析:分析结果是单个值

    • max
    • min
    • avg
    • sum
    • cardinality:类似 distinct count
      • 注意 cardinality 对 keyword 类型数据和 text 类型数据的区别
      • keyword 类型不会进行分词处理,而 text 类型会进行分词处理
  • 多值分析:分析结果是多个值

2.1,示例

示例,一个员工表定义:

DELETE /employees
PUT /employees/
{
"mappings" : {
"properties" : {
"age" : {
"type" : "integer"
},
"gender" : {
"type" : "keyword"
},
"job" : {
"type" : "text",
"fields" : {
"keyword" : { # 子字段名称
"type" : "keyword", # 子字段类型
"ignore_above" : 50
}
}
},
"name" : {
"type" : "keyword"
},
"salary" : {
"type" : "integer"
}
}
}
}

插入一些测试数据:

PUT /employees/_bulk
{ "index" : { "_id" : "1" } }
{ "name" : "Emma","age":32,"job":"Product Manager","gender":"female","salary":35000 }
{ "index" : { "_id" : "2" } }
{ "name" : "Underwood","age":41,"job":"Dev Manager","gender":"male","salary": 50000}
{ "index" : { "_id" : "3" } }
{ "name" : "Tran","age":25,"job":"Web Designer","gender":"male","salary":18000 }
{ "index" : { "_id" : "4" } }
{ "name" : "Rivera","age":26,"job":"Web Designer","gender":"female","salary": 22000}
{ "index" : { "_id" : "5" } }
{ "name" : "Rose","age":25,"job":"QA","gender":"female","salary":18000 }
{ "index" : { "_id" : "6" } }
{ "name" : "Lucy","age":31,"job":"QA","gender":"female","salary": 25000}
{ "index" : { "_id" : "7" } }
{ "name" : "Byrd","age":27,"job":"QA","gender":"male","salary":20000 }
{ "index" : { "_id" : "8" } }
{ "name" : "Foster","age":27,"job":"Java Programmer","gender":"male","salary": 20000}
{ "index" : { "_id" : "9" } }
{ "name" : "Gregory","age":32,"job":"Java Programmer","gender":"male","salary":22000 }
{ "index" : { "_id" : "10" } }
{ "name" : "Bryant","age":20,"job":"Java Programmer","gender":"male","salary": 9000}
{ "index" : { "_id" : "11" } }
{ "name" : "Jenny","age":36,"job":"Java Programmer","gender":"female","salary":38000 }
{ "index" : { "_id" : "12" } }
{ "name" : "Mcdonald","age":31,"job":"Java Programmer","gender":"male","salary": 32000}
{ "index" : { "_id" : "13" } }
{ "name" : "Jonthna","age":30,"job":"Java Programmer","gender":"female","salary":30000 }
{ "index" : { "_id" : "14" } }
{ "name" : "Marshall","age":32,"job":"Javascript Programmer","gender":"male","salary": 25000}
{ "index" : { "_id" : "15" } }
{ "name" : "King","age":33,"job":"Java Programmer","gender":"male","salary":28000 }
{ "index" : { "_id" : "16" } }
{ "name" : "Mccarthy","age":21,"job":"Javascript Programmer","gender":"male","salary": 16000}
{ "index" : { "_id" : "17" } }
{ "name" : "Goodwin","age":25,"job":"Javascript Programmer","gender":"male","salary": 16000}
{ "index" : { "_id" : "18" } }
{ "name" : "Catherine","age":29,"job":"Javascript Programmer","gender":"female","salary": 20000}
{ "index" : { "_id" : "19" } }
{ "name" : "Boone","age":30,"job":"DBA","gender":"male","salary": 30000}
{ "index" : { "_id" : "20" } }
{ "name" : "Kathy","age":29,"job":"DBA","gender":"female","salary": 20000}

min 聚合分析:

# Metric 聚合,找到最低的工资
POST employees/_search
{
"size": 0,
"aggs": {
"min_salary": {
"min": { # 聚合类型,求最小值
"field":"salary"
}
}
}
} # 返回结果
"hits": {
"total": {
"value": 20, # 一共统计了多少条数据
"relation": "eq"
},
"max_score": null,
"hits": [...] # 因为 size 为 0
},
"aggregations": {
"min_salary": { # 自定义的聚合名称
"value": 9000,
}
}

stats 聚合分析:

# 输出多值
POST employees/_search
{
"size": 0,
"aggs": {
"stats_salary": {
"stats": { # stats 聚合
"field":"salary"
}
}
}
} # 返回多值结果
"aggregations": {
"stats_salary": { # 自定义的聚合名称
"count": 20,
"min": 9000,
"max": 50000,
"avg": 24700,
"sum": 494000
}
}

2.2,top_hits 示例

# 指定 size,不同岗位中,年纪最大的3个员工的信息
POST employees/_search
{
"size": 0,
"aggs":{
"old_employee":{ # 聚合名称
"top_hits":{ # top_hits 分桶
"size":3,
"sort":[ # 根据 age 倒序排序,选前 3 个
{"age":{"order":"desc"}}
]
}
}
}
}

3,Bucket 聚合

Bucket 聚合按照一定的规则,将文档分配到不同的中,达到分类的目的。

Bucket 聚合支持嵌套,也就是在桶里再次分桶。

Bucket 聚合算法:

  • Terms:根据关键字(字符串)分桶。text 类型的字段需要打开 fielddata 配置。

    • 注意 keyword 类型不会做分词处理,text 类型会做分词处理。
    • 另外 size 参数可以控制桶的数量
  • Range:按照范围进行分桶,主要针对数字类型的数据
  • Date range
  • Histogram:直方图分桶,指定一个间隔值,来进行分桶。
  • Date histogram

3.1,Terms 示例

示例:

# 对 keword 进行聚合
POST employees/_search
{
"size": 0, # size 为 0
"aggs": {
"jobs": { # 自定义聚合名称
"terms": { # terms 聚合
"field":"job.keyword" # job 字段的 keyword 子字段
}
}
}
} # 返回值结构示例
"aggregations": {
"genres": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [ # 很多桶,这是一个数组
{
"key": "electronic",
"doc_count": 6
},
{
"key": "rock",
"doc_count": 3
},
{
"key": "jazz",
"doc_count": 2
}
]
}
}

对 Text 字段进行 terms 聚合查询会出错,示例:

# 对 Text 字段进行 terms 聚合查询
POST employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field":"job" # job 是 text 类型
}
}
}
} # 对 Text 字段打开 fielddata,以支持 terms aggregation
PUT employees/_mapping
{
"properties" : {
"job":{
"type": "text",
"fielddata": true # 打开 fielddata
}
}
}

3.2,Terms 性能优化

当某个字段的写入和 Terms 聚合比较频繁的时候,可用通过打开 eager_global_ordinals 配置来对 Terms 操作进行优化。

示例:

PUT index_name
{
"mappings": {
"properties": {
"foo": { # 字段名称
"type": "keyword",
"eager_global_ordinals": true # 打开
}
}
}
}

3.3,嵌套聚合示例

Bucket 聚合支持添加子聚合来进一步分析,子聚合可以是一个 Metrics 或者 Bucket

示例 1:

# 指定 size,不同岗位中,年纪最大的3个员工的信息
POST employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": { # 先做了一个 terms 分桶
"field":"job.keyword"
},
"aggs":{ # 嵌套一个聚合,称为子聚合,
"old_employee":{ # 聚合名称
"top_hits":{ # top_hits 分桶
"size":3,
"sort":[ # 根据 age 倒序排序,选前 3 个
{"age":{"order":"desc"}}
]
}
}
}
}
}
}

示例 2 :

POST employees/_search
{
"size": 0,
"aggs": {
"Job_salary_stats": {
"terms": { # 先做了一个 terms 分桶
"field": "job.keyword"
},
"aggs": {
"salary": {
"stats": { # 子聚合是一个 stats
"field": "salary"
}
}
}
}
}
} # 多次嵌套
POST employees/_search
{
"size": 0,
"aggs": { # 第 1 层
"Job_gender_stats": {
"terms": {
"field": "job.keyword" # 先根据岗位分桶
},
"aggs": { # 第 2 层
"gender_stats": {
"terms": {
"field": "gender" # 再根据性别分桶
},
"aggs": { # 第 3 层
"salary_stats": {
"stats": { # 最后根据工资统计 stats
"field": "salary"
}
}
}
}
}
}
}
}

3.4,Range 示例

对员工的工资进行区间聚合:

# Salary Ranges 分桶,可以自己定义 key
POST employees/_search
{
"size": 0,
"aggs": {
"salary_range": { # 自定义聚合名称
"range": { # range 聚合
"field":"salary", # 聚合的字段
"ranges":[ # range 聚合规则/条件
{
"to":10000 # salary < 10000
},
{
"from":10000, # 10000 < salary < 20000
"to":20000
},
{ # 如果没有定义 key,ES 会自动生成
"key":"可以使用 key 自定义名称",
"from":20000 # salary > 20000
}
]
}
}
}
}

3.5,Histogram 示例

示例,工资0到10万,以 5000一个区间进行分桶:

# Salary Histogram
POST employees/_search
{
"size": 0,
"aggs": {
"salary_histrogram": { # 自定义聚合名称
"histogram": { # histogram 聚合
"field":"salary", # 聚合的字段
"interval":5000, # 区间值
"extended_bounds":{ # 范围
"min":0,
"max":100000
}
}
}
}
}

4,Pipeline 聚合

Pipeline 聚合用于对其它聚合的结果进行再聚合。

根据 Pipeline 聚合原聚合的位置区别,分为两类:

  • Pipeline 聚合原聚合同级,称为 Sibling 聚合

    • Max_bucketMin_bucketAvg_bucketSum_bucket
    • Stats_bucketExtended-Status_bucket
    • Percentiles_bucket
  • Pipeline 聚合内嵌在原聚合之内,称为 Parent 聚合
    • Derivative:求导
    • Cumulative-sum:累计求和
    • Moving-function:滑动窗口

4.1,Sibling 聚合示例

示例:

# 平均工资最低的工作类型
POST employees/_search
{
"size": 0,
"aggs": {
"jobs": { # 自定义聚合名称
"terms": {
"field": "job.keyword", # 先对岗位类型进行分桶
"size": 10
},
"aggs": {
"avg_salary": {
"avg": {
"field": "salary" # 再计算每种工资岗位的平价值
}
}
}
},
"min_salary_by_job":{ # 自定义聚合名称
"min_bucket": { # pipeline 聚合
"buckets_path": "jobs>avg_salary"
} # 含义是:对 jobs 中的 avg_salary 进行一个 min_bucket 聚合
}
}
}

4.2,Parent 聚合示例

示例:

# 示例 1
POST employees/_search
{
"size": 0,
"aggs": {
"age": { # 自定义聚合名称
"histogram": {
"field": "age",
"min_doc_count": 1,
"interval": 1
},
"aggs": {
"avg_salary": { # 自定义聚合名称
"avg": {
"field": "salary"
}
}, # 自定义聚合名称
"derivative_avg_salary":{ # 注意 derivative 聚合的位置,与 avg_salary 同级
"derivative": { # 而不是与 age 同级
"buckets_path": "avg_salary" # 注意这里不再有箭头 >
}
}
}
}
}
} # 示例 2
POST employees/_search
{
"size": 0,
"aggs": {
"age": {
"histogram": {
"field": "age",
"min_doc_count": 1,
"interval": 1
},
"aggs": {
"avg_salary": {
"avg": {
"field": "salary"
}
},
"cumulative_salary":{
"cumulative_sum": { # 累计求和
"buckets_path": "avg_salary"
}
}
}
}
}
}

5,聚合的作用范围

ES 聚合的默认作用范围是 Query 的查询结果,如果没有写 Query,那默认就是在索引的所有数据上做聚合。

比如:

POST employees/_search
{
"size": 0,
"query": { # 在 query 的结果之上做聚合
"range": {
"age": {"gte": 20}
}
},
"aggs": {
"jobs": {
"terms": {"field":"job.keyword"}
}
}
}

ES 支持通过以下方式来改变聚合的作用范围:

  • Query:ES 聚合的默认作用范围。

    • 一般设置 size 为 0
    • 如果没有写 Query,那默认就是在索引的所有数据上做聚合。
  • Filter:写在某个聚合的内部,只控制某个聚合的作用范围。
    • 一般设置 size 为 0
  • Post Filter:对聚合没有影响,只是对聚合的结果进行再过滤。
    • 不再设置 size 为 0
    • 使用场景:获取聚合信息,并获取符合条件的文档。
  • Global:会覆盖掉 Query 的影响。

5.1,Filter 示例

示例:

POST employees/_search
{
"size": 0,
"aggs": {
"older_person": { # 自定义聚合名称
"filter":{ # 通过 filter 改变聚合的作用范围
"range":{
"age":{"from":35}
}
}, # end older_person
"aggs":{ # 在 filter 的结果之上做聚合
"jobs":{ # 自定义聚合名称
"terms": {"field":"job.keyword"}
}
}
}, # end older_person
"all_jobs": { # 又一个聚合,没有 filter
"terms": {"field":"job.keyword"}
}
}
}

5.2,Post Filter 示例

示例:

POST employees/_search
{
"aggs": {
"jobs": { # 自定义聚合名称
"terms": {"field": "job.keyword"}
}
}, # end aggs
"post_filter": { # 一个 post_filter,对聚合的结果进行过滤
"match": {
"job.keyword": "Dev Manager"
}
}
}

5.3,Global 示例

POST employees/_search
{
"size": 0,
"query": { # 一个 query
"range": {
"age": {"gte": 40}
}
},
"aggs": {
"jobs": { # 一个聚合
"terms": {"field":"job.keyword"}
},
"all":{ # 又一个聚合,名称为 all
"global":{}, # 这里的 global 会覆盖掉上面的 query,使得聚合 all 的作用范围不受 query 的影响
"aggs":{ # 子聚合
"salary_avg":{ # 自定义聚合名称
"avg":{"field":"salary"}
}
}
}
}
}

6,聚合中的排序

6.1,基于 count 的排序

聚合中的排序使用 order 字段,默认按照 _count_key 进行排序。

  • _count:表示按照文档数排序,如果不指定 _count,默认按照降序进行排序。
  • _key:表示关键字(字符串值),如果文档数相同,再按照 key 进行排序。

示例 1:

# 使用 count 和 key
POST employees/_search
{
"size": 0,
"query": {
"range": {
"age": {"gte": 20}
}
},
"aggs": {
"jobs": { # 自定义聚合名称
"terms": { # terms 聚合
"field":"job.keyword",
"order":[ # order 排序
{"_count":"asc"}, # 先安装文档数排序
{"_key":"desc"} # 如果文档数相同,再按照 key 排序
]
}
}
}
}

6.2,基于子聚合的排序

也可以基于子聚合排序。

示例 2:

# 先对工作种类进行分桶
# 再以工作种类的平均工资进行排序
POST employees/_search
{
"size": 0,
"aggs": {
"jobs": { # 自定义聚合名称
"terms": {
"field":"job.keyword",
"order":[ # 基于子聚合的排序
{"avg_salary":"desc"}
]
}, # end terms
"aggs": { # 子聚合
"avg_salary": { # 子聚合名称
"avg": {"field":"salary"}
}
}
} # end jobs
}
}

如果子聚合是多值输出,也可以基于 子聚合名.属性 来进行排序,如下:

POST employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field":"job.keyword",
"order":[ # 基于子聚合的属性排序
{"stats_salary.min":"desc"}
]
}, # end terms
"aggs": {
"stats_salary": { # 子聚合是多值输出
"stats": {"field":"salary"}
}
}
} # end jobs
}
}

7,聚合分析的原理及精准度

下面介绍聚合分析的原理及精准度的问题。

7.1,分布式系统的三个概念

分布式系统中有三个概念:

  • 数据量
  • 精准度
  • 实时性

对于分布式系统(数据分布在不同的分片上),这三个指标不能同时具备,同时只能满足其中的 2 个条件

  • Hadoop 离线计算:可以同时满足大数据量和精准度
  • 近似计算:可以同时满足大数据量和实时性
  • 有限数据计算:可以同时满足精准度和实时性

ES 属于近似计算,具备了数据量实时性的特点,失去了精准度

7.2,聚合分析的原理

ES 是一个分布式系统,数据分布在不同的分片上。

因此,ES 在进行聚合分析时,会先在每个主分片上做聚合,然后再将每个主分片上的聚合结果进行汇总,从而得到最终的聚合结果。

7.3,聚合分析的精准度

分布式聚合的原理,会天生带来精准度的问题,但并不是所有的聚合分析都有精准度问题:

  • 比如 Min 聚合 就不会有精准度问题。

    • 因为求总的最小值,与先在所有主分片求最小值,再汇总每个主分片的最小值,它们最终的结果是一样的。
  • 比如 Terms 聚合 就有精准度问题。

下面来看下 Terms 聚合存在的问题,下图中的:

  • A(6) 表示 A 类的文档数有 6 个。
  • B(4) 表示 B 类的文档数有 4 个。
  • C(4) 表示 C 类的文档数有 4 个。
  • D(3) 表示 D 类的文档数有 3 个。

下图是 Terms 聚合流程:

上图中,在进行 Terms 聚合时(最终结果只要按照数量排序的前 3 个),需要分别在分片 P0P1上做聚合,然后再将它们的聚合结果进行汇总。

正确的聚合结果应该是 A(12),B(6),D(6),但是由于分片的原因,ES 计算出来的结果是 A(12),B(6),C(4)。这就是 Terms 聚合存在的精准度问题。

7.4,show_term_doc_count_error 参数

打开 show_term_doc_count_error 配置可以使得 terms 聚合的返回结果中有一个 doc_count_error_upper_bound 值(最小为0),通过该值可以了解精准程度;该值越小,说明 Terms 的精准度越高

POST index_name/_search
{
"size": 0,
"aggs": {
"weather": { # 自定义聚合名称
"terms": { # terms 聚合
"field":"OriginWeather",
"show_term_doc_count_error":true # 打开
}
}
}
}

7.5,如何提高 terms 精准度

提高 terms 聚合的精准度有两种方式:

  • 将主分片数设置为 1。

    • 因为 terms 的不准确是由于分片导致的,如果将主分片数设置为 1,就不存在不准确的问题。
    • 这种方式在数据量不是很大的时候,可以是使用。
  • shard_size 的值尽量调大(意味着从分片上额外获取更多的数据,从而提升准确度)。
    • shard_size 值变大后,会使得计算量变大,进而使得ES 的整体性能变低,精准度变高
    • 所以需要权衡 shard_size 值与精准度的平衡。
    • shard_size 值的默认值是 【size * 1.5 + 10】。

设置 shard_size 的语法:

POST my_flights/_search
{
"size": 0,
"aggs": {
"weather": {
"terms": {
"field":"OriginWeather",
"size":1,
"shard_size":1,
"show_term_doc_count_error":true
}
}
}
}

(本节完。)


推荐阅读:

ElasticSearch 查询

ElasticSearch URI 查询

ElasticSearch DSL 查询

ElasticSearch 文档及操作

ElasticSearch 搜索模板与建议


欢迎关注作者公众号,获取更多技术干货。

ElasticSearch 聚合分析的更多相关文章

  1. ElasticSearch聚合分析

    聚合用于分析查询结果集的统计指标,我们以观看日志分析为例,介绍各种常用的ElasticSearch聚合操作. 目录: 查询用户观看视频数和观看时长 聚合分页器 查询视频uv 单个视频uv 批量查询视频 ...

  2. ElasticSearch聚合分析API——非常详细,如果要全面了解的话,最好看这个

    转自:http://www.tianyiqingci.com/2016/04/11/esaggsapi/ 前言 说完了ES的索引与检索,接着再介绍一个ES高级功能API – 聚合(Aggregatio ...

  3. Elasticsearch 6.x版本全文检索学习之聚合分析入门

    1.什么是聚合分析? 答:聚合分析,英文为Aggregation,是es除搜索功能外提供的针对es数据做统计分析的功能.特点如下所示: a.功能丰富,提供Bucket.Metric.Pipeline等 ...

  4. Elasticsearch 之聚合分析入门

    本文主要介绍 Elasticsearch 的聚合功能,介绍什么是 Bucket 和 Metric 聚合,以及如何实现嵌套的聚合. 首先来看下聚合(Aggregation): 什么是 Aggregati ...

  5. elasticsearch系列六:聚合分析(聚合分析简介、指标聚合、桶聚合)

    一.聚合分析简介 1. ES聚合分析是什么? 聚合分析是数据库中重要的功能特性,完成对一个查询的数据集中数据的聚合计算,如:找出某字段(或计算表达式的结果)的最大值.最小值,计算和.平均值等.ES作为 ...

  6. ElasticStack学习(八):ElasticSearch索引模板与聚合分析初探

    一.Index Template与Dynamic Template的概念 1.Index Template:它是用来根据提前设定的Mappings和Settings,并按照一定的规则,自动匹配到新创建 ...

  7. Elasticsearch系列---常见搜索方式与聚合分析

    概要 本篇主要介绍常见的6种搜索方式.聚合分析语法,基本是上机实战,可以和关系型数据库作对比,如果之前了解关系型数据库,那本篇只需要了解搜索和聚合的语法规则就可以了. 搜索响应报文 以上篇建立的mus ...

  8. Elasticsearch学习笔记(三)聚合分析Agg

    一.设置fielddata PUT /index/_mapping/type {     "properties":{          "fieldName" ...

  9. Elasticsearch学习之深入聚合分析四---案例实战

    1. 需求:比如有一个网站,记录下了每次请求的访问的耗时,需要统计tp50,tp90,tp99 tp50:50%的请求的耗时最长在多长时间tp90:90%的请求的耗时最长在多长时间tp99:99%的请 ...

随机推荐

  1. 提供读取excel 的方法

    /** * 对外提供读取excel 的方法 * */ public static List<List<Object>> readExcel(String path) throw ...

  2. 网际互连__TCP/IP三次握手和四次挥手

    在TCP/IP协议中,TCP协议提供可靠的连接服务. 位码即tcp标志位,有6种标示: SYN(synchronous建立联机).ACK(acknowledgement 确认).PSH(push传送) ...

  3. 设计模式(二)——Java简单工厂模式

    简单工厂模式 案例: 披萨的项目(要便于披萨种类的扩展,要便于维护) 1)披萨的种类很多(比如 GreekPizz.CheesePizz 等) 2)披萨的制作有 prepare,bake, cut, ...

  4. 记录tomcat服务器开启关闭时间

    1.IO流 package com.zy.exercise; import java.io.File; import java.io.FileNotFoundException; import jav ...

  5. Codeforces 1364C - Ehab and Prefix MEXs

    题意:给1e5的数组a 保证 ai <= ai+1  ai<=i  求一个一样长的数组b 使得mex(b1,b2···bi) = ai QAQ:不知道为啥这1600分的题比赛时出不了 啊啊 ...

  6. CF1401-C. Mere Array

    CF1401-C. Mere Array 题意: 给出一个长度为\(n\)的数组\(a\),你可以对这个数组进行如下操作:对于数组\(a\)中任意的两个元素\(a_i\).\(a_j\),若\(gcd ...

  7. MySQL 语句及其种类

    DDL(Data Definition Language) DDL(Data Definition Language),数据定义语言 CREATE:创建数据库和表等对象 DROP:删除数据库和表等对象 ...

  8. HDU - 4455 Substrings(非原创)

    XXX has an array of length n. XXX wants to know that, for a given w, what is the sum of the distinct ...

  9. webpack 性能优化 dll 分包

    webpack 性能优化 dll 分包 html-webpack-externals-plugin DLLPlugin https://www.webpackjs.com/configuration/ ...

  10. 使用 js 实现十大排序算法: 冒泡排序

    使用 js 实现十大排序算法: 冒泡排序 冒泡排序 refs xgqfrms 2012-2020 www.cnblogs.com 发布文章使用:只允许注册用户才可以访问!