ELK
liduoan.efls Engineer

ElasticSearch简介

Elasticsearch是用Java开发并且是当前最流行的开源的企业级搜索引擎。能够达到实时搜索,稳定,可靠,快速,安装使用方便。客户端支持Java、.NET(C#)、PHP、Python、Ruby等多种语言。

ES与Lucene的关系

Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库(框架)。但是想要使用Lucene,必须使用Java来作为开发语言并将其直接集成到你的应用中,并且Lucene的配置及使用非常复杂,你需要深入了解检索的相关知识来理解它是如何工作的。Lucene缺点:

  1. 只能在Java项目中使用,并且要以jar包的方式直接集成项目中;
  2. 使用非常复杂,创建索引和搜索索引代码繁杂;
  3. 不支持集群环境-,索引数据不同步(不支持大型项目);
  4. 索引数据如果太多就不行,索引库和应用所在同一个服务器,共同占用硬盘。

上述Lucene框架中的缺点,ES全部都能解决。

Es与Solr

当单纯的对已有数据进行搜索时,Solr更快:

image

当实时建立索引时Solr会产生io阻塞(Solr需要从磁盘读数据),查询性能较差,Elasticsearch具有明显的优势:

image

大型互联网公司,实际生产环境测试,将搜索引擎从Solr转到 Elasticsearch以后的平均查询速度有了50倍的提升:

image

总结一下:

  1. Solr 利用 Zookeeper 进行分布式管理,而Elasticsearch 自身带有分布式协调管理功能。
  2. Solr 支持更多格式的数据,比如JSON、XML、CSV,而 Elasticsearch 仅支持json文件格式。
  3. Solr 在传统的搜索应用中表现好于 Elasticsearch,但在处理实时搜索应用时效率明显低于 Elasticsearch。
  4. Solr 是传统搜索应用的有力解决方案,但 Elasticsearch更适用于新兴的实时搜索应用。

Es与关系型数据库

image

全文检索

全文检索是指:

对下图进行一个简要解释,说明全文检索的流程和理论:

首先我们对三个文本进行分词处理,得到对应的表,表中的index表示属于第几个文本【也就是文本的id

之后我们去重处理,通过图中可以发现,hello在第1、2个文本,elasticsearch在2、3文本。

之后我们对word进行排序处理,是字母序【个人认为这是为了检索的更快,二分速度结束。

现在当我们输入关键字的时候,我们在最后一张表中可以发现关键字存在的文本index是哪些,如此便可把对应文本输出出来。

搜索迅速的原因在于:单词是有限的,那么当我们每次进行词条添加时,对应的关键字可能都已经存在过了,仅仅需要在关键字的index中添加记录即可。

image

  • 通过一个程序扫描文本中的每一个单词,针对单词建立索引,并保存该单词在文本中的位置、以及出现的次数
  • 用户查询时,通过之前建立好的索引来查询,将索引中单词对应的文本位置、出现的次数返回给用户,因为有了具体文本的位置,所以就可以将具体内容读取出来了

索引就类似于目录,平时我们使用的都是索引,都是通过主键定位到某条数据。那么倒排索引刚好相反,数据对应到主键。

核心概念

  • 索引index

    一个索引就是一个拥有几分相似特征的文档的集合。比如说,可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。 一个索引由一个名字来标识(必须全部是小写字母的),并且当我们要对对应于这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。

  • 映射mapping

    ElasticSearch中的映射(Mapping)用来定义一个文档。mapping是处理数据的方式和规则方面做一些限制,如某个字段的数据类型、默认值、分词器、是否被索引等等,这些都是映射里面可以设置的。

  • 字段field

相当于是数据表的字段|列。

  • 字段类型type

每一个字段都应该有一个对应的类型,例如Text、Keyword、Byte等。

  • 文档document

一个文档是一个可被索引的基础信息单元,类似一条记录。文档以JSON(Javascript Object Notation)格式来表示。

  • 集群 cluster

一个集群就是由一个或多个节点组织在一起,它们共同持有整个的数据,并一起提供索引和搜索功能。

  • 节点 node

一个节点是集群中的一个服务器,作为集群的一部分,它存储数据,参与集群的索引和搜索功能。 一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,每个节点都会被安排加入到一个叫做elasticsearch的集群中。这意味着,如果在网络中启动了若干个节点,并假定它们能够相互发现彼此,它们将会自动地形成并加入到一个叫做elasticsearch的集群中。在一个集群里,可以拥有任意多个节点。而且,如果当前网络中没有运行任何Elasticsearch节点,这时启动一个节点,会默认创建并加入一个叫做elasticsearch的集群。

  • 分片shards

一个索引可以存储超出单个结点硬件限制的大量数据。比如,一个具有10亿文档的索引占据1TB的磁盘空间,而任一节点都没有这样大的磁盘空间;或者单个节点处理搜索请求,响应太慢。

为了解决这个问题,Elasticsearch提供了将索引划分成多份的能力,这些份就叫做分片。当创建一个索引的时候,可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的索引,这个索引可以被放置到集群中的任何节点上。分片很重要,主要有两方面的原因:

  1. 允许水平分割/扩展你的内容容量。
  2. 允许在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量。

至于一个分片怎样分布,它的文档怎样聚合回搜索请求,是完全由Elasticsearch管理的,对于作为用户来说这些都是透明的。

  • 副本replicas

在一个网络/云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的,Elasticsearch允许你创建分片的一份或多份拷贝,这些拷贝叫做副本分片,或者直接叫副本。副本之所以重要,有两个主要原因:

  1. 在分片/节点失败的情况下,提供了高可用性。注意到复制分片不要与原/主要(original/primary)分片置于同一节点上是非常重要的。
  2. 扩展搜索量/吞吐量,因为搜索可以在所有的副本上并行运行。

每个索引可以被分成多个分片,一个索引有0个或者多个副本。一旦设置了副本,每个索引就有了主分片和副本分片,分片和副本的数量可以在索引创建的时候指定。在索引创建之后,可以在任何时候动态地改变副本的数量,但是不能改变分片的数量。

ES数据基本操作

ES是面向文档(document oriented)的,这意味着它可以存储整个对象或文档(document)

。然而它不仅仅是存储,还会索引(index)每个文档的内容使之可以被搜索。在ES中,你可以对文档(而非成行成列的数据)进行索引、搜索、排序、过滤。ES使用JSON作为文档序列化格式,JSON现在已经被大多语言所支持,而且已经成为NoSQL领域的标准格式。

ES使用Restful风格进行操作。Restful是一种面向资源的架构风格,可以简单理解为:使用URL定位资源,用HTTP动词(GET,POST,DELETE,PUT)描述操作。 基于Restful,ES和所有客户端的交互都是使用JSON格式的数据。使用Restful的好处:透明性,暴露资源存在。充分利用 HTTP 协议本身语义,不同请求方式进行不同的操作

  • 操作索引
1
2
3
4
5
6
7
8
// 创建索引
PUT /es_db

// 查询索引
GET /es_db

// 删除索引
DELETE /es_db
  • 添加文档
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// ES7版本后淡化了类型的概念,所有的类型名称都是_doc
PUT /es_db/_doc/1
{
"name": "张三",
"sex": 1,
"age": 25,
"address": "广州天河公园",
"remark": "java developer"
}

PUT /es_db/_doc/2
{
"name": "李四",
"sex": 1,
"age": 28,
"address": "广州荔湾大厦",
"remark": "java assistant"
}

PUT /es_db/_doc/3
{
"name": "rod",
"sex": 0,
"age": 26,
"address": "广州白云山公园",
"remark": "php developer"
}

PUT /es_db/_doc/4
{
"name": "admin",
"sex": 0,
"age": 22,
"address": "长沙橘子洲头",
"remark": "python assistant"
}

PUT /es_db/_doc/5
{
"name": "小明",
"sex": 0,
"age": 19,
"address": "长沙岳麓山",
"remark": "java architect assistant"
}

从 ES 7.0 开始,Type 被废弃

在 7.0 以及之后的版本中 Type 被废弃了。一个 index 中只有一个默认的 type,即 _doc

ES 的Type 被废弃后,库表合一,Index 既可以被认为对应 MySQL 的 Database,也可以认为对应 table。

也可以这样理解:

  • ES 实例:对应 MySQL 实例中的一个 Database。
  • Index 对应 MySQL 中的 Table 。
  • Document 对应 MySQL 中表的记录。
  • 修改文档
1
2
3
4
5
6
7
8
9
// PUT /索引名称/类型/id
PUT /es_db/_doc/1
{
"name": "小黑",
"sex": 1,
"age": 25,
"address": "张家界森林公园",
"remark": "php developer assistant"
}

我们对返回值进行一个审查:

如果不添加id,那么ES会帮我们自动生成一个id。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index" : "es_demo", //位于那个索引--->库
"_type" : "_doc", //何种类型 7以后统一为认为_doc
"_id" : "5", //唯一id
"_version" : 2, //数据可能会被更新,故而有版本号
"result" : "updated", //当前操作是属于更新操作
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 5,
"_primary_term" : 2
}

