前言

温馨提醒:阅读本文需要30分钟

最近在公司这个月搞了一个基于elasticSearch7.5搜索引擎的搜索模块,在开发的过程中发现自己对于es高版本的high rest client是使用熟练度还需要进一步提高,以及对于es索引模板相关知识有所欠缺。抽空写一篇关于es高版本搭配cerebro的操作教程以及clent APi的相关知识的介绍以及生产环境中es的多分片,多副本的集群部署方面的技术博客。

ElasticSearch介绍

Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎。它能很方便的使大量数据具有搜索、分析和探索的能力。充分利用Elasticsearch的水平伸缩性,能使数据在生产环境变得更有价值。Elasticsearch 的实现原理主要分为以下几个步骤,首先用户将数据提交到Elasticsearch 数据库中,再通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据,当用户搜索数据时候,再根据权重将结果排名,打分,再将返回结果呈现给用户。

ElasticSearch安装

本文的安装以window版本的es为例子,首先下载es7.5.0压缩版,解压后访问\bin,启动elasticsearch.bat。访问http:localhost:9200 就可以看到es的版本等相关信息。

cerebro安装

本文的安装以window版本的cerebro为例子,首先下载cerebro0.8.5压缩版,解压后访问\bin,启动cerebro.bat。访问http:localhost:9000,在输入框输入http:localhost:9200就可以连接到刚才安装的es了。下面会介绍如何使用cerebro来创建索引模板以及索引模板中的具体意义。

索引模板介绍

索引模板,英文名为Index Template。它是一种机制,这种机制允许我们定义一种模板,这种模板当新索引创建时将被自动应用。模板包含settings和mapping,以及这个模板是否可被应用于新的索引。

索引模板有以下限制:

  1. 索引模板仅在一个索引新创建时才起作用,修改模板不会影响已创建的索引。
  2. 可以设定多个索引模板,这些索引模板的设置将被merge在一起。
  3. 可以通过对索引模板指定order的数值,来控制merge的过程。

模板定义可以如下:

{
  "order": 0,                               // 模板优先级
  "template": "template_name*",             // 模板匹配的名称方式
  "settings": {...},                        // 索引设置
  "mappings": {...},                        // 索引中各字段的映射定义
  "aliases": {...}                          // 索引的别名
}

接下来我们来进行创建索引模板,创建索引模板需要使用put,

索引模板创建

索引模板,英文名为Index Template。它是一种机制,这种机制允许我们定义一种模板,这种模板当新索引创建时将被自动应用。模板包含settings和mapping,以及这个模板是否可被应用于新的索引。

curl -XPUT ip:9200/_template/bywin_all   /*linux创建索引模板命令 代表创建一个名为peopel_all的索引模板*/

