一.Elastic Stack
1.什么是 ELK
ELK 是包含但不限于 Elasticsearch
、Logstash
、Kibana
三个开源软件的组成的一个整体。这三个软件合成 ELK。是用于数据抽取(Logstash
)、搜索分析(Elasticsearch
)、数据展现(Kibana
)的一整套解决方案,所以也称作 ELK stack.
随着 elk 的发展,又有新成员 Beats、Elastic cloud 的加入,所以就形成了 Elastic Stack。所以说,ELK 是旧的称呼,Elastic Stack 是新的名字。
2.Elastic Stack 特色
处理方式灵活
:elasticsearch 是目前最流行的准实时全文检索引擎,具有高速检索大数据的能力。配置简单
:安装 elk 的每个组件,仅需配置每个组件的一个配置文件即可。修改处不多,因为大量参数已经默认配在系统中,修改想要修改的选项即可。接口简单
:采用 json 形式 RESTFULAPI 接受数据并响应,无关语言。性能高效
:elasticsearch 基于优秀的全文搜索技术 Lucene,采用倒排索引,可以轻易地在百亿级别数据量下,搜索出想要的内容,并且是秒级响应。灵活扩展
:elasticsearch 和 logstash 都可以根据集群规模线性拓展,elasticsearch 内部自动实现集群协作。数据展现华丽
:kibana 作为前端展现工具,图表华丽,配置简单。
3.组件介绍
Elasticsearch:
Elasticsearch 是使用 java 开发,基于 Lucene、分布式、通过 Restful 方式进行交互的近实时搜索平台框架。它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,restful 风格接口,多数据源,自动搜索负载等。
Logstash:
Logstash 基于 java 开发,是一个数据抽取转化工具。一般工作方式为 c/s 架构,client 端安装在需要收集信息的主机上,server 端负责将收到的各节点日志进行过滤、修改等操作在一并发往 elasticsearch 或其他组件上去。
Kibana:
Kibana 基于 nodejs,也是一个开源和免费的可视化工具。Kibana 可以为 Logstash 和 ElasticSearch 提供的日志分析友好的 Web 界面,可以汇总、分析和搜索重要数据日志。
Beats:
Beats 平台集合了多种单一用途数据采集器。它们从成百上千或成千上万台机器和系统向 Logstash 或 Elasticsearch 发送数据。
4.Beats 组成
PacketbeatI
:轻量型网络数据采集器,用于深挖网线上传输的数据,了解应用程序动态。Packetbeat 是一款轻量型网络数据包分析器,能够将数据发送至 Logstash 或 Elasticsearch。其支持 ICMP(v4 and v6)、DNS、HTTP、Mysql、PostgreSQL、Redis、MongoDB、Memcache 等协议.Filebeat
:轻量型日志采集器。当您要面对成百上千、甚至成千上万的服务器、虚拟机和容器生成的日志时,请告别 SSH 吧。Filebeat 将为您提供一种轻量型方法,用于转发和汇总日志与文件,让简单的事情不再繁杂。Metricbeat
:轻量型指标采集器。Metricbeat 能够以一种轻量型的方式,输送各种系统和服务统计数据,从 CPU 到内存,从 Redis 到 Nginx,不一而足。可定期获取外部系统的监控指标信息,其可以监控、收集 Apache http、HAProxy, MongoDB, MySQL, Nginx,PostgreSQL,Redis, System.Zookeeper 等服务。Winlogbeat
:轻量型 Windows 事件日志采集器。用于密切监控基于 Windows 的基础设施上发生的事件。Winlogbeat 能够以一种轻量型的方式,将 Windows 事件日志实时地流式传输至 Elasticsearch 和 Logstash。Auditbeat
:轻量型审计日志采集器。收集您 Linux 审计框架的数据,监控文件完整性。Auditbeat 实时采集这些事件,然后发送到 Elastic Stack 其他部分做进一步分析。
5.搜索是什么
概念:用户输入想要的关键词,返回含有该关键词的所有信息。使用场景:
- 互联网搜索:谷歌、百度、各种新闻首页
- 站内搜索(垂直搜索):企业 OA 查询订单、人员、部门,电商网站内部搜索商品(淘宝、京东)场景。
6.数据库弊端
存储问题:
电商网站商品上亿条时,涉及到单表数据过大必须拆分表,数据库磁盘占用过大必须分库 mycat性能问题:
解决上面问题后,查询“笔记本电脑”等关键词时,上亿条数据的商品名字段逐行扫描,性能跟不上。不能分词:
如搜索“笔记本电脑”,只能搜索完全和关键词一样的数据,那么数据量小时,搜索“笔记电脑”,“电脑”数据要不要给用户。
7.全文检索
倒排索引:
数据存储时,经过分词建立 term 索引库。
倒排索引
源于实际应用中需要根据属性的值来查找记录。这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted index)。带有倒排索引的文件我们称为倒排索引文性,简称倒排文件(inverted file)。
Lucene:
就是一个 jar 包,里面封装了全文检索的引擎、搜索的算法代码。开发时,引入 lucene 的 jar 包,通过 api 开发搜索相关业务。底层会在磁盘建立索引库。
8.Kibana 是什么?
Kibana
是一个开源的分析与可视化平台,设计出来用于和 Elasticsearch
一起使用的。你可以用 kibana
搜索、查看存放在 Elasticsearch
中的数据。Kibana
与 Elasticsearch
的交互方式是各种不同的图表、表格、地图等,直观的展示数据,从而达到高级的数据分析与可视化的目的。
Elasticsearch
、Logstash
和 Kibana
这三个技术就是我们常说的 ELK 技术栈,可以说这三个技术的组合是大数据领域中一个很巧妙的设计。一种很典型的 MVC
思想,模型持久层,视图层和控制层。Logstash
担任控制层的角色,负责搜集和过滤数据。Elasticsearch
担任数据持久层的角色,负责储存数据。Kibana 担任视图层角色,拥有各种维度的查询和分析,并使用图形化的界面展示存放在 Elasticsearch
中的数据。
二.Elasticsearch
1.什么是 Elasticsearch
ElasticSearch
是一个基于 Lucene
的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful web 接口。官网地址
2.Elasticsearch 的功能
- 分布式的
搜索引擎
和数据分析引擎
- 搜索:互联网搜索、电商网站站内搜索、OA 系统查询
- 数据分析:电商网站查询近一周哪些品类的图书销售前十;新闻网站,最近 3 天阅读量最高的十个关键词,舆情分析。
全文检索
,结构化检索,数据分析
- 全文检索:搜索商品名称包含 java 的图书 select *from books where book_name like"%java%".
- 结构化检索:搜索商品分类为 spring 的图书都有哪些,selectfrom bookswhere category_id='spring'
- 数据分析:分析每一个分类下有多少种图书,select category_id,count(*)from books group by category_id
- 对
海量数据
进行近实时的处理
- 分布式:ES 自动可以将海量数据分散到多台服务器上去存储和检索,经行并行查询,提高搜索效率。相对的,Lucene 是单机应用。
- 近实时:数据库上亿条数据查询,搜索一次耗时几个小时,是批处理(batch-processing)。而 es 只需秒级即可查询海量数据,所以叫近实时。秒级。
3.Elasticsearch 的使用场景
- 维基百科,类似百度百科,“网络七层协议”的维基百科,全文检索,高亮,搜索推荐
- Stack Overflow(国外的程序讨论论坛),相当于程序员的贴吧。遇到 it 问题去上面发帖,热心网友下面回帖解答。
- GitHub《开源代码管理),搜索上千亿行代码。
- 电商网站,检索商品
- 日志数据分析,logstash 采集日志,ES 进行复杂的数据分析(ELK 技术,elasticsearch+logstash+kibana )
- 商品价格监控网站,用户设定某商品的价格阈值,当低于该阈值的时候,发送通知消息给用户,比如说订阅《java 编程思想》的监控,如果价格低于 27 块钱,就通知我,我就去买。
- BI 系统,商业智能(Business lntelligence)。大型连锁超市,分析全国网点传回的数据,分析各个商品在什么季节的销售量最好、利润最高。成本管理,店面租金、员工工资、负债等信息进行分析。从而部署下一个阶段的战略目标。
- 百度搜索,第一次查询,使用 es。OA、ERP 系统站内搜索。
4.Elasticsearch 的特点
可拓展性:
大型分布式集群(数百台服务器)技术,处理 PB 级数据,大公司可以使用。小公司数据量小,也可以部署在单机。大数据领域使用广泛。技术整合:
将全文检索、数据分析、分布式相关披术整合在一起:lucene(全文检索),商用的数据分析软件(BI 软件),分布式数据库(mycat)部署简单:
开箱即用,很多默认配置不需关心,解压完成直接运行即可。拓展时,只需多部署几个实例即可,负载均衡、分片迁移集群内部自己实施。接口简单:
使用 restful api 经行交互,跨语言。功能强大:
Elasticsearch 作为传统数据库的一个补充,提供了数据库所不不能提供的很多功能,如全文检索,同义词处理,相关度排名。
5.lucene 和 elasticsearch 的关系
Lucene:
最先进功能最强大的搜索库,直接基于 lucene 开发,非常复杂,api 复杂.
Elasticsearch:
基于 lucene,封装了许多 lucene 底层功能,提供简单易用的 restful api 接口和许多语言的客户端,如 java 的高级客户端(Java Hijgh Level REST Client)和底层客户端(Java Low Level REST Client)
6.Elasticsearch 核心概念
NRT(Near Realtime):近实时
- 写入数据时,过 1 秒才会被搜索到,因为内部在分词、录入索引。
- es 搜索时:搜索和分析数据需要秒级出结果。
Cluster:集群
包含一个或多个启动着 es 实例的机器群。通常一台机器起一个 es 实例。同一网络下,集名一样的多个 es 实例自动组成集群,自动均衡分片等行为。默认集群名为"elasticsearch"。Node:节点
每个 es 实例称为一个节点。节点名自动分配,也可以手动配置。Document:文档
es 中的最小数据单元。一个 document 就像数据库中的一条记录。通常以 json 格式显示。多个 document 存储于一个索引(Index)中。
Index:索引
包含一堆有相似结构的文档数据。索引创建规则:
- 仅限小写字母
- 不能包含\、/、*、?、”、<、>、|、#以及空格符等特殊符号
- 从 7.0 版本开始不再包含冒号
- 不能以、或+开头或者_下划线开头
- 不能超过 255 个字节
Field:字段
就像数据库中的列(Columns),定义每个 document 应该有的字段。Type:类型
每个索引里都可以有一个或多个 type,type 是 index 中的一个逻辑数据分类,type 下的 document.都有相同的 field.
注意:6.0 之前的版本有 type(类型)概念,type 相当于关系数据库的表,ES 官方将在 ES9.0 版本中彻底删除 type。shard:分片
index 数据过大时,将 index 里面的数据,分为多个 shard,分布式的存储在各个服务器上面。可以支持海量数据和高并发,提升性能和吞吐量,充分利用多台机器的 cpu。replica:副本
在分布式环境下,任何一台机器都会随时宕机,如果宕机,index 的一个分片没有,导致此 index 不能搜索。所以,为了保证数据的安全,我们会将每个 index 的分片经行备份,存储在另外的机器上。保证少数机器宕机 es 集群仍可以搜索。
能正常提供查询和插入的分片我们叫做主分片(primary shard),其余的我们就管他们叫做备份的分片(replica shard)。es6 默认新建索引时,5 分片,1 副本,也就是一主一备,共 10 个分片。所以,es 集群最小规模为两台。es7 1 分片,1 副本,一共 2 分片。
7.核心概念对比
关系型数据库 mysql | 非关系型数据库 Elasticsearch |
数据库 Database | 索引 Index |
表 Table | 索引 Index(原为 Type) |
数据行 Row | 文档 Document |
数据列 Column | 字段 Field |
约束 Schema | 映射 Mapping |
8.文档数据格式
- 应用系统的数据结构都是面向对象的,具有复杂的数据结构。
- 对象存储到数据库,需要将关联的复杂对象属性插到另一张表,查询时再拼接起来。
- es 面向文档,文档中存储的数据结构,与对象一致。所以一个对象可以直接存成一个文档。
- es 的
document
用json
数据格式来表达。
而在 es 中,一个学生存成文档如下:
{
"id": "1",
"name": "张三",
"last_name": "zhang",
"classInfo": {
"id": "1",
"className": "三年二班"
}
}
9.悲观锁乐观锁
为控制并发问题,我们通常采用锁机制。分为悲观锁和乐观锁两种机制。
悲观锁
:很悲观,所有情况都上锁。此时只有一个线程可以操作数据。具体例子为数据库中的行级锁、表级锁、读锁、写锁等。悲观锁特点:
优点是方便,直接加锁,对程序透明。缺点是效率低。乐观锁:
很乐观,对数据本身不加锁。提交数据时,通过一种机制验证是否存在冲突,如 es 中通过版本号验证。乐观锁特点:
优点是并发能力高。缺点是操作繁琐,在提交数据时,可能反复重试多次。
10.ES 并发控制
基于_version
的版本控制,es 对于文档的增删改都是基于版本号
。
PUT /book/_doc/2
{
"id":1,
"title":"这是一11文章",
"content":"xxxxx",
"comment":"备注信息",
"mobile":"13344556677"
}
{
"_index": "book",
"_type": "_doc",
"_id": "2",
"_version": 20,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 26,
"_primary_term": 1
}
多次执行返回的_version 是递增的
11.配置文件详解
配置文件的地址,可以参考部署 elasticsearch 的基础文档里面有说明.
#配置es的集群名称,默认是elasticsearch,es会自动发现在同一网段下的es,如果在同一网段下有多个集群,就可以用这个属性来区分不同的集群。
cluster.name: elasticsearch
#节点名,默认随机指定一个name列表中名字,该列表在es的jar包中config文件夹里name.txt文件中,其中有很多作者添加的有趣名字。
node.name: "Franz Kafka"
#指定该节点是否有资格被选举成为node,默认是true,es是默认集群中的第一台机器为master,如果这台机挂了就会重新选举master。
node.master: true
#指定该节点是否存储索引数据,默认为true。
node.data: true
#设置默认索引分片个数,默认为5片。
index.number_of_shards: 5
#设置默认索引副本个数,默认为1个副本。
index.number_of_replicas: 1
#设置配置文件的存储路径,默认是es根目录下的config文件夹。
path.conf: /path/to/conf
#设置索引数据的存储路径,默认是es根目录下的data文件夹,可以设置多个存储路径,用逗号隔开,例:
path.data: /path/to/data
path.data: /path/to/data1,/path/to/data2
#设置临时文件的存储路径,默认是es根目录下的work文件夹。
path.work: /path/to/work
#设置日志文件的存储路径,默认是es根目录下的logs文件夹
path.logs: /path/to/logs
#设置插件的存放路径,默认是es根目录下的plugins文件夹
path.plugins: /path/to/plugins
#设置为true来锁住内存。因为当jvm开始swapping时es的效率 会降低,所以要保证它不swap,可以把ES_MIN_MEM和ES_MAX_MEM两个环境变量设置成同一个值,并且保证机器有足够的内存分配给es。 同时也要允许elasticsearch的进程可以锁住内存,linux下可以通过`ulimit -l unlimited`命令。
bootstrap.mlockall: true
#设置绑定的ip地址,可以是ipv4或ipv6的,默认为0.0.0.0。
network.bind_host: 192.168.0.1
#设置其它节点和该节点交互的ip地址,如果不设置它会自动判断,值必须是个真实的ip地址。
network.publish_host: 192.168.0.1
#这个参数是用来同时设置bind_host和publish_host上面两个参数。
network.host: 192.168.0.1
#设置节点间交互的tcp端口,默认是9300。
transport.tcp.port: 9300
#设置是否压缩tcp传输时的数据,默认为false,不压缩。
transport.tcp.compress: true
#设置对外服务的http端口,默认为9200。
http.port: 9200
#设置内容的最大容量,默认100mb
http.max_content_length: 100mb
#是否使用http协议对外提供服务,默认为true,开启。
http.enabled: false
#gateway的类型,默认为local即为本地文件系统,可以设置为本地文件系统,分布式文件系统,hadoop的HDFS,和amazon的s3服务器,其它文件系统的设置方法下次再详细说。
gateway.type: local
#设置集群中N个节点启动时进行数据恢复,默认为1。
gateway.recover_after_nodes: 1
#设置初始化数据恢复进程的超时时间,默认是5分钟。
gateway.recover_after_time: 5m
#设置这个集群中节点的数量,默认为2,一旦这N个节点启动,就会立即进行数据恢复。
gateway.expected_nodes: 2
#初始化数据恢复时,并发恢复线程的个数,默认为4。
cluster.routing.allocation.node_initial_primaries_recoveries: 4
#添加删除节点或负载均衡时并发恢复线程的个数,默认为4。
cluster.routing.allocation.node_concurrent_recoveries: 2
#设置数据恢复时限制的带宽,如入100mb,默认为0,即无限制。
indices.recovery.max_size_per_sec: 0
#设置这个参数来限制从其它分片恢复数据时最大同时打开并发流的个数,默认为5。
indices.recovery.concurrent_streams: 5
#设置这个参数来保证集群中的节点可以知道其它N个有master资格的节点。默认为1,对于大的集群来说,可以设置大一点的值(2-4)
discovery.zen.minimum_master_nodes: 1
#设置集群中自动发现其它节点时ping连接超时时间,默认为3秒,对于比较差的网络环境可以高点的值来防止自动发现时出错。
discovery.zen.ping.timeout: 3s
#设置是否打开多播发现节点,默认是true。
discovery.zen.ping.multicast.enabled: false
#设置集群中master节点的初始列表,可以通过这些节点来自动发现新加入集群的节点。
discovery.zen.ping.unicast.hosts: ["host1", "host2:port", "host3[portX-portY]"]
#下面是一些查询时的慢日志参数设置
index.search.slowlog.level: TRACE
index.search.slowlog.threshold.query.warn: 10s
index.search.slowlog.threshold.query.info: 5s
index.search.slowlog.threshold.query.debug: 2s
index.search.slowlog.threshold.query.trace: 500ms
index.search.slowlog.threshold.fetch.warn: 1s
index.search.slowlog.threshold.fetch.info: 800ms
index.search.slowlog.threshold.fetch.debug:500ms
index.search.slowlog.threshold.fetch.trace: 200ms
三.进阶学习
1.隐藏特性
分布式机制
:分布式数据存储及共享。分片机制
:数据存储到哪个分片,副本数据写入。集群发现机制
:cluster discovery。新启动 es 实例,自动加入集群。shard 负载均衡
:大量数据写入及查询,es 会将数据平均分配。shard 副本
:新增副本数,分片重分配。
扩容:
- 垂直扩容:使用更加强大的服务器替代老服务器。但单机存储及运算能力有上线。且成本直线上升。如 10t 服务器 1 万。单个 10T 服务器可能 20 万。
- 水平扩容:采购更多服务器,加入集群。大数据。
rebalance:
新增或滅少 es 实例时,es 集群会将数据重新分配。
master节点:
- 创建删除节点
- 创建蒯除索引
节点对等:
- 节点对等,每个节点都能接收所有的请求
- 自动请求路由
- 响应收集
shard&replica机制:
- 每个 index 包含一个或多个 shard
- 每个 shard 都是一个最小工作单元,承载部分数据,lucene 实例,完整的建立索引和处理请求的能力
- 增减节点时,shard 会自动在 nodes 中负载均衡
- primary shard 和 replica shard,每个 document 肯定只存在于某一个 primary shard 以及其对应的 replica shard 中,不可能存在于多个 primary shard
- replica shard 是 primary shard 的副本,负责容错,以及承担读请求负载
- primary shard 的数量在创建索引的时候就固定了,replica shard 的数量可以随时修改
- primary shard 的默认数量是 1,replica 默认是 1,默认共有 2 个 shard,1 个 primary shard , 1 个 replica shard 注意:es7 以前 primary shard 的默认数量是 5,replica 默认是 1,默认有 10 个 shard,5 个 primary shard,5 个 replica shard
- primary shard 不能和自己的 replica shard 放在同一个节点上(否则节点宕机,primary shard 和副本都丢失,起不到容错的作用),但是可以和其他 primary shard 的 replica shard 放在同一个节点上
2.单 node 创建 index
number_of_shards 是指索引要做多少个分片,只能在创建索引时指定,后期无法修改。number_of_replicas 是指每个分片有多少个副本,后期可以动态修改
- 单 node 环境下,创建一个 index,有 3 个 primary shard,3 个 replica shard
- 集群 status 是 yellow
- 这个时候,只会将 3 个 primary shard 分配到仅有的一个 node 上去,另外 3 个 replica shard 是无法分配的
- 集群可以正常工作,但是一旦出现节点宕机,数据全部丢失,而且集群不可用,无法承接任何请求
PUT /test_index1
{
"settings" : {
"number_of_shards" : 3,
"number_of_replicas" : 1
}
}
3.横向扩容
- 分片自动负载均衡,分片向空闲机器转移。
- 每个节点存储更少分片,系统资源给与每个分片的资源更多,整体集群性能提高。
- 扩容极限:节点数大于整体分片数,则必有空闲机器。
- 容错性:只要一个索引的所有主分片在,集群就就可以运行。
4.数据路由
一个文档,最终会落在主分片的一个分片上,到底应该在哪一个分片?这就是数据路由。
路由算法:
shard = hash(routing) % number_of_primary_shards
哈希值对主分片数取模。
举例:
对一个文档经过 crud 时,都会带一个路由值 routing number。默认为文档_id(可能是手动指定,也可能是自动生成)。
存储 1 号文档,经过哈希计算,哈希值为 2,此索引有 3 个主分片,那么计算 2%3=2,就算出此文档在 P2 分片上。
决定一个 document 在哪个 shard 上,最重要的一个值就是 routing 值,默认是_id,也可以手动指定,相同的 routing 值,每次过来,从 hash 函数中,产出的 hash 值一定是相同的
手动指定routing number
PUT /test_index/_doc/15?routing=num
{
"num": 0,
"tags": []
}
场景:在程序中,架构师可以手动指定已有数据的一个属性为路由值,好处是可以定制一类文档数据存储到一个分片中。缺点是设计不好,会造成数据倾斜。
所以,不同文档尽量放到不同的索引中。剩下的事情交给 es 集群自己处理。
涉及到以往数据的查询搜索,所以一旦建立索引,主分片数不可变。
检索文件的步骤:
从主分片或者副本分片检索文档的步骤顺序:
- 客户端发送请求到一个
coordinate node
。 - 协调节点将搜索请求转发到所有的 shard 对应的
primary shard
或replica shard
,都可以。 - query phase:每个 shard 将自己的搜索结果(其实就是一些
doc id
)返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。 - fetch phase:接着由协调节点根据
doc id
去各个节点上拉取实际的document
数据,最终返回给客户端。
注意:
- 在处理读取请求时,协调节点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。
- 在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的
5.增删改内部机制
增删改可以看做 update,都是对数据的改动。一个改动请求发送到 es 集群,经历以下四个步骤:
- 客户端选择一个
node
发送请求过去,这个 node 就是coordinating node
(协调节点) coordinating node
,对document
进行路由,将请求转发给对应的node
(有 primary shard)- 实际的
node
上的primary shard
处理请求,然后将数据同步到replica node
。 coordinating node
,如果发现primary node
和所有replica node
都搞定之后,就返回响应结果给客户端。
6.查询的内部机制
- 客户端发送请求到任意一个
node
,成为coordinate node
coordinate node
对document
进行路由,将请求转发到对应的node
,此时会使用round-robin
随机轮询算法,在primary shard
以及其所有replica
中随机选择一个,让读请求负载均衡- 接收请求的
node
返回document
给coordinate node
coordinate node
返回document
给客户端- 特殊情况:
document
如果还在建立索引过程中,可能只有 primary shard 有,任何一个 replica shard 都没有,此时可能会导致无法读取到 document,但是 document 完成索引建立之后,primary shard 和 replica shard 就都有了。
7.bulk api 奇特的 json 格式
POST /_bulk
{"action": {"meta"}}\n
{"data"}\n
{"action": {"meta"}}\n
{"data"}\n
[
{
"action": {
"method": "create"
},
"data": {
"id": 1,
"field1": "java",
"field1": "spring"
}
},
{
"action": {
"method": "create"
},
"data": {
"id": 2,
"field1": "java",
"field1": "spring"
}
}
]
首先,bulk 中的每个操作都可能要转发到不同的 node 的 shard 去执行
如果采用比较良好的 json 数组格式,允许任意的换行,整个可读性非常棒,读起来很爽,es 拿到那种标准格式的 json 串以后,要按照下述流程去进行处理
- 将 json 数组解析为 JSONArray 对象,这个时候,整个数据,就会在内存中出现一份一模一样的拷贝,一份数据是 json 文本,一份数据是 JSONArray 对象
- 解析 json 数组里的每个 json,对每个请求中的 document 进行路由
- 为路由到同一个 shard 上的多个请求,创建一个请求数组。100 请求中有 10 个是到 P1.
- 将这个请求数组序列化
- 将序列化后的请求数组发送到对应的节点上去
耗费更多内存,更多的 jvm gc 开销,之前提到过 bulk size 最佳大小的那个问题,一般建议说在几千条那样,然后大小在 10MB 左右,所以说,可怕的事情来了。假设说现在 100 个 bulk 请求发送到了一个节点上去,然后每个请求是 10MB,100 个请求,就是 1000MB = 1GB,然后每个请求的 json 都 copy 一份为 jsonarray 对象,此时内存中的占用就会翻倍,就会占用 2GB 的内存,甚至还不止。因为弄成 jsonarray 之后,还可能会多搞一些其他的数据结构,2GB+的内存占用。
占用更多的内存可能就会积压其他请求的内存使用量,比如说最重要的搜索请求,分析请求,等等,此时就可能会导致其他请求的性能急速下降。
另外的话,占用内存更多,就会导致 java 虚拟机的垃圾回收次数更多,跟频繁,每次要回收的垃圾对象更多,耗费的时间更多,导致 es 的 java 虚拟机停止工作线程的时间更多。
现在的奇特格式:
POST /_bulk
{ "delete": { "_index": "test_index", "_id": "5" }} \n
{ "create": { "_index": "test_index", "_id": "14" }}\n
{ "test_field": "test14" }\n
{ "update": { "_index": "test_index", "_id": "2"} }\n
{ "doc" : {"test_field" : "bulk test"} }\n
- 不用将其转换为 json 对象,不会出现内存中的相同数据的拷贝,直接按照换行符切割 json
- 对每两个一组的 json,读取 meta,进行 document 路由
- 直接将对应的 json 发送到 node 上去
最大的优势在于,不需要将 json 数组解析为一个 JSONArray 对象,形成一份大数据的拷贝,浪费内存空间,尽可能地保证性能。
8.写入机制
先写入内存 buffer,在 buffer 里的时候数据是搜索不到的;同时将数据写入 translog 日志文件。
如果 buffer 快满了,或者到一定时间,就会将内存 buffer 数据 refresh
到一个新的 segment file
中,但是此时数据不是直接进入 segment file
磁盘文件,而是先进入 os cache
。这个过程就是 refresh
。
每隔 1 秒钟,es 将 buffer 中的数据写入一个新的segment file
,每秒钟会产生一个新的磁盘文件segment file
,这个 segment file
中就存储最近 1 秒内 buffer 中写入的数据。
但是如果 buffer 里面此时没有数据,那当然不会执行 refresh 操作,如果 buffer 里面有数据,默认 1 秒钟执行一次 refresh 操作,刷入一个新的 segment file 中。
操作系统里面,磁盘文件其实都有一个东西,叫做 os cache
,即操作系统缓存,就是说数据写入磁盘文件之前,会先进入 os cache
,先进入操作系统级别的一个内存缓存中去。只要 buffer
中的数据被 refresh 操作刷入 os cache
中,这个数据就可以被搜索到了。
为什么叫 es 是准实时的?NRT
,全称 near real-time
。默认是每隔 1 秒 refresh 一次的,所以 es 是准实时的,因为写入的数据 1 秒之后才能被看到。可以通过 es 的 restful api
或者 java api
,手动执行一次 refresh 操作,就是手动将 buffer 中的数据刷入 os cache
中,让数据立马就可以被搜索到。只要数据被输入 os cache
中,buffer 就会被清空了,因为不需要保留 buffer 了,数据在 translog 里面已经持久化到磁盘去一份了。
重复上面的步骤,新的数据不断进入 buffer 和 translog,不断将 buffer
数据写入一个又一个新的 segment file
中去,每次 refresh
完 buffer 清空,translog 保留。随着这个过程推进,translog 会变得越来越大。当 translog 达到一定长度的时候,就会触发 commit
操作。
commit 操作发生第一步,就是将 buffer 中现有数据 refresh
到 os cache
中去,清空 buffer。然后,将一个 commit point
写入磁盘文件,里面标识着这个 commit point
对应的所有 segment file
,同时强行将 os cache
中目前所有的数据都 fsync
到磁盘文件中去。最后清空 现有 translog 日志文件,重启一个 translog,此时 commit 操作完成。
这个 commit 操作叫做 flush
。默认 30 分钟自动执行一次 flush
,但如果 translog 过大,也会触发 flush
。flush 操作就对应着 commit 的全过程,我们可以通过 es api,手动执行 flush 操作,手动将 os cache 中的数据 fsync 强刷到磁盘上去。
translog 日志文件的作用是什么?你执行 commit 操作之前,数据要么是停留在 buffer 中,要么是停留在 os cache 中,无论是 buffer 还是 os cache 都是内存,一旦这台机器死了,内存中的数据就全丢了。所以需要将数据对应的操作写入一个专门的日志文件 translog
中,一旦此时机器宕机,再次重启的时候,es 会自动读取 translog 日志文件中的数据,恢复到内存 buffer 和 os cache 中去。
translog 其实也是先写入 os cache 的,默认每隔 5 秒刷一次到磁盘中去,所以默认情况下,可能有 5 秒的数据会仅仅停留在 buffer 或者 translog 文件的 os cache 中,如果此时机器挂了,会丢失 5 秒钟的数据。但是这样性能比较好,最多丢 5 秒的数据。也可以将 translog 设置成每次写操作必须是直接 fsync
到磁盘,但是性能会差很多。
实际上你在这里,如果面试官没有问你 es 丢数据的问题,你可以在这里给面试官炫一把,你说,其实 es 第一是准实时的,数据写入 1 秒后可以搜索到;可能会丢失数据的。有 5 秒的数据,停留在 buffer、translog os cache、segment file os cache 中,而不在磁盘上,此时如果宕机,会导致 5 秒的数据丢失。
总结一下,数据先写入内存 buffer,然后每隔 1s,将数据 refresh 到 os cache,到了 os cache 数据就能被搜索到(所以我们才说 es 从写入到能被搜索到,中间有 1s 的延迟)。每隔 5s,将数据写入 translog 文件(这样如果机器宕机,内存数据全没,最多会有 5s 的数据丢失),translog 大到一定程度,或者默认每隔 30mins,会触发 commit 操作,将缓冲区的数据都 flush 到 segment file 磁盘文件中。
数据写入 segment file 之后,同时就建立好了倒排索引。
9.bulk 性能问题
Elasticsearch bulk 操作的性能问题可能有很多原因,以下是一些常见的原因及对应的解决方案:
- 索引分片过多:如果索引的分片过多,会导致 bulk 操作的性能下降。因为 bulk 操作会同时操作多个分片,分片过多会导致操作变慢。解决方案是减少索引的分片数,通常建议不要超过 5 个分片。
- 磁盘 I/O 瓶颈:如果磁盘 I/O 性能不足,会导致 bulk 操作的性能下降。可以通过优化磁盘 I/O 性能来提高 bulk 操作的性能,例如使用更快的磁盘、使用 RAID 阵列等。
- 索引过大:如果索引过大,会导致 bulk 操作的性能下降。因为 bulk 操作需要同时操作多个文档,文档数量过多会导致操作变慢。可以通过分割索引或者使用时间序列索引等方式来解决问题。
- 网络带宽瓶颈:如果网络带宽不足,会导致 bulk 操作的性能下降。可以通过优化网络带宽来提高 bulk 操作的性能,例如使用更快的网络、增加网络带宽等。
- 索引设置不合理:如果索引的设置不合理,也会导致 bulk 操作的性能下降。例如,如果禁用了副本,可以通过启用副本来提高 bulk 操作的性能。
- 索引的文档大小不合适:如果索引的文档大小过大或者过小,也会影响 bulk 操作的性能。可以根据实际情况调整文档大小,通常建议文档大小控制在 1MB 左右。以上是一些可能导致 Elasticsearch bulk 操作性能下降的原因及对应的解决方案,可以根据实际情况进行优化。
四.Mapping 映射
1.Mapping 映射
概念:自动或手动为 index 中的_doc 建立的一种数据结构和相关配置,简称为 mapping 映射。
插入几条数据,让 es 自动为我们建立一个索引
PUT /website/_doc/1
{
"post_date": "2019-01-01",
"title": "my first article",
"content": "this is my first article in this website",
"author_id": 11400
}
PUT /website/_doc/2
{
"post_date": "2019-01-02",
"title": "my second article",
"content": "this is my second article in this website",
"author_id": 11400
}
PUT /website/_doc/3
{
"post_date": "2019-01-03",
"title": "my third article",
"content": "this is my third article in this website",
"author_id": 11400
}
对比数据库建表语句
create table website(
post_date date,
title varchar(50),
content varchar(100),
author_id int(11)
);
动态映射:dynamic mapping,自动为我们建立 index,以及对应的 mapping,mapping 中包含了每个 field 对应的数据类型,以及如何分词等设置。
重点:我们当然,也可以手动在创建数据之前,先创建 index,以及对应的 mapping
GET /website/_mapping/
{
"website" : {
"mappings" : {
"properties" : {
"author_id" : {
"type" : "long"
},
"content" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"post_date" : {
"type" : "date"
},
"title" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
尝试各种搜索
GET /website/_search?q=2019 0条结果
GET /website/_search?q=2019-01-01 1条结果
GET /website/_search?q=post_date:2019-01-01 1条结果
GET /website/_search?q=post_date:2019 0 条结果
搜索结果为什么不一致,因为 es 自动建立 mapping 的时候,设置了不同的 field 不同的 data type。不同的 data type 的分词、搜索等行为是不一样的。
2.精确匹配与全文搜索
exact value 精确匹配:
2019-01-01,exact value,搜索的时候,必须输入 2019-01-01,才能搜索出来
如果你输入一个 01,是搜索不出来的 select * from book where name= 'java'
full text 全文检索:
搜“笔记电脑”,笔记本电脑词条也要搜索出来,是如何做到的呢?
select * from book where name like '%java%'
- 缩写 vs. 全称:cn vs. china
- 格式转化:like liked likes
- 大小写:Tom vs tom
- 同义词:like vs love
不是单纯的只是匹配完整的一个值,而是可以对值进行拆分词语后(分词)进行匹配,也可以通过缩写、时态、大小写、同义词等进行匹配。
3.倒排索引核心原理
计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引
,指明该词在文章中出现的次数和位置
,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这种建立索引的方式叫倒排索引。当数据写入 ES 时,数据将会通过分词被切分为不同的 term,ES 将 term 与其对应的文档列表建立一种映射关系,这种结构就是倒排索引。如下图所示
为了进一步提升索引的效率,ES
在 term
的基础上利用 term
的前缀或者后缀构建了term index
, 用于对 term
本身进行索引,ES
实际的索引结构如下图所示:
这样当我们去搜索某个关键词时,ES
首先根据它的前缀
或者后缀
迅速缩小关键词的在 term dictionary
中的范围,大大减少了磁盘IO的次数
。
举例
doc1:I really liked my small dogs, and I think my mom also liked them.
doc2:He never liked any dogs, so I hope that my mom will not expect me to liked him.
分词,初步的倒排索引的建立
term | doc1 | doc2 |
I | * | * |
really | * | |
liked | * | * |
my | * | * |
small | * | |
dogs | * | |
and | * | |
think | * | |
mom | * | * |
also | * | |
them | * | |
He | * | |
never | * | |
any | * | |
so | * | |
hope | * | |
that | * | |
will | * | |
not | * | |
expect | * | |
me | * | |
to | * | |
him | * |
演示了一下倒排索引最简单的建立的一个过程
mother like little dog,不可能有任何结果
这不是我们想要的结果。同义词 mom\mother 在我们人类看来是一样。想进行标准化操作。
重建倒排索引:
normalization 正规化,建立倒排索引的时候,会执行一个操作,也就是说对拆分出的各个单词进行相应的处理,以提升后面搜索的时候能够搜索到相关联的文档的概率
时态的转换,单复数的转换,同义词的转换,大小写的转换
- mom ―> mother
- liked ―> like
- small ―> little
- dogs ―> dog
重新建立倒排索引,加入 normalization,再次用 mother liked little dog 搜索,就可以搜索到了
word | doc1 | doc2 | normalization |
I | * | * | |
really | * | ||
like | * | * | liked ―> like |
my | * | * | |
little | * | small ―> little | |
dog | * | dogs ―> dog | |
and | * | ||
think | * | ||
mother | * | * | mom ―> mother |
also | * | ||
them | * | ||
He | * | ||
never | * | ||
any | * | ||
so | * | ||
hope | * | ||
that | * | ||
will | * | ||
not | * | ||
expect | * | ||
me | * | ||
to | * | ||
him | * |
重新搜索
搜索:mother liked little dog
对搜索条件经行分词 normalization
mother
liked -》like
little
dog
doc1 和 doc2 都会搜索出来
4.分词器 analyzer
作用:
- 切分词语
- normalization
- 提升 recall 召回率
analyzer:
给你一段句子,然后将这段句子拆分成一个一个的单个的单词,同时对每个单词进行 normalization(时态转换,单复数转换)
recall召回率:
搜索的时候,增加能够搜索到的结果的数量
analyzer 组成部分:
- character filter:在一段文本进行分词之前,先进行预处理,比如说最常见的就是,过滤 html 标签
- tokenizer:分词,hello you and me --> hello, you, and, me
- token filter:
- lowercase
- stop word: a/the/an --> 干掉
- synonymom
- dogs --> dog 单复数
- liked --> like 时态
- Tom --> tom 大小写
- mother --> mom 近义词
- small --> little 近义词
一个分词器,将一段文本进行各种处理,最后处理好的结果才会拿去建立倒排索引。
5.内置分词器
例句:Set the shape to semi-transparent by calling set_trans(5)
- standard analyzer 标准分词器:set, the, shape, to, semi, transparent, by, calling, set_trans, 5(默认的是 standard)
- simple analyzer 简单分词器:set, the, shape, to, semi, transparent, by, calling, set, trans
- whitespace analyzer:Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
- language analyzer(特定的语言的分词器,比如说,english,英语分词器):set, shape, semi, transpar, call, set_tran, 5
6.根据字段分词策略
query string 必须以和 index 建立时相同的 analyzer 进行分词
query string 对 exact value 和 full text 的区别对待
- date:exact value 精确匹配
- text: full text 全文检索
#测试分词器
GET /_analyze
{
"analyzer": "standard",
"text": "Text to analyze 80"
}
返回值:
{
"tokens": [
{
"token": "text",
"start_offset": 0,
"end_offset": 4,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "to",
"start_offset": 5,
"end_offset": 7,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "analyze",
"start_offset": 8,
"end_offset": 15,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "80",
"start_offset": 16,
"end_offset": 18,
"type": "<NUM>",
"position": 3
}
]
}
- token 实际存储的 term 关键字
- position 在此词条在原文本中的位置
- start_offset/end_offset 字符在原始字符串中的位置
7.mapping 总结
- 往 es 里面直接插入数据,es 会自动建立索引,同时建立对应的 mapping。(dynamic mapping)
- mapping 中就自动定义了每个 field 的数据类型
- 不同的数据类型(比如说 text 和 date),可能有的是 exact value,有的是 full text
- exact value,在建立倒排索引的时候,分词的时候,是将整个值一起作为一个关键词建立到倒排索引中的;full text,会经历各种各样的处理,分词,normaliztion(时态转换,同义词转换,大小写转换),才会建立到倒排索引中。
- 同时呢,exact value 和 full text 类型的 field 就决定了,在一个搜索过来的时候,对 exact value field 或者是 full text field 进行搜索的行为也是不一样的,会跟建立倒排索引的行为保持一致;比如说 exact value 搜索的时候,就是直接按照整个值进行匹配,full text query string,也会进行分词和 normalization 再去倒排索引中去搜索
- 可以用 es 的 dynamic mapping,让其自动建立 mapping,包括自动设置数据类型;也可以提前手动创建 index 和 tmapping,自己对各个 field 进行设置,包括数据类型,包括索引行为,包括分词器,等。
8.核心的数据类型
类型 | 说明 |
string | text and keyword |
Numeric | long, integer, short, byte, double, float, half_float, scaled_float |
Date | date |
Date nanoseconds | date_nanos |
Boolean | boolean |
Binary | binary |
Range | integer_range, float_range, long_range, double_range, date_range |
dynamic mapping 推测规则:
- true or false --> boolean
- 123 --> long
- 123.45 --> double
- 2019-01-01 --> date
- "hello world" --> text/keywod
查看 mapping:
GET /index/_mapping/
9.手动管理 mapping
查询所有索引的映射:
GET /_mapping
创建映射:
创建索引后,应该立即手动创建映射
PUT book/_mapping
{
"properties": {
"name": {
"type": "text"
},
"description": {
"type": "text",
"analyzer":"english",
"search_analyzer":"english"
},
"pic":{
"type":"text",
"index":false
},
"studymodel":{
"type":"text"
}
}
}
10.Text 文本类型
analyzer:
通过 analyzer 属性指定分词器。
上边指定了 analyzer 是指在创建索引和搜索时都使用 english,如果单独想定义搜索时使用的分词器则可以通过 search_analyzer 属性。
index:
index 属性指定是否索引。默认为 index=true,即要进行索引,只有进行索引才可以从索引库搜索到。
但是也有一些内容不需要索引,比如:商品图片地址只被用来展示图片,不进行搜索图片,此时可以将 index 设置为 false。
删除索引,重新创建映射,将 pic 的 index 设置为 false,尝试根据 pic 去搜索,结果搜索不到数据。
store:
是否在 source 之外存储,每个文档索引后会在 ES 中保存一份原始文档,存放在"_source"中,一般情况下不需要设置 store 为 true,因为在_source 中已经有一份原始文档了。
测试
PUT book/_mapping
{
"properties": {
"name": {
"type": "text"
},
"description": {
"type": "text",
"analyzer":"english",
"search_analyzer":"english"
},
"pic":{
"type":"text",
"index":false
},
"studymodel":{
"type":"text"
}
}
}
插入文档:
PUT /book/_doc/1
{
"name":"Bootstrap开发框架",
"description":"Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较为广泛。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。",
"pic":"group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg",
"studymodel":"201002"
}
Get /book/_search?q=name:开发
Get /book/_search?q=description:开发
Get /book/_search?q=pic:group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg
Get /book/_search?q=studymodel:201002
通过测试发现:name 和 description 都支持全文检索,pic 不可作为查询条件。
11.keyword 关键字
目前已经取代了"index": false。
上边介绍的 text 文本字段在映射时要设置分词器,keyword 字段为关键字字段,通常搜索 keyword 是按照整体搜索,所以创建 keyword 字段的索引时是不进行分词的,比如:邮政编码、手机号码、身份证等。
keyword 字段通常用于过虑、排序、聚合等。
12.date 日期类型
日期类型不用设置分词器。通常日期类型的字段用于排序。
format:
通过 format 设置日期格式
下边的设置允许 date 字段存储年月日时分秒、年月日格式。
{
"properties": {
"timestamp": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"
}
}
}
插入文档:
Post book/doc/3
{
"name": "spring 开发基础",
"description": "spring 在 java 领域非常流行,java 程序员都在用。",
"studymodel": "201001",
"pic": "group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg",
"timestamp": "2018-07-04 18:28:58"
}
13.数值类型
1、尽量选择范围小的类型,提高搜索效率
2、对于浮点数尽量用比例因子
,比如一个价格字段,单位为元,我们将比例因子设置为 100 这在 ES 中会按分 存储,映射如下:
"price": {
"type": "scaled_float",
"scaling_factor": 100
},
由于比例因子为 100,如果我们输入的价格是 23.45 则 ES 中会将 23.45 乘以 100 存储在 ES 中。
如果输入的价格是 23.456,ES 会将 23.456 乘以 100 再取一个接近原始值的数,得出 2346。
使用比例因子的好处是整型比浮点型更易压缩,节省磁盘空间。
更新已有映射,并插入文档:
PUT book/doc/3
{
"name": "spring开发基础",
"description": "spring 在java领域非常流行,java程序员都在用。",
"studymodel": "201001",
"pic":"group1/M00/00/01/wKhlQFqO4MmAOP53AAAcwDwm6SU490.jpg",
"timestamp":"2018-07-04 18:28:58",
"price":38.6
}
14.修改映射
只能创建 index 时手动建立 mapping,或者新增 field mapping,但是不能 update field mapping。
因为已有数据按照映射早已分词存储好。如果修改,那这些存量数据怎么办。
删除映射:
通过删除索引来删除映射。
新增一个字段 mapping
PUT /book/_mapping/
{
"properties": {
"new_field": {
"type": "text",
"index": "false"
}
}
}
如果修改 mapping,会报错
PUT /book/_mapping/
{
"properties": {
"studymodel": {
"type": "keyword"
}
}
}
返回:
{
"error": {
"root_cause": [
{
"type": "illegal_argument_exception",
"reason": "mapper [studymodel] of different type, current_type [text], merged_type [keyword]"
}
],
"type": "illegal_argument_exception",
"reason": "mapper [studymodel] of different type, current_type [text], merged_type [keyword]"
},
"status": 400
}
15.复杂数据类型
multivalue field:
{
"tags": ["tag1", "tag2"]
}
建立索引时与 string 是一样的,数据类型不能混
empty field:
null,[],[null]
object field:
PUT /company/_doc/1
{
"address": {
"country": "china",
"province": "guangdong",
"city": "guangzhou"
},
"name": "jack",
"age": 27,
"join_date": "2019-01-01"
}
address:object 类型
查询映射
GET /company/_mapping
{
"company" : {
"mappings" : {
"properties" : {
"address" : {
"properties" : {
"city" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"country" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"province" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
},
"age" : {
"type" : "long"
},
"join_date" : {
"type" : "date"
},
"name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
object
{
"address": {
"country": "china",
"province": "guangdong",
"city": "guangzhou"
},
"name": "jack",
"age": 27,
"join_date": "2017-01-01"
}
底层存储格式
{
"name": [jack],
"age": [27],
"join_date": [2017-01-01],
"address.country": [china],
"address.province": [guangdong],
"address.city": [guangzhou]
}
对象数组:
{
"authors": [
{ "age": 26, "name": "Jack White" },
{ "age": 55, "name": "Tom Jones" },
{ "age": 39, "name": "Kitty Smith" }
]
}
存储格式:
{
"authors.age": [26, 55, 39],
"authors.name": [jack, white, tom, jones, kitty, smith]
}
五.索引 index
1.索引管理
直接 put 数据
PUT index/_doc/1
es 会自动生成索引,并建立动态映射 dynamic mapping
在生产上,我们需要自己手动建立索引和映射,为了更好地管理索引。就像数据库的建表语句一样。
创建索引:
创建索引的语法
PUT /index
{
"settings": { ... any settings ... },
"mappings": {
"properties" : {
"field1" : { "type" : "text" }
}
},
"aliases": {
"default_index": {}
}
}
举例:
PUT /my_index
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"field1":{
"type": "text"
},
"field2":{
"type": "text"
}
}
},
"aliases": {
"default_index": {}
}
}
索引别名
插入数据
POST /my_index/_doc/1
{
"field1":"java",
"field2":"js"
}
查询数据 都可以查到
GET /my_index/_doc/1
GET /default_index/_doc/1
查询索引:
GET /my_index/_mapping
GET /my_index/_setting
修改索引:
修改副本数
PUT /my_index/_settings
{
"index" : {
"number_of_replicas" : 2
}
}
删除索引:
DELETE /my_index
DELETE /index_one,index_two
DELETE /index_*
DELETE /_all
为了安全起见,防止恶意删除索引,删除时必须指定索引名:
#elasticsearch.yml文件中
action.destructive_requires_name: true
2.默认的分词器
standard 是默认的分词器
分词器有三个组件:
- character filter
- tokenizer
- filter
分词器功能:
- standard tokenizer:以单词边界进行切分
- standard token filter:什么都不做
- lowercase token filter:将所有字母转换为小写
- stop token filer(默认被禁用):移除停用词,比如 a the it 等等
3.修改分词器
启用 english 停用词 token filter
PUT /my_index
{
"settings": {
"analysis": {
"analyzer": {
"es_std": {
"type": "standard",
"stopwords": "_english_"
}
}
}
}
}
测试分词
GET /my_index/_analyze
{
"analyzer": "standard",
"text": "a dog is in the house"
}
GET /my_index/_analyze
{
"analyzer": "es_std",
"text":"a dog is in the house"
}
4.定制化自己的分词器
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": {
"&_to_and": {
"type": "mapping",
"mappings": ["&=> and"]
}
},
"filter": {
"my_stopwords": {
"type": "stop",
"stopwords": ["the", "a"]
}
},
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": ["html_strip", "&_to_and"],
"tokenizer": "standard",
"filter": ["lowercase", "my_stopwords"]
}
}
}
}
}
测试
GET /my_index/_analyze
{
"analyzer": "my_analyzer",
"text": "tom&jerry are a friend in the house, <a>, HAHA!!"
}
设置字段使用自定义分词器
PUT /my_index/_mapping/
{
"properties": {
"content": {
"type": "text",
"analyzer": "my_analyzer"
}
}
}
5.type 底层结构及弃用原因
type,是一个 index 中用来区分类似的数据的,类似的数据,但是可能有不同的 fields,而且有不同的属性来控制索引建立、分词器 field 的 value,在底层的 lucene 中建立索引的时候,全部是 opaque bytes 类型,不区分类型的。lucene 是没有 type 的概念的,在 document 中,实际上将 type 作为一个 document 的 field 来存储,即_type,es 通过_type 来进行 type 的过滤和筛选。
es 中不同 type 存储机制:
一个 index 中的多个 type,实际上是放在一起存储的,因此一个 index 下,不能有多个 type 重名,而类型或者其他设置不同的,因为那样是无法处理的
{
"goods": {
"mappings": {
"electronic_goods": {
"properties": {
"name": {
"type": "string"
},
"price": {
"type": "double"
},
"service_period": {
"type": "string"
}
}
},
"fresh_goods": {
"properties": {
"name": {
"type": "string"
},
"price": {
"type": "double"
},
"eat_period": {
"type": "string"
}
}
}
}
}
}
PUT /goods/electronic_goods/1
{
"name": "小米空调",
"price": 1999.0,
"service_period": "one year"
}
PUT /goods/fresh_goods/1
{
"name": "澳洲龙虾",
"price": 199.0,
"eat_period": "one week"
}
es 文档在底层的存储是这样子的
{
"goods": {
"mappings": {
"_type": {
"type": "string",
"index": "false"
},
"name": {
"type": "string"
}
"price": {
"type": "double"
}
"service_period": {
"type": "string"
},
"eat_period": {
"type": "string"
}
}
}
}
底层数据存储格式
{
"_type": "electronic_goods",
"name": "小米空调",
"price": 1999.0,
"service_period": "one year",
"eat_period": ""
}
{
"_type": "fresh_goods",
"name": "澳洲龙虾",
"price": 199.0,
"service_period": "",
"eat_period": "one week"
}
type 弃用:
同一索引下,不同 type 的数据存储其他 type 的 field 大量空值,造成资源浪费。
所以,不同类型数据,要放到不同的索引中。es9 中,将会彻底删除 type。
6.定制 dynamic mapping
dynamic属性值介绍:
- true:遇到陌生字段,就进行 dynamic mapping
- false:新检测到的字段将被忽略。这些字段将不会被索引,因此将无法搜索,但仍将出现在返回点击的源字段中。这些字段不会添加到映射中,必须显式添加新字段。
- strict:遇到陌生字段,就报错
创建 mapping
PUT /my_index
{
"mappings": {
"dynamic": "strict",
"properties": {
"title": {
"type": "text"
},
"address": {
"type": "object",
"dynamic": "true"
}
}
}
}
插入数据
PUT /my_index/_doc/1
{
"title": "my article",
"content": "this is my article",
"address": {
"province": "guangdong",
"city": "guangzhou"
}
}
报错
{
"error": {
"root_cause": [
{
"type": "strict_dynamic_mapping_exception",
"reason": "mapping set to strict, dynamic introduction of [content] within [_doc] is not allowed"
}
],
"type": "strict_dynamic_mapping_exception",
"reason": "mapping set to strict, dynamic introduction of [content] within [_doc] is not allowed"
},
"status": 400
}
自定义dynamic mapping策略:
es 会根据传入的值,推断类型。
date_detection日期探测:
默认会按照一定格式识别 date,比如 yyyy-MM-dd。但是如果某个 field 先过来一个 2017-01-01 的值,就会被自动 dynamic mapping 成 date,后面如果再来一个"hello world"之类的值,就会报错。可以手动关闭某个 type 的 date_detection,如果有需要,自己手动指定某个 field 为 date 类型。
PUT /my_index
{
"mappings": {
"date_detection": false,
"properties": {
"title": {
"type": "text"
},
"address": {
"type": "object",
"dynamic": "true"
}
}
}
}
测试:
PUT /my_index/_doc/1
{
"title": "my article",
"content": "this is my article",
"address": {
"province": "guangdong",
"city": "guangzhou"
},
"post_date":"2019-09-10"
}
查看映射:
GET /my_index/_mapping
自定义日期格式:
PUT my_index
{
"mappings": {
"dynamic_date_formats": ["MM/dd/yyyy"]
}
}
插入数据:
PUT my_index/_doc/1
{
"create_date": "09/25/2019"
}
numeric_detection 数字探测:
虽然 json 支持本机浮点和整数数据类型,但某些应用程序或语言有时可能将数字呈现为字符串。通常正确的解决方案是显式地映射这些字段,但是可以启用数字检测(默认情况下禁用)来自动完成这些操作。
PUT my_index
{
"mappings": {
"numeric_detection": true
}
}
PUT my_index/_doc/1
{
"my_float": "1.0",
"my_integer": "1"
}
7.dynamic mapping 模版
PUT /my_index
{
"mappings": {
"dynamic_templates": [
{
"en": {
"match": "*_en",
"match_mapping_type": "string",
"mapping": {
"type": "text",
"analyzer": "english"
}
}
}
]
}
}
插入数据
PUT /my_index/_doc/1
{
"title": "this is my first article"
}
PUT /my_index/_doc/2
{
"title_en": "this is my first article"
}
搜索
GET my_index/_search?q=first
GET my_index/_search?q=is
测试结果说明:
title 没有匹配到任何的 dynamic 模板,默认就是 standard 分词器,不会过滤停用词,is 会进入倒排索引,用 is 来搜索是可以搜索到的
title_en 匹配到了 dynamic 模板,就是 english 分词器,会过滤停用词,is 这种停用词就会被过滤掉,用 is 来搜索就搜索不到了
模板写法:
PUT my_index
{
"mappings": {
"dynamic_templates": [
{
"integers": {
"match_mapping_type": "long",
"mapping": {
"type": "integer"
}
}
},
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"type": "text",
"fields": {
"raw": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
]
}
}
8.特殊 mapping 设置
结构化搜索:
默认情况下,elasticsearch 将字符串字段映射为带有子关键字字段的文本字段。但是,如果只对结构化内容进行索引,而对全文搜索不感兴趣,则可以仅将“字段”映射为“关键字”。请注意,这意味着为了搜索这些字段,必须搜索索引所用的完全相同的值。
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword"
}
}
}
仅搜索:
与前面的示例相反,如果您只关心字符串字段的全文搜索,并且不打算对字符串字段运行聚合、排序或精确搜索,您可以告诉弹性搜索将其仅映射为文本字段
{
"strings_as_text": {
"match_mapping_type": "string",
"mapping": {
"type": "text"
}
}
}
norms 不关心评分:
norms 是指标时间的评分因素。如果您不关心评分,例如,如果您从不按评分对文档进行排序,则可以在索引中禁用这些评分因子的存储并节省一些空间。
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": {
"type": "text",
"norms": false,
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
9.零停机重建索引
场景:
一个 field 的设置是不能被修改的,如果要修改一个 Field,那么应该重新按照新的 mapping,建立一个 index,然后将数据批量查询出来,重新用 bulk api 写入 index 中。
批量查询的时候,建议采用 scroll api,并且采用多线程并发的方式来 reindex 数据,每次 scoll 就查询指定日期的一段数据,交给一个线程即可。
示例:
一开始,依靠 dynamic mapping,插入数据,但是不小心有些数据是 2019-09-10 这种日期格式的,所以 title 这种 field 被自动映射为了 date 类型,实际上它应该是 string 类型的
PUT /my_index/_doc/1
{
"title": "2019-09-10"
}
PUT /my_index/_doc/2
{
"title": "2019-09-11"
}
当后期向索引中加入 string 类型的 title 值的时候,就会报错
PUT /my_index/_doc/3
{
"title": "my first article"
}
报错
{
"error": {
"root_cause": [
{
"type": "mapper_parsing_exception",
"reason": "failed to parse [title]"
}
],
"type": "mapper_parsing_exception",
"reason": "failed to parse [title]",
"caused_by": {
"type": "illegal_argument_exception",
"reason": "Invalid format: \"my first article\""
}
},
"status": 400
}
如果此时想修改 title 的类型,是不可能的
PUT /my_index/_mapping
{
"properties": {
"title": {
"type": "text"
}
}
}
报错
{
"error": {
"root_cause": [
{
"type": "illegal_argument_exception",
"reason": "mapper [title] of different type, current_type [date], merged_type [text]"
}
],
"type": "illegal_argument_exception",
"reason": "mapper [title] of different type, current_type [date], merged_type [text]"
},
"status": 400
}
此时,唯一的办法,就是进行 reindex,也就是说,重新建立一个索引,将旧索引的数据查询出来,再导入新索引。
如果说旧索引的名字,是 old_index,新索引的名字是 new_index,终端 java 应用,已经在使用 old_index 在操作了,难道还要去停止 java 应用,修改使用的 index 为 new_index,才重新启动 java 应用吗?这个过程中,就会导致 java 应用停机,可用性降低。
所以说,给 java 应用一个别名,这个别名是指向旧索引的,java 应用先用着,java 应用先用 prod_index alias 来操作,此时实际指向的是旧的 my_index
PUT /my_index/_alias/prod_index
新建一个 index,调整其 title 的类型为 string
PUT /my_index_new
{
"mappings": {
"properties": {
"title": {
"type": "text"
}
}
}
}
使用 scroll api 将数据批量查询出来
GET /my_index/_search?scroll=1m
{
"query": {
"match_all": {}
},
"size": 1
}
返回
{
"_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAADpAFjRvbnNUWVZaVGpHdklqOV9zcFd6MncAAAAAAAA6QRY0b25zVFlWWlRqR3ZJajlfc3BXejJ3AAAAAAAAOkIWNG9uc1RZVlpUakd2SWo5X3NwV3oydwAAAAAAADpDFjRvbnNUWVZaVGpHdklqOV9zcFd6MncAAAAAAAA6RBY0b25zVFlWWlRqR3ZJajlfc3BXejJ3",
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 3,
"max_score": null,
"hits": [
{
"_index": "my_index",
"_type": "my_type",
"_id": "1",
"_score": null,
"_source": {
"title": "2019-01-02"
},
"sort": [0]
}
]
}
}
采用 bulk api 将 scoll 查出来的一批数据,批量写入新索引
POST /_bulk
{ "index": { "_index": "my_index_new", "_id": "1" }}
{ "title": "2019-09-10" }
反复循环 8~9,查询一批又一批的数据出来,采取 bulk api 将每一批数据批量写入新索引
将 prod_index alias 切换到 my_index_new 上去,java 应用会直接通过 index 别名使用新的索引中的数据,java 应用程序不需要停机,零提交,高可用
POST /_aliases
{
"actions": [
{ "remove": { "index": "my_index", "alias": "prod_index" }},
{ "add": { "index": "my_index_new", "alias": "prod_index" }}
]
}
直接通过 prod_index 别名来查询
GET /prod_index/_search
六.Ik 分词器
1.中文分词器
standard 分词器,仅适用于英文。
GET /_analyze
{
"analyzer": "standard",
"text": "中华人民共和国人民大会堂"
}
我们想要的效果是什么:中华人民共和国,人民大会堂,英文却拆成一个一个的词
IK 分词器就是目前最流行的 Elasticsearch 中文分词器
2. 安装
官网:https://github.com/medcl/elasticsearch-analysis-ik
下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases
根据 es 版本下载相应版本包。 我用的是 v7.12.0 的版本,注意版本对齐
#进入插件目录
/usr/local/elasticsearch-7.12.0/plugins
#创建ik目录
mkdir ik
#从mac传文件到服务器
scp /Users/qinyingjie/Downloads/elasticsearch-analysis-ik-7.12.0.zip root@47.119.160.231:/usr/local/elasticsearch-7.12.0/plugins/ik
#解压
unzip elasticsearch-analysis-ik-7.12.0.zip
#重启es
3.ik 分词器的使用
两个重要的属性:
- ik_max_word: 会将文本做最细粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民大会堂,人民大会,大会堂”,会穷尽各种可能的组合;
- ik_smart: 会做最粗粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国,人民大会堂”。
存储时,使用 ik_max_word,搜索时,使用 ik_smart
PUT /my_index
{
"mappings": {
"properties": {
"text": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
}
}
}
搜索
GET /my_index/_search?q=中华人民共和国人民大会堂
4.ik 配置文件
ik 配置文件地址:es/plugins/ik/config 目录
- IKAnalyzer.cfg.xml:用来配置自定义词库
- main.dic:ik 原生内置的中文词库,总共有 27 万多条,只要是这些单词,都会被分在一起
- preposition.dic: 介词
- quantifier.dic:放了一些单位相关的词,量词
- suffix.dic:放了一些后缀
- surname.dic:中国的姓氏
- stopword.dic:英文停用词
ik 原生最重要的两个配置文件
- main.dic:包含了原生的中文词语,会按照这个里面的词语去分词
- stopword.dic:包含了英文的停用词
停用词,stopword:
- 例如: a the and at but
- 一般,像停用词,会在分词的时候,直接被干掉,不会建立在倒排索引中
5.自定义词库
- 自己建立词库:每年都会涌现一些特殊的流行词,网红,蓝瘦香菇,喊麦,鬼畜,一般不会在 ik 的原生词典里
- 自己补充自己的最新的词语,到 ik 的词库里面
- IKAnalyzer.cfg.xml:ext_dict,创建 mydict.dic。
- 补充自己的词语,然后需要重启 es,才能生效
- 自己建立停用词库:比如了,的,啥,么,我们可能并不想去建立索引,让人家搜索
custom/ext_stopword.dic,已经有了常用的中文停用词,可以补充自己的停用词,然后重启 es
6.使用 mysql 热更新
- 每次都是在 es 的扩展词典中,手动添加新词语,很坑
- 每次添加完,都要重启 es 才能生效,非常麻烦
- es 不停机,我们直接在外部某个地方添加新的词语,es 中立即热加载到这些新词语
热更新的方案:
- 基于 ik 分词器原生支持的热更新方案,部署一个 web 服务器,提供一个 http 接口,通过 modified 和 tag 两个 http 响应头,来提供词语的热更新
- 修改 ik 分词器源码,然后手动支持从 mysql 中每隔一定时间,自动加载新的词库
用第二种方案,第一种,ik git 社区官方都不建议采用,觉得不太稳定
七.search 搜索
1.query string search
无条件搜索所有
GET /book/_search
{
"took": 969,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": 1.0,
"hits": [
{
"_index": "book",
"_type": "_doc",
"_id": "1",
"_score": 1.0,
"_source": {
"name": "Bootstrap开发",
"description": "Bootstrap是由Twitter推出的一个前台页面开发css框架,是一个非常流行的开发框架,此框架集成了多种页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长css页面开发的程序人员)轻松的实现一个css,不受浏览器限制的精美界面css效果。",
"studymodel": "201002",
"price": 38.6,
"timestamp": "2019-08-25 19:11:35",
"pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": ["bootstrap", "dev"]
}
}
}
]
}
}
解释:
- took:耗费了几毫秒
- timed_out:是否超时,这里是没有
- _shards:到几个分片搜索,成功几个,跳过几个,失败几个。
- hits.total:查询结果的数量,3 个 document
- hits.max_score:score 的含义,就是 document 对于一个 search 的相关度的匹配分数,越相关,就越匹配,分数也高
- hits.hits:包含了匹配搜索的 document 的所有详细数据
2.带参数搜索
与 http 请求传参类似
GET /book/_search?q=name:java&sort=price:desc
类比 sql: select * from book where name like ’ %java%’ order by price desc
{
"took": 2,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "book",
"_type": "_doc",
"_id": "2",
"_score": null,
"_source": {
"name": "java编程思想",
"description": "java语言是世界第一编程语言,在软件开发领域使用人数最多。",
"studymodel": "201001",
"price": 68.6,
"timestamp": "2019-08-25 19:11:35",
"pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": ["java", "dev"]
},
"sort": [68.6]
}
]
}
}
3. timeout
GET /book/_search?timeout=10ms
设置搜索的超时时间,到超时就返回
全局设置:配置文件中设置 search.default_search_timeout:100ms。默认不超时。
4.多索引搜索
multi-index 搜索模式
告诉你如何一次性搜索多个 index 和多个 type 下的数据
#所有索引下的所有数据都搜索出来
/_search
#指定一个index,搜索其下所有的数据
/index1/_search
#同时搜索两个index下的数据
/index1,index2/_search
#按照通配符去匹配多个索引
/index*/_search
应用场景:生产环境 log 索引可以按照日期分开。
log_to_es_20190910
log_to_es_20190911
log_to_es_20180910
5.分页搜索
分页搜索的语法:
sql: select * from book limit 1,5
- GET /book/_search?size=10
- GET /book/_search?size=10&from=0
- GET /book/_search?size=10&from=20
- GET /book_search?from=0&size=3
6.deep paging
根据相关度评分倒排序,所以分页过深,协调节点会将大量数据聚合分析。
deep paging 性能问题:
- 消耗网络带宽,因为所搜过深的话,各 shard 要把数据传递给 coordinate node,这个过程是有大量数据传递的,消耗网络。
- 消耗内存,各 shard 要把数据传送给 coordinate node,这个传递回来的数据,是被 coordinate node 保存在内存中的,这样会大量消耗内存。
- 消耗 cup,coordinate node 要把传回来的数据进行排序,这个排序过程很消耗 cpu。
所以:鉴于 deep paging 的性能问题,所有应尽量减少使用。
7.query string 基础语法
- GET /book/_search?q=name:java
- GET /book/_search?q=+name:java
- GET /book/_search?q=-name:java
说明:
q=field:search content 的语法,就是根据字段进行搜索
+与没有+一样都是必须包含,-是必须不包含
8._all metadata 的原理和作用
不带字段的查询,会存在 all 索引中_,方便查询
GET /book/_search?q=java
直接可以搜索所有的 field,任意一个 field 包含指定的关键字就可以搜索出来。我们在进行中搜索的时候,难道是对 document 中的每一个 field 都进行一次搜索吗?不是的。
es 中_all 元数据。建立索引的时候,插入一条 docunment,es 会将所有的 field 值经行全量分词,把这些分词,放到_all field 中。在搜索的时候,没有指定 field,就在_all 搜索。
举例:
{
name:jack
email:123@qq.com
address:beijing
}
_all : jack,123@qq.com,beijing
9.query DSL
query string 后边的参数原来越多,搜索条件越来越复杂,不能满足需求。
GET /book/_search?q=name:java&size=10&from=0&sort=price:desc
DSL:Domain Specified Language,特定领域的语言
es 特有的搜索语言,可在请求体中携带搜索条件,功能强大。
查询全部 GET /book/_search
GET /book/_search
{
"query": { "match_all": {} }
}
排序 GET /book/_search?sort=price:desc
GET /book/_search
{
"query" : {
"match" : {
"name" : " java"
}
},
"sort": [
{ "price": "desc" }
]
}
分页查询 GET /book/_search?size=10&from=0
GET /book/_search
{
"query": { "match_all": {} },
"from": 0,
"size": 1
}
指定返回字段 GET /book/ _search? _source=name,studymodel
GET /book/_search
{
"query": { "match_all": {} },
"_source": ["name", "studymodel"]
}
通过组合以上各种类型查询,实现复杂查询。
10.Query DSL 语法
{
QUERY_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
{
QUERY_NAME: {
FIELD_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
}
GET /test_index/_search
{
"query": {
"match": {
"test_field": "test"
}
}
}
11.组合多个搜索条件
搜索需求:title 必须包含 elasticsearch,content 可以包含 elasticsearch 也可以不包含,author_id 必须不为 111
sql where and or !=
初始数据:
POST /website/_doc/1
{
"title": "my hadoop article",
"content": "hadoop is very bad",
"author_id": 111
}
POST /website/_doc/2
{
"title": "my elasticsearch article",
"content": "es is very bad",
"author_id": 112
}
POST /website/_doc/3
{
"title": "my elasticsearch article",
"content": "es is very goods",
"author_id": 111
}
搜索:
GET /website/_doc/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "elasticsearch"
}
}
],
"should": [
{
"match": {
"content": "elasticsearch"
}
}
],
"must_not": [
{
"match": {
"author_id": 111
}
}
]
}
}
}
返回:
{
"took": 488,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 0.47000363,
"hits": [
{
"_index": "website",
"_type": "_doc",
"_id": "2",
"_score": 0.47000363,
"_source": {
"title": "my elasticsearch article",
"content": "es is very bad",
"author_id": 112
}
}
]
}
}
更复杂的搜索需求:
select * from test_index where name='tom' or (hired =true and (personality ='good' and rude != true ))
GET /test_index/_search
{
"query": {
"bool": {
"must": { "match":{ "name": "tom" }},
"should": [
{ "match":{ "hired": true }},
{ "bool": {
"must":{ "match": { "personality": "good" }},
"must_not": { "match": { "rude": true }}
}}
],
"minimum_should_match": 1
}
}
}
12.full-text search 全文检索
重新创建 book 索引
PUT /book/
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"name":{
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"description":{
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"studymodel":{
"type": "keyword"
},
"price":{
"type": "double"
},
"timestamp": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
},
"pic":{
"type":"text",
"index":false
}
}
}
}
插入数据
PUT /book/_doc/1
{
"name": "Bootstrap开发",
"description": "Bootstrap是由Twitter推出的一个前台页面开发css框架,是一个非常流行的开发框架,此框架集成了多种页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长css页面开发的程序人员)轻松的实现一个css,不受浏览器限制的精美界面css效果。",
"studymodel": "201002",
"price":38.6,
"timestamp":"2019-08-25 19:11:35",
"pic":"group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": [ "bootstrap", "dev"]
}
PUT /book/_doc/2
{
"name": "java编程思想",
"description": "java语言是世界第一编程语言,在软件开发领域使用人数最多。",
"studymodel": "201001",
"price":68.6,
"timestamp":"2019-08-25 19:11:35",
"pic":"group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": [ "java", "dev"]
}
PUT /book/_doc/3
{
"name": "spring开发基础",
"description": "spring 在java领域非常流行,java程序员都在用。",
"studymodel": "201001",
"price":88.6,
"timestamp":"2019-08-24 19:11:35",
"pic":"group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": [ "spring", "java"]
}
搜索
GET /book/_search
{
"query" : {
"match" : {
"description" : "java程序员"
}
}
}
13._score 初探
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 2.137549,
"hits": [
{
"_index": "book",
"_type": "_doc",
"_id": "3",
"_score": 2.137549,
"_source": {
"name": "spring开发基础",
"description": "spring 在java领域非常流行,java程序员都在用。",
"studymodel": "201001",
"price": 88.6,
"timestamp": "2019-08-24 19:11:35",
"pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": ["spring", "java"]
}
},
{
"_index": "book",
"_type": "_doc",
"_id": "2",
"_score": 0.57961315,
"_source": {
"name": "java编程思想",
"description": "java语言是世界第一编程语言,在软件开发领域使用人数最多。",
"studymodel": "201001",
"price": 68.6,
"timestamp": "2019-08-25 19:11:35",
"pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": ["java", "dev"]
}
}
]
}
}
结果分析:
- 建立索引时, description 字段 term 倒排索引
- java 2,3
- 程序员 3
- 搜索时,直接找 description 中含有 java 的文档 2,3,并且 3 号文档含有两个 java 字段,一个程序员,所以得分高,排在前面。2 号文档含有一个 java,排在后面。
14.DSL 语法练习
14.1.match_all
GET /book/_search
{
"query": {
"match_all": {}
}
}
14.2.match
GET /book/_search
{
"query": {
"match": {
"description": "java程序员"
}
}
}
14.3.multi_match
GET /book/_search
{
"query": {
"multi_match": {
"query": "java程序员",
"fields": ["name", "description"]
}
}
}
14.4.range query 范围查询
GET /book/_search
{
"query": {
"range": {
"price": {
"gte": 80,
"lte": 90
}
}
}
}
14.5.term query
字段为 keyword 时,存储和搜索都不分词
GET /book/_search
{
"query": {
"term": {
"description": "java程序员"
}
}
}
14.6.terms query
GET /book/_search
{
"query": { "terms": { "tag": [ "search", "full_text", "nosql" ] }}
}
14.7.exist query
查询有某些字段值的文档
GET /_search
{
"query": {
"exists": {
"field": "join_date"
}
}
}
14.8.Fuzzy query
返回包含与搜索词类似的词的文档,该词由 Levenshtein 编辑距离度量。
包括以下几种情况:
- 更改角色(box→fox)
- 删除字符(aple→apple)
- 插入字符(sick→sic)
- 调换两个相邻字符(ACT→CAT)
GET /book/_search
{
"query": {
"fuzzy": {
"description": {
"value": "jave"
}
}
}
}
14.9.IDs
GET /book/_search
{
"query": {
"ids" : {
"values" : ["1", "4", "100"]
}
}
}
14.10.prefix 前缀查询
GET /book/_search
{
"query": {
"prefix": {
"description": {
"value": "spring"
}
}
}
}
14.11.regexp query
正则查询
GET /book/_search
{
"query": {
"regexp": {
"description": {
"value": "j.*a",
"flags" : "ALL",
"max_determinized_states": 10000,
"rewrite": "constant_score"
}
}
}
}
15.Filter
需求:用户查询 description 中有"java 程序员",并且价格大于 80 小于 90 的数据。
GET /book/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"description": "java程序员"
}
},
{
"range": {
"price": {
"gte": 80,
"lte": 90
}
}
}
]
}
}
}
使用 filter:
GET /book/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"description": "java程序员"
}
}
],
"filter": {
"range": {
"price": {
"gte": 80,
"lte": 90
}
}
}
}
}
}
16.filter 与 query 对比
filter,仅仅只是按照搜索条件过滤出需要的数据而已,不计算任何相关度分数,对相关度没有任何影响。
query,会去计算每个 document 相对于搜索条件的相关度,并按照相关度进行排序。
应用场景:
一般来说,如果你是在进行搜索,需要将最匹配搜索条件的数据先返回,那么用 query 如果你只是要根据一些条件筛选出一部分数据,不关注其排序,那么用 filter
性能比较:
- filter,不需要计算相关度分数,不需要按照相关度分数进行排序,同时还有内置的自动 cache 最常使用 filter 的数据
- query,相反,要计算相关度分数,按照分数进行排序,而且无法 cache 结果
17.定位错误语法
验证错误语句:
GET /book/_validate/query?explain
{
"query": {
"mach": {
"description": "java程序员"
}
}
}
返回:
{
"valid": false,
"error": "org.elasticsearch.common.ParsingException: no [query] registered for [mach]"
}
正确
GET /book/_validate/query?explain
{
"query": {
"match": {
"description": "java程序员"
}
}
}
返回
{
"_shards": {
"total": 1,
"successful": 1,
"failed": 0
},
"valid": true,
"explanations": [
{
"index": "book",
"valid": true,
"explanation": "description:java description:程序员"
}
]
}
一般用在那种特别复杂庞大的搜索下,比如你一下子写了上百行的搜索,这个时候可以先用 validate api 去验证一下,搜索是否合法。
合法以后,explain 就像 mysql 的执行计划,可以看到搜索的目标等信息。
18.默认排序规则
默认情况下,是按照_score 降序排序的
然而,某些情况下,可能没有有用的_score,比如说 filter
GET book/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"description": "java程序员"
}
}
]
}
}
}
当然,也可以是 constant_score
19.定制排序规则
相当于 sql 中 order by ?sort=sprice:desc
GET /book/_search
{
"query": {
"constant_score": {
"filter" : {
"term" : {
"studymodel" : "201001"
}
}
}
},
"sort": [
{
"price": {
"order": "asc"
}
}
]
}
20.Text 字段排序问题
如果对一个 text field 进行排序,结果往往不准确,因为分词后是多个单词,再排序就不是我们想要的结果了。
通常解决方案是,将一个 text field 建立两次索引,一个分词,用来进行搜索;一个不分词,用来进行排序。
PUT /website
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"content": {
"type": "text"
},
"post_date": {
"type": "date"
},
"author_id": {
"type": "long"
}
}
}
}
插入数据
PUT /website/_doc/1
{
"title": "first article",
"content": "this is my second article",
"post_date": "2019-01-01",
"author_id": 110
}
PUT /website/_doc/2
{
"title": "second article",
"content": "this is my second article",
"post_date": "2019-01-01",
"author_id": 110
}
PUT /website/_doc/3
{
"title": "third article",
"content": "this is my third article",
"post_date": "2019-01-02",
"author_id": 110
}
搜索
GET /website/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"title.keyword": {
"order": "desc"
}
}
]
}
21.Scroll 分批查询
场景:下载某一个索引中 1 亿条数据,到文件或是数据库。
不能一下全查出来,系统内存溢出。所以使用 scoll 滚动搜索技术,一批一批查询。
scoll 搜索会在第一次搜索的时候,保存一个当时的视图快照,之后只会基于该旧的视图快照提供数据搜索,如果这个期间数据变更,是不会让用户看到的
每次发送 scroll 请求,我们还需要指定一个 scoll 参数,指定一个时间窗口,每次搜索请求只要在这个时间窗口内能完成就可以了。
搜索
GET /book/_search?scroll=1m
{
"query": {
"match_all": {}
},
"size": 3
}
返回
{
"_scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAMOkWTURBNDUtcjZTVUdKMFp5cXloVElOQQ==",
"took": 3,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": 1.0,
"hits": []
}
}
获得的结果会有一个 scoll_id,下一次再发送 scoll 请求的时候,必须带上这个 scoll_id
GET /_search/scroll
{
"scroll": "1m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAMOkWTURBNDUtcjZTVUdKMFp5cXloVElOQQ=="
}
与分页区别:
- 分页是给用户看的 deep paging
- scroll 是用户系统内部操作,如下载批量数据,数据转移。零停机改变索引映射。
21.fielddata
计算每个 tags 下的商品数量
设置字段"fielddata": true,这个步骤很重要,不然会报错
在 es 中,text 类型的字段使用一种叫做 fielddata 的查询时内存数据结构。当字段被排序,聚合或者通过脚本访问时这种数据结构会被创建。它是通过从磁盘读取每个段的整个反向索引来构建的,然后存存储在 java 的堆内存中。
fileddata 默认是不开启的。Fielddata 可能会消耗大量的堆空间,尤其是在加载高基数文本字段时。一旦 fielddata 已加载到堆中,它将在该段的生命周期内保留。此外,加载 fielddata 是一个昂贵的过程,可能会导致用户遇到延迟命中。这就是默认情况下禁用 fielddata 的原因。如果尝试对文本字段进行排序,聚合或脚本访问,将看到以下异常:
“Fielddata is disabled on text fields by default. Set fielddata=true on [your_field_name] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory.”
在启用 fielddata 之前,请考虑使用文本字段进行聚合,排序或脚本的原因。这样做通常没有意义。text 字段在索引例如 New York 这样的词会被分词,会被拆成 new,york。在此字段上面来一个 terms 的聚合会返回一个 new 的 bucket 和一个 york 的 bucket,当你想只返回一个 New York 的 bucket 的时候就会出现问题。
PUT /book/_mapping/
{
"properties": {
"tags": {
"type": "text",
"fielddata": true
}
}
}
22.minimum_should_match
minimum_should_match 参数用于设置返回的文档必须匹配的最少 should 分支数。参数值合法格式如下:
类型 | 示例 | 说明 |
正整数 | 3 | 无论有多少 should 分支,必须匹配指定数量的 should 分支。 |
负整数 | -2 | 最多可以有多少个 should 分支不匹配。 |
正百分数 | 70% | 必须匹配 should 分支总数的指定百分比数量的 should 分支,分支数向下取整。例如总共有 5 个 should 分支,则至少匹配 3 个。 |
负百分数 | -20% | 最多可以有 should 分支总数的指定百分比数量的 should 分支不匹配,分支数向下取整。例如总共有 7 个 should 分支,则最多有 1 个 should 分支不匹配。 |
组合 | 2<70% | 一个正整数,然后是<,然后是上述的任意类型的说明符以此做为条件说明符。当 should 分支总数小于等于指定的数量时,则必须匹配所有 should 分支,当 should 分支总数大于指定的数量时,则应用指定的说明符。如示例中当 should 分支总数小于等于 2 时,则必须匹配所有分支,当 should 分支总数大于 2 时,则至少需要匹配 2 个 should 分支。 |
多组合 | 2<70% 6<-1 | 多个条件说明符用空格分隔,每个条件说明符只有在 should 分支数大于指定数量时有效。如示例中,当 should 分支总数小于等于 2 时,则必须全部匹配,当 should 分支总数大于 2 小于等于 6 时,则至少匹配 70%的 should 分支,当 should 分支总数大于 6 时,则最多有 1 个 should 分支不匹配。 |
如果基于说明符计算后不需要匹配 should 分支,那么布尔查询的通用规则:当布尔查询中没有 must 分支时必须匹配至少一个 should 分支,在检索时仍适用。无论计算的结果有多大或多小,需要匹配的 should 分支数永远不会小于 1 或大于 should 分支总数。
八.评分机制详解
1.评分机制 TF\IDF
TF-IDF(Term Frequency-Inverse Document Frequency)是一种用于信息检索和文本挖掘的统计方法,用以评估一个词在一个文档集中一个特定文档的重要程度。这个评分机制考虑了一个词语在特定文档中的出现频率(Term Frequency,TF)和在整个文档集中的逆文档频率(Inverse Document Frequency,IDF)。
TF(Term Frequency)
词频(Term Frequency,TF)表示一个词在一个特定文档
中出现的频率。这通常是该词在文档中出现次数与文档的总词数之比。
IDF(Inverse Document Frequency)
逆文档频率(Inverse Document Frequency,IDF)是一个词在文档集
中的重要性的度量。如果一个词很常见,出现在很多文档中(例如“和”,“是”等),那么它可能不会携带有用的信息。IDF度量就是为了降低这些常见词在文档相似性度量中的权重。
2.score 是如何被计算出来的
GET /book/_search?explain=true
{
"query": {
"match": {
"description": "java程序员"
}
}
}
返回
{
"took": 5,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 2.137549,
"hits": [
{
"_shard": "[book][0]",
"_node": "MDA45-r6SUGJ0ZyqyhTINA",
"_index": "book",
"_type": "_doc",
"_id": "3",
"_score": 2.137549,
"_source": {
"name": "spring开发基础",
"description": "spring 在java领域非常流行,java程序员都在用。",
"studymodel": "201001",
"price": 88.6,
"timestamp": "2019-08-24 19:11:35",
"pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": ["spring", "java"]
},
"_explanation": {
"value": 2.137549,
"description": "sum of:",
"details": [
{
"value": 0.7936629,
"description": "weight(description:java in 0) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.7936629,
"description": "score(freq=2.0), product of:",
"details": [
{
"value": 2.2,
"description": "boost",
"details": []
},
{
"value": 0.47000363,
"description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
"details": [
{
"value": 2,
"description": "n, number of documents containing term",
"details": []
},
{
"value": 3,
"description": "N, total number of documents with field",
"details": []
}
]
},
{
"value": 0.7675597,
"description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
"details": [
{
"value": 2.0,
"description": "freq, occurrences of term within document",
"details": []
},
{
"value": 1.2,
"description": "k1, term saturation parameter",
"details": []
},
{
"value": 0.75,
"description": "b, length normalization parameter",
"details": []
},
{
"value": 12.0,
"description": "dl, length of field",
"details": []
},
{
"value": 35.333332,
"description": "avgdl, average length of field",
"details": []
}
]
}
]
}
]
},
{
"value": 1.3438859,
"description": "weight(description:程序员 in 0) [PerFieldSimilarity], result of:",
"details": [
{
"value": 1.3438859,
"description": "score(freq=1.0), product of:",
"details": [
{
"value": 2.2,
"description": "boost",
"details": []
},
{
"value": 0.98082924,
"description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
"details": [
{
"value": 1,
"description": "n, number of documents containing term",
"details": []
},
{
"value": 3,
"description": "N, total number of documents with field",
"details": []
}
]
},
{
"value": 0.6227967,
"description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
"details": [
{
"value": 1.0,
"description": "freq, occurrences of term within document",
"details": []
},
{
"value": 1.2,
"description": "k1, term saturation parameter",
"details": []
},
{
"value": 0.75,
"description": "b, length normalization parameter",
"details": []
},
{
"value": 12.0,
"description": "dl, length of field",
"details": []
},
{
"value": 35.333332,
"description": "avgdl, average length of field",
"details": []
}
]
}
]
}
]
}
]
}
},
{
"_shard": "[book][0]",
"_node": "MDA45-r6SUGJ0ZyqyhTINA",
"_index": "book",
"_type": "_doc",
"_id": "2",
"_score": 0.57961315,
"_source": {
"name": "java编程思想",
"description": "java语言是世界第一编程语言,在软件开发领域使用人数最多。",
"studymodel": "201001",
"price": 68.6,
"timestamp": "2019-08-25 19:11:35",
"pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"tags": ["java", "dev"]
},
"_explanation": {
"value": 0.57961315,
"description": "sum of:",
"details": [
{
"value": 0.57961315,
"description": "weight(description:java in 0) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.57961315,
"description": "score(freq=1.0), product of:",
"details": [
{
"value": 2.2,
"description": "boost",
"details": []
},
{
"value": 0.47000363,
"description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
"details": [
{
"value": 2,
"description": "n, number of documents containing term",
"details": []
},
{
"value": 3,
"description": "N, total number of documents with field",
"details": []
}
]
},
{
"value": 0.56055,
"description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
"details": [
{
"value": 1.0,
"description": "freq, occurrences of term within document",
"details": []
},
{
"value": 1.2,
"description": "k1, term saturation parameter",
"details": []
},
{
"value": 0.75,
"description": "b, length normalization parameter",
"details": []
},
{
"value": 19.0,
"description": "dl, length of field",
"details": []
},
{
"value": 35.333332,
"description": "avgdl, average length of field",
"details": []
}
]
}
]
}
]
}
]
}
}
]
}
}
3.分析如何被匹配上
分析一个 document 是如何被匹配上的
- 最终得分
- IDF 得分
GET /book/_explain/3
{
"query": {
"match": {
"description": "java程序员"
}
}
}
4.Doc value
搜索的时候,要依靠倒排索引;排序的时候,需要依靠正排索引,看到每个 document 的每个 field,然后进行排序,所谓的正排索引,其实就是 doc values
在建立索引的时候,一方面会建立倒排索引,以供搜索用;一方面会建立正排索引,也就是 doc values,以供排序,聚合,过滤等操作使用
doc values 是被保存在磁盘上的,此时如果内存足够,os 会自动将其缓存在内存中,性能还是会很高;如果内存不足够,os 会将其写入磁盘上
倒排索引
doc1: hello world you and me
doc2: hi, world, how are you
term | doc1 | doc2 |
hello | * | |
world | * | * |
you | * | * |
and | * | |
me | * | |
hi | * | |
how | * | |
are | * |
搜索时:
hello you --> hello, you
hello --> doc1
you --> doc1,doc2
doc1: hello world you and me
doc2: hi, world, how are you
sort by 出现问题
正排索引
doc1: { "name": "jack", "age": 27 }
doc2: { "name": "tom", "age": 30 }
document | name | age |
doc1 | jack | 27 |
doc2 | tom | 30 |
5.query phase
- 搜索请求发送到某一个 coordinate node,构构建一个 priority queue,长度以 paging 操作 from 和 size 为准,默认为 10
- coordinate node 将请求转发到所有 shard,每个 shard 本地搜索,并构建一个本地的 priority queue
- 各个 shard 将自己的 priority queue 返回给 coordinate node,并构建一个全局的 priority queue
6.replica shard 提升吞吐量
replica shard 如何提升搜索吞吐量
一次请求要打到所有 shard 的一个 replica/primary 上去,如果每个 shard 都有多个 replica,那么同时并发过来的搜索请求可以同时打到其他的 replica 上去
7.fetch phbase 工作流程
- coordinate node 构建完 priority queue 之后,就发送 mget 请求去所有 shard 上获取对应的 document
- 各个 shard 将 document 返回给 coordinate node
- coordinate node 将合并后的 document 结果返回给 client 客户端
一般搜索,如果不加 from 和 size,就默认搜索前 10 条,按照_score 排序
8.搜索参数小总结
preference:
决定了哪些 shard 会被用来执行搜索操作
_primary, _primary_first, _local, _only_node:xyz, _prefer_node:xyz, _shards:2,3
bouncing results 问题,两个 document 排序,field 值相同;不同的 shard 上,可能排序不同;每次请求轮询打到不同的 replica shard 上;每次页面上看到的搜索结果的排序都不一样。这就是 bouncing result,也就是跳跃的结果。
搜索的时候,是轮询将搜索请求发送到每一个 replica shard(primary shard),但是在不同的 shard 上,可能 document 的排序不同
解决方案就是将 preference 设置为一个字符串,比如说 user_id,让每个 user 每次搜索的时候,都使用同一个 replica shard 去执行,就不会看到 bouncing results 了
timeout:
主要就是限定在一定时间内,将部分获取到的数据直接返回,避免查询耗时过长
routing:
document 文档路由,_id 路由,routing=user_id,这样的话可以让同一个 user 对应的数据到一个 shard 上去
search_type:
default:query_then_fetch
dfs_query_then_fetch,可以提升 revelance sort 精准度
9.bucket 和 metric
bucket:一个数据分组
city name北京 张三北京 李四天津 王五天津 赵六
天津 王麻子
划分出来两个 bucket,一个是北京 bucket,一个是天津 bucket北京 bucket:包含了 2 个人,张三,李四上海 bucket:包含了 3 个人,王五,赵六,王麻子
metric:对一个数据分组执行的统计
metric,就是对一个 bucket 执行的某种聚合分析的操作,比如说求平均值,求最大值,求最小值
select count(*) from book group by studymodel
- bucket:group by studymodel --> 那些 studymodel 相同的数据,就会被划分到一个 bucket 中
- metric:count(*),对每个 user_id bucket 中所有的数据,计算一个数量。还有 avg(),sum(),max(),min()
九.es7 sql 新特性
1.快速入门
POST /_sql?format=txt
{
"query": "SELECT * FROM tvs"
}
2.启动方式
- http 请求
- 客户端:elasticsearch-sql-cli.bat
- 代码
3.sql 翻译
POST /_sql/translate
{
"query": "SELECT * FROM tvs "
}
返回:
{
"size": 1000,
"_source": false,
"stored_fields": "_none_",
"docvalue_fields": [
{
"field": "brand"
},
{
"field": "color"
},
{
"field": "price"
},
{
"field": "sold_date",
"format": "epoch_millis"
}
],
"sort": [
{
"_doc": {
"order": "asc"
}
}
]
}
4.与其他 DSL 结合
POST /_sql?format=txt
{
"query": "SELECT * FROM tvs",
"filter": {
"range": {
"price": {
"gte" : 1200,
"lte" : 2000
}
}
}
}
十.Logstash 学习
1.什么是 Logstash
logstash 是一个数据抽取工具,将数据从一个地方转移到另一个地方。如 hadoop 生态圈的 sqoop 等。
logstash 之所以功能强大和流行,还与其丰富的过滤器插件是分不开的,过滤器提供的并不单单是过滤的功能,还可以对进入过滤器的原始数据进行复杂的逻辑处理,甚至添加独特的事件到后续流程中。Logstash 配置文件有如下三部分组成,其中 input、output 部分是必须配置,filter 部分是可选配置,而 filter 就是过滤器插件
,可以在这部分实现各种日志过滤功能。
2.配置文件
input {
#输入插件
}
filter {
#过滤匹配插件
}
output {
#输出插件
}
标准输入stdin:
input{
stdin{
}
}
output {
stdout{
codec=>rubydebug
}
}
读取 TCP 网络数据:
input {
tcp {
port => "1234"
}
}
filter {
grok {
match => { "message" => "%{SYSLOGLINE}" }
}
}
output {
stdout{
codec=>rubydebug
}
}
3.启动操作
#windows启动
logstash.bat -e 'input{stdin{}} output{stdout{}}'
#脚本启动
logstash.bat -f ../config/test1.conf
4.input 原理
logstash 使用一个名为 filewatch 的 ruby gem 库来监听文件变化,并通过一个叫.sincedb 的数据库文件来记录被监听的日志文件的读取进度(时间戳),这个 sincedb 数据文件的默认路径在 <path.data>/plugins/inputs/file 下面,文件名类似于.sincedb_123456,而<path.data>表示 logstash 插件存储目录,默认是 LOGSTASH_HOME/data。
input {
file {
path => ["/var/*/*"]
start_position => "beginning"
}
}
output {
stdout{
codec=>rubydebug
}
}
默认情况下,logstash 会从文件的结束位置开始读取数据,也就是说 logstash 进程会以类似 tail -f 命令的形式逐行获取数据。
5.Grok 正则捕获
grok 是一个十分强大的 logstash filter 插件,他可以通过正则解析任意文本,将非结构化日志数据弄成结构化和方便查询的结构。他是目前 logstash 中解析非结构化日志数据最好的方式。
Grok 的语法规则是:
%{语法: 语义}
例如输入的内容为:
172.16.213.132 [07/Feb/2019:16:24:19 +0800] "GET / HTTP/1.1" 403 5039
- %{IP:clientip}匹配模式将获得的结果为:clientip: 172.16.213.132
- %{HTTPDATE:timestamp}匹配模式将获得的结果为:timestamp: 07/Feb/2018:16:24:19 +0800
- %{QS:referrer}匹配模式将获得的结果为:referrer: "GET / HTTP/1.1"
下面是一个组合匹配模式,它可以获取上面输入的所有内容:
%{IP:clientip}\ \[%{HTTPDATE:timestamp}\]\ %{QS:referrer}\ %{NUMBER:response}\ %{NUMBER:bytes}
通过上面这个组合匹配模式,我们将输入的内容分成了五个部分,即五个字段,将输入内容分割为不同的数据字段,这对于日后解析和查询日志数据非常有用,这正是使用 grok 的目的。
例子:
input{
stdin{}
}
filter{
grok{
match => ["message","%{IP:clientip}\ \[%{HTTPDATE:timestamp}\]\ %{QS:referrer}\ %{NUMBER:response}\ %{NUMBER:bytes}"]
}
}
output{
stdout{
codec => "rubydebug"
}
}
输入内容:
172.16.213.132 [07/Feb/2019:16:24:19 +0800] "GET / HTTP/1.1" 403 5039
时间处理(Date):
date 插件是对于排序事件和回填旧数据尤其重要,它可以用来转换日志记录中的时间字段,变成 LogStash::Timestamp 对象,然后转存到@timestamp 字段里,下面是 date 插件的一个配置示例(这里仅仅列出 filter 部分):
filter {
grok {
match => ["message", "%{HTTPDATE:timestamp}"]
}
date {
match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
}
}
GeoIP 地址查询归类:
filter {
geoip {
source => "ip_field"
}
}
kibana测试Grok:
6.Mutate 插件
数据修改(Mutate):
正则表达式替换匹配字段
gsub 可以通过正则表达式替换字段中匹配到的值,只对字符串字段有效,下面是一个关于 mutate 插件中 gsub 的示例(仅列出 filter 部分):
filter {
mutate {
gsub => ["filed_name_1", "/" , "_"]
}
}
这个示例表示将 filedname_1 字段中所有"/"字符替换为""。
分隔符分割字符串为数组:
split 可以通过指定的分隔符分割字段中的字符串为数组,下面是一个关于 mutate 插件中 split 的示例(仅列出 filter 部分):
filter {
mutate {
split => ["filed_name_2", "|"]
}
}
这个示例表示将 filed_name_2 字段以"|"为区间分隔为数组。
重命名字段:
rename 可以实现重命名某个字段的功能,下面是一个关于 mutate 插件中 rename 的示例(仅列出 filter 部分):
filter {
mutate {
rename => { "old_field" => "new_field" }
}
}
这个示例表示将字段 old_field 重命名为 new_field。
删除字段:
remove_field 可以实现删除某个字段的功能,下面是一个关于 mutate 插件中 remove_field 的示例(仅列出 filter 部分):
filter {
mutate {
remove_field => ["timestamp"]
}
}
这个示例表示将字段 timestamp 删除。
综合例子:
input {
stdin {}
}
filter {
grok {
match => { "message" => "%{IP:clientip}\ \[%{HTTPDATE:timestamp}\]\ %{QS:referrer}\ %{NUMBER:response}\ %{NUMBER:bytes}" }
remove_field => [ "message" ]
}
date {
match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
}
mutate {
convert => [ "response","float" ]
rename => { "response" => "response_new" }
gsub => ["referrer","\"",""]
split => ["clientip", "."]
}
}
output {
stdout {
codec => "rubydebug"
}
7.output
output 是 Logstash 的最后阶段,一个事件可以经过多个输出,而一旦所有输出处理完成,整个事件就执行完成。 一些常用的输出包括:
- file: 表示将日志数据写入磁盘上的文件。
- elasticsearch:表示将日志数据发送给 Elasticsearch。Elasticsearch 可以高效方便和易于查询的保存数据。
输出到标准输出(stdout):
output {
stdout {
codec => rubydebug
}
}
保存为文件file:
output {
file {
path => "/data/log/%{+yyyy-MM-dd}/%{host}_%{+HH}.log"
}
}
输出到 elasticsearch:
output {
elasticsearch {
host => ["192.168.1.1:9200","172.16.213.77:9200"]
index => "logstash-%{+YYYY.MM.dd}"
}
}
- host:是一个数组类型的值,后面跟的值是 elasticsearch 节点的地址与端口,默认端口是 9200。可添加多个地址。
- index:写入 elasticsearch 的索引的名称,这里可以使用变量。Logstash 提供了%{+YYYY.MM.dd}这种写法。在语法解析的时候,看到以+ 号开头的,就会自动认为后面是时间格式,尝试用时间格式来解析后续字符串。这种以天为单位分割的写法,可以很容易的删除老的数据或者搜索指定时间范围内的数据。此外,注意索引名中不能有大写字母。
- manage_template:用来设置是否开启 logstash 自动管理模板功能,如果设置为 false 将关闭自动管理模板功能。如果我们自定义了模板,那么应该设置为 false。
- template_name:这个配置项用来设置在 Elasticsearch 中模板的名称。
8.综合案例
input {
file {
path => ["D:/ES/logstash-7.3.0/nginx.log"]
start_position => "beginning"
}
}
filter {
grok {
match => { "message" => "%{IP:clientip}\ \[%{HTTPDATE:timestamp}\]\ %{QS:referrer}\ %{NUMBER:response}\ %{NUMBER:bytes}" }
remove_field => [ "message" ]
}
date {
match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
}
mutate {
rename => { "response" => "response_new" }
convert => [ "response","float" ]
gsub => ["referrer","\"",""]
remove_field => ["timestamp"]
split => ["clientip", "."]
}
}
output {
stdout {
codec => "rubydebug"
}
elasticsearch {
host => ["localhost:9200"]
index => "logstash-%{+YYYY.MM.dd}"
}
}
十一.其他问题
1.kibana 作用是什么?
- 是什么:elk 中数据展现工具。
- 使用:建立索引模式,index partten
- discover 中使用 DSL 搜索。
- 可视化,绘制图形
- 仪表盘,将各种可视化图形放入,形成大屏幕。
- 使用模板数据指导绘图
- 监控,日志,APM 等功能非常丰富。
2.集群部署
结点的三个角色:
主结点:master 节点主要用于集群的管理及索引 比如新增结点、分片分配、索引的新增和删除等。 数据结点:data 节点上保存了数据分片,它负责索引和搜索操作。 客户端结点:client 节点仅作为请求客户端存在,client 的作用也作为负载均衡器,client 节点不存数据,只是将请求均衡转发到其它结点。
通过下边两项参数来配置结点的功能:
node.master: #是否允许为主结点
node.data: #允许存储数据作为数据结点
node.ingest: #是否允许成为协调节点
四种组合方式:
master=true,data=true:即是主结点又是数据结点
master=false,data=true:仅是数据结点
master=true,data=false:仅是主结点,不存储数据
master=false,data=false:即不是主结点也不是数据结点,此时可设置ingest为true表示它是一个客户端。
3.查询优化手段有哪些?
设计阶段调优:
- 根据业务增量需求,采取基于日期模板创建索引,通过 roll over API 滚动索引;
- 使用别名进行索引管理;
- 每天凌晨定时对索引做 force_merge 操作,以释放空间;
- 采取冷热分离机制,热数据存储到 SSD,提高检索效率;冷数据定期进行 shrink 操作,以缩减存储;
- 采取 curator 进行索引的生命周期管理;
- 仅针对需要分词的字段,合理的设置分词器;
- Mapping 阶段充分结合各个字段的属性,是否需要检索、是否需要存储等。
写入调优:
- 写入前副本数设置为 0;
- 写入前关闭 refresh_interval 设置为-1,禁用刷新机制;
- 写入过程中:采取 bulk 批量写入;
- 写入后恢复副本数和刷新间隔;
- 尽量使用自动生成的 id。
查询调优:
- 禁用 wildcard;
- 禁用批量 terms(成百上千的场景);
- 充分利用倒排索引机制,能 keyword 类型尽量 keyword;
- 数据量大时候,可以先基于时间敲定索引再检索;
- 设置合理的路由机制。其他调优
4.脑裂现象
同时如果由于网络或其他原因导致集群中选举出多个 Master 节点,使得数据更新时出现不一致,这种现象称之为脑裂,即集群中不同的节点对于 Master 的选择出现了分歧,出现了多个 Master 竞争。
脑裂问题可能有以下几个原因造成:
- 网络问题: 集群间的网络延迟导致一些节点访问不到 Master,认为 Master 挂掉了从而选举出新的 Master,并对 Master 上的分片和副本标红,分配新的主分片。
- 节点负载: 主节点的角色既为 Master 又为 Data,访问量较大时可能会导致 ES 停止响应(假死状态)造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。 内存回收: 主节点的角色既为 Master 又为 Data,当 Data 节点上的 ES 进程占用的内存较大,引发 JVM 的大规模内存回收,造成 ES 进程失去响应。
为了避免脑裂现象的发生,我们可以从原因着手通过以下几个方面来做出优化措施:
- 适当调大响应时间,减少误判。 通过参数 discovery.zen.ping_timeout 设置节点状态的响应时间,默认为 3s,可以适当调大。
如果 Master 在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。调大参数(如 6s,discovery.zen.ping_timeout:6
),可适当减少误判。
- 选举触发。 我们需要在候选集群中的节点的配置文件中设置参数
discovery.zen.munimum_master_nodes
的值。
这个参数表示在选举主节点时需要参与选举的候选主节点的节点数,默认值是 1,官方建议取值(master_eligibel_nodes2)+1
,其中master_eligibel_nodes
为候选主节点的个数。 这样做既能防止脑裂现象的发生,也能最大限度地提升集群的高可用性,因为只要不少于 discovery.zen.munimum_master_nodes
个候选节点存活,选举工作就能正常进行。 当小于这个值的时候,无法触发选举行为,集群无法使用,不会造成分片混乱的情况。
- 角色分离。 即是上面我们提到的候选主节点和数据节点进行角色分离,这样可以减轻主节点的负担,防止主节点的假死状态发生,减少对主节点“已死”的误判。