前言
对大量数据的存储,检索和分析是现在开发中不可避免的,但以往使用SQL数据库存储、检索数据,显然已无法支持现在动辄千万上亿的海量数据,大量数据写入数据库导致数据写入慢和查询检索更慢,无疑是现在大数据存储和分析的痛点和亟需解决的问题,故需要引入专业引擎Elasticsearch来存储、搜索和分析数据,解决海量数据存储和搜索的问题
一、Elasticsearch简介
Elasticsearch 是一个分布式、高扩展、高实时的RESTful 风格的搜索与数据分析引擎。它能很方便的使大量数据具有搜索、分析和探索的能力。充分利用Elasticsearch的水平伸缩性,能使数据在生产环境变得更有价值。Elasticsearch 的实现原理主要分为以下几个步骤,首先用户将数据提交到Elasticsearch 数据库中,再通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据,当用户搜索数据时候,再根据权重将结果排名,打分,再将返回结果呈现给用户。
二、Elasticsearch概念和术语介绍
1、文档
- 文档为ES(Elasticsearch简称,后文续用)存储数据的基础单元,一条数据就是一个文档;
- 对比SQL数据库概念,可以理解文档为表中的一行数据为一个文档;
- ES的文档为单数或多数实体或对象可以被序列化为包含键值对的 JSON 对象;
示例:
{
"name": "John Smith",
"age": 42,
"confirmed": true,
"join_date": "2014-06-01",
"home": {
"lat": 51.5,
"lon": 0.1
},
"accounts": [
{
"type": "facebook",
"id": "johnsmith"
},
{
"type": "twitter",
"id": "johnsmith"
}
]
}
2、索引
- 索引为文档集合,一个索引应该是因共同的特性被分组到一起的文档集合,例如存储同一天的日志文档在索引“log-2020.08.16”或存储所有的产品在索引"products"中;
- 对比SQL数据库概念,可以理解索引为一张表;
3、文档元数据
一个文档不仅仅包含它的数据,也包含元数据有关文档的信息。三个必须的元数据元素如下:
- _index 文档在哪存放
- _type 文档表示的对象类别
- _id 文档唯一标识
_index为文档所在的索引,_type为文档类型用于同索引下不同类别文档的区分,_id 是一个字符串,为文档标识
通过以上三个元数据元素可以准确的找到其标记的文档
4、elasticsearch URL的基本格式
http://localhost:9200/<index>/<type>/[<id>]
其中index、type是必须提供的。id是可选的,不提供es会自动生成。这个三个即上述文档元数据,可准确的定位到某个文档
注: 在url后面加"?pretty",会让返回结果以工整的方式展示出来,适用所有操作数据类的url。"?"表示引出条件,"pretty"是条件内容。
采用http的不同动作,来区分不同操作:
- GET: 获取资源
- POST:创建或更新资源
- PUT: 创建或更新资源
- DELETE:删除资源
以下示例均不在写ip和端口,请在实际使用中添加
三、Elasticsearch索引管理
1、创建索引
索引在创建文档时可自动创建,自动创建的索引采用的是默认的配置,当然我们也可以手动创建索引,并且设置我们想要的配置(例如设置主分片数量、分析器和映射等等),手动创建方式为:
PUT /zhang-local-2020.08.26
{
"settings": { ... any settings ... },
"mappings": {
"type_one": { ... any mappings ... },
"type_two": { ... any mappings ... },
...
}
}
2、删除索引
删除索引可删除单个、多个、全部,方式分别为:
删除单个索引:
DELETE /zhang-local-2020.08.26
删除多个索引:
DELETE /zhang-local-2020.08.26,zhang-local-2020.08.25
或
DELETE /zhang-local-*
删除全部索引:
DELETE /_all
或
DELETE /*
四、Elasticsearch数据文档的增删改
1、创建新文档
向zhang-local-2020.08.26索引中添加一条日志记录
POST /zhang-local-2020.08.26/doc/
{
"endTime": "2020-07-10 17:02:21",
"requestIp": "127.0.0.1",
"receiveHost": "127.0.0.1:7002",
"startTime": "2020-07-10 17:02:21",
"crtDate": "2020-07-10 17:02:21",
"capAddress": "",
"msgId": "202007101702215395068",
"serviceCode": "openRest_open_POST"
}
此处URL中没有写id,交由ES自动生成,也可自己的指定例如:
PUT /zhang-local-2020.08.26/doc/123
{
"endTime": "2020-07-10 17:02:21",
"requestIp": "127.0.0.1",
"receiveHost": "127.0.0.1:7002",
"startTime": "2020-07-10 17:02:21",
"crtDate": "2020-07-10 17:02:21",
"capAddress": "",
"msgId": "202007101702215395068",
"serviceCode": "openRest_open_POST"
}
说明:只有当确定id时,才能使用PUT,否者会报错
2、删除文档
DELETE /zhang-local-2020.08.26/doc/123
由index、type、id准确定位文档然后删除
3、更新文档
更新可以直接覆盖,直接拿新日志覆盖原来的id为123的日志,例如:
PUT /zhang-local-2020.08.26/doc/123
{
"endTime": "2020-07-10 17:02:21",
"requestIp": "127.0.0.1",
"receiveHost": "127.0.0.1:7002",
"startTime": "2020-07-10 17:02:21",
"crtDate": "2020-07-10 17:02:21",
"capAddress": "123",
"msgId": "202007101702215395068",
"serviceCode": "openRest_open_POST"
}
也可以单个字段更新,例如:
POST /zhang-local-2020.08.26/doc/123/_update
{
"doc": {
"requestIp": "127.0.0.2"
}
}
注意:URL后要多加/_update,要更新的字段放在“doc”节点下
五、Elasticsearch数据文档查询
1、单个文档查询
GET /zhang-local-2020.08.26/doc/123
2、多条件检索
2.1、精确查找和匹配
精确查找和匹配(term、terms和match、match_all、match_phrase)
term和match的区别是在开启分词器的前提下,若没有开启分词器(插入文档进行分词和查询语句分词),二者使用上在使用时未见差异,均为关键词查找
下面设置字段分词器的方式:
PUT zhang-local-2020.08.27
{
"mappings": {
"doc": {
"properties": {
"title":{
"type": "text",
"analyzer": "whitespace",
"search_analyzer": "whitespace"
}
}
}
}
}
对文档中的title字段开启空格分词器(standard),analyzer为文档分词设置,search_analyzer为查询分词设置
term和match的区别:
- term是精确查询,直接对关键词进行查找
- match是模糊查询,对查找的关键词进行分词,然后按分词匹配查找
我们先放一些数据:
POST /zhang-local-2020.08.27/doc/124
{
"title": "张鹏",
"content": "people very love China"
}
POST /zhang-local-2020.08.27/doc/125
{
"title": "张 鹏",
"content": "people very love China"
}
POST /zhang-local-2020.08.27/doc/126
{
"title": "张 三",
"content": "people very love FuYang"
}
POST /zhang-local-2020.08.27/doc/128
{
"title": "王 鹏",
"content": "people very love JieShou"
}
这里我们开启的是title字段的空格分词器,即当有空格时会被分词,那么当我们写入上诉文档时,实际在ES中存储了"张鹏"、“张”、“鹏”、“三”、“王” 这几个关键词,“张 鹏”、“张 三”、"王 鹏"都会被分词
下面我们用term和match分别查找"张鹏",示例:
GET /zhang-local-2020.08.27/doc/_search
{
"query": {
"term": {
"title": "张鹏"
}
}
}
结果:
{
"hits": {
"total": 1,
"max_score": 1.0925692,
"hits": [
{
"_index": "zhang-local-2020.08.27",
"_type": "doc",
"_id": "124",
"_score": 1.0925692,
"_source": {
"title": "张鹏",
"content": "people very love China"
}
}
]
}
}
GET /zhang-local-2020.08.27/doc/_search
{
"query": {
"match": {
"title": "张鹏"
}
}
}
结果:
{
"hits": {
"total": 1,
"max_score": 1.0925692,
"hits": [
{
"_index": "zhang-local-2020.08.27",
"_type": "doc",
"_id": "124",
"_score": 1.0925692,
"_source": {
"title": "张鹏",
"content": "people very love China"
}
}
]
}
}
结果是相同的,二者没有区别,因为这里"张鹏"是个完整的词(无空格),分词不分词都一样,只能找到id为124这条文档;
接着我们用term和match分别查找"张 鹏",示例:
GET /zhang-local-2020.08.27/doc/_search
{
"query": {
"term": {
"title": "张 鹏"
}
}
}
结果:
{
"hits": {
"total": 0,
"max_score": null,
"hits": []
}
}
GET /zhang-local-2020.08.27/doc/_search
{
"query": {
"match": {
"title": "张 鹏"
}
}
}
结果:
{
"hits": {
"total": 3,
"max_score": 1.3411059,
"hits": [
{
"_index": "zhang-local-2020.08.27",
"_type": "doc",
"_id": "125",
"_score": 1.3411059,
"_source": {
"title": "张 鹏",
"content": "people very love China"
}
},
{
"_index": "zhang-local-2020.08.27",
"_type": "doc",
"_id": "127",
"_score": 0.43445712,
"_source": {
"title": "王 鹏",
"content": "people very love JieShou"
}
},
{
"_index": "zhang-local-2020.08.27",
"_type": "doc",
"_id": "126",
"_score": 0.2876821,
"_source": {
"title": "张 三",
"content": "people very love FuYang"
}
}
]
}
}
这里就看出二者区别:
- 用term查找"张 鹏",查找结果为空,因为term为精确查询,直接对关键词进行查找,所以直接用"张 鹏"关键词去查询,在文档写入时分词后最终得到的关键词分别为"张鹏"、“张”、“鹏”、“三”、“王”,显然没有匹配的,故结果为空;
- 用match查找"张 鹏",得到了3条结果,因为match对查找的关键词进行分词,然后按分词匹配查找,这里"张 鹏"分词为"张"、“鹏”,那么用"张"匹配得到125-“张 鹏”,126-“张 三”,用"鹏"匹配得到125-“张 鹏”,127-“王 鹏”,合并结果集,故得到3条文档;
term和terms的区别:
- term为单关键词查询,一次只能查询一个关键词
- terms为多关键词查询,一次可查询多个关键词
示例:
GET /zhang-local-2020.08.27/doc/_search
{
"query": {
"term": {
"title": "张鹏"
}
}
}
结果:
{
"hits": {
"total": 1,
"max_score": 1.1727304,
"hits": [
{
"_index": "zhang-local-2020.08.27",
"_type": "doc",
"_id": "124",
"_score": 1.1727304,
"_source": {
"title": "张鹏",
"content": "people very love China"
}
}
]
}
}
GET /zhang-local-2020.08.27/doc/_search
{
"query": {
"terms": {
"title": ["张鹏","王"]
}
}
}
结果:
{
"hits": {
"total": 2,
"max_score": 1,
"hits": [
{
"_index": "zhang-local-2020.08.27",
"_type": "doc",
"_id": "124",
"_score": 1,
"_source": {
"title": "张鹏",
"content": "people very love China"
}
},
{
"_index": "zhang-local-2020.08.27",
"_type": "doc",
"_id": "127",
"_score": 1,
"_source": {
"title": "王 鹏",
"content": "people very love JieShou"
}
}
]
}
}
match_all:
查询所有文档,示例:
GET /zhang-local-2020.08.27/doc/_search
{
"query": {
"match_all": {}
}
}
会得到所有文档,返回报文就不放了,太长了
match_phrase:
- match_phrase 称为短语搜索,要求所有的分词必须同时出现在文档中,同时位置必须紧邻一致
- match只要其中一个关键词能匹配上就行,不考虑关键词间的联系,故上述查找"张 鹏"连带着把带"张"关键词和带"鹏"关键词的记录一并查出来,这显然不是所有情况都需要的
现在只想查询"张 鹏"的文档,这里就可以用match_phrase,示例:
GET /zhang-local-2020.08.27/doc/_search
{
"query": {
"match_phrase": {
"title": "张 鹏"
}
}
}
结果:
{
"total": 1,
"max_score": 1.3411059,
"hits": [
{
"_index": "zhang-local-2020.08.27",
"_type": "doc",
"_id": "125",
"_score": 1.3411059,
"_source": {
"title": "张 鹏",
"content": "people very love China"
}
}
]
}
2.2、bool组合过滤器
- must、should、must_not过滤器介绍
must
所有的语句都 必须(must) 匹配,与 AND 等价。
must_not
所有的语句都 不能(must not) 匹配,与 NOT 等价。
should
至少有一个语句要匹配,与 OR 等价。
- bool过滤器
bool (布尔)过滤器。 这是个 复合过滤器(compound filter) ,它可以接受多个其他过滤器作为参数(包括它自己都可以),并将这些过滤器结合成各式各样的布尔(逻辑)组合
bool过滤器组成部分示例:
{
"bool" : {
"must" : [],
"should" : [],
"must_not" : []
}
}
每个过滤器都是可选的,但只能存在一个同类型过滤器(例如:bool下只能有一个must过滤器)
下面我们对比SQL举例说明过滤器的使用:
SQL:
SELECT *
FROM user
WHERE (age = 20 OR userId = '1')
AND name != 'zhang'
AND sex = 'man'
ES:
{
"query": {
"bool": {
"should" : [
{ "term" : {"age" : 20}},
{ "term" : {"userId" : "1"}}
],
"must_not" : {
"term" : {"name" : "zhang"}
},
"must" : {
"term" : {"sex" : "man"}
}
}
}
}
- bool过滤器嵌套
尽管 bool 是一个复合的过滤器,可以接受多个子过滤器,需要注意的是 bool 过滤器本身仍然还只是一个过滤器。 这意味着我们可以将一个 bool 过滤器置于其他 bool 过滤器内部,这为我们提供了对任意复杂布尔逻辑进行处理的能力。即bool过滤器可以嵌套一层或多层使用。
SQL对比示例:
SQL:
SELECT *
FROM user
WHERE (age = 20 OR (age = 40 AND userId = '1'))
AND name != 'zhang'
AND sex = 'man'
ES:
{
"query": {
"bool": {
"should" : [
{ "term" : {"age" : 20}},
{ "bool" : {
"must": [
{"term": {"age": 40}},
{"term": {"userId": "1"}}
]
}
}
],
"must_not" : {
"term" : {"name" : "zhang"}
},
"must" : {
"term" : {"sex" : "man"}
}
}
}
}
2.3、filter过滤器
个人拙见filter过滤器一般用于数值,日期的过滤,可对单值,多值,范围过滤
- 过滤单值
GET /zhang-local-2020.08.27/doc/_search
{
"query": {
"bool": {
"filter" : {
"term" : {
"age" : 20
}
}
}
}
}
- 过滤多值
GET /zhang-local-2020.08.27/doc/_search
{
"query": {
"bool": {
"filter" : {
"terms" : {
"age" : [20, 30]
}
}
}
}
}
和单值过滤的基本相同,只是term换成了terms,单个数值换成了数组
- 过滤范围
filter过滤器用的最多的就是范围过滤,上诉的单值和多值都可用must来代替的,范围使用range查询,并提供包含和不包含这两种范围表达式,表达式如下:
gt: > 大于
lt: < 小于
gte: >= 大于或等于
lte: <= 小于或等于
数值范围过滤示例:
GET /zhang-local-2020.08.27/doc/_search
{
"query": {
"bool": {
"filter" : {
"range": {
"age": {
"gte": 10,
"lte": 20
}
}
}
}
}
上诉示例查找"age" 大于等于10,小于等于20的数据
也可单个使用,即只使用"gte",大于等10
日期范围过滤示例:
GET /zhang-local-2020.08.27/doc/_search
{
"query": {
"bool": {
"filter" : {
"range": {
"timestamp": {
"gt": "2020-01-01 00:00:00",
"lt": "2020-10-01 00:00:00"
}
}
}
}
}
range 查询支持对 日期计算(date math) 进行操作,比方说,如果我们想查找时间戳在过去一小时内的所有文档:
"range" : {
"timestamp" : {
"gt" : "now-1h"
}
}
日期计算还可以被应用到某个具体的时间,并非只能是一个像 now 这样的占位符。只要在某个日期后加上一个双管符号 (||) 并紧跟一个日期数学表达式就能做到:
"range" : {
"timestamp" : {
"gt" : "2014-01-01 00:00:00",
"lt" : "2014-01-01 00:00:00||+1M"
}
}
早于 2014 年 1 月 1 日加 1 月(2014 年 2 月 1 日 零时)
支持数学表达式(+、-、/):
表达式 | 含义 |
---|---|
+1h | 加1小时 |
-1d | 减1天 |
/d | 四舍五入到最近的一天 |
支持数学表达式的时间单位:
单位 | 含义 | 单位 | 含义 |
---|---|---|---|
y | 年 | M | 月 |
w | 星期 | d | 天 |
h | 小时 | H | 小时 |
m | 分钟 | s | 秒 |
- filter和其他过滤器组合
上面介绍了 must、should、must_not过滤器,filter过滤器可以和他们一起组合使用
{
"query": {
"bool": {
"should" : [],
"must_not" : [],
"must" :[],
"filter" : {
"range": {}
}
}
}
}
2.4、查询分页和排序
- 分页
Elasticsearch 接受 from 和 size 参数:
size
显示应该返回的结果数量,默认是 10
from
显示应该跳过的初始结果数量,默认是 0
可在请求报文中设置分页信息,如获取第10条后20条数据
{
"query": {
"bool": {
"should" : [],
"must_not" : [],
"must" :[],
"filter" : {
"range": {}
}
}
},
"size": 10,
"from": 20
}
- 排序
sort参数排序设置,后面跟排序字段,order中设置排序方式(正序asc、倒序desc)
{
"query" : {
"bool" : {
"filter" : { "term" : { "user_id" : 1 }}
}
},
"sort": { "date": { "order": "desc" }}
}
3、聚合检索
3.1、聚合概念
类似于 DSL 查询表达式,聚合也有 可组合的语法:独立单元的功能可以被混合起来提供你需要的自定义行为。这意味着只需要学习很少的基本概念,就可以得到几乎无尽的组合。
要掌握聚合,你只需要明白两个主要的概念:
桶(Buckets)
满足特定条件的文档的集合
指标(Metrics)
对桶内的文档进行统计计算
每个聚合都是一个或者多个桶和零个或者多个指标的组合。翻译成粗略的SQL语句来解释吧:
SELECT COUNT(color)
FROM table
GROUP BY color
COUNT(color) 相当于指标。
GROUP BY color 相当于桶。
桶在概念上类似于 SQL 的分组(GROUP BY),而指标则类似于 COUNT() 、 SUM() 、 MAX() 等统计方法。
3.2、简单聚合
首先我们先来个简单的聚合,汽车经销商可能会想知道哪个颜色的汽车销量最好,用聚合可以轻易得到结果,用 terms 桶操作:
GET /cars/transactions/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color"
}
}
}
}
- 聚合操作被置于顶层参数 aggs 之下(如果你愿意,完整形式 aggregations 同样有效)。
- 然后,可以为聚合指定一个我们想要名称,本例中是: popular_colors 。
- 最后,定义单个桶的类型 terms 。
上诉聚合返回全部的结果,当我们只想要数量TOP5时,可以这样请求:
GET /cars/transactions/_search
{
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color",
"size": 5,
"order": {
"_count": "asc"
}
}
}
}
}
- size指定返回多少个分组
- order排序方式
terms为桶聚合,相当于MySQL的group by操作,在桶聚合的过程中还可以进行指标聚合,相当于mysql做group by之后,再做各种max、min、avg、sum、stats,详细示例详见下文“嵌套聚合”章节
桶聚合类型:terms、filter、filters、range、date_range、date_histogram、histogram
指标聚合类型:max、min、avg、sum、stats
3.3、过滤和聚合组合
聚合可以在过滤数据后进行,即对数据进行过滤检索后,再进行聚合,示例如下:
{
"query": {
"bool": {
"should" : [...],
"must_not" : [...],
"must" :[...],
"filter" : {
"range": {
"price": {
"gte": 10000
}
}
}
}
},
"size" : 0,
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color"
}
}
}
}
注意当不需要过滤检索的数据(即返回报文中的hits数据),建议将size设置为0
3.4、嵌套聚合
实际使用中,一个聚合往往不能满足我们的需求,这时就需要多个嵌套组合使用,例如,每种颜色汽车的平均价格是多少
为了获取更多信息,我们需要告诉 Elasticsearch 使用哪个字段,计算何种度量。 这需要将度量 嵌套 在桶内, 度量会基于桶内的文档计算统计结果。
让我们继续为汽车的例子加入 average 平均度量:
GET /cars/transactions/_search
{
"size" : 0,
"aggs": {
"colors": {
"terms": {
"field": "color"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
- 为度量新增 aggs 层。
- 为度量指定名字: avg_price 。
- 最后,为 price 字段定义 avg 度量
3.5、条形图聚合
聚合还有一个特性就是能够十分容易地将它们转换成图表和图形。
直方图 histogram 特别有用。 它本质上是一个条形图。 创建直方图需要指定一个区间,如果我们要为售价创建一个直方图,可以将间隔设为 20,000。这样做将会在每个 $20,000 档创建一个新桶,然后文档会被分到对应的桶中。
我们希望知道每个售价区间内汽车的销量。我们还会想知道每个售价区间内汽车所带来的收入,可以通过对每个区间内已售汽车的售价求和得到。
那么可以用 histogram 和一个嵌套的 sum 度量得到我们想要的答案:
GET /cars/transactions/_search
{
"size" : 0,
"aggs":{
"price":{
"histogram":{
"field": "price",
"interval": 20000
},
"aggs":{
"revenue": {
"sum": {
"field" : "price"
}
}
}
}
}
}
- histogram 桶要求两个参数:一个数值字段以及一个定义桶大小间隔。
- sum 度量嵌套在每个售价区间内,用来显示每个区间内的总收入。
3.6、时间条形图聚合
还有一个功能强大且使用频率最高的条形图统计,date_histogram,时间条形图,它能对时间进行间隔分组,可在 时间 维度上构建指标分析,在实际使用中意义重大
date_histogram 与 通常的 histogram 类似。 但不是在代表数值范围的数值字段上构建 buckets,而是在时间范围上构建 buckets。 因此每一个 bucket 都被定义成一个特定的日期大小 (比如, 1个月 或 2.5 天 )。
构建一个简单的折线图来回答如下问题: 每月销售多少台汽车?
GET /cars/transactions/_search
{
"size" : 0,
"aggs": {
"sales": {
"date_histogram": {
"field": "sold",
"interval": "month",
"format": "yyyy-MM-dd"
}
}
}
}
- interval,时间间隔要求是日历术语 (如每个 bucket 1 个月)。
- format,我们提供日期格式以便 buckets 的键值便于阅读。
当然我们也可以在只要指定日期内的分组,例如2020年10月1号9点到21点,某地铁站每小时的人流量
GET /peoples/transactions/_search
{
"size" : 0,
"aggs": {
"sales": {
"date_histogram": {
"field": "@timestamp",
"interval": "1h",
"format": "yyyy-MM-dd HH",
"min_doc_count" : 0,
"time_zone" : "+08:00",
"extended_bounds" : {
"min" : "2020-10-01 09",
"max" : "2020-10-01 21"
}
}
}
}
}
- min_doc_count,这个参数强制返回空 buckets,即默认时间段内没有数据,也会返回一个空 buckets
- time_zone,时区设置,ES系统默认使用0时区,中国为东八区,需加8小时
- extended_bounds 数强制返回这个时间段内的结果,其他的都不要
说明:extended_bounds只负责强制分组,无过滤功能,上诉统计中会将21点后的数据全都算在21点上,故要展示某个时间范围的统计数据,需配置bool的过滤一起使用。