POST和PUT都能起到创建或更新的作用,它们的区别如下:

  1. PUT需要对一个具体的资源进行操作也就是要确定id才能进行更新/创建,不加id会报异常;而POST是可以针对整个资源集合进行操作的,如果不写id就由ES生成一个唯一id进行创建新文档,如果填了id那就针对这个id的文档进行创建/更新。
  2. PUT只会将json数据都进行替换,POST只会更新相同字段的值。
  3. PUT与DELETE都是幂等性操作,即不论操作多少次结果都一样。
  • 查询文档
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
1. 查询指定id文档
GET /索引名称/类型/id

2. 查询所有文档
GET /索引名称/类型/_search

3. 等值、大于、小于查询,A字段的值为B
GET /索引名称/类型/_search?q=A:B
GET /索引名称/类型/_search?q=A:<B
GET /索引名称/类型/_search?q=A:>B

4. 范围查询,A字段的范围是m到n
GET /索引名称/类型/_search?q=A[m TO n]

5. 多id查询
GET /索引名称/类型/_mget
{
"ids":["1","2"]
}

6. 分页查询
GET /索引名称/类型/_search?from=0&size=1

7. 投影
GET /索引名称/类型/_search?_source=字段1,字段2

8. 排序(desc降序、asc升序)
GET /索引名称/类型/_search?sort=字段:desc
  • 删除文档
