本文大部分内容转载至(
ElasticSearch权威指南中文版
@路小磊
)
在本节中,我们会以一个具体的实力来复习一下ElasticSearch里的索引,搜索和聚合的功能。
一、索引
1.建立一个员工目录,需求如下:
1.数据能够包含多个值的标签、数字和纯文本。
2.检索任何员工的所有信息。
3.支持结构化搜索,例如查找30岁以上的员工。
4.支持简单的全文搜索和更复杂的短语(phrase)搜索
5.高亮搜索结果中的关键字
6.能够利用图表管理分析这些数据
2.索引员工文档
我们首先要做的是存储员工数据,每个文档代表一个员工。在Elasticsearch中存储数据的行为就叫做索引(indexing),不过在索引之前,我们需要明确数据应该存储在哪里。
在Elasticsearch中,文档归属于一种类型(type),而这些类型存在于索引(index)中,我们可以画一些简单的对比图来类比传统关系型数据库:
Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices -> Types -> Documents -> Fields
Elasticsearch集群可以包含多个索引(indices)(数据库),每一个索引可以包含多个类型(types)(表),每一个类型包含多个文档(documents)(行),然后每个文档包含多个字段(Fields)(列)。
所以为了创建员工目录,我们将进行如下操作(如果没用特殊说明,以后这些操作默认在Sense下执行):
-
为每个员工的文档(document)建立索引,每个文档包含了相应员工的所有信息。
-
每个文档的类型为employee。
-
employee类型归属于索引megacorp。
-
megacorp索引存储在Elasticsearch集群中。
PUT /megacorp/employee/
1
{
"first_name"
:
"Card"
,
"last_name"
:
"Minutch"
,
"age"
:
24
,
"about"
:
"I love basketball and games"
,
"interests"
:[
"LOL"
,
"Music"
,
"NBA"
]
}
|
执行后返回:
{
"_index"
:
"megacorp"
,
"_type"
:
"employee"
,
"_id"
:
"1"
,
"_version"
:
1
,
"created"
:
true
}
|
我们看到path:/megacorp/employee/1
包含三部分信息:
megacorp | 索引名 |
employee | 类型名 |
1 | 这个员工的ID |
名字
|
说明
|
---|
接下来,让我们在目录中加入更多员工信息:
PUT /megacorp/employee/
2
{
"first_name"
:
"YiLin"
,
"last_name"
:
"Wang"
,
"age"
:
25
,
"about"
:
"She is a dancer"
,
"interests"
:[
"Movie"
,
"Dance"
]
}
|
PUT /megacorp/employee/
3
{
"first_name"
:
"Huan"
,
"last_name"
:
"Zhou"
,
"age"
:
23
,
"about"
:
"I like singing!"
,
"interests"
:[
"Movie"
,
"Sing"
,
"NBA"
]
}
|
二、搜索
1.检索文档
现在Elasticsearch中已经存储了一些数据,我们可以根据业务需求开始工作了。第一个需求是能够检索单个员工的信息。
1)、这对于Elasticsearch来说非常简单。我们只要执行HTTP GET请求并指出文档的“地址”——索引、类型和ID既可。根据这三部分信息,我们就可以返回原始JSON文档:
GET /megacorp/employee/
1
|
{
"_index"
:
"megacorp"
,
"_type"
:
"employee"
,
"_id"
:
"1"
,
"_version"
:
1
,
"found"
:
true
,
"_source"
: {
"first_name"
:
"Card"
,
"last_name"
:
"Minutch"
,
"age"
:
24
,
"about"
:
"i love basketball and games"
,
"interests"
: [
"LOL"
,
"Music"
,
"NBA"
]
}
}
|
2.简单搜索
GET
请求非常简单——你能轻松获取你想要的文档。让我们来进一步尝试一些东西,比如简单的搜索!
2)、我们尝试一个最简单的搜索全部员工的请求:
GET /megacorp/employee/_search
|
你可以看到我们依然使用megacorp
索引和employee
类型,但是我们在结尾使用关键字_search
来取代原来的文档ID。响应内容的hits
数组中包含了我们所有的三个文档。默认情况下搜索会返回前10个结果。
{
"took"
:
33
,
"timed_out"
:
false
,
"_shards"
: {
"total"
:
5
,
"successful"
:
5
,
"failed"
:
0
},
"hits"
: {
"total"
:
3
,
"max_score"
:
1
,
"hits"
: [
{
"_index"
:
"megacorp"
,
"_type"
:
"employee"
,
"_id"
:
"1"
,
"_score"
:
1
,
"_source"
: {
"first_name"
:
"Card"
,
"last_name"
:
"Minutch"
,
"age"
:
24
,
"about"
:
"i love basketball and games"
,
"interests"
: [
"LOL"
,
"Music"
,
"NBA"
]
}
},
{
"_index"
:
"megacorp"
,
"_type"
:
"employee"
,
"_id"
:
"2"
,
"_score"
:
1
,
"_source"
: {
"first_name"
:
"YiLin"
,
"last_name"
:
"Wang"
,
"age"
:
25
,
"about"
:
"She is a dancer"
,
"interests"
: [
"Movie"
,
"Dance"
]
}
},
{
"_index"
:
"megacorp"
,
"_type"
:
"employee"
,
"_id"
:
"3"
,
"_score"
:
1
,
"_source"
: {
"first_name"
:
"Huan"
,
"last_name"
:
"Zhou"
,
"age"
:
23
,
"about"
:
"I like singing!"
,
"interests"
: [
"Movie"
,
"Sing"
,
"NBA"
]
}
}
]
}
}
|
3)、接下来,让我们搜索姓氏中包含“Minutch”的员工。要做到这一点,我们将在命令行中使用轻量级的搜索方法。这种方法常被称作查询字符串(query string)搜索,因为我们像传递URL参数一样去传递查询语句:
GET /megacorp/employee/_search?q=last_name=Minutch
|
{
"took"
:
15
,
"timed_out"
:
false
,
"_shards"
: {
"total"
:
5
,
"successful"
:
5
,
"failed"
:
0
},
"hits"
: {
"total"
:
1
,
"max_score"
:
0.01125201
,
"hits"
: [
{
"_index"
:
"megacorp"
,
"_type"
:
"employee"
,
"_id"
:
"1"
,
"_score"
:
0.01125201
,
"_source"
: {
"first_name"
:
"Card"
,
"last_name"
:
"Minutch"
,
"age"
:
24
,
"about"
:
"i love basketball and games"
,
"interests"
: [
"LOL"
,
"Music"
,
"NBA"
]
}
}
]
}
}
|
3.使用DSL语句查询
查询字符串搜索便于通过命令行完成特定(ad hoc)的搜索,但是它也有局限性(参阅简单搜索章节)。Elasticsearch提供丰富且灵活的查询语言叫做DSL查询(Query DSL),它允许你构建更加复杂、强大的查询。
DSL(Domain Specific Language特定领域语言)以JSON请求体的形式出现。我们可以这样表示之前关于“Minutch”的查询:
GET /megacorp/employee/_search
{
"query"
:{
"match"
: {
"last_name"
:
"Minutch"
}
}
}
|
这会返回与之前查询相同的结果。你可以看到有些东西改变了,我们不再使用查询字符串(query string)做为参数,而是使用请求体代替。这个请求体使用JSON表示,其中使用了match
语句(查询类型之一,具体我们以后会学到)。
4.更复杂的搜索
1)、我们让搜索稍微再变的复杂一些。我们依旧想要找到姓氏为“Minutch”的员工,但是我们只想得到年龄大于30岁的员工。我们的语句将添加过滤器(filter),它使得我们高效率的执行一个结构化搜索:
GET /megacorp/employee/_search
{
"query"
:{
"filtered"
: {
"query"
: {
"match"
: {
"last_name"
:
"Minutch"
<
2
>
}
},
"filter"
: {
"range"
: {
"age"
: {
"gte"
:
30
<
1
>
}
}
}
}
}
}
|
- <1> 这部分查询属于区间过滤器(range filter),它用于查找所有年龄大于30岁的数据——
gt
为"greater than"的缩写。 - <2> 这部分查询与之前的
match
语句(query)一致。
结果如下:
{
"took"
:
9
,
"timed_out"
:
false
,
"_shards"
: {
"total"
:
5
,
"successful"
:
5
,
"failed"
:
0
},
"hits"
: {
"total"
:
0
,
"max_score"
:
null
,
"hits"
: []
}
}
|
2)、我们依旧想要找到姓氏为“Minutch”的员工,但是我们只想得到年龄小于30岁的员工。我们的语句将添加过滤器(filter),它使得我们高效率的执行一个结构化搜索:
GET /megacorp/employee/_search
{
"query"
:{
"filtered"
: {
"query"
: {
"match"
: {
"last_name"
:
"Minutch"
<
2
>
}
},
"filter"
: {
"range"
: {
"age"
: {
"lte"
:
30
<
1
>
}
}
}
}
}
}
|
{
"took"
:
8
,
"timed_out"
:
false
,
"_shards"
: {
"total"
:
5
,
"successful"
:
5
,
"failed"
:
0
},
"hits"
: {
"total"
:
1
,
"max_score"
:
0.30685282
,
"hits"
: [
{
"_index"
:
"megacorp"
,
"_type"
:
"employee"
,
"_id"
:
"1"
,
"_score"
:
0.30685282
,
"_source"
: {
"first_name"
:
"Card"
,
"last_name"
:
"Minutch"
,
"age"
:
24
,
"about"
:
"i love basketball and games"
,
"interests"
: [
"LOL"
,
"Music"
,
"NBA"
]
}
}
]
}
}
|
5.全文搜索
到目前为止搜索都很简单:搜索特定的名字,通过年龄筛选。让我们尝试一种更高级的搜索,全文搜索——一种传统数据库很难实现的功能。
我们将会搜索所有喜欢“basketball games dancer”的员工:
GET /megacorp/employee/_search
{
"query"
:{
"match"
: {
"about"
:
"basketball games dancer"
}
}
}
|
{
"took"
:
13
,
"timed_out"
:
false
,
"_shards"
: {
"total"
:
5
,
"successful"
:
5
,
"failed"
:
0
},
"hits"
: {
"total"
:
2
,
"max_score"
:
0.05038611
,
"hits"
: [
{
"_index"
:
"megacorp"
,
"_type"
:
"employee"
,
"_id"
:
"1"
,
"_score"
:
0.05038611
,
"_source"
: {
"first_name"
:
"Card"
,
"last_name"
:
"Minutch"
,
"age"
:
24
,
"about"
:
"i love basketball and games"
,
"interests"
: [
"LOL"
,
"Music"
,
"NBA"
]
}
},
{
"_index"
:
"megacorp"
,
"_type"
:
"employee"
,
"_id"
:
"2"
,
"_score"
:
0.010844367
,
"_source"
: {
"first_name"
:
"YiLin"
,
"last_name"
:
"Wang"
,
"age"
:
25
,
"about"
:
"She is a dancer"
,
"interests"
: [
"Movie"
,
"Dance"
]
}
}
]
}
}
|
默认情况下,Elasticsearch根据结果相关性评分来对结果集进行排序,所谓的「结果相关性评分」就是文档与查询条件的匹配程度。很显然,排名第一的Minutch
的about
字段明确的写到“basketball and games”。
但是为什么Wang
也会出现在结果里呢?原因是“dancer”在她的abuot
字段中被提及了。因为只有“dancer”被提及而“backetball games”没有,所以她的_score
要低于Minutch。
这个例子很好的解释了Elasticsearch如何在各种文本字段中进行全文搜索,并且返回相关性最大的结果集。相关性(relevance)的概念在Elasticsearch中非常重要,而这个概念在传统关系型数据库中是不可想象的,因为传统数据库对记录的查询只有匹配或者不匹配。
6.高亮我们的搜索
很多应用喜欢从每个搜索结果中高亮(highlight)匹配到的关键字,这样用户可以知道为什么这些文档和查询相匹配。在Elasticsearch中高亮片段是非常容易的。
让我们在之前的语句上增加highlight
参数:
GET /megacorp/employee/_search
{
"query"
:{
"match"
: {
"about"
:
"basketball games Dancer"
}
},
"highlight"
: {
"fields"
: {
"about"
:{}
}
}
}
|
当我们运行这个语句时,会命中与之前相同的结果,但是在返回结果中会有一个新的部分叫做highlight
,这里包含了来自about
字段中的文本,并且用<em></em>
来标识匹配到的单词。
{
"took"
:
116
,
"timed_out"
:
false
,
"_shards"
: {
"total"
:
5
,
"successful"
:
5
,
"failed"
:
0
},
"hits"
: {
"total"
:
2
,
"max_score"
:
0.05038611
,
"hits"
: [
{
"_index"
:
"megacorp"
,
"_type"
:
"employee"
,
"_id"
:
"1"
,
"_score"
:
0.05038611
,
"_source"
: {
"first_name"
:
"Card"
,
"last_name"
:
"Minutch"
,
"age"
:
24
,
"about"
:
"i love basketball and games"
,
"interests"
: [
"LOL"
,
"Music"
,
"NBA"
]
},
"highlight"
: {
"about"
: [
"i love <em>basketball</em> and <em>games</em>"
]
}
},
{
"_index"
:
"megacorp"
,
"_type"
:
"employee"
,
"_id"
:
"2"
,
"_score"
:
0.010844367
,
"_source"
: {
"first_name"
:
"YiLin"
,
"last_name"
:
"Wang"
,
"age"
:
25
,
"about"
:
"She is a dancer"
,
"interests"
: [
"Movie"
,
"Dance"
]
},
"highlight"
: {
"about"
: [
"She is a <em>dancer</em>"
]
}
}
]
}
}
|
三、聚合
1.分析
最后,我们还有一个需求需要完成:允许管理者在职员目录中进行一些分析。 Elasticsearch有一个功能叫做聚合(aggregations),它允许你在数据上生成复杂的分析统计。它很像SQL中的GROUP BY
但是功能更强大。
1)、举个例子,让我们找到所有职员中最大的共同点(兴趣爱好)是什么:
GET /megacorp/employee/_search
{
"aggs"
: {
"all_interests"
: {
"terms"
: {
"field"
:
"interests"
}
}
}
}
|
根据结果:两人爱看电影,两人爱nba,一个人爱跳舞....
{
"took"
:
27
,
"timed_out"
:
false
,
"_shards"
: {
"total"
:
5
,
"successful"
:
5
,
"failed"
:
0
},
"hits"
: {
...
},
"aggregations"
: {
"all_interests"
: {
"doc_count_error_upper_bound"
:
0
,
"sum_other_doc_count"
:
0
,
"buckets"
: [
{
"key"
:
"movie"
,
"doc_count"
:
2
},
{
"key"
:
"nba"
,
"doc_count"
:
2
},
{
"key"
:
"dance"
,
"doc_count"
:
1
},
{
"key"
:
"lol"
,
"doc_count"
:
1
},
{
"key"
:
"music"
,
"doc_count"
:
1
},
{
"key"
:
"sing"
,
"doc_count"
:
1
}
]
}
}
}
|
2)、所有姓"Minutch"的人的爱好
GET /megacorp/employee/_search
{
"query"
: {
"match"
: {
"last_name"
:
"Minutch"
}
},
"aggs"
: {
"all_interests"
: {
"terms"
: {
"field"
:
"interests"
}
}
}
}
|
{
"took"
:
15
,
"timed_out"
:
false
,
"_shards"
: {
"total"
:
5
,
"successful"
:
5
,
"failed"
:
0
},
"hits"
: {
"total"
:
1
,
"max_score"
:
0.30685282
,
"hits"
: [
{
"_index"
:
"megacorp"
,
"_type"
:
"employee"
,
"_id"
:
"1"
,
"_score"
:
0.30685282
,
"_source"
: {
"first_name"
:
"Card"
,
"last_name"
:
"Minutch"
,
"age"
:
24
,
"about"
:
"i love basketball and games"
,
"interests"
: [
"LOL"
,
"Music"
,
"NBA"
]
}
}
]
},
"aggregations"
: {
"all_interests"
: {
"doc_count_error_upper_bound"
:
0
,
"sum_other_doc_count"
:
0
,
"buckets"
: [
{
"key"
:
"lol"
,
"doc_count"
:
1
},
{
"key"
:
"music"
,
"doc_count"
:
1
},
{
"key"
:
"nba"
,
"doc_count"
:
1
}
]
}
}
}
|
3)、聚合也允许分级汇总。例如,让我们统计每种兴趣下职员的平均年龄:
GET /megacorp/employee/_search
{
"aggs"
: {
"all_interests"
: {
"terms"
: {
"field"
:
"interests"
},
"aggs"
: {
"avg_age"
: {
"avg"
: {
"field"
:
"age"
}
}
}
}
}
}
|
{
"took"
:
64
,
"timed_out"
:
false
,
"_shards"
: {
"total"
:
5
,
"successful"
:
5
,
"failed"
:
0
},
"hits"
: {
...
},
"aggregations"
: {
"all_interests"
: {
"doc_count_error_upper_bound"
:
0
,
"sum_other_doc_count"
:
0
,
"buckets"
: [
{
"key"
:
"movie"
,
"doc_count"
:
2
,
"avg_age"
: {
"value"
:
24
}
},
{
"key"
:
"nba"
,
"doc_count"
:
2
,
"avg_age"
: {
"value"
:
23.5
}
},
{
"key"
:
"dance"
,
"doc_count"
:
1
,
"avg_age"
: {
"value"
:
25
}
},
{
"key"
:
"lol"
,
"doc_count"
:
1
,
"avg_age"
: {
"value"
:
24
}
},
{
"key"
:
"music"
,
"doc_count"
:
1
,
"avg_age"
: {
"value"
:
24
}
},
{
"key"
:
"sing"
,
"doc_count"
:
1
,
"avg_age"
: {
"value"
:
23
}
}
]
}
}
}
|
该聚合结果比之前的聚合结果要更加丰富。我们依然得到了兴趣以及数量(指具有该兴趣的员工人数)的列表,但是现在每个兴趣额外拥有avg_age
字段来显示具有该兴趣员工的平均年龄。
即使你还不理解语法,但你也可以大概感觉到通过这个特性可以完成相当复杂的聚合工作,你可以处理任何类型的数据