{
    "template":"bywin_all*",
    "order":6,				/* 索引的优先级,如果再创建一个相同的索引模板优先级设置为7,就可以优先使用新创建的模板*/
    "index_patterns":[		/*索引的名称支持通配符,标识可以匹配bywin_all开头的索引*/
        "bywin_all*"
    ],
    "settings":{      /*索引模板中的 setting 部分一般定义的是索引的主分片、拷贝分片、刷新时间、自定义分析器等。*/
        "refresh_interval":"100s",  /*刷新间隔100秒,当数据添加到索引后并不能马上被查询到,等到索引刷新后才会被查询到。*/
        "store.preload":[   /*预加载 告知操作系统在打开时将热索引文件的内容加载到内存中。默认值为空,即不提前加载索引到内存中,本示例中设置了fullTxt,xm表示提前加载fullTxt和xm的数据到内存中,加快索引速度。*/
            "fullText",
            "xm"
        ],
        "merge.policy.max_merged_segment":"1000mb",/*归并策略  大于这个大小的segment不参与归并 */
        "translog.durability":"async",/*fsync:对应系统级别的中的direct I/O,对直接写入到磁盘中,不经过page cache*/
		/*async:对应系统级别的buffer I/O,会写到page cache,在{index.translog.sync_interval}时间以后会把page cache中的内容再写入到磁盘。比fsync快,但是在index.translog.sync_interval期间机器宕机的话,这些数据没写入到磁盘中,会丢失。*/
        "translog.flush_threshold_size":"2gb",/*translog的大小超过这个参数后flush*/
        "translog.sync_interval":"20s",	/*控制translog多久fsync到磁盘*/
        "number_of_shards":"12", /*    主副本的分片数,默认是5个,最大值限制为1024个,这个值是分片数可适当的增加,提高索引的并发性能,但是分片越多,也会导致资源耗费越高,索引要根据访问并发数和ES集群的资源来设置。*/
        "number_of_replicas":"0"/*对于集群数据节点 >=2 的场景,建议副本至少设置为 1(一主一从,共两个副本), 可以提高集群容错和搜索吞吐量(副本分片可用于查询)。就是 你有三个服务器部署了es,你在这个服务器上给索引设置成两个副本,会自动关联另外两个服务器作为副本。有5个服务器 可以设置4个副本。5=4+1*/
    },
    "mappings":{
        "dynamic_templates":[ /*dynamic_templates配置动态类型转换,将一个类型转换为另一个类型*/
            {
                "string_fields":{
                    "match":"*",
                    "match_mapping_type":"string",
                    "mapping":{
                        "type":"keyword"
                    }
                }
            }
        ],
        "_source":{
            "excludes":[
                "fullText"
            ]
        },
        "properties":{ /*字段的映射*/
            "vircleNested":{ /*具体的字段映射*/
                "type":"nested"  /*嵌套类型 这个字段里面可能还有数组的形式保存数据 比如车辆信息 富婆有好几辆车*/
            },
            "lxdhNested":{
                "type":"nested"
            },
            "fullText":{
                "type":"text", /*分词*/
                "store":"true",
                "index":"true"	/* 设置为true, 索引*/
            },
            "xm":{
                "type":"text",  /*分词 在es6.0之后 取消了string 分出来text和keywork 一个分词 一个关键字*/ 
                "fields":{
                    "keyword":{  /* 基于这个映射你即可以在xm字段上进行全文搜索*/
                        "type":"keyword", /*也可以通过xm.keyword字段实现关键词搜索及数据聚合.*/
                        "ignore_above":256
                    }
                }
            }
        }
    }
}

规定时间格式的索引模板

{
    "template": "detail_organization*",
    "order": 10,
    "index_patterns": [
        "detail_organization*"
    ],
    "settings": {
        "refresh_interval": "100s",
        "merge.policy.max_merged_segment": "1000mb",
        "translog.durability": "async",
        "translog.flush_threshold_size": "2gb",
        "translog.sync_interval": "20s",
        "number_of_shards": "12",
        "number_of_replicas": "0"
    },
    "mappings": {
		/*这是个大坑 我晕*/
		/*这个动态时间模板 es这个玩应有点意思 不支持常用格式yyyy-MM-dd HH:mm:ss|,支持yyyy-MM-dd,大部分需要存入的时间都是第一种所以需要定义下*/
		"dynamic_date_formats":"date_optional_time||yyyy-MM-dd HH:mm:ss.SSS||yyyy-MM-dd||yyyy-MM-dd HH:mm:ss||epoch_millis",
        "dynamic_templates": [
            {
                "string_fields": {
                    "match": "*",
                    "match_mapping_type": "string",
                    "mapping": {
                        "type": "keyword"
                    }
                }
            },
			{
                "date_fields": {
                    "match": "*",
                    "match_mapping_type": "date",
                    "mapping": {
                        "type": "date",
						/*理由同上 感觉上面的动态模板设置了,这个可以不用设置*/
						/*不过这个es时间真的给我秀到了,而且索引模板建立完之后还不能改,想修改需要删除重建,*/
						/*而且索引模板变动不会影响已经使用之前的索引的索引 这个真的秀 那我改索引模板 我索引还需要重新建立一个新的*/
						"format":"yyyy-MM-dd HH:mm:ss.SSS||yyyy-MM-dd||yyyy-MM-dd HH:mm:ss||epoch_second"
                    }
                }
            }
        ],
		"_source": {
			"enabled": true
		}
        "properties": {
          
        }
    }
}