1
DELETE /索引名称/类型/id

批量操作

  • 批量获取文档数据

批量获取文档数据是通过_mget的API来实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET _mget
{
"docs": [
{
"_index": "es_db",
"_type": "_doc",
"_id": 1
},
{
"_index": "es_db",
"_type": "_doc",
"_id": 2
}
]
}

也可以将索引、类型的信息写在url中:

1
2
3
4
5
6
7
8
9
10
11
GET /es_db/_doc/_mget
{
"docs": [
{
"_id": 3
},
{
"_id": 4
}
]
}
  • 批量操作文档数据,批量对文档进行写操作是通过_bulk的API来实现的

  • 请求方式:POST

  • 请求地址:_bulk

  • 请求参数:通过_bulk操作文档,一般至少有两行参数(或偶数行参数)

    • 第一行参数为指定操作的类型及操作的对象(index,type和id)
    • 第二行参数才是操作的数据

参数类似于:

1
2
{"actionName":{"_index":"indexName", "_type":"typeName","_id":"id"}}
{"field1":"value1", "field2":"value2"}

批量创建文档create

1
2
3
4
5
POST _bulk
{"create":{"_index":"article", "_type":"_doc", "_id":3}}
{"id":3,"title":"文章1","content":"内容1","tags":["java", "面向对象"],"create_time":1554015482530}
{"create":{"_index":"article", "_type":"_doc", "_id":4}}
{"id":4,"title":"文章2","content":"内容2","tags":["java", "面向对象"],"create_time":1554015482530}

普通创建或全量替换index

1
2
3
4
5
POST _bulk
{"index":{"_index":"article", "_type":"_doc", "_id":3}}
{"id":3,"title":"图灵徐庶老师(一)","content":"图灵学院徐庶老师666","tags":["java", "面向对象"],"create_time":1554015482530}
{"index":{"_index":"article", "_type":"_doc", "_id":4}}
{"id":4,"title":"图灵诸葛老师(二)","content":"图灵学院诸葛老师NB","tags":["java", "面向对象"],"create_time":1554015482530}
  • 如果原文档不存在,则是创建
  • 如果原文档存在,则是替换(全量修改原文档)
  • 区别在于actionName

批量删除:

1
2
3
POST _bulk
{"delete":{"_index":"article", "_type":"_doc", "_id":3}}
{"delete":{"_index":"article", "_type":"_doc", "_id":4}}

依靠唯一id进行删除,也就不需要那个field对参数了

批量修改:

1
2
3
4
5
POST _bulk
{"update":{"_index":"article", "_type":"_doc", "_id":3}}
{"doc":{"title":"ES大法必修内功"}}
{"update":{"_index":"article", "_type":"_doc", "_id":4}}
{"doc":{"create_time":1554018421008}}

这里是依靠唯一id确定位置,然后对所需要修改的值进行处理就好了

文档映射

简介

ES中映射可以分为动态映射和静态映射:

  • 动态映射

在关系数据库中,需要事先创建数据库,然后在该数据库下创建数据表,并创建表字段、类型、长度、主键等,最后才能基于表插入数据。而Elasticsearch中不需要定义Mapping映射(即关系型数据库的表、字段等),在文档写入Elasticsearch时,会根据文档字段自动识别类型,这种机制称之为动态映射。动态映射规则如下:

image

  • 静态映射

