极客时间——ElasticSearch核心技术与实战

时间:Oct. 25, 2021 分类:

目录:

概述

起源为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

插件的安装

$ 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和setting
  • GET <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
    }
}

当一个索引被创建

  1. 使用默认的mapping和setting
  2. 应用order数值低的index template
  3. 应用order数据高的index template覆盖之前的设定
  4. 应用创建索引指定的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称为主分片,副本分片可以动态调整
  • 提高系统的读取效率,和吞吐量

主分片过多会影响性能,副本分片数设置过多会降低集群整体的写入性能

当节点故障后

  1. 选举master节点
  2. 副本分片升级为主分片
  3. 生成副本分片

文档分布式存储

分片的潜在算法有

  • 随机
  • 维护映射
  • 计算分片

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

  1. 调用refresh清空indexbuffer
  2. 调用fsync将缓存的segments写入磁盘
  3. 清空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提供的正排索引实现有两种

  1. Fielddate 在1.x版本和之前默认采用
  2. DocValues 在2.x版本和之后默认采用,对列式存储,对text字段无效
对比 Doc Values Fielddate
何时创建 和倒排索引一起 搜索时创建
创建位置 磁盘文件 JVM Heap
优点 避免内存占用 索引速度快不占用磁盘空间
缺点 降低索引速度,占用额外磁盘 文档过多,动态创建开销较大,占用Jvm Heap过多

Fielddata在搜索的时候可以打开

在索引的mappings中可以关闭

{
    "properties": {
        "user_name": {
            "type": "keyword",
            "doc_values": false,
        }
    }
}

不需要排序和聚合的时候就可以关闭