浅谈Es是如何实现数据持久化

我们把数据写到磁盘后,还要调用fsync才能把数据刷到磁盘中,如果不这样做在系统掉电的时候就会导致数据丢失,这个原理相信大家都清楚,elasticsearch为了高可靠性必须把所有的修改持久化到磁盘中。

elastic底层采用的是lucene这个库来实现倒排索引的功能,在lucene的概念里每一条记录称为document(文档),lucene使用segment(分段)来存储数据,用commit point来记录所有segment的元数据,一条记录要被搜索到,必须写入到segment中,这一点非常重要,后面会介绍为什么elastic搜索是near-realtime(接近实时的)而不是实时的。

elastic使用translog来记录所有的操作,我们称之为write-ahead-log,我们新增了一条记录时,es会把数据写到translog和in-memory buffer(内存缓存区)中,如下图所示:

es2.png

内存缓存区和translog就是near-realtime的关键所在,前面我们讲过新增的索引必须写入到segment后才能被搜索到,因此我们把数据写入到内存缓冲区之后并不能被搜索到,如果希望该文档能立刻被搜索,需要手动调用refresh操作。

refresh操作

默认情况下,es每隔一秒钟执行一次refresh,可以通过参数index.refresh_interval来修改这个刷新间隔,执行refresh操作具体做了哪些事情呢?

  • 所有在内存缓冲区中的文档被写入到一个新的segment中,但是没有调用fsync,因此内存中的数据可能丢失
  • segment被打开使得里面的文档能够被搜索到
  • 清空内存缓冲区

执行refresh后的状态如下图所示:

es3.png

refresh的开销比较大,我在自己环境上测试10W条记录的场景下refresh一次大概要14ms,因此在批量构建索引时可以把refresh间隔设置成-1来临时关闭refresh,等到索引都提交完成之后再打开refresh,可以通过如下接口修改这个参数:

curl -XPUT 'localhost:9200/test/_settings' -d '{
    "index" : {
        "refresh_interval" : "-1"
    }
}'

另外当你在做批量索引时,可以考虑把副本数设置成0,因为document从主分片(primary shard)复制到从分片(replica shard)时,从分片也要执行相同的分析、索引和合并过程,这样的开销比较大,你可以在构建索引之后再开启副本,这样只需要把数据从主分片拷贝到从分片:

curl -XPUT 'localhost:9200/my_index/_settings' -d ' {
    "index" : {
        "number_of_replicas" : 0
    }
}'

执行完批量索引之后,把刷新间隔改回来:

curl -XPUT 'localhost:9200/my_index/_settings' -d '{
    "index" : {
        "refresh_interval" : "1s"
    } 
}'

你还可以强制执行一次refresh以及索引分段的合并:

curl -XPOST 'localhost:9200/my_index/_refresh'
curl -XPOST 'localhost:9200/my_index/_forcemerge?max_num_segments=5'

flush操作

随着translog文件越来越大时要考虑把内存中的数据刷新到磁盘中,这个过程称为flush,flush过程主要做了如下操作:

  • 把所有在内存缓冲区中的文档写入到一个新的segment中
  • 清空内存缓冲区
  • 往磁盘里写入commit point信息
  • 文件系统的page cache(segments) fsync到磁盘
  • 删除旧的translog文件,因此此时内存中的segments已经写入到磁盘中,就不需要translog来保障数据安全了

flush之后的状态如下所示:

es4.png