静态映射是在Elasticsearch中也可以事先定义好映射,包含文档的各字段类型、分词器等,这种方式称之为静态映射。

核心类型

  • 字符串:string类型包含 text 和 keyword

    • text:该类型被用来索引长文本,在创建索引前会将这些文本进行分词,转化为词的组合,建立索引;允许ES来检索这些词,text类型不能用来排序和聚合。
    • keyword:该类型不能分词,可以被用来检索过滤、排序和聚合,keyword类型不可用text进行分词模糊检索。
  • 数值型:long、integer、short、byte、double、float

  • 日期型:date

  • 布尔型:boolean

映射操作

创建映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
PUT /es_db
{
"mappings": {
"properties": {
"name": {
"type": "keyword",
"index": true,
"store": true
},
"sex": {
"type": "integer",
"index": true,
"store": true
},
"age": {
"type": "integer",
"index": true,
"store": true
},
"book": {
"type": "text",
"index": true,
"store": true,
"analyzer": "ik_smart",
"search_analyzer": "ik_smart"
},
"address": {
"type": "text",
"index": true,
"store": true
}
}
}
}

获取文档映射:

1
GET /es_db/_mapping

对已存在的mapping映射进行修改:

  1. 如果要推倒现有的映射,你得重新建立一个静态索引
  2. 然后把之前索引里的数据导入到新的索引里
  3. 删除原创建的索引
  4. 为新索引起个别名, 为原索引名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 数据迁移
POST _reindex
{
"source": {
"index": "db_index"
},
"dest": {
"index": "db_index_2"
}
}

// 删除原索引
DELETE /db_index

// 修改新索引的别名
PUT /db_index_2/_alias/db_index

通过这几个步骤就实现了索引的平滑过渡,并且是零停机。

DSL查询

基本增删改查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//创建索引
//格式: PUT /索引名称
PUT /es_demo

//查询索引
//格式: GET /索引名称
GET /es_demo

//删除索引
//格式: DELETE /索引名称
DELETE /es_demo

//添加文档
//格式: PUT /索引名称/类型/id
PUT /es_demo/_doc/1
{
"name": "张三",
"sex": 1,
"age": 21,
"address": "南京",
"remark": "java developer"
}

//删除文档
//DELETE /索引名称/类型/id
DELETE /es_demo/_doc/1

//修改文档
//PUT /索引名称/类型/id
PUT /es_demo/_doc/1
{
"name": "李多安",
"sex": 1,
"age": 21,
"address": "深圳",
"remark": "go developer"
}

//查询文档
//GET /索引名称/类型/id
GET /es_demo/_doc/1

注意:POST和PUT都能起到创建/更新的作用

需要注意的是PUT需要对一个具体的资源进行操作也就是要确定id才能进行更新/创建,

而POST是可以针对整个资源集合进行操作的,如果不写id就由ES生成一个唯一id进行创建新文档,如果填了id那就针对这个id的文档进行创建/更新

PUT只会将json数据都进行替换, POST只会更新相同字段的值

PUT与DELETE都是幂等性操作, 即不论操作多少次, 结果都一样

简单查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//查询当前类型中的所有文档 _search
//格式: GET /索引名称/类型/_search
//举例: GET /es_demo/_doc/_search
//SQL:  select * from student

//条件查询
格式: GET /索引名称/类型/_search?q=*:***
举例: GET /es_demo/_doc/_search?q=age:28
SQL:  select * from student where age = 28

//范围查询
格式: GET /索引名称/类型/_search?q=***[25 TO 26]
举例: GET /es_demo/_doc/_search?q=age[25 TO 26]
SQL:  select * from student where age between 25 and 26

//根据多个ID进行批量查询
格式: GET /索引名称/类型/_mget
举例: GET /es_demo/_doc/_mget 
{
 "ids":["1","2"]  
 }
SQL:  select * from student where id in (1,2)

//查询年龄小于等于
格式: GET /索引名称/类型/_search?q=age:<=**
举例: GET /es_demo/_doc/_search?q=age:<=28
SQL:  select * from student where age <= 28

//查询年龄大于
格式: GET /索引名称/类型/_search?q=age:>**
举例: GET /es_demo/_doc/_search?q=age:>28
SQL:  select * from student where age > 28

//分页查询
格式: GET /索引名称/类型/_search?q=age[25 TO 26]&from=0&size=1
举例: GET /es_demo/_doc/_search?q=age[25 TO 26]&from=0&size=1
SQL:  select * from student where age between 25 and 26 limit 0, 1

//对查询结果只输出某些字段
格式: GET /索引名称/类型/_search?_source=字段,字段
举例: GET /es_demo/_doc/_search?_source=name,age
SQL:  select name,age from student

