极客时间——ElasticSearch核心技术与实战
目录:
概述
起源为Lucene,一个诞生于1999年的基于Java开发的搜索引擎类库,具备高性能和易扩展的功能,但是有类库只支持Java并且不能水平扩展
2004年ShayBanon基于lucene开发了Compass,然后在2010年重写了Compass命名为ElasticSearch,使其支持分布式水平扩展和提供接口被任何编程语言调用
版本迭代
- 0.4 2010年2月
- 1.0 2014年1月
- 2.0 2015年10月
- 5.0 2016年10月
- 6.0 2017年10月
- 7.0 2019年4月
5.X特性
- Lucene6.x,打分机制由TF-IDF改为BM25
- 内部引擎避免同一文档并发更新的竞争锁
- 增加ProfileAPI
6.X特性
- Lucene7.x
- 跨集群复制
- 索引生命周期管理
- 基于操作的数据复制框架,加快恢复数据
- 索引时排序
7.X特性
- Lucene8.x
- Security功能
安装上手
Elasticsearch
config/jvm.options
- Xmx和Xms设置为一样
- Xmx不要超过机器内存的一半
- 不要超过30GB, https://www.elastic.co/cn/blog/a-heap-of-trouble
插件的安装
$ bin/elasticsearch-plugin list
$ bin/elasticsearch-plugin install analysis-icu
单节点启动集群
$ bin/elasticsearch -E node.name=node1 -E cluster.name=whysdomain -E path.data=node1_data -d
$ bin/elasticsearch -E node.name=node2 -E cluster.name=whysdomain -E path.data=node2_data -d
$ bin/elasticsearch -E node.name=node3 -E cluster.name=whysdomain -E path.data=node3_data -d
$ curl 127.0.0.1:9200/_cat/nodes
Kibana
在devtool中"cmd+/"获取API的help
Cerebro
ElasticSearch入门
基本概念:索引、文档和RESTAPI
ElasticSearch是面向文档的,文档为可搜索数据的最小单位
文档会被序列化为Json存储在ElasticSearch
- Json对象由字段组成
- 每个字段都有对应的字段类型(字符串/数值/布尔/日期/二进制/范围类型),并且支持嵌套,不用指定格式可以自行选择
每个文档有一个ID,可以自行指定或者由ElasticSearch自己生成
文档的元数据
- _index文档所属索引
- _type文档所属类型(在7.0之前可以设置多个type)
- _id文档唯一id
- _source文档的原始json数据
- _version文档版本信息
- _score相关性打分
索引是一类文档的集合
每个索引有自己的mapping定义,描述文档的字段名和字段类型
ElasticSearch实用的为倒排索引
与关系型数据库的对应关系
- table:index
- row:document
- colume:filed
- schema:mapping
- sql:dsl
RESTAPI
GET <index>
: 查看索引的mapping和settingGET <index>/_count
: 查看索引的文档总数POST <index>/_search
: 查看索引的文档的前10行,了解文档格式GET _cat/indices/kibana*?v&s=index
: 通配符查询索引GET _cat/indices?v&health=green
: 状态为绿的索引GET _cat/indices?v&docs.count:desc
: 按照索引个数倒排GET _cat/indices?v&h=i,tm&s=tm:desc
: 查看索引占用的内存GET _cat/indices/kibana*?v&pri&h=health,index,pri,rep,docs,count,mt
: 查看索引的具体字段
基本概念:节点、集群、分片和副本
分布式集群
- 服务可用性 允许节点停止服务
- 数据可用行 部分节点丢失,不损失数据
- 可扩展性
节点角色
master
节点可以设置node.master=false,否则为master-eligible可以参与master节点的选举
第一个节点启动的时候会把自己选举为主节点
每个节点保存集群的状态,但是只有master节点能修改集群状态信息
集群状态ClusterState有
- 所有节点信息
- 所有索引和其相关的mapping和setting信息
- 分片和路由信息
data
保存数据
ingest
数据前置处理转换的节点
coordinating Node
负责接收Client请求,将请求分发到合适的节点,把结果汇总返回给Client,默认每个节点都会有这个作用
其他的节点类型
- Hot和Warm,不同硬件配置的DataNode
- MachineLearnigNode,负责机器学习运算Job的节点
- TribeNode 连接到不同的ElasticSearch并将其作为一个集群处理
分片
- 主分片用于解决数据水平扩展问题
- 副本用于解决数据高可用的问题,副本是主分片的拷贝,也可以解决数据的吞吐量的问题
主分片数量在索引创建的时候设置完,就不能再进行更改了,所以需要提前做好容量规划
- 分片数过小会导致后续无法通过增加节点水平扩容,单个分片数据量大,重新分配耗时长
- 分片数过大会影响结果相关性打分,也会单节点过多的分片导致资源浪费,也会影响性能
集群状况可以通过health接口查看,yellow为副本分片未正常分配,Red为主分片未能分配
GET _cluster/health
GET _cat/nodes
GET _cat/shards
Cerebro监听9000端口,可视化的集群监控
文档CURD和批量操作
????
# index
PUT
Bulk、Mget和Msearch
常见状态码
- 429集群过于繁忙
倒排索引
正排常见的例子就是目录,文档ID到文档内容的关联关系,而倒排,就是文档到文件ID的关联关系
正排索引
文档ID | 文档内容 |
---|---|
1 | Mastering ElasticSearch |
2 | ElasticSearch Server |
3 | ElasticSearch Essentials |
倒排索引
Term | Count | Document:Position |
---|---|---|
ElasticSearch | 3 | 1:1,2:0,3:0 |
Mastering | 1 | 1:0 |
Server | 1 | 2:1 |
Essentials | 1 | 3:1 |
倒排索引包含两个部分
- 单词词典,记录所有文档的单词,记录与倒排列表的关系,比较大一般是B+树或者哈希链实现
- 倒排列表,记录了单词对应的文档结合,由倒排索引项组成,有文档ID,词频,位置(用于语句搜索phrase query)和偏移(开始结束位置用于高亮)
ElasticSearch的Json文档中的每个字段,都有自己的倒排索引
可以在mapping中对某些字段不进行索引,优点是节省空间,缺点是字段无法被搜索
Analyzer进行分词
Analysis文本分析是把全文转换为一系列单词的过程,也称分词
Analysis通过Analyzer实现,可以使用内置的分词器,也可以按需定制分析器
在数据写入的时候要转换词条,在匹配Query语句的时候也需要使用相同的分析器对查询语句分析
分词器有专门处理分词的组件,Analyzer由三部分组成
CharacterFilter->Tokenizer->TokenFiters
- CharacterFilter针对原始文本处理,例如去掉html标签
- Tokenizer按照规则切分为单词
- TokenFiters对单词进行加工
内置分词器有很多,可以通过analyze查看分词器的流程
中文分词可以用的icu_analyzer
SearchAPI
支持
/_search
/index1/_search
/index1,index2/_search
/index*/_search
支持GET和POST的方式来搜索,示例
GET /index1/_search?q=name:Eddie
GET -H 'Content-Type:application/json' /index1/_search -d '{"query": {"match_all":{}}}'
会返回查询结果的相关性,而搜索引擎会需要查询的问题相关性,实时性等进行重新评分
URLSearch
GET /index/_search?q=2012&df=title&sort=year:desc&from=0&size=10&timeout=1s
{"profile": true}
- q 查询语句
- df 默认字段,不指定就对所有字段进行查询,也可以合并写为q=title:2012
- sort 排序
- from和size 用于分页
- profile 查看查询如何执行
TermQuery和PhraseQuery
- beautiful mind等效于beautiful or mind,url支持空格的就是q=beautiful mind
- "beautiful mind"等效于beautiful and mind,并且前后顺序也需要一致
分组和引号
- 泛查询是TermQuery,示例title:beautiful mind
- 分组是Boolquery,示例title:(beautiful AND mind)
- 引号是Phrase,示例title:"beautiful mind"
Bool操作
- AND/OR/NOT(必须大写)或者&&、
||
和!
分组操作
+
表示must-
表示must not
示例title:(+matrix -reloaded)
通配符查询(效率较低内存,占用过大,不建议使用特别是放在最前边)
- title:mi?d
- title:be*
正则表达
- title:[bt]oy
模糊匹配和近似查询
- title:befutifl~1
- title:"lord rings"~2
RequestBody和QureyDSL简介
分页
{
"from":10,
"size":20,
"query":{"match_all":{}}
}
排序
{
"sort":[{"order_data":"desc"}],
"query":{"match_all":{}}
}
查询结果过滤了,并且支持通配符
{
"_source":["order_data","category"],
"query":{"match_all":{}}
}
脚本字段,可以根据已有字段获取一个新字段,例如需要进行汇率计算
{
"script_fields":{
"new_field": {
"script": {
"lang": "painless",
"source": "doc['order_data'].value+'hello'"
}
}
},
"query":{"match_all":{}}
}
插叙表达式
{
"query":{
"match":{
"comment":{
"query":"Last Chrimas",
"operator":"AND"
}
}
}
}
也可以使用slop等参数
QueryString和SimpleQueryString
querystring
{
"query":{
"query_string":{
"default_field": "name",
"query": "Ruan AND Yiming"
}
}
}
或
{
"query":{
"query_string":{
"fields": ["name","about"]
"query": "(Ruan AND Yiming) OR (Java AND Elasticsearch)"
}
}
}
simple querystring对一些例如复杂操作会进行禁止
Dynamic Mapping和常见字段类型
Mapping类似schema的定义
- 定义索引中的字段和名称
- 定义字段和数据类型
- 字段和倒排索引的相关配置
Mapping可以将Json文档映射成Lucene需要的扁平格式
一个Mapping属于一个索引Type,一个文档也属于一个Type,在7.0开始,不需要在Mapping中定义指定的type信息
字段类型包括
- text/keyword
- data
- integer/floating
- boolean
- ipv4/ipv6
- 对象类型/嵌套类型
- 特殊类型,例如geo_point
Dynamic Mapping机制
- 在写入文档的时候,如果索引不存在自动创建
- 无需定义Mapping,根据文档信息自动生成
查看Mapping
GET movies/_mapping
Mapping的字段类型更新
- 对于一个新的字段,如果dynamic为true,mapping直接更新,如果为false,新字段无法被索引,但是会在
_source
字段,如果为strict,文档写入失败 - 对于一个已有字段,一旦有数据写入不支持修改字段定义
如果需要修改字段类型,需要重建索引
设置Dynamic
PUT movies
{
"mappings": {
"_doc": {
"dynamic": "false"
}
}
}
显示定义Mapping
可以通过临时的Mapping做一下测试
字段的index字段可以设置为false,就不会创建倒排索引,减少磁盘的开销
PUT users
{
"mappings": {
"properties": {
"name": {
"type": "text"
},
"mobile": {
"type": "text",
"index": false
}
}
}
}
对于倒排索引提供了4种不同级别的index_options配置
- docs 记录doc id
- freqs 记录doc id和term frequencies
- positions 记录doc id、term frequencies和term position
- offsets 记录doc id、term frequencies、term position和character offects
text默认记录类型为positions,其他默认为docs,记录的内容越多,占用存储空间越大
插入值如果为NULL,有需要对NULL进行搜索,不过只有keyword支持设置null_value
PUT users
{
"mappings": {
"properties": {
"name": {
"type": "text"
},
"mobile": {
"type": "text",
"null_value": "NULL"
}
}
}
}
GET users/_search?q=mobile:NULL
_all
在7.0之后copy_to
代替
PUT users
{
"mappings": {
"properties": {
"first_name": {
"type": "text",
"copy_to": "full_name"
},
"last_name": {
"type": "text",
"copy_to": "full_name"
}
}
}
}
GET users/_search?q=full_name:(Wang Hongyu)
- 满足特定搜索需求
- copy_to将数值拷贝到目标字段
- copy_to的目标字段不出现在_source中
不提供数组类型,但是任何字段都可以包含多个相同类型的数值
PUT users
{
"name": "why",
"mobile": ["123", "234"]
}
多字段特性和Mapping中配置自定义Analyzer
多字段特性
- 实现精确匹配: 增加keyword字段
- 使用不同的analyzer: 不同语言、拼音,搜索和索引使用不同的analyzer
PUT products
{
"mappings": {
"propertites": {
"company": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"comment": {
"type": "text",
"fields": {
"english_comment": {
"type": "text",
"analyzer": "english",
"search_analyzer": "english"
}
}
}
}
}
}
- keyword是一个准确的词,例如数字,日期或者字符串("apple store")
- text是非结构化的文本数据,需要进行分词
自定义Analyzer还是三类
- CharacterFilter文本处理,例如html处理,大小写,但是会影响position和offset
- Tokenizer分词
- TokenFilter进行增加修改或者删除等,例如近义词
POST _analyze
{
"tokenizer": "keyword",
"char_filter": ["html_strip"],
"text": "<b>hello world</b>"
}
POST _analyze
{
"tokenizer": "standard",
"char_filter": [
{
"type": "mapping",
"mappings": [":)=>happy", ":(=>sad"]
}
],
"text": ":)"
}
// 还支持正则
POST _analyze
{
"tokenizer": "standard",
"char_filter": [
{
"type": "pattern_replace",
"pattern": "http://(.*)",
"replacement": "$1"
}
],
"text": "http://www.whysdomain.com"
}
Index Template和Dynamic Template
Index Template是引用在索引上的,可以按照一定规则匹配索引,并为索引创建Mappings和Settings
- 模板只会在新创建的时候产生作用
- 多个索引模板,设置会进行merge
- 可以指定order值控制merge的过程
PUT _template/template_default
{
"index_patterns": ["*"],
"order": 0,
"version": 1,
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
}
}
PUT _template/template_default
{
"index_patterns": ["test*"],
"order": 1,
"settings": {
"number_of_shards": 1,
"number_of_replicas": 2
},
"mappings": {
// 关闭字符串自动变为日期类型
"date_detection": false,
// 开启数字类型
"numeric_detection": true
}
}
当一个索引被创建
- 使用默认的mapping和setting
- 应用order数值低的index template
- 应用order数据高的index template覆盖之前的设定
- 应用创建索引指定的setting和mapping覆盖模板的设定
Dynamic Template是应用在索引上的,根据ElasticSearch识别的数据类型,结合字段命名来动态设置字段类型
- 所有字符串类型设置为keyword,或者关闭keyword字段
- is开头的设置为boolean
- long_开头的设置为boolean
PUT _template/template_default
{
"dynamic_templates": [
{
"full_name": {
"path_match": "name.*",
"path_unmatch": "name.*",
"mapping": {
"type": "text",
"copy_to": "full_name"
}
}
}
]
}
ElasticSearch聚合分析简介
ElasticSearch除了支持搜索,还支持统计分析
集合的分类
- Bucket Aggregation 一些满足条件的文档类
- Metric Aggregation 对文档字段进行统计分析
- Pipeline Aggregation 对其他聚合结果进行二次分析
- Matrix Aggregation 对多个字段提供结果矩阵
// Bucket
GET kibana_sample_data_flights/_search
{
"size": 0,
"aggs": {
"flight_dest": {
"terms": {
"field": "DestCountry"
}
}
}
}
// Metrics
GET kibana_sample_data_flights/_search
{
"size": 0,
"aggs": {
"flight_dest": {
"terms": {
"field": "DestCountry"
}
},
"aggs": {
"average_price": {
"avg": {
"field": "AvgTicketPrice"
}
},
"max_price": {
"max": {
"field": "AvgTicketPrice"
}
},
}
}
}
// Pipeline
GET kibana_sample_data_flights/_search
{
"size": 0,
"aggs": {
"flight_dest": {
"terms": {
"field": "DestCountry"
}
},
"aggs": {
"average_price": {
"avg": {
"field": "AvgTicketPrice"
}
},
"weather": {
"terms": {
"field": "DestWeather"
}
},
}
}
}
深入搜索
基于词项和基于全文的搜索
对于做了分词处理的
对于term查询的时候不会进行分词处理,和原来的值不一样就会搜索不到,需要
- 单独指定分词后的数据
- 使用desc.keyword
POST /products/_search
{
"query": {
"term": {
"desc": {
"value": "iphone"
}
}
}
}
通过符合查询constant_score转为filter,可以取消分数计算
POST /products/_search
{
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": "desc.keyword"
}
}
}
}
全文检索通过match等操作,会进行分词处理
结构化搜索
数字查询和日志查询支持range
POST /products/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"price": {
"gte": "20",
"lte": "30"
}
}
}
}
}
}
POST /products/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"date": {
"gte": "now-1y"
}
}
}
}
}
}
还有exist判断数据是否存在
POST /products/_search
{
"query": {
"constant_score": {
"filter": {
"exits": {
"filed": "date"
}
}
}
}
}
搜索的相关性算法
相关性是描述文档和查询语句的匹配程度
- 5.0之前算法使用的TF-IDF,对于词在所有文档中出现的比例使用log函数进行打分求和
- 5.0之后算法使用的BM25
当TF无限增长的时候,TF-IDF的值还会继续增大,而BM25会趋于一个数值
可以通过boosting控制相关度
POST /products/_search
{
"query": {
"boosting": {
"postive": {
"term": {
"content": "elasticsearch"
}
},
"negative": {
"term": {
"content": "like"
}
},
"negative_boost": 0.2
}
}
}
boost默认为1
- 当boost大于1,打分相关度会提升
- 当boost介于0和1,打分相关度会降低
- 当boost小于0,会贡献负分
Query&Filtering与多字符串多字段查询
bool查询
- must 必须匹配,贡献算分
- should 选择性匹配,贡献算分
- must_not 必须不匹配
- filter 必须匹配,不贡献算分
POST /products/_search
{
"query": {
"bool": {
"must": {
"term": {"price": "30"}
},
"filter": {
"term": {"avaliable": "true"}
},
"must_not": {
"range": {
"price": {
"lte": 10
}
},
"should": [
{"term": {"productID.keyword": "JODL"}},
{"term": {"productID.keyword": "XHDk"}}
],
"minimum_should_match": 1
}
}
}
对于数组的包含,可以通过人为增加_count
字段,再匹配_count
字段
跨集群搜索
在早期通过tribenode实现的跨集群搜索,存在一些问题
- 以Clientnode的方式加入每个集群,这样集群master节点的任务变更需要tribenode回应才能继续
- tribenode不保存cluster的state信息,一旦重启初始化很慢
- 多集群索引重名,只能设置一种Prefer规则
在5.3版本引入了跨集群搜索的功能
- 允许任何节点扮演federated节点,代理搜索请求
- 不需要以Clientnode的方式加入集群
配置方式
PUT _cluster/settings
{
"persistent": {
"cluster": {
"cluster_one": {
"seed": [
"127.0.0.1:9300"
],
"skip_unavailable": true
},
"cluster_two": {
"seed": [
"127.0.0.1:9301"
]
},
"cluster_three": {
"seed": [
"127.0.0.1:9302"
]
}
}
}
}
查询的时候指定集群
GET movies,cluster_two:movies,cluster_three:movies/_search
{
"query": {"match": {"title": "matrix"}}
}
分布式特性及分布式搜索机制
集群分布式模型
不同集群通过cluster.name区分
节点之间会互相ping,NodeId低的会被选举为主节点,其他节点会加入集群,当发现选中的主节点丢失,就会选举新的master
当分布式的经典网络问题出现,一个节点与其他节点不能通信,就会造成脑裂,在网络恢复之后无法正确恢复
避免脑裂的方式可以是限定选举条件,只有超过半数以上的节点才能进行选举
在7.0开始,节点会自行完成选举,并记录状态
分片和集群故障转移
副本分片
- 提高数据的可用性,一旦主分片丢失,副本分片可以Promote称为主分片,副本分片可以动态调整
- 提高系统的读取效率,和吞吐量
主分片过多会影响性能,副本分片数设置过多会降低集群整体的写入性能
当节点故障后
- 选举master节点
- 副本分片升级为主分片
- 生成副本分片
文档分布式存储
分片的潜在算法有
- 随机
- 维护映射
- 计算分片
ElasticSearch默认使用的_routing值为id,也可以自行指定,例如相同分类分配到相同分片
分片及其生命周期
倒排索引不可变性,优点有
- 不存在并发写文件的问题,避免锁带来的性能问题
- 一旦读取到内核文件系统缓存,就留在缓存,性能会有提升
- 缓存容易生成和维护/数据可以被压缩
在Lucene中,单个倒排索引文件被称为Segment,多个Segment汇总到一起就是Lucene的Index,也就是ElasticSearch的shard,记录Segment信息的为CommitPoint。
当有新文档写入就生成新的Segment,在查询的时候汇总Segments并对结果汇总,删除文档信息保存在.del文件
写入的时候会先写入到indexbuffer并记录到分片的transaction log落盘,然后定时刷新(或者indexbufer写满默认为jvm的10%)indexbuffer到segment,频率默认为1s,可以通过index.refresh_interval
配置,refresh之后就可以被搜索到了
ElasticSearch的flush
- 调用refresh清空indexbuffer
- 调用fsync将缓存的segments写入磁盘
- 清空transaction log
触发条件
- 默认30min
- transaction log写满,默认512MB
ElasticSearch会自行进行Merge,将Segment合并,并删除已删除文档
手动Merge
POST <index>/_forcemerge
41 分布式搜索机制
ElasticSearch的搜索分为两个阶段
- Query
- Fetch
示例3分片的索引
- Query阶段,当请求发到Es节点,以Coordinating节点的身份,在主副分片中随机选3个,发送查询请求,被选中的分片执行查询,然后进行排序,返回from+size个排序的文档id和排序值给到Coordinating节点
- Fecth阶段,Coordinating节点会将Query阶段从每个分片获取排序后的文档id列表重新排序,选取from到from+size的size个文档,通过multiGet方式获取相应分片的文档
当分片过多的时候,汇总处理的时候就数据就非常多了,对深度分页的性能有
计算分不准的问题
- 数据量足够大保证均匀分布在各个分片,一般结果不会有偏差
- 使用DFSQueryThenFetch到每个分片把各个分片的词频和文档频率进行收集,进行完整的相关性算分,不过性能较差,搜索的URL指定
search_type=dfs_query_then_fetch
42 排序及DocValues&Fielddate
默认排序是算分,也可以指定排序
{
"sort":[{"order_data":"desc"}],
"query":{"match_all":{}}
}
sort是一个数组,可以支持多个字段排序
倒排索引是无法提供排序支持的,ElasticSearch提供的正排索引实现有两种
- Fielddate 在1.x版本和之前默认采用
- DocValues 在2.x版本和之后默认采用,对列式存储,对text字段无效
对比 | Doc Values | Fielddate |
---|---|---|
何时创建 | 和倒排索引一起 | 搜索时创建 |
创建位置 | 磁盘文件 | JVM Heap |
优点 | 避免内存占用 | 索引速度快不占用磁盘空间 |
缺点 | 降低索引速度,占用额外磁盘 | 文档过多,动态创建开销较大,占用Jvm Heap过多 |
Fielddata在搜索的时候可以打开
在索引的mappings中可以关闭
{
"properties": {
"user_name": {
"type": "keyword",
"doc_values": false,
}
}
}
不需要排序和聚合的时候就可以关闭