es有几个条件来决定是否flush到磁盘,不同版本的es参数有所不同,大家可以参考es对应版本的文档来查看这几个参数:es translog,这里介绍下1.7版本的flush参数:

  • index.translog.flush_threshold_ops,执行多少次操作后执行一次flush,默认无限制
  • index.translog.flush_threshold_size,translog的大小超过这个参数后flush,默认512mb
  • index.translog.flush_threshold_period,多长时间强制flush一次,默认30m
  • index.translog.interval,es多久去检测一次translog是否满足flush条件

上面的参数是es多久执行一次flush操作,在系统恢复过程中es会比较translog和segments中的数据来保证数据的完整性,为了数据安全es默认每隔5秒钟会把translog刷新(fsync)到磁盘中,也就是说系统掉电的情况下es最多会丢失5秒钟的数据,如果你对数据安全比较敏感,可以把这个间隔减小或者改为每次请求之后都把translog fsync到磁盘,但是会占用更多资源;这个间隔是通过下面2个参数来控制的:

  • index.translog.sync_interval 控制translog多久fsync到磁盘,最小为100ms
  • index.translog.durability translog是每5秒钟刷新一次还是每次请求都fsync,这个参数有2个取值:request(每次请求都执行fsync,es要等translog fsync到磁盘后才会返回成功)和async(默认值,translog每隔5秒钟fsync一次)

读者需要弄清楚flush和fsync的区别,flush是把内存中的数据(包括translog和segments)都刷到磁盘,而fsync只是把translog刷新的磁盘(确保数据不丢失)。

索引常用命令

查询索引模板

curl -XGET http://ip:9200/_cat/templates /*查看服务器上所有索引模板情况*/
---------------------------------------------------------------------------------------------
detail_temp                     [detail*]                    6          
.slm-history                    [.slm-history-1*]            2147483647 
.watch-history-10               [.watcher-history-10*]       2147483647 
case_all                        [case_all*]                  1          
.transform-internal-003         [.transform-internal-003]    0          7050099
.monitoring-beats               [.monitoring-beats-7-*]      0          7000199
.monitoring-alerts-7            [.monitoring-alerts-7]       0          7000199
.triggered_watches              [.triggered_watches*]        2147483647 
.monitoring-logstash            [.monitoring-logstash-7-*]   0          7000199
.monitoring-kibana              [.monitoring-kibana-7-*]     0          7000199
.ml-state                       [.ml-state*]                 0          7050099
organization_all                [organization_all*]          1          
.logstash-management            [.logstash]                  0          
.management-beats               [.management-beats]          0          70000
logstash                        [logstash-*]                 0          60001
.transform-notifications-000001 [.transform-notifications-*] 0          7050099
.ml-meta                        [.ml-meta]                   0          7050099
.monitoring-es                  [.monitoring-es-7-*]         0          7000199
.watches                        [.watches*]                  2147483647 
.ml-config                      [.ml-config]                 0          7050099
.ml-anomalies-                  [.ml-anomalies-*]            0          7050099
.ml-notifications-000001        [.ml-notifications-000001]   0          7050099

查看detail_temp模板信息
curl -XGET ip:9200/_template/detail_temp
  
删除模板
curl -XDELETE ip:9200/_template/detail_temp

创建索引

put testCreateIndex /*创建名为testCreateIndex的索引*/

查询索引信息

get indexName/_search  /*查询索引中搜索的信息*/
get indexName/_doc/id    /* _doc 指的是文档类型  es6.0之后类型都为doc  没有document*/

添加数据到索引

post indexName/_doc 	/*采用post命令不需要指定id 添加成功后自动给你分配一个随机的id*/
put  indexName/_doc/id 	/*采用put方式需要指定id*/

删除命令

Delete 呗 ( ̄_, ̄ )

常见查询字段类型

term 精确匹配

​ 核心功能:不受到分词器的影响,属于完整的精确匹配。
​ 应用场景:精确、精准匹配。
​ 适用类型:keyword。

prefix 前缀匹配

​ 核心功能:前缀匹配。
​ 应用场景:前缀自动补全的业务场景。
​ 适用类型:keyword。