//对查询结构进行排序
格式: GET /索引名称/类型/_search?sort=字段 desc
举例: GET /es_demo/_doc/_search?sort=age:desc
SQL:  select * from student order by age desc 【逆序

批量操作

批量获取文档数据

批量获取

批量获取文档数据是使用_mget的API来实现的。

1
2
3
4
5
6
7
8
9
10
11
GET /es_db/_doc/_mget
{
"docs": [
{
"_id": 3
},
{
"_id": 4
}
]
}

批量操作

  • 批量操作文档数据,批量对文档进行写操作是通过_bulk的API来实现的

  • 请求方式:POST

  • 请求地址:_bulk

  • 请求参数:通过_bulk操作文档,一般至少有两行参数(或偶数行参数)

    • 第一行参数为指定操作的类型及操作的对象(index,type和id)
    • 第二行参数才是操作的数据
1
2
{"actionName":{"_index":"indexName", "_type":"typeName","_id":"id"}}
{"field1":"value1", "field2":"value2"}

批量新增文档

1
2
3
4
5
POST _bulk
{"create":{"_index":"article", "_type":"_doc", "_id":3}}
{"id":3,"title":"文章1","content":"内容1","tags":["java", "面向对象"],"create_time":1554015482530}
{"create":{"_index":"article", "_type":"_doc", "_id":4}}
{"id":4,"title":"文章2","content":"内容2","tags":["java", "面向对象"],"create_time":1554015482530}

批量删除:

1
2
3
POST _bulk
{"delete":{"_index":"article", "_type":"_doc", "_id":3}}
{"delete":{"_index":"article", "_type":"_doc", "_id":4}}

依靠唯一id进行删除,也就不需要那个field对参数了

批量修改:

1
2
3
4
5
POST _bulk
{"update":{"_index":"article", "_type":"_doc", "_id":3}}
{"doc":{"title":"ES大法必修内功"}}
{"update":{"_index":"article", "_type":"_doc", "_id":4}}
{"doc":{"create_time":1554018421008}}

这里是依靠唯一id确定位置,然后对所需要修改的值进行处理就好了

批量操作的增删改都是依靠actionName来决定任务的。

DSL查询

image

无条件查询

无查询条件是查询所有,默认是查询所有的,或者使用match_all表示所有:

1
2
3
4
5
6
GET /es_db/_doc/_search
{
"query":{
"match_all":{}
}
}

有条件查询

有条件查询分为叶子查询、组合查询和连接查询。

叶子查询

模糊匹配

模糊匹配主要是针对文本类型的字段,文本类型的字段会对内容进行分词。

查询时也会对搜索条件进行分词,然后通过倒排索引查找到匹配的数据,模糊匹配主要通过match等参数来实现。

模糊匹配主要有三种查询matchprefixregexp三种类型。

  • match

