Elasticsearch是一个开源的分布式、RESTful 风格的搜索和数据分析引擎,它的底层是开源库Apache Lucene,Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库——无论是开源还是私有,但它也仅仅只是一个库。为了充分发挥其功能,你需要使用 Java 并将 Lucene 直接集成到应用程序中,且lucene的使用十分负责,你需要了解很多搜索领域的专业知识才可能明白它的原理,为了解决lucene的易用性问题,Elasticsearch应运而生。
Elasticsearch不仅仅是对Lucene做了简单的封装,它本身是一个可以独立部署的高可用的分布式系统,可弹性伸缩支持多至上百个节点,且有配套的可视化管理平台(kibana),极大的降低elasticsearch的运维成本,它封装了一套统一而又简洁的REST接口,对外提供搜索能力,所以依赖的系统可以是任何语言实现的,只需要通过HTTP请求与elasticsearch交互即可,除此之外,依托于分布式的优势,Elasticsearch的性能十分优秀,可轻松支持数亿量级的文档搜索。由于Elasticsearch的功能强大和使用简单,维基百科、卫报、Stack Overflow、GitHub等都纷纷采用它来做搜索。现在,Elasticsearch已成为全文搜索领域的主流软件之一。
elasticsearch的应用场景非常广泛,由于它是一个搜索引擎,所有依赖于搜索能力的场景都可以用到elasticsearch,包括但不限以下所列举的场景:
●index:索引,类似于mysql中的database,一个索引就是一个库,一般具有相同特性的document会存放在一个index里,index需要定义对应的setiings,如配置索引的刷新时间/分片数量,分析器等配置信息。
●type:type是定义在index下的,类似于数据库中的表,一个索引下可以有多个type,type必须定一个mapping信息,即定一个这个type有哪些字段,分别是什么类型,索引时如何处理这个字段。
○注:type在6.0版本后已经逐渐被废弃了,也就是说一个index就对应一张表了。
●document:文档,是es中可被索引的最小单位,类似于数据库中的一行记录,以json格式序列化保存,每个document都会有一个ID主键和一个版本号,document是不可更改的,每次更新都是将之前的记录删除,再保存新的记录,并将版本号+1。
●shard:分片,elasticsearch是分布式的,分片就是用来定义索引的数据是如何分布的,每个index索引都需要定义主分片/和副本分片的数量,ES会根据分片的数量来将索引的数据均匀的分发到集群的节点中,充分利用集群的优势,不过这里需要注意主分片的数量是不可改变的,所以我们在使用ES的时候需要做好容量规划,如果后续需要增加分片时,则需要重新索引数据。
head 示例
head是一个能管理elasticsearch集群的工具,提供了可视化界面进行管理,非常实用
如果我们要搜索某段文本中是否包含某个关键字,如果是关系型数据库,如mysql,则需要用到like模糊匹配查询,如select * from table where conent like’%keyword%',我们都知道这样的模糊匹配是无法走索引的,这个sql会导致全表扫描,当数据量很大时,速度就很慢了,而elasticsearch却能很快的搜索出对应的文档,这是因为elasticsearch采用了一种叫“倒排索引”的数据结构来实现。elasticsearch在索引文档时,会对这个字段的值进行分析处理,将这个字段的值拆分成一个一个词条(Term),每个词条都会反向指向到文档,这样就可以通过关键字搜索到对应的文档内容
例如:
文档1: “hello world”
文档2:“hello, nice to meet you”
文档3:“you are really nice”
词条 | 文档编号 |
---|---|
hello | [“文档1”,“文档2”] |
world | [“文档1”] |
nice | [“文档2”,“文档3”] |
to | [“文档2”] |
meet | [“文档2”] |
you | [“文档2”,“文档3”] |
are | [“文档3”] |
really | [“文档3”] |
这样就可以很快的根据关键字搜索到对应的文档了,如果关键词不是很多的情况下,elasticsearch会将倒排索引都存放于内存中,所以搜索的速度很快,如果关键词很多的话,内存不能放下所有的内容,elasticsearch会前置再生成一层索引,类似于词典一样,如A开头的关键词在磁盘中的哪个位置,这样就可以减少磁盘的访问次数,提升性能,具体逻辑可以参考下图:
通过上诉分析我们可以发现,能否通过某个关键字搜索到对应的文档,取决于对文档字段的切词逻辑,在上面举的例子中,如果我搜索"hell"这个关键词是搜不到任何文档的,因为没有切分出"hell"这个词条,词条的切分在es中是非常重要的一部分,我们需要根据业务来选择合适的分词器,比如我们需要非常精准的匹配,那么我们可能就要选择ngram分词器(每1个或2个字符切分为一个词条,这样搜索的精准性很高,但是会产生大量的词条,性能和存储都消耗很大),又或者我们的文档内容都是中文,我们就需要选择适合中文的分词器(如ik分词器,中文的语法和英文不同,需要按照中文的规则来分词,才能产生比较好的搜索效果)。
GET /_analyze
{
"analyzer": "standard",
"text": "The programmer's holiday is 1024!"
}
standard (过滤标点符号)
The |programmer’s |holiday |is |1024
simple (过滤数字和标点符号)
The| programmer |s| holiday |is
whitespace (不过滤,按照空格分隔)
The| programmer’s| holiday |is |1024!
分词器选择,Elasticsearch默认的分词器是standard分词器,它只能将中文分成单个字,所以我们选择常用的中文分词器IK作为默认分词器,ik 带有两个分词器:
ik_max_word:会将文本做最细粒度的拆分;尽可能多的拆分出词语,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”,会穷尽各种可能的组合;
ik_smart:会做最粗粒度的拆分;已被分出的词语将不会再次被其它词语占有,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,国歌”。
单字词库
IK默认不会将所有的汉字都拆出来做倒排索引,这样会导致单个汉字不会被拆分做搜索条件,比如我们搜索“阿里巴巴”,IK只会搜索“阿里”,“巴巴”、“阿里巴巴”对应的索引,而不会搜索"阿"、”里”、“巴”对应的数据,要解决此问题,我们需要通过导入扩展词典的方式处理,导入的扩展词典就是我们需要分词的单个字,当我们创建的句子中包含字典中的字的时候就会对单个字进行索引,从而实现单字查询。网络上有很多单字词库,可以下载到词库。
停止词典
中文中有很多没有实际意义的词比如”的”,“也”等,这些没必要分词,所以我们设置了停止字典,把这些没有意义此设置进行曲防止索引,在1中的配置中也说明了停止词典的配置方式,另外在阿里云控制台也可以在IK自定义词库里设置停止词典
当存在大量文档时,搜索某个关键字可能会匹配到非常多的文档,如果能将用户希望看到的文档排在前面,那将会极大的提高搜索的体验,但如何才能实现这个效果呢?elasticsearch默认采用了一种相关性评分的算法,ES会针对匹配到关键词的文档逐一打分,根据打分的结果进行排序,评分高的排在前面,即代表这个文档和关键字的相关性最高,这里简单介绍下ES的默认评分机制。
ES的评分机制与以下几个因素有关:
elasticsearch会结合这几个因素计算出每个文档相对这个搜索的关键词的评分,并且按照评分排序返回结果,当然你也可以指定排序的字段,如按照日期倒序返回。
除此之外,elasticsearch支持自定义评分函数,如果你觉的内置的评分机制不符合业务诉求,可以在搜索的时候指定一个评分的函数(一段脚本),elasticsearch会用这段脚本去替换内置的评分逻辑。
上面有提到,elasticsearch是一个分布式的搜索引擎,有多个节点组成,每个索引都是由多个分片组成的(每个分片分布在不同的节点上),所以我们每次搜索的时候,收到请求的节点会作为客户端,向每个分片发起请求进行搜索,然后再将数据汇总进行处理(分页/排序),最终返回给接口调用方:
如果我们用过数据库的分库分表就知道,业务数据被打散在多个表时,如果需要同时查询多张表,那么数据的分页和排序都会有问题(排序不准确或者数量不正确),如果要解决这个问题就需要进行额外的操作,在性能上就会有一定的损失,elasticsearch的分片就类似分库分表,所以它也会遇到同样的问题,elasticsearch提供了4种搜索方式来供用户选择,是牺牲数据的准确性来提高性能,还是降低性能保证数据的准确:
1.query and fetch
向索引的所有分片都发出查询请求, 各分片返回的时候把元素文档 ( document)和计算后的排名信息一起返回。
这种搜索方式是最快的。因为相比下面的几种搜索方式,这种查询方法只需要去 shard查询一次。 但是各个 shard 返回的结果的数量之和可能是用户要求的size的n倍。
优点:这种搜索方式是最快的。因为相比后面的几种es的搜索方式,这种查询方法只需要去shard查询一次。
缺点:返回的数据量不准确,可能返回(N*分片数量)的数据并且数据排名也不准确,通过上面打分部分的描述我们知道,打分需要依赖“逆向文档频率”这个因素,这个因素需要计算关键词在所有文档种出现的频率,但是这种搜索方式只会在当前分片上去计算,所以这个因素其实是不准确的,导致评分也不准确。
2.query then fetch(ES默认搜索类型)
先向所有的 shard 发出请求,各分片只返回文档id(不包括文档内容)和排名相关的信息(也就是文档对应的分值), 然后按照各分片返回的文档的分数进行重新排序和排名,取前 size 个文档,然后再根据文档 id 去相关的 shard 取 document。这种方式返回的 document 数量与用户要求的大小是相等的。
优点:返回的数据量是准确的。
缺点:性能一般,并且数据排名不准确(不准确的原因同上)。
3.DFS query and fetch
逻辑大概和第一种搜索方式类似,只不过这种方式有个DFS的步骤,为了解决排名不准确的问题,也就是在进行查询之前, 先对所有分片发送请求, 把所有分片中的词频和文档频率等打分依据全部汇总到一块, 再执行后面的操作,由于有了全局的文档频率因素,所以排名是准确的
优点:排名准确。
缺点:返回的数据量不准确,性能一般。
4.DFS query then fetch
逻辑大概和第二种搜索方式类似,这种方式也解决了排名不准确的问题,而且返回的数据也是准确的,但是性能是最差的
优点:排名准确/数据准确。
缺点:性能很慢。
我们可以根据自己业务需求来选择对应的搜索方式,一般来讲默认的搜索方式可以满足大部分的诉求。
这里主要介绍下常用的一些API,elasticsearch提供了rest风格的API,要注意http的method类型,共有四种(GET/POST/PUT/DELETE)
PUT /news-index // 索引名称
{
"mappings": { // 设置mappings
"news": { // 这是type
"properties": { // 设置属性字段
"create_date": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SZ"
},
"content": {
"type": "text"
},
"author": {
"type": "text"
},
"news_type": {
"type": "keyword"
},
"title": {
"type": "text"
}
}
}
},
"settings": {
"index": {
"refresh_interval": "30s", // 索引刷新时间,索引写入不是立马生效,es会根据这个设置定时刷新索引
"number_of_shards": "5", // 主分片数量
"number_of_replicas": "1"// 副本分片数量
}
}
}
一般只能修改副本数量和刷新时间,主分片数量不可修改
PUT news-index/_settings
{
"refresh_interval": "10s", // 修改刷新时间
"number_of_replicas" : "2" // 修改副本数量
}
写入文档可以指定ID,也可以不指定,不指定的话elasticsearch会帮我们生成
//指定id,这个1就是ID
POST /news-index/_doc/1
{
"create_date":"2022-02-11 13:50:53",
"content":"这是一条新闻内容",
"title":"这是一条新闻标题",
"author":"作者名称",
"news_type":"type1"
}
PUT /news-index/_update/101
{
"doc":{
"content":"更新内容",
}
}
搜索接口是elasicsearch最复杂的接口,ES设计了一套强大而又复杂的基于json格式的DSL搜索语法,详细的语法可以参考官网,这里只介绍一些常用的搜索语法
●精确匹配:
GET /news-index/_search
{
"size":10, // 分页参数
"from":0,
"query":{
"term":{
"news_type":"type1" // 精准匹配news_type
}
}
}
这里需要注意term query不能用于text类型的字段搜索,上面有提到,text类型的字段会被分析切词,切分成多个词条,而且有可能会过滤掉一些字符,term query属于精确匹配,去搜索text类型的字段基本上匹配不上。
●分词匹配:
GET /news-index/_search
{
"size":10, // 分页参数
"from":0,
"query":{
"match":{
"content":"quick brown fox"
}
}
}
match匹配属于比较宽松的匹配,只要倒排索引中命中这2个关键字即可,不会考虑关键词在字段内容中出现的顺序,比如上面的搜索,可以搜索到“The quick brown fox jumps over the lazy dog”,也可以搜索到“the brown fox was very quick”,如果需要更准确的匹配,可以用短语匹配。
●短语匹配
短语匹配和分词匹配的逻辑类似,会现在倒排索引里寻找对应的词条,然后根据词条找到对应的文档,但是短语匹配会检查词条在文档中的位置,必须要符合顺序且是相邻的才算匹配上
GET /news-index/_search
{
"size":10, // 分页参数
"from":0,
"query":{
"match_phrase":{
"content":"quick brown fox",
"slop":0
}
}
}
这里会严格匹配“quick brown fox”,以上面举的例子为例,这个搜索就只能搜到“The quick brown fox jumps over the lazy dog”,当然有时候我们不希望这么严格的匹配,这里有一个slop参数可以调节,slop参数代表可以搜索的词条之间最多可以相差几个位置,比如上面这个搜索 我们设置slop=1,搜索词改为“quick fox“,那么我们还是可以匹配到“The quick brown fox jumps over the lazy dog”,因为短语匹配会检查词条出现的顺序和配置,所以它的性能比match要慢的多,大概有10倍的差距
●bool查询
elasticsearch的搜索DSL强大之处就在于它支持逻辑连接符,你可以用bool查询来组合搜索条件,类似于SQL中的"AND"和"OR"连接符。举个例子:
GET /news-index/_search
{
"from": 0,
"size": 10,
"query": {
"bool": {
"should": [
{
"bool": {
"must": [
{
"match_phrase": {
"content": "quick brown fox",
"slop": 0
}
},
{
"term": {
"news_type": "type1"
}
}
]
}
},
{
"bool": {
"must": [
{
"match_phrase": {
"content": "quick fox",
"slop": 1
}
},
{
"term": {
"news_type": "type2"
}
}
]
}
}
]
}
}
}
这里的must就相当于and,should代表or,上面的搜索代表需要短语匹配“quick brown fox”且news_type=type1 或者 短语匹配“quick fox”且news_type=type2的文档,elasticsearch的bool匹配十分强大,可以构造出逻辑很复杂的搜索请求。
Elasticsearch默认按照搜索相关性分数_score来进行排序,_score是个浮点数值,默认按照此浮点数进行倒序排序。如果我们想要通过字段进行排序,可以使用sort参数进行排序,示例如下:
{
"query": {//搜索关键字是"阿里"的数据,并按照商标申请时间倒序排序
"match": {
"name": "阿里"
}
},
"sort": [
{
"applyDate": {
"order": "desc"
}
}
]
}
Elasticsearch常用分页方式有两种,这两种的方式各有优缺点,需要根据具体场景选择,具体如下:
(1)from+size分页:from即开始索引,size为每页数量,此分页方式原理如下:
{
"query": {
"match": {
"name": "阿里"
}
},
"sort": [
{
"applyDate": {
"order": "desc"
}
}
],
"from": 2,//查询第2页,每页10条
"size": 10
}
(2)scroll方式:即使用游标的方式进行查询,原理如下:
scroll是游标的方式,类似于关系型数据库中的游标,第一次查询根据查询条件、size返回查询结果及scroll_id,scroll_id在设置的查询时间内有效,第二次查询使用第一次查询获取的scroll_id进行查询,以此类推,直到查询不到数据,scroll查询方式不会产生from+size性能开销较大的问题,但是此种查询不能按顺序查询数据,不能查询上一页和下一页,不能跳页,所以scroll查询方式一般用于后端批量任务的执行,而不用于前端查询分页。scroll查询示例如下:
GET trademark_index_daily/trademark_contents/_search?scroll=1m
{ //第一次查询设置scroll有效时间为1分钟,并获取到scroll_id
"query": {
"match": {
"name": "阿里"
}
},
"sort": [
{
"applyDate": {
"order": "desc"
}
}
],
"size": 10
}
GET /_search/scroll//注意:此处不要在加索引名称和文档类型
{//根据第一次的scroll_id继续查询下一页数据
"scroll":"1m",
"scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAERtxFkpDV2RsRnVXVHhTelFQNmJ3UGc3c3cAAAAAAAvYQRZsZ19hNUM4QlNZMlRpcnNxLWlJWWlBAAAAAAALp8sWSUw5SHdMZUtUemVIY3pUa0VkYnR1ZwAAAAAAC6fMFklMOUh3TGVLVHplSGN6VGtFZGJ0dWcAAAAAABDqIxZRYVFKX2poelF6YXRvUXBzRVRXdG5R"
}
Elasticsearch的聚合功能和关系型数据库中聚合类型,可以实现count、sum、max等聚合分析功能,聚合API基本格式为:
{
“aggregations" : { // 表示聚合操作,可以使用aggs替代
"<aggregation_name>" : { // 聚合名,可以是任意的字符串。用做响应的key,便于快速取得正确的响应数据。
"<aggregation_type>" : { // 聚合类别,就是各种类型的聚合,如min等
// 聚合体,不同的聚合有不同的body
}
[," aggregations" : { []+ } ]? // 嵌套的子聚合,可以有0或多个
}
[," <aggregation_name_2>" : { ... } ]* // 另外的聚合,可以有0或多个
}
主要聚合方式如下:
(1)Min:查询最小值,示例:
GET trademark_index_daily/trademark_contents/_search
{//查询商标名称匹配"阿里",id的最小值
"query": {
"match": {
"name": "阿里"
}
},
"aggs": {
"min_id":{
"min":{
"field":"id"
}
}
}
}
最终的聚合结果为最后的聚合值:
"aggregations": {
"min_id": {
"value": 206
}
}
(2)Max:查询最大值,和最小值类似,不再举例
(3)Sum:求和,示例:
(4) Avg:求平均值
(5)Terms:分组统计,如商标业务中对商标一级分类进行分组统计:
GET trademark_index_daily/trademark_contents/_search
{
"query": {
"term": {
"name": "阿里"
}
},
"aggs": {
"trademark_num":{
"terms":{
"field":"classification",//分组统计字段
"size" : 10,//最大分组返回数量
"min_doc_count" : 1,
"shard_min_doc_count" : 0,
"show_term_doc_count_error" : false,
"order" : [
{
"_count" : "desc"//按照分组统计数量倒序排序
},
{
"_term" : "asc"
}
]
}
}
}
}
ES的主分片数量是不可修改的,一旦我们某个索引达到了容量上线,就必须要做索引迁移,将数据迁移到新的索引中(分片数量更多),如何才能在不影响业务的情况下平滑的迁移索引呢?
这里先介绍下ES的2个能力,可以基于这2个能力做到索引数据的平滑迁移
有了这2个能力基本上可以做到数据的平迁,但是还需要解决一个问题就是迁移过程中的变更,比较简单粗暴的办法就是禁止数据写入,这样在迁移过程中不会产生新的变更,但是这样业务上有损,所以一个平滑迁移的办法就是做双写,在迁移的过程中,既向老索引里写入/变更数据,也向新索引里写入/变更数据,同时reindex需要设置为“只创建新索引里没有的数据”。
除此之外,双写的逻辑也要根据业务场景来考虑,比如更新或删除一条数据时,如果新的索引里还没有这条数据(可能reindex还没处理到这条数据),那么要先从老索引里同步过来再更新,或者写入ES每次都是全量更新,就不用考虑这个问题了。
elasticsearch的性能调优是个复杂的工程,要根据具体的场景来选择优化的手段,这里主要介绍下我在优化ES性能过程中的一些经验。