wildcard 模糊匹配

​ 核心功能:匹配具有匹配通配符表达式 keyword 类型的文档。支持的通配符:,它匹配任何字符序列(包括空字符序列);?,它 匹配任何单个字符。
​ 应用 场景:请注意,选型务必要慎重!此查询可能很慢多组关键次的情况下可能会导致宕机,因为它需要遍历多个术语。为了防止非 常慢的通配符查询,通配符 不能以任何一个通配符
或?开头。
​ 适用类型:keyword。

match 分词匹配

​ 核心功能:全文检索,分词词项匹配。
​ 应用场景:实际业务中较少使用,原因:匹配范围太宽泛,不够准确。
​ 适用类型:text。

match_phrase 短语匹配

​ 核心功能:match_phrase 查询首先将查询字符串解析成一个词项列表,然后对这些词项进行搜索; 只保留那些包含 全部 搜索词项,且 位置"position" 与搜索词 项相同的文档。
​ 应用场景:业务开发中 90%+ 的全文检索都会使用 match_phrase 或者 query_string 类型,而不是 match。
​ 适用类型:text。

multi_match 多组匹配

​ 核心功能:match query 针对多字段的升级版本。
​ 应用场景:多字段检索。
​ 适用类型:text。

query_string 类型

​ 核心功能:支持与或非表达式+其他N多配置参数。
​ 应用场景:业务系统需要支持自定义表达式检索。
​ 适用类型:text。

bool 组合匹配

​ 核心功能:多条件组合综合查询。
​ 应用场景:支持多条件组合查询的场景。
​ 适用类型:text 或者 keyword。一个 bool 过滤器由三部分组成:
​ must ——所有的语句都 必须(must) 匹配,与 AND 等价。
​ must_not ——所有的语句都 不能(must not) 匹配,与 NOT 等价。
​ should ——至少有一个语句要匹配,与 OR 等价。
​ filter——必须匹配,运行在非评分&过滤模式。

range范围搜索类型

​ 适用类型:long,integer,double或者date

​ 之前的一个需求是可以选择时间范围来查找es的数据,在Java中Client API就使用:

BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.must(QueryBuilders.rangeQuery("date").from(startTime).to(endTime));
queryBuilder.must(boolQueryBuilder);

High Rest Client API

es7.0之后Java语法普遍都采用High Rest Client APi 来进行操作拼接请求调用es的搜索服务。接下来会介绍API的具体使用场景以及写法。