通过match关键词模糊匹配条件内容(通过match查询一个keyword字段时,如果查询内容和该字段内容一模一样,也是可以查出来的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
POST /es_db/_doc/_search
{
"from": 0,
"size": 2,
"query": {
"match": {
"address": "南京" // 如果address是keyword类型的,使用match查询必须完全一致才能查到
}
}
}

// multi_match多字段模糊匹配查询
//两个字段中存在一个就可以
POST /es_db/_doc/_search
{
"query": {
"multi_match": {
"query": "张三",
"fields": [
"address",
"name"
]
}
}
}

// query_string未指定字段条件查询(查询所有字段)
POST /es_db/_doc/_search
{
"query": {
"query_string": {
"query": "广州 OR 长沙"
}
}
}

// query_string指定字段条件查询
POST /es_db/_doc/_search
{
"query": {
"query_string": {
"query": "admin OR 长沙",
"fields": [
"name",
"address"
]
}
}
}

match条件还支持以下参数:

  • query : 指定匹配的值
  • operator : 匹配条件类型
    • and : 条件分词后都要匹配
    • or : 条件分词后有一个匹配即可(默认)
  • minmum_should_match : 指定最小匹配的数量

注意query_string的用法,这个地方的如果不加field列,就会所有字段扫描。

  • prefix

前缀匹配

1
2
3
4
5
6
7
8
9
10
GET /es_db/_search
{
"query": {
"prefix": {
"name.keyword": {
"value": "li"
}
}
}
}

使用前缀匹配通常针对keyword类型字段(大小写敏感),也就是不分词的字段。前缀搜索效率比较低,并且前缀搜索不会计算相关度分数。前缀越短,效率越低。如果使用前缀搜索,建议使用长前缀。因为前缀搜索需要扫描完整的索引内容,所以前缀越长,相对效率越高。

  • wildcard

ES中也有通配符。但是和java还有数据库不太一样。通配符可以在倒排索引中使用,也可以在keyword类型字段中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
// ? 匹配一个任意字符
// * 匹配0~n个任意字符

GET /es_db/_search
{
"query": {
"wildcard": {
"name": {
"value": "*d*n*"
}
}
}
}
  • regexp

通过正则表达式来匹配数据

1
2
3
4
5
6
7
8
GET /es_db/_search
{
"query": {
"regexp": {
"name": "[A-z].+"
}
}
}

常用符号:

[] 表示范围,如: [0-9]是0~9的范围数字

. 表示一个字符

+ 表示前面的表达式可以出现多次

  • fuzzy

模糊查询

1
2
3
4
5
6
7
8
9
10
11
GET /es_db/_search
{
"query": {
"fuzzy": {
"remark": {
"value": "jeva",
"fuzziness": 2
}
}
}
}

搜索的时候,可能搜索条件文本输入错误,如:hello world -> hello word。这种拼写错误还是很常见的。fuzzy技术就是用于解决错误拼写的(在英文中很有效,在中文中几乎无效)。其中fuzziness代表value的值word可以修改多少个字母来进行拼写错误的纠正(修改字母的数量包含字母变更、增加或减少)。

精确匹配
  • term

单个条件相等

1
2
3
4
5
6
7
8
9
// term查询不会对字段进行分词查询,会采用精确匹配 
POST /es_db/_doc/_search
{
"query": {
"term": {
"name": "admin"
}
}
}
  • terms

单个字段属于某个值数组内的值

1
2
3
4
5
6
7
8
POST /es_db/_doc/_search
{
"query": {
"terms": {
"name": ["admin","liduoan"]
}
}
}
  • range

字段属于某个范围内的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 分页、范围、投影、排序
POST /es_db/_doc/_search
{
"query": {
"range": {
"age": {
"gte": 25,
"lte": 28
}
}
},
"from": 0,
"size": 2,
"_source": [
"name",
"age",
"address"
],
"sort": {
"age": "desc"
}
}

range:范围关键字

gte:大于等于

lte:小于等于

gt:大于

lt:小于

now:当前时间

1. match

match:模糊匹配,需要指定字段名,但是输入会进行分词,比如”hello world”会进行拆分为hello和world,然后匹配,如果字段中包含hello或者world,或者都包含的结果都会被查询出来,也就是说match是一个部分匹配的模糊查询。查询条件相对来说比较宽松。

2、term

term: 这种查询和match在有些时候是等价的,比如我们查询单个的词hello,那么会和match查询结果一样,但是如果查询”hello world”,结果就相差很大,因为这个输入不会进行分词,就是说查询的时候,是查询字段分词结果中是否有”hello world”的字样,而不是查询字段中包含”hello world”的字样。当保存数据”hello world”时,ES会对字段内容进行分词,”hello world”会被分成hello和world,不存在”hello world”,因此这里的查询结果会为空。这也是term查询和match的区别。

3. match_phase

match_phase:会对输入做分词,但是需要结果中也包含所有的分词,而且顺序要求一样。以”hello world”为例,要求结果中必须包含hello和world,而且还要求他们是连着的,顺序也是固定的,hello that world不满足,world hello也不满足条件。

4、query_string

query_string:和match类似,但是match需要指定字段名,query_string是在所有字段中搜索,范围更广泛。

查询与过滤

DSL查询语言中存在两种,查询DSL(query DSL)和过滤DSL(filter DSL)。它们两个的区别如下图:

image

query DSL

在查询上下文中,查询不仅会检查文档是否匹配,还会计算匹配的相关度

如何验证匹配很好理解,如何计算相关度呢?ES中索引的数据都会存储一个_score分值,分值越高就代表越匹配。

另外关于某个搜索的分值计算还是很复杂的,因此也需要一定的时间。

filter DSL

在过滤器上下文中,只会判断文档是否匹配。答案很简单,是或者不是。它不会去计算任何分值,也不会关心返回的排序问题,因此效率会高一点。过滤上下文是在使用filter参数时候的执行环境。另外,经常使用过滤器,ES会自动的缓存过滤器的内容,这对于查询来说,会提高很多性能。

组合条件查询

组合条件查询是将叶子条件查询语句进行组合而形成的一个完整的查询条件:

  • bool : 各条件之间有and、or或not的关系
    • must : 各个条件都必须满足,即各条件是and的关系
    • should : 各个条件有一个满足即可,即各条件是or的关系
    • must_not : 不满足所有条件,即各条件是not的关系
    • filter : 不计算相关度评分,它不计算_score即相关度评分,效率更高
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
GET /test_a/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"f": "java spark"
}
}
],
"should": [
{
"match_phrase": {
"f": {
"query": "java spark",
"slop": 50
}
}
}
]
}
}
}

