目录
Docker容器中运行ElasticSearch、Kibana、cerebro
Elasticsearch的一点背景
Elasticsearch是一个实时的分布式搜索分析引擎。它被用作全文检索,结构化搜索,分析以及这三个功能的组合,Elasticsearch的准确定义是:
- 一个分布式的实时文档存储,每个分区 可以被索引与搜索
- 一个分布式实时分析搜索引擎
- 能胜任上百个服务下游的扩展,并支持PB级的结构化或者非结构化数据
Elasticsearch是一个分布式文档存储。Elasticsearch不会将信息存储为列数据的行,而是存储已序列化为JSON文档的复杂数据结构。当集群中有多个Elasticsearch节点时,存储的文档会分布在整个集群中,并且可以从任何节点立即访问。
数据输入
存储文档后,Elasticsearch会在几乎实时的情况下对其进行索引和完全搜索。Elasticsearch使用称为倒排索引(inverted index)的数据结构,该结构支持非常快速的全文本搜索。反向索引列出了出现在任何文档中的每个唯一单词,并标识了每个单词出现的所有文档。
索引可以认为是文档的优化集合,每个文档都是字段的集合,这些字段是包含数据的键值对。默认情况下,Elasticsearch对每个字段中的所有数据建立索引,并且每个索引字段都具有专用的优化数据结构。例如,文本字段存储在倒排索引中,数字字段和地理字段存储在BKD树中。
当然,也可以自定义规则来控制动态映射,也可以显式定义映射以完全控制字段的存储和索引方式。
定义自己的映射可以:
- 区分全文字符串字段和精确值字符串字段
- 执行特定于语言的文本分析
- 优化字段以进行部分匹配
- 使用自定义日期格式
- 使用无法自动检测到的 数据类型,例如
geo_point
和geo_shape
数据输出
Elasticsearch REST API支持
- 结构化查询
- 全文查询
- 结合了两者的复杂查询
结构化查询类似于您可以在SQL中构造的查询类型。例如,您可以搜索索引中的gender
和age
字段,employee
然后按hire_date
字段对匹配项进行排序。
全文查询会找到所有与查询字符串匹配的文档,并按相关性对它们进行归还-它们与您的搜索词的匹配程度如何。
Elasticsearch聚合能够构建数据的复杂摘要并深入了解关键指标,模式和趋势。
集群
可以将服务器(节点)添加到集群以增加容量,Elasticsearch会自动在所有可用节点之间分配数据和查询负载。节点越多越好。
Elasticsearch索引实际上只是一个或多个物理碎片的逻辑分组,其中每个碎片实际上是一个独立的索引。通过在多个分片之间的索引中分配文档,并在多个节点之间分配这些分片,Elasticsearch可以确保冗余,这既可以防止硬件故障,又可以在将节点添加到集群中时提高查询能力。随着集群的增长(或收缩),Elasticsearch会自动迁移碎片以重新平衡集群。
分片有两种类型:主数据库和副本数据库。索引中的每个文档都属于一个主分片。副本分片是主分片的副本。副本可提供数据的冗余副本,以防止硬件故障并提高处理读取请求(如搜索或检索文档)的能力。
当然,查询速度也跟分片的大小和数目有关,分片越多,维护这些索引的开销就越大。分片大小越大,当Elasticsearch需要重新平衡集群时,分片移动所需的时间就越长。
在实际应用中,平均分片大小保持在几GB到几十GB之间。另外,节点可以容纳的分片数量与可用堆空间成比例。通常,每GB堆空间中的分片数量应少于20。(这一句没看懂)
集群灾备
出于性能原因,群集内的节点必须位于同一网络上。跨不同数据中心中的节点在群集中平衡分片的时间太长了。但是这样如何应对灾难呢?跨集群复制(Cross-cluster replication,CCR)。
CCR可以实现自动将索引从主群集同步到可以用作热备份的辅助远程群集,如果主群集发生故障,则辅助群集可以接管。另外,辅助集群也可以提供服务,使得服务在地理上更接近用户。
跨集群复制是主动-被动模式。主群集上的索引是活动的领导者索引,并处理所有写请求。复制到辅助群集的索引是只读关注者。
集群管理
可以使用Kibana用作控制中心来管理和监视集群。
Docker容器中运行ElasticSearch、Kibana、cerebro
ElasticSearch
首先将image从docker上pull下来:
docker pull docker.elastic.co/elasticsearch/elasticsearch:7.9.2
接下来可以启动单一节点的集群
docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.9.2
接下里在浏览器里面输入http://localhost:9200/
出现页面:
说明已经正常启动。
Kibana
Kibana是一个针对Elasticsearch的开源分析及可视化平台,用来搜索、查看交互存储在Elasticsearch索引中的数据。使用Kibana,可以通过各种图表进行高级数据分析及展示。
接下来安装Kibana,和ElasticSearch差不多
docker pull kibana:7.9.2
注意:Kibana的版本要和ElasticSearch一样
理论上讲把Kibana放在容器里面已经可以正常运行了,但是要和ElasticSearch连接起来。
踩坑:去官网上查看如何run起来,docker命令如下:
docker run --link YOUR_ELASTICSEARCH_CONTAINER_NAME_OR_ID:elasticsearch -p 5601:5601 {docker-repo}:{version}
这个坑踩坑3分钟,填坑10小时。
我踩过的坑会在后面详细讲。
这个页面就是Kibana的页面了
cerebro
cerebro是对集群进行管理的工具,可以直管的看到集群的状态以及存储的信息。
同理安装cerebro:
docker pull lmenezes/cerebro
docker run -d --name cerebro -p 9100:9000 [imageID]
踩坑:刚开始连接不上,显示集群健康值: 未连接。
参考了博客:
打开路径 "..\elasticsearch\config\ " 下的 elasticsearch.yml 文件,在文件末尾添加如下代码
http.cors.enabled: true
http.cors.allow-origin: "*"
http.cors.allow-methods: OPTIONS, HEAD, GET, POST, PUT, DELETE
http.cors.allow-headers: "X-Requested-With, Content-Type, Content-Length, X-User"
上图这个就是cerebro的界面了,我这个已经有数据了,所以集群健康值是yello。
可以查看运行的状态:
可以看到STATUS都是UP了。
踩坑
主要是在安装Kibana之后,连接Kibana和ElasticSearch的时候遇到的问题。
1.出现Kibana server is not ready yet
网上常见的这个问题的原因是:
- kibana和ElasticSearch版本不匹配
我在安装的时候我这个就是匹配的。
- 需要修改./config/Kibana.yml里面的信息,因为“访问localhost:9200并不是容器的ip地址,要让Kibana访问容器的地址”
因此我修改了地址,把kibana.yml里面的elasticsearch:9200修改成了容器ip:9200,并没有解决问题
- 防火墙问题
因为在Kiban容器中pinglocalhost的ping的通的,但是ping一下localhost:9200是不通的,需要操作防火墙。当时我觉得很有道理,查看一下容器的日志发现是:
Unable to revive connection: http://localhost:9200/
于是就去查两个docker通信的问题,看了很多博客还是没有什么用。
解决方法:Kibana在run起来的时候就按照官方文档的命令,不要看csdn和博客园上的大明白的分析教学。
ElasticSearch的使用
集群命令
ES集群相关命令主要是_cat命令
命令 | 效果 |
---|---|
/_cat/allocation | 查看单节点的shard分配整体情况 |
/_cat/shards | 查看各shard的详细情况 |
/_cat/shards/{index} | 查看指定分片的详细情况 |
/_cat/master | 查看master节点信息 |
/_cat/nodes | 查看所有节点信息 |
/_cat/indices | 查看集群中所有index的详细信息 |
/_cat/indices/{index} | 查看集群中指定index的详细信息 |
/_cat/segments | 查看各index的segment详细信息,包括segment名, 所属shard, 内存(磁盘)占用大小, 是否刷盘 |
/_cat/segments/{index} | 查看指定index的segment详细信息 |
/_cat/count | 查看当前集群的doc数量 |
/_cat/count/{index} | 查看指定索引的doc数量 |
/_cat/recovery | 查看集群内每个shard的recovery过程.调整replica |
/_cat/recovery/{index} | 查看指定索引shard的recovery过程 |
/_cat/health | 查看集群当前状态:红、黄、绿 |
/_cat/pending_tasks | 查看当前集群的pending task |
/_cat/aliases | 查看集群中所有alias信息,路由配置等 |
/_cat/aliases/{alias} | 查看指定索引的alias信息 |
/_cat/thread_pool | 查看集群各节点内部不同类型的threadpool的统计信息 |
/_cat/plugins | 查看集群各个节点上的plugin信息 |
/_cat/fielddata | 查看当前集群各个节点的fielddata内存使用情况 |
/_cat/fielddata/{fields} | 查看指定field的内存使用情况,里面传field属性对应的值 |
/_cat/nodeattrs | 查看单节点的自定义属性 |
/_cat/repositories | 输出集群中注册快照存储库 |
/_cat/templates | 输出当前正在存在的模板信息 |
技巧:每个命令都支持使用?v参数,让输出内容表格显示表头; pretty则让输出缩进更规范
例如:
GET /_cat/nodes?v&pretty
可以看到当前的状态
heap.percent
堆内存占用百分比ram.percent
内存占用百分比cpu
CPU占用百分比master
*
表示节点是集群中的主节点
name
节点名
GET /_cat/indices?v
可以看到:
health
索引的健康状态index
索引名pri
索引主分片
数量rep
索引复制分片
数store.size
索引主分片
复制分片
总占用存储空间pri.store.size
索引总占用空间, 不计算复制分片
占用空间
索引CRUD命令
索引(Index):是文档(Document)的容器,是一类文档的集合。
索引(名词)相当于SQL中的一个数据库(Database)。索引由其名称(必须为全小写字符)进行标识。
索引(动词):保存一个文档到索引(名词)的过程。这非常类似于SQL语句中的 INSERT关键词。如果该文档已存在时那就相当于数据库的UPDATE。
倒排索引:关系型数据库通过增加一个B+树索引到指定的列上,以便提升数据检索速度。ElasticSearch索引使用了一个叫做倒排索引的结构来达到相同的目的。
另外放几条命令在下面,了解语法:
创建索引
PUT /student
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"name": {
"type":"text"
},
"country": {
"type":"keyword"
},
"age": {
"type":"integer"
},
"date": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
}
}
}
_cat/indices?v&health=yellow #查询健康状态为yellow的索引
_cat/indices?v&health=yellow&s=docs.count:desc #根据文档数量进行索引排序
curl -X GET "localhost:9200/my_index/_stats?pretty" #索引详细信息
文档CRUD命令
我采用的测试数据:
POST /school/student/_bulk
{ "index": { "_id": 1 }}
{ "name" : "liubei", "age" : 20 , "sex": "boy", "birth": "1996-01-02" , "about": "i like diaocan he girl" }
{ "index": { "_id": 2 }}
{ "name" : "guanyu", "age" : 21 , "sex": "boy", "birth": "1995-01-02" , "about": "i like diaocan" }
{ "index": { "_id": 3 }}
{ "name" : "zhangfei", "age" : 18 , "sex": "boy", "birth": "1998-01-02" , "about": "i like travel" }
{ "index": { "_id": 4 }}
{ "name" : "diaocan", "age" : 20 , "sex": "girl", "birth": "1996-01-02" , "about": "i like travel and sport" }
{ "index": { "_id": 5 }}
{ "name" : "panjinlian", "age" : 25 , "sex": "girl", "birth": "1991-01-02" , "about": "i like travel and wusong" }
{ "index": { "_id": 6 }}
{ "name" : "caocao", "age" : 30 , "sex": "boy", "birth": "1988-01-02" , "about": "i like xiaoqiao" }
{ "index": { "_id": 7 }}
{ "name" : "zhaoyun", "age" : 31 , "sex": "boy", "birth": "1997-01-02" , "about": "i like travel and music" }
{ "index": { "_id": 8 }}
{ "name" : "xiaoqiao", "age" : 18 , "sex": "girl", "birth": "1998-01-02" , "about": "i like caocao" }
{ "index": { "_id": 9 }}
{ "name" : "daqiao", "age" : 20 , "sex": "girl", "birth": "1996-01-02" , "about": "i like travel and history" }
文档(Document):Index 里面单条的记录称为Document。等同于关系型数据库表中的行。
要先理解 GET/PUT/POST/DELETE
命令 | 效果 |
---|---|
POST /uri | 创建 |
DELETE /uri/xxx | 删除 |
PUT /uri/xxx | 更新或创建 |
GET /uri/xxx | 查看 |
POST和PUT
POST和PUT的区别?
- 在ElasticSearch中,如果不确定文档的ID,那么就需要用POST,它可以自己生成唯一的文档ID。如果确定文档的ID,那么就可以用PUT,当然也可以用POST,它们都可以创建或修改文档(如果是修改,那么_version版本号提高1)
- PUT、GET、DELETE是幂等的,而POST并不一定是幂等。如果你对POST也指定了文档ID,那它其实和PUT没啥区别,那它就是幂等。如果你没有指定文档ID那么就不是幂等操作了,因为同一数据,你执行多次POST,那么生成多个UUID的文档(一串很长的随机的id),也就是每POST一次都会新增一条数据。
PUT /school/student/10
{
"name" : "张三",
"age" : "20",
"sex" : "boy",
"birth":"1999-04-01",
"about":"info"
}
执行结果:
可以看见张三信息被放进来了,再次执行一遍这个语句
结果:此时状态就是"result" : "updated"
下面我们采用POST方式不指定id:
可以看出分配了一个很长的id字符串。当然,POST也可以显示的指定id。
GET
文档查看可以执行以下命令:
GET /student/_doc/1
更新
商品PUT和POST执行的时候,如果指定的文档ID存在,那么就可以执行更新操作。
但是如果想对部分数据进行update,可以采用关键字_upda
显示:
{
"_index" : "school",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 12,
"_primary_term" : 1
}
文档删除
语法:
DELETE /{index}/{type}/{id}
Query查询和Filter查询
Query和Filter有很大区别。
Query context 查询上下文:这种语句在执行时既要计算文档是否匹配,还要计算文档相对于其他文档的匹配度有多高,匹配度越高,_score
分数就越高
Filter context 过滤上下文:过滤上下文中的语句在执行时只关心文档是否和查询匹配,不计算匹配度,也就是得分。
在查询的时候采用_search
Query查询
match查询
操作 | 效果 |
---|---|
match query | 知道分词器的存在,会对filed进行分词操作,然后再查询 |
match_all | 查询所有文档 |
multi_match | 可以指定多个字段 |
match_phrase | 短语匹配查询,ElasticSearch引擎首先分析(analyze)查询字符串,从分析后的文本中构建短语查询,这意味着必须匹配短语中的所有分词 |
#1、 查询年龄为3的(命中:ID = 1)
GET student/_search
{
"query":{
"match":{"age": 5}
}
}
#2、查询生日里包含'1999-04-01'的 (命中 ID = 10,pKzQT3UBWgjgMOKNVM1u)
GET school/_search
{
"query":{
"match":{"birth": "1999-04-01"}
}
}
#3、查询索引所有文档 (命中 ID = 1,2,3,4,5,6,7,8,9,10,pKzQT3UBWgjgMOKNVM1u)
GET school/_search
{
"query":{
"match_all": {}
}
}
#4、查询about和sex包含'boy' 的(命中 ID = 1,2)
GET school/_search
{
"query":{
"multi_match": {
"query": "diaocan",
"fields":["about","sex"]
}
}
}
#5、查询about里包含'girl'的 (命中 1)
GET school/_search
{
"query": {
"match_phrase": {
"about": "girl"
}
}
}
注意:
- 文档字段属性如果是一个keyword类型,那就需要完全匹配才能命中。好比这个字段值是12345,那么你不论是1234还是123456都不会命中。
- 如果是match_phrase,那就是真正的包含关系。好比这个字段值是12345,那么你是1234就会命中,而123456不会命中。因为12345包含1234而不包含123456。
term查询和terms查询
关键字 | 效果 |
---|---|
term query | 会去倒排索引中寻找确切的term,它并不知道分词器的存在。这种查询适合keyword 、numeric、date。 |
term | 查询某个字段为该关键词的文档(它是相等关系而不是包含关系) |
terms | 查询某个字段里含有多个关键词的文档 |
其他地方看到的例子,这里借鉴一下:
#1、查询地址等于'香港'的文档
GET student/_search
{
"query":{
"term":{ "address":"香港"}
}
}
#如果仅检索'香'那是无法命中的,因为keyword需要完全匹配才能命中
#2、查询地址等于"香港"或"北京"的
GET student/_search
{
"query":{
"terms":{
"address":["香港","北京"]
}
}
}
控制查询返回的数量
另外也可以控制查询返回的数量,例如:
#返回前两条数据
GET school/_search
{
"from":0,
"size":2,
"query":{
"match":{"birth": "1999-04-01"}
}
}
指定返回的字段
例如只返回名字和年龄。
#返回age为20的人的name和age
GET school/_search
{
"_source":["name","age"],
"query":{
"match":{"age": 20}
}
}
排序
GET school/_search
{
"query":{
"match":{"age": 20}
},
"sort":[{
"age":{"order": "desc"}
}]
}
在后面加上sort字段即可。
范围查询
|
|
---|---|
range | 实现范围查询 |
include_lower | 是否包含范围的左边界,默认是true |
include_upper | 是否包含范围的右边界,默认是true |
wildcard查询
GET school/_search
{
"query": {
"range": {
"birth": {
"from": "1950-01-11",
"to": "1999-04-11",
"include_lower": true,
"include_upper": false
}
}
}
}
允许使用通配符* 和 ?来进行查询:
* 代表0个或多个字符
? 代表任意一个字符
GET school/_search
{
"query": {
"wildcard": {
"name": "张*"
}
}
}
fuzzy实现模糊查询
模糊查询可以在Match和 Multi-Match查询中使用以便解决拼写的错误,模糊度是基于Levenshteindistance计算与原单词的距离。使用如下:
GET school/_search
{
"query": {
"fuzzy": {
"name": {
"value": "zhangfie" #注意是zhangfie,正确的是zhangfei
}
}
}
}
Filter查询
filter是不计算相关性的,同时可以cache。因此,filter速度要快于query。
GET school/_search
{
"post_filter":{
"term":{"age": 5}
}
}
复合查询
复合查询有:
- bool query(布尔查询)
- boosting query(提高查询)
- constant_score(固定分数查询)
- dis_max(最佳匹配查询)
- function_score(函数查询)
bool query(布尔查询)
可以理解成通过布尔逻辑将较小的查询组合成较大的查询。
bool查询包含四种操作符,分别是must,should,must_not,filter。他们均是一种数组,数组里面是对应的判断条件。子句,必须匹配,但不贡献算分
操作符 | 含义 |
---|---|
must | 必须匹配,贡献算分 |
must_not | 过滤子句,必须不能匹配,但不贡献算分 |
should | 选择性匹配,至少满足一条,贡献算分 |
filter | 过滤子句,必须匹配,但不贡献算分 |
官方举例:
POST _search
{
"query": {
"bool" : {
"must" : {
"term" : { "user" : "kimchy" }
},
"filter": {
"term" : { "tag" : "tech" }
},
"must_not" : {
"range" : {
"age" : { "gte" : 10, "lte" : 20 }
}
},
"should" : [
{ "term" : { "tag" : "wow" } },
{ "term" : { "tag" : "elasticsearch" } }
],
"minimum_should_match" : 1,
"boost" : 1.0
}
}
}
在filter元素下指定的查询对评分没有影响 , 评分返回为0。分数仅受已指定查询的影响。
官方样例:
GET _search
{
"query": {
"bool": {
"filter": {
"term": {
"status": "active"
}
}
}
}
}
这个例子查询查询为所有文档分配0分,因为没有指定评分查询。
嵌套使用bool查询
官方样例:
# 嵌套,实现了 should not 逻辑
POST /products/_search
{
"query": {
"bool": {
"must": {
"term": {
"price": "30"
}
},
"should": [
{
"bool": {
"must_not": {
"term": {
"avaliable": "false"
}
}
}
}
],
"minimum_should_match": 1
}
}
}
boosting query 提高查询
在上面的复合查询我们可以通过must_not+must
先剔除不想匹配的文档,再获取匹配的文档,但是有一种场景就是我并不需要完全剔除,而是把需要剔除的那部分文档的分数降低。这个时候就可以使用boosting query。例如在我在一个文档中想查询apple但是不包括pie,可以通过must和must_not实现,但是实际上开发过程中,如果出现pie,我并不想把这条记录完全过滤掉,而是希望降低他的分数,让它也出现在列表中,只是查询结果可能比较靠后。这种情况下就可以使用这种方法。
例子:
首先我们创建一个索引并添加数据:
POST /news/_bulk
{ "index": { "_id": 1 }}
{ "content":"Apple Mac" }
{ "index": { "_id": 2 }}
{ "content":"Apple iPad" }
{ "index": { "_id": 3 }}
{ "content":"Apple employee like Apple Pie and Apple Juice" }
如果使用bool的must和must_not复合查询,查询文档中需要包含apple,但是文档中不包含pie的:
#must_not的方式,将3的记录强制排除掉 (结果 1->2)
POST news/_search
{
"query": {
"bool": {
"must": {
"match":{"content":"apple"}
},
"must_not": {
"match":{"content":"pie"}
}
}
}
}
如果在实际开发过程中,如果出现pie,我并不想把这条记录完全过滤掉,而是希望降低他的分数,让它也出现在列表中,只是查询结果可能比较靠后,可以这样:
# 通过Boosting的方式,将3的记录也纳入结果集,只是排名会靠后。(结果 1->2->3)
POST news/_search
{
"query": {
"boosting": {
"positive": {
"match": {
"content": "apple"
}
},
"negative": {
"match": {
"content": "pie"
}
},
"negative_boost": 0.5
}
}
}
boosting需要搭配三个关键字:positive , negative , negative_boost
只有匹配了positive查询的文档才会被包含到结果集中,但是同时匹配negative查询的文档会被降低其相关度,通过将文档原本的_score和
negative_boost参数进行相乘来得到新的_score。因此,negative_boost参数一般小于1.0。在上面的例子中,任何包含了指定负面词条的文档的_score都会是其原本_score的一半。
场景举例
我们通过去索引中搜索 '苹果公司' 相关的信息,然后我们在查询中的信息为 '苹果'。
1)那么我们查询的条件是:must = '苹果'。也就是文档中必须包含'苹果'
但是我们需要的结果是苹果公司相关信息,如果你的文档是 '苹果树','苹果水果',那么其实此苹果非彼苹果如果匹配到其实没有任何意义。
2)那么我们修改查询条件为: must = '苹果' AND must_not = '树 or 水果'
就是说就算文档包含了苹果,但因为包含了树或者水果那么我们也会过滤这条文档信息,因为我们要查的苹果公司相关信息,如果你是苹果树那对我来讲确实是不匹配,所以直接过滤掉,看是没啥问题。但是你想,这样做是不是太粗暴了,因为一个文档中包含'苹果'和'树'那不代表一定是苹果树,而可能是 '苹果公司组织员工一起去种树' 那么这条文档理应出现而不是直接过滤掉,所以我们就可以用boosting query
。就像上面这个例子一样。
constant_score(固定分数查询)
常量分值查询,目的就是返回指定的score,一般都结合filter使用,因为filter context忽略score。
例如:
POST news/_search
{
"query": {
"constant_score": {
"filter": {
"match": {
"content":"apple"
}
},
"boost": 2.5
}
}
}
查询的结果分数都是2.5:
查询结果
dis_max(最佳匹配查询)
只是取分数最高的那个query的分数而已。例如:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 2.5,
"hits" : [
{
"_index" : "news",
"_type" : "_doc",
"_id" : "1",
"_score" : 2.5,
"_source" : {
"content" : "Apple Mac"
}
},
{
"_index" : "news",
"_type" : "_doc",
"_id" : "2",
"_score" : 2.5,
"_source" : {
"content" : "Apple iPad"
}
},
{
"_index" : "news",
"_type" : "_doc",
"_id" : "3",
"_score" : 2.5,
"_source" : {
"content" : "Apple employee like Apple Pie and Apple Juice"
}
}
]
}
}
官方样例:
GET /_search
{
"query": {
"dis_max" : {
"queries" : [
{ "term" : { "title" : "Quick pets" }},
{ "term" : { "body" : "Quick pets" }}
],
"tie_breaker" : 0.7
}
}
}
计算方法是:取分数最高的那个query的分数,同时其它子查询查询的分数乘以tie_breaker。
假设一条文档的'title'查询得分是 1,'body'查询得分是1.6。那么总得分为:1.6+1*0.7 = 2.3。
如果我们去掉"
tie_breaker" : 0.7 ,那么tie_breaker默认为0,那么这条文档的得分就是 1.6 + 1*0 = 1.6。
function_score(函数查询)
function_score是处理分值计算过程的终极工具。它让你能够对所有匹配了主查询的每份文档调用一个函数来调整甚至是完全替换原来的_score。
(没看懂)
要使用function_score,用户必须定义一个查询和一个或多个函数,这些函数计算查询返回的每个文档的新分数。
它拥有几种预先定义好了的函数:
weight 对每份文档适用一个简单的提升,且该提升不会被归约:当weight为2时,结果为2 * _score。
field_value_factor 使用文档中某个字段的值来改变_score,比如将受欢迎程度或者投票数量考虑在内。
random_score 使用一致性随机分值计算来对每个用户采用不同的结果排序方式,对相同用户仍然使用相同的排序方式。
衰减函数(Decay Function) - linear,exp,gauss
使用场景:
- 假设我们又一个资讯类APP我们希望让人阅读量高的文章出现在结果列表的头部,但是主要的排序依据仍然是全文搜索分值。
- 当用户搜索酒店,它的要求是 1、离他目前位置1Km内 2、价格在500元内。如果我们只是使用一个 filter 排除所有市中心方圆 1KM以外的酒店, 再用一个filter排除每晚价格超过500元的酒店,这种作法太过强硬,可能有一间房在2K米,但是超级便宜一晚只要100元,用户可能会因此愿意妥协住这间房。
聚合查询
在Mysql中,我们可以获取一组数据的 最大值(Max)、最小值(Min)。同样我们能够对这组数据进行 分组(Group)。那么对于Elasticsearch中也可以实现同样的功能。
Elasticsearch除全文检索功能外提供的针对Elasticsearch数据做统计分析的功能。它的实时性高,所有的计算结果都是即时返回。
Elasticsearch将聚合分析主要分为如下4类:
类别 | 含义 |
---|---|
Metric(指标) | 指标分析类型,如计算最大值、最小值、平均值等等 (对桶内的文档进行聚合分析的操作) |
Bucket(桶) | 分桶类型,类似SQL中的GROUP BY语法 (满足特定条件的文档的集合) |
Pipeline(管道) | 管道分析类型,基于上一级的聚合分析结果进行在分析 |
Matrix(矩阵) | 矩阵分析类型(聚合是一种面向数值型的聚合,用于计算一组文档字段中的统计信息) |
一般实际开发中,用到的比较多的就是Metric和Bucket。
指标(metric)
- 桶能让我们划分文档到有意义的集合,但是最终我们需要的是对这些桶内的文档进行一些指标的计算。分桶是一种达到目的地的手段:它提供了一种给文档分组的方法来让我们可以计算感兴趣的指标。
- 大多数指标是简单的数学运算(如:最小值、平均值、最大值、汇总),这些是通过文档的值来计算的。
Metric聚合分析分为单值分析和多值分析两类。
如:
- 单值分析,只输出一个分析结果:min,max,avg,sum,cardinality
- 多值分析,输出多个分析结果 stats,extended_stats,percentile,percentile_rank,top hits
平均值Avg
计算从聚合文档中提取的数值的平均值。
POST /exams/_search?size=0
{
"aggs" : {
"avg_grade" : { "avg" : { "field" : "grade" } }
}
}
Max(最大值)
计算从聚合文档中提取的数值的最大值:
POST /exams/_search?size=0
{
"aggs" : {
"max_grade" : { "max" : { "field" : "grade" } }
}
}
Sum(总和)
计算从聚合文档中提取的数值的总和:
POST /sales/_search?size=0
{
"query" : {
"constant_score" : {
"filter" : {
"match" : { "type" : "hat" }
}
}
},
"aggs" : {
"hat_prices" : { "sum" : { "field" : "price" } }
}
}
Cardinality(唯一值)
cardinality 求唯一值,即不重复的字段有多少(相当于mysql中的distinct)
POST /sales/_search?size=0
{
"aggs" : {
"type_count" : {
"cardinality" : {
"field" : "type"
}
}
}
}
Stats(统计)
stats 统计,请求后会直接显示多种聚合结果
POST /exams/_search?size=0
{
"aggs" : {
"grades_stats" : { "stats" : { "field" : "grade" } }
}
}
返回
{
...
"aggregations": {
"grades_stats": {
"count": 2,
"min": 50.0,
"max": 100.0,
"avg": 75.0,
"sum": 150.0
}
}
}
Percentiles(百分比)
对指定字段的值按从小到大累计每个值对应的文档数的占比,返回指定占比比例对应的值。
默认按照[ 1, 5, 25, 50, 75, 95, 99 ]来统计:
GET latency/_search
{
"size": 0,
"aggs" : {
"load_time_outlier" : {
"percentiles" : {
"field" : "load_time"
}
}
}
}
结果:
{
...
"aggregations": {
"load_time_outlier": {
"values" : {
"1.0": 5.0,
"5.0": 25.0,
"25.0": 165.0,
"50.0": 445.0,
"75.0": 725.0,
"95.0": 945.0,
"99.0": 985.0
}
}
}
}
也可以自定义分位值:
GET latency/_search
{
"size": 0,
"aggs" : {
"load_time_outlier" : {
"percentiles" : {
"field" : "load_time",
"percents" : [95, 99, 99.9]
}
}
}
}
Percentile Ranks
上面是通过百分比求文档值,这里通过文档值求百分比
GET latency/_search
{
"size": 0,
"aggs" : {
"load_time_ranks" : {
"percentile_ranks" : {
"field" : "load_time",
"values" : [500, 600]
}
}
}
}
返回结果
{
...
"aggregations": {
"load_time_ranks": {
"values" : {
"500.0": 55.1,
"600.0": 64.0
}
}
}
}
结果含义:时间小于500的文档占比为55.1%,时间小于600的文档占比为64%
Top Hits
一般用于分桶后获取该桶内匹配前n的文档列表。
POST /sales/_search?size=0
{
"aggs": {
"top_tags": {
"terms": {
"field": "type", #根据type进行分组 每组显示前3个文档
"size": 3
},
"aggs": {
"top_sales_hits": {
"top_hits": {
"sort": [
{
"date": {
"order": "desc" #按照时间进行倒叙排序
}
}
],
"_source": {
"includes": [ "date", "price" ] #只显示文档指定字段
},
"size" : 1
}
}
}
}
}
}
Bucket聚合
Bucket可以理解为一个桶,它会遍历文档中的内容,凡是符合某一要求的就放入一个桶中,分桶相当与SQL中的group by。
桶的关键字有:
Terms Aggregation
Filter Aggregation
Histogram Aggregation
Range Aggregation
Date Aggregation
Terms Aggregation
直接上例子:
创建索引:
汽车(价格、颜色、品牌、销售时间)
PUT cars
{
"mappings": {
"properties": {
"price": {
"type":"long"
},
"color": {
"type":"keyword"
},
"brand": {
"type":"keyword"
},
"sellTime": {
"type":"date"
}
}
}
}
添加索引数据:
POST /cars/_bulk
{ "index": {}}
{ "price" : 80000, "color" : "red", "brand" : "BMW", "sellTime" : "2014-01-28" }
{ "index": {}}
{ "price" : 85000, "color" : "green", "brand" : "BMW", "sellTime" : "2014-02-05" }
{ "index": {}}
{ "price" : 120000, "color" : "green", "brand" : "Mercedes", "sellTime" : "2014-03-18" }
{ "index": {}}
{ "price" : 105000, "color" : "blue", "brand" : "Mercedes", "sellTime" : "2014-04-02" }
{ "index": {}}
{ "price" : 72000, "color" : "green", "brand" : "Audi", "sellTime" : "2014-05-19" }
{ "index": {}}
{ "price" : 60000, "color" : "red", "brand" : "Audi", "sellTime" : "2014-06-05" }
{ "index": {}}
{ "price" : 40000, "color" : "red", "brand" : "Audi", "sellTime" : "2014-07-01" }
{ "index": {}}
{ "price" : 35000, "color" : "blue", "brand" : "Honda", "sellTime" : "2014-08-12" }
下面就开始进行分桶操作:
根据品牌分桶
GET cars/_search?size=0
{
"aggs" : {
"genres" : {
"terms" : { "field" : "brand" }
}
}
}
返回结果:
分桶后只显示文档数量前3的桶
GET cars/_search?size=0
{
"aggs" : {
"cars" : {
"terms" : {
"field" : "brand",
"size" : 3
}
}
}
}
结果:
分桶后排序
GET cars/_search?size=0
{
"aggs" : {
"genres" : {
"terms" : {
"field" : "brand",
"order" : { "_count" : "asc" }
}
}
}
}
显示文档数量大于3的桶
GET cars/_search?size=0
{
"aggs" : {
"brands" : {
"terms" : {
"field" : "brand",
"min_doc_count": 3
}
}
}
}
使用精确指定的词条进行分桶
GET /cars/_search?size=0
{
"aggs" : {
"JapaneseCars" : {
"terms" : {
"field" : "brand",
"include" : ["BMW", "Audi"]
}
}
}
}
Filter Aggregation and Filters Aggregation
指具体的域和具体的值,可以说是在 Terms Aggregation 的基础上进行了过滤,只对特定的值进行了聚合。
例如:
过滤获取品牌为BMW的桶,并求该桶平均值
GET /cars/_search?size=0
{
"aggs" : {
"brands" : {
"filter" : { "term": { "brand": "BMW" } },
"aggs" : {
"avg_price" : { "avg" : { "field" : "price" } }
}
}
}
}
Filter Aggreagtion 只能指定一个过滤条件,响应也只是单个桶。如果想要只对多个特定值进行聚合,使用Filter Aggreagtion只能进行多次请求。而使用 Filters Aggreagation 就可以解决上述的问题。
例如:
过滤获取品牌为BMW的或者color为绿色的桶
GET /cars/_search?size=0
{
"size": 0,
"aggs" : {
"cars" : {
"filters" : {
"filters" : {
"colorBucket" : { "match" : { "color" : "red" }},
"brandBucket" : { "match" : { "brand" : "Audi" }}
}
}
}
}
}
Histogram Aggreagtion
Histogram与Terms聚合类似,都是数据分组,区别是Terms是按照Field的值分组,而Histogram可以按照指定的间隔对Field进行分组,
例如:
根据价格区间为10000分桶
GET /cars/_search?size=0
{
"aggs" : {
"prices" : {
"histogram" : {
"field" : "price",
"interval" : 10000
}
}
}
}
Range Aggregation
根据用户传递的范围参数作为桶,进行相应的聚合。在同一个请求中,可以传递多组范围,每组范围作为一个桶。
例如:
根据价格区间分桶
GET /cars/_search?size=0
{
"aggs" : {
"price_ranges" : {
"range" : {
"field" : "price",
"ranges" : [
{ "to" : 50000 },
{ "from" : 5000, "to" : 80000 },
{ "from" : 80000 }
]
}
}
}
}
运行结果:
"aggregations" : {
"price_ranges" : {
"buckets" : [
{
"key" : "*-50000.0",
"to" : 50000.0,
"doc_count" : 2
},
{
"key" : "5000.0-80000.0",
"from" : 5000.0,
"to" : 80000.0,
"doc_count" : 4
},
{
"key" : "80000.0-*",
"from" : 80000.0,
"doc_count" : 4
}
]
}
}
也可以指定key的名称,例如:
GET /cars/_search?size=0
{
"aggs" : {
"price_ranges" : {
"range" : {
"field" : "price",
"ranges" : [
{ "key" : "xiaoyu", "to" : 50000 },
{ "key" : "baohan", "from" : 5000, "to" : 80000 },
{ "key" : "dayu", "from" : 80000 }
]
}
}
}
}
Date Aggregation
针对于时间格式数据的直方图聚合,基本的特性与Histogram Aggregation一致。
例如以下场景:
按月分桶显示每个月的销量
POST /cars/_search?size=0
{
"aggs" : {
"sales_over_time" : {
"date_histogram" : {
"field" : "sellTime",
"interval" : "1M",
"format" : "yyyy-MM-dd"
}
}
}
}
结果:
运行结果
"aggregations" : {
"sales_over_time" : {
"buckets" : [
{
"key_as_string" : "2014-01-01",
"key" : 1388534400000,
"doc_count" : 1
},
{
"key_as_string" : "2014-02-01",
"key" : 1391212800000,
"doc_count" : 1
},
{
"key_as_string" : "2014-03-01",
"key" : 1393632000000,
"doc_count" : 1
},
{
"key_as_string" : "2014-04-01",
"key" : 1396310400000,
"doc_count" : 1
},
{
"key_as_string" : "2014-05-01",
"key" : 1398902400000,
"doc_count" : 1
},
{
"key_as_string" : "2014-06-01",
"key" : 1401580800000,
"doc_count" : 1
},
{
"key_as_string" : "2014-07-01",
"key" : 1404172800000,
"doc_count" : 1
},
{
"key_as_string" : "2014-08-01",
"key" : 1406851200000,
"doc_count" : 1
}
]
}
}
根据指定时间区间分桶:
POST /cars/_search?size=0
{
"aggs": {
"range": {
"date_range": {
"field": "sellTime",
"format": "MM-yyyy",
"ranges": [
{ "to": "now-10M/M" },
{ "from": "now-10M/M" }
]
}
}
}
}
Date Range
是
针对于时间格式数据的范围聚合。
上面的意思是10个月前的分为一个桶,10个月前之后的分为一个桶。
结果:
"aggregations" : {
"range" : {
"buckets" : [
{
"key" : "*-12-2019",
"to" : 1.5751584E12,
"to_as_string" : "12-2019",
"doc_count" : 8
},
{
"key" : "12-2019-*",
"from" : 1.5751584E12,
"from_as_string" : "12-2019",
"doc_count" : 0
}
]
}
}
分词器
Analysis和Analyzer
Analysis:文本分析是把全文本转换一系列单词(term/token)的过程,也叫分词。Analysis是通过Analyzer来实现的。
当一个文档被索引时,每个Field都可能会创建一个倒排索引(Mapping可以设置不索引该Field)。
倒排索引的过程就是将文档通过Analyzer分成一个一个的Term,每一个Term都指向包含这个Term的文档集合。当查询query时,Elasticsearch会根据搜索类型决定是否对query进行analyze,然后和倒排索引中的term进行相关性查询,匹配相应的文档。
分析器(analyzer)都由三种构件块组成的:character filters,tokenizers,token filters。
character filter 字符过滤器:在一段文本进行分词之前,先进行预处理,比如说最常见的就是,过滤html标签(<span>hello<span> --> hello),& --> and(I&you --> I and you)
Tokenizers 分词器:英文分词可以根据空格将单词分开,中文分词比较复杂,可以采用机器学习算法来分词。
Token filters Token过滤器:将切分的单词进行加工。大小写转换(例将“Quick”转为小写),去掉词(例如停用词像“a”、“and”、“the”等等),或者增加词(例如同义词像“jump”和“leap”)。
处理过程:analyzer = CharFilters(0个或多个) + Tokenizer(恰好一个) + TokenFilters(0个或多个)
Elasticsearch的内置分词器
-
Standard Analyzer - 默认分词器,按词切分,小写处理
-
Simple Analyzer - 按照非字母切分(符号被过滤), 小写处理
-
Stop Analyzer - 小写处理,停用词过滤(the,a,is)
-
Whitespace Analyzer - 按照空格切分,不转小写
-
Keyword Analyzer - 不分词,直接将输入当作输出
-
Patter Analyzer - 正则表达式,默认\W+(非字符分割)
-
Language - 提供了30多种常见语言的分词器
-
Customer Analyzer 自定义分词器
例如可以在创建索引时指定分词器:
PUT new_index
{
"settings": {
"analysis": {
"analyzer": {
"std_folded": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"asciifolding"
]
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "std_folded" #指定分词器
},
"content": {
"type": "text",
"analyzer": "whitespace" #指定分词器
}
}
}
}
ElasticSearch内置分词器
常见的是:Standard Analyzer、Simple Analyzer、whitespace Analyzer
Standard Analyzer(默认)
standard 是默认的分析器。它提供了基于语法的标记化(基于Unicode文本分割算法),适用于大多数语言。
例如我们对下面的例子进行分析:
POST _analyze
{
"analyzer": "standard",
"text": "Like X 国庆放假的"
}
分析结果是:
{
"tokens" : [
{
"token" : "like",
"start_offset" : 0,
"end_offset" : 4,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "x",
"start_offset" : 5,
"end_offset" : 6,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "国",
"start_offset" : 7,
"end_offset" : 8,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "庆",
"start_offset" : 8,
"end_offset" : 9,
"type" : "<IDEOGRAPHIC>",
"position" : 3
},
{
"token" : "放",
"start_offset" : 9,
"end_offset" : 10,
"type" : "<IDEOGRAPHIC>",
"position" : 4
},
{
"token" : "假",
"start_offset" : 10,
"end_offset" : 11,
"type" : "<IDEOGRAPHIC>",
"position" : 5
},
{
"token" : "的",
"start_offset" : 11,
"end_offset" : 12,
"type" : "<IDEOGRAPHIC>",
"position" : 6
}
]
}
可以看出将
Like X 国庆放假的
分解成了:
like x 国 庆 节 放 假 的
simple分析器当它遇到只要不是字母的字符,就将文本解析成term,而且所有的term都是小写的。可以这样理解:
先按照空格分词,英文大写转小写,如果不是英文不再继续分词。
例如如果对:
Like X 国庆放假 的
进行分词,会得到:
like x 国庆放假 的。
标准分析器接受下列参数:
- max_token_length : 最大token长度,默认255
- stopwords : 预定义的停止词列表,如_english_或 包含停止词列表的数组,默认是_none_
- stopwords_path : 包含停止词的文件路径
例如我们自定义一个my_english_analyzer:
PUT new_index
{
"settings": {
"analysis": {
"analyzer": {
"my_english_analyzer": {
"type": "standard", #设置分词器为standard “ max_token_length”:5,#设置分词最大为5 “ stopwords”:“ _english_”#设置过滤词 } } } } }
空白分析仪
跟随空格进行分词,英文大小写保持和之前的一致,中文不再分词。例如对下面的示例进行分词:
Abc D我是样例
分析得到:
Abc D我是样例
中文分词
中文的分词器现在大家比较常见的是IK分词器,当然也有些其他的smartCN,HanLP。
安装IK分词器
在ElasticSearch的容器中执行:
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.9.2/elasticsearch-analysis-ik-7.9.2.zip
注意:版本要和ElasticSearch保持一致
安装后重启一下ElasticSearch
IK使用
IK有两种颗粒度的分解:
ik_smart:会做最粗粒度的划分
ik_max_word:纳入文本做最细粒度的划分
例如我们采用ik_smart分词:
每个国家有每个国家的国歌
GET / _analyze { “ text”:“每个国家有每个国家的国歌”, “ analyzer”:“ ik_smart” }
得到的结果是:
{
"tokens" : [
{
"token" : "每个",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "国",
"start_offset" : 2,
"end_offset" : 3,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "家有",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "每个",
"start_offset" : 5,
"end_offset" : 7,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "国家",
"start_offset" : 7,
"end_offset" : 9,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "的",
"start_offset" : 9,
"end_offset" : 10,
"type" : "CN_CHAR",
"position" : 5
},
{
"token" : "国歌",
"start_offset" : 10,
"end_offset" : 12,
"type" : "CN_WORD",
"position" : 6
}
]
}
可以看到,分成了
每个 国 家有 每个 国家 的 国歌
那么如果采用ik_max_word结果是:
{
"tokens" : [
{
"token" : "每个",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "国家",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "家有",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD", “位置”:2 }, { “ token”:“每个”, “ start_offset”:5 “ end_offset”:7 “ type”:“ CN_WORD”, “位置”:3 }, { “ token”:“国家”, “ start_offset”:7 “ end_offset”:9 “ type”:“ CN_WORD”, “位置”:4 }, { “ token”:“的”, “ start_offset”:9 “ end_offset”:10, “ type”:“ CN_CHAR”, “位置”:5 }, { “ token”:“国歌”, “ start_offset”:10, “ end_offset”:12 “ type”:“ CN_WORD”, “位置”:6 } ] }
分变为:
每个国家家有每个国家的国歌
这样对这句话进行了更加细致的细分。