//创建SearchRequest。如果没有参数,这将与所有索引冲突。
SearchRequest searchRequest = new SearchRequest("your's indexName"); 
//大多数搜索参数已添加到中SearchSourceBuilder。它为搜索请求正文中的所有内容提供设置器。
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
//向中添加match_all查询SearchSourceBuilder。
//searchSourceBuilder.query(QueryBuilders.matchAllQuery());
/*查询拼接例子*/
//建立一个boolQuery查询 包含must, should, must_not, or filter 
/*must:返回的文档必须满足must子句的条件,并且参与计算分值
filter:返回的文档必须满足filter子句的条件。不参与计算分值
must_not:返回的文档必须不满足must_not定义的条件。不参与评分。
should:返回的文档可能满足should子句的条件。在一个Bool查询中,如果没有must或者filter,有一个或者多个should子句,那么只要满足一个就可以返回*/
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
//嵌套建立一个查询
BoolQueryBuilder queryBuilder2 = QueryBuilders.boolQuery();
//继续嵌套
BoolQueryBuilder boolQueryBuilder2 = QueryBuilders.boolQuery();
//来一个for循环-for循环 假如一个数组里很多参数都要匹配
for (Map.Entry<String,String> param :filterParam.entrySet()){
    String [] item=param.getValue()
    for(String value:item) {
		//最里层的使用must 精确匹配name 为tom的 和其他的查询是and关系  
		boolQueryBuilder2.must(QueryBuilders.termQuery("name", "tom"));
		//使用should 模糊匹配手机号 包含4781的信息 和其他的查询是or的关系
		boolQueryBuilder2.should(QueryBuilders.wildcardQuery("phone", "*" + "4781" + "*"));    
 	}
    //第一个循环结束 加入到第二个建立的bool中
	queryBuilder2.must(boolQueryBuilder2);
}
//大循环结束 在加入到总的bool中 包含另一个查询但在过滤器上下文中执行的查询。所有匹配的文档都给出相同的“常量”_score。
queryBuilder.must(QueryBuilders.constantScoreQuery(queryBuilder2));
//再建立一个
BoolQueryBuilder queryBuilder3 = QueryBuilders.boolQuery();
//拼接查询方式 采用should 采用模糊匹配sex
queryBuilder3.should(QueryBuilders.wildcardQuery("sex","*"+"男"+"*"));
/*采用should方式  分词匹配内容为haha的   matchPhraseQuery是先将haha进行分词 再去es中content分词之后的情况进行匹配 es中 content需要设置分次text*/
queryBuilder3.should(QueryBuilders.matchPhraseQuery("content","haha"));
//加入到总的bool中
queryBuilder.must(queryBuilder3);
//再来一个范围的继续搞
BoolQueryBuilder queryBuilder4 = QueryBuilders.boolQuery();
//must查询时间字段 在开始时间和结束时间之间
queryBuilder4.must(QueryBuilders.rangeQuery("date").from(startTime).to(endTime));
//加入到总的bool中
queryBuilder.must(queryBuilder4);
//设置查询可以是任何类型QueryBuilder
searchSourceBuilder.query(queryBuilder);
//设置from  开始的数值 预设为0。等同于msyql的分页页码
searchSourceBuilder.from(start);
//设置size用于确定要返回的数量 等同于msyql的分页每页数量
searchSourceBuilder.size(pageSize);; 
//设置一个可选的超时时间,以控制允许搜索的时间。
searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
//降序排列_score(默认)
sourceBuilder.sort(new ScoreSortBuilder().order(SortOrder.DESC)); 
//也按_id字段升序排序
sourceBuilder.sort(new FieldSortBuilder("id").order(SortOrder.ASC));
//也按_id字段降序排序
searchSourceBuilder.sort(SortBuilders.fieldSort("id").order(SortOrder.DESC));
//在此之后 添加SearchSourceBuilder到SearchRequest中
searchRequest.source(searchSourceBuilder); 
//设置执行搜索本地分片。默认设置是随机分片。
searchRequest.preference("_local");
//设置路由 我没用过 可以省略
searchRequest.routing(""); 
//查询通过原声查询client.search进行查询 这里写一个查询模板实现类 查询结果通过SearchResponse接受
SearchResponse searchResponse = elasticsearchTemplate.search(searchRequest);
//想要的到返回文档 我们需要先获取SearchHits 
SearchHits searchHits = searchResponse.getHits();
//查询出来的数量
long total = searchResponse.getHits().getTotalHits().value
for (SearchHit hit : searchHits) {
    // do something with the SearchHit
    String index = hit.getIndex();
	String id = hit.getId();
	float score = hit.getScore();
    //具体数据了奥
    String sourceAsString = hit.getSourceAsString();
    //给他转换下 字符串转对象
    Map map = JsonUtils.string2Obj(hit.getSourceAsString(), Map.class);
}
//TODO 再封装数据给前端。。。。。。
    //字符串转对象
    public static <T> T string2Obj(String str,Class<T> clazz){
        if (StringUtils.isEmpty(str) || clazz == null){
            return null;
        }
        try {
            return clazz.equals(String.class)? (T) str :objectMapper.readValue(str,clazz);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

总结

知识只有分享出来才有价值。如果有问题的话,可以在关于我的页面,通过我的邮箱联系我进行探讨。

Q.E.D.


Remain true to our original aspiration.