must/filter/shoud/must_not的子条件是通过term/terms/range/ids/exists/match叶子条件作为参数的。
以上参数,当只有一个搜索条件时,must等对应的是一个对象;
当是多个条件时,对应的是一个数组。

搜索精度控制

先看下面一条查询:

1
2
3
4
5
6
7
8
9
10
11
GET /es_db/_search
{
"query": {
"match": {
"remark": {
"query": "java developer",
"operator": "and" // 表示两个词都要包含才能匹配
}
}
}
}

上述语法中,如果将operator的值改为or。则与第一个案例搜索语法效果一致。默认的ES执行搜索的时候,operator就是or。

如果在搜索的结果document中,需要remark字段中包含多个搜索词条中的一定比例,可以使用下述语法实现搜索。其中minimum_should_match可以使用百分比固定数字。百分比代表query搜索条件中词条百分比,如果无法整除,向下匹配(如query条件有3个单词,如果使用百分比提供精准度计算,那么是无法除尽的,如果需要至少匹配两个单词,则需要用67%来进行描述。如果使用66%描述,ES则认为匹配一个单词即可)。固定数字代表query搜索条件中的词条,至少需要匹配多少个:

1
2
3
4
5
6
7
8
9
10
11
GET /es_db/_search
{
"query": {
"match": {
"remark": {
"query": "java architect assistant",
"minimum_should_match": "68%"
}
}
}
}

如果使用should+bool搜索的话,也可以控制搜索条件的匹配度。具体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
GET /es_db/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"remark": "java"
}
},
{
"match": {
"remark": "developer"
}
},
{
"match": {
"remark": "assistant"
}
}
],
"minimum_should_match": 2
}
}
}

代表搜索的document中的remark字段中,必须匹配java、developer、assistant三个词条中的至少2个。

DSL聚合

聚合的格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /index_name/type_name/_search
{
"aggs": {
"定义分组名称(最外层)": {
"分组策略如:terms、avg、sum": {
"field": "根据哪一个字段分组",
"其他参数": ""
},
"aggs": {
"分组名称1": {},
"分组名称2": {}
}
}
}
}

aggs可以嵌套定义,可以水平定义。嵌套定义称为下钻分析,而水平定义就是平铺多个分组方式。

Bucket与Metric

  • bucket就是一个聚合搜索时的数据分组。如:销售部门有员工张三和李四,开发部门有员工王五和赵六。那么根据部门分组聚合得到结果就是两个bucket。销售部门bucket中有张三和李四,开发部门 bucket中有王五和赵六。
  • metric就是对一个bucket数据执行的统计分析。如上述案例中,开发部门有2个员工,销售部门有2个员工,这就是metric。metric有多种统计,如:求和,最大值,最小值,平均值等。
1
2
3
# 用一个大家容易理解的SQL语法来解释
select count(*) from table group by column
# 那么group by column分组后的每组数据就是bucket,再对每个分组执行的count(*)就是metric。

我们准备一些汽车的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
PUT /cars
{
"mappings": {
"properties": {
"price": {
"type": "long"
},
"color": {
"type": "keyword"
},
"brand": {
"type": "keyword"
},
"model": {
"type": "keyword"
},
"sold_date": {
"type": "date"
},
"remark": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}

POST /cars/_bulk
{ "index": {}}
{ "price" : 258000, "color" : "金色", "brand":"大众", "model" : "大众迈腾", "sold_date" : "2021-10-28","remark" : "大众中档车" }
{ "index": {}}
{ "price" : 123000, "color" : "金色", "brand":"大众", "model" : "大众速腾", "sold_date" : "2021-11-05","remark" : "大众神车" }
{ "index": {}}
{ "price" : 239800, "color" : "白色", "brand":"标志", "model" : "标志508", "sold_date" : "2021-05-18","remark" : "标志品牌全球上市车型" }
{ "index": {}}
{ "price" : 148800, "color" : "白色", "brand":"标志", "model" : "标志408", "sold_date" : "2021-07-02","remark" : "比较大的紧凑型车" }
{ "index": {}}
{ "price" : 1998000, "color" : "黑色", "brand":"大众", "model" : "大众辉腾", "sold_date" : "2021-08-19","remark" : "大众最让人肝疼的车" }
{ "index": {}}
{ "price" : 218000, "color" : "红色", "brand":"奥迪", "model" : "奥迪A4", "sold_date" : "2021-11-05","remark" : "小资车型" }
{ "index": {}}
{ "price" : 489000, "color" : "黑色", "brand":"奥迪", "model" : "奥迪A6", "sold_date" : "2022-01-01","remark" : "政府专用?" }
{ "index": {}}
{ "price" : 1899000, "color" : "黑色", "brand":"奥迪", "model" : "奥迪A 8", "sold_date" : "2022-02-12","remark" : "很贵的大A6。。。" }

简单聚合

只执行聚合分组,不做复杂的聚合统计。在ES中最基础的聚合为terms,在ES中默认为分组数据做排序时,使用的是doc_count数据执行降序排列,也就是说包含文档越多的分组排在越前面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /cars/_search
{
"size": 0, // size为0表示不返回原数据,只返回聚合结果
"aggs": {
"group_by_color": {
"terms": {
"field": "color",
"order": {
"_count": "desc" // 默认就是根据该值排序
}
}
}
}
}

平均值

本案例先根据color执行聚合分组,在此分组的基础上,对组内数据执行聚合统计,这个组内数据的聚合统计就是metric:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GET /cars/_search
{
"size": 0,
"aggs": {
"group_by_color": { // 根据颜色聚合
"terms": {
"field": "color",
"order": {
"avg_by_price": "asc" // 根据平均价格排序
}
},
"aggs": {
"avg_by_price": { // 求出每组的平均价格
"avg": {
"field": "price"
}
}
}
}
}
}

当然还有更复杂的,先根据color聚合分组,在组内根据brand再次聚合分组,最后求出同一个color分组中每个brand分组的平均价格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
GET /cars/_search
{
"size": 0,
"aggs": {
"group_by_color": { // 根据color分组
"terms": {
"field": "color"
},
"aggs": {
"group_by_brand": { // 根据brand分组
"terms": {
"field": "brand",
"order": {
"avg_by_price": "desc"
}
},
"aggs": {
"avg_by_price": { // 最后求brand分组的平均值
"avg": {
"field": "price"
}
}
}
}
}
}
}
}

最大最小与总和

求出每个color分组的最高价格、最低价格和总价格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
GET /cars/_search
{
"size": 0,
"aggs": {
"group_by_color": {
"terms": {
"field": "color"
},
"aggs": {
"max_price": {
"max": {
"field": "price"
}
},
"min_price": {
"min": {
"field": "price"
}
},
"sum_price": {
"sum": {
"field": "price"
}
}
}
}
}
}

在常见的业务常见中,聚合分析,最常用的种类就是统计数量、最大、最小、平均、总计等。通常占有聚合业务中的60%以上的比例,小型项目中,甚至占比85%以上。

排序

对聚合统计数据进行排序。如统计每个品牌的汽车销量和销售总额,按照销售总额的降序排列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GET /cars/_search
{
"size": 0,
"aggs": {
"group_of_brand": {
"terms": {
"field": "brand",
"order": {
"sum_of_price": "desc"
}
},
"aggs": {
"sum_of_price": {
"sum": {
"field": "price"
}
}
}
}
}
}

如果有多层aggs,执行下钻聚合的时候,也可以根据最内层聚合数据执行排序。如:统计每个品牌中每种颜色车辆的销售总额,并根据销售总额降序排列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
GET /cars/_search
{
"aggs": {
"group_by_brand": {
"terms": {
"field": "brand"
},
"aggs": {
"group_by_color": {
"terms": {
"field": "color",
"order": {
"sum_of_price": "desc"
}
},
"aggs": {
"sum_of_price": {
"sum": {
"field": "price"
}
}
}
}
}
}
}
}

但是注意:只能组内数据排序,而不能跨组实现排序。

排名

如果要统计不同brand汽车中价格排名最高的车型该如何做?可以使用top_hits来实现,其中属性如下:

  • size代表取组内多少条数据(默认为10);
  • sort代表组内使用什么字段什么规则排序(默认使用_doc的asc规则排序);
  • _source代表结果中包含document中的那些字段(默认包含全部字段)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
GET cars/_search
{
"size": 0,
"aggs": {
"group_by_brand": { // 根据brand分组
"terms": {
"field": "brand"
},
"aggs": {
"top_car": { // 统计最高价格的车
"top_hits": { // 通过top_hits进行统计
"size": 1, // 只保留一个,那它就是最高的
"sort": [ // 根据价格降序
{
"price": {
"order": "desc"
}
}
],
"_source": { // 只保留model和price字段
"includes": [
"model",
"price"
]
}
}
}
}
}
}
}