ES总结
本文总结了我这两个多月以来在公司所涉及到的ES相关的开发经验。
公司系统的数据并不是很大,只有五十万左右,但数据的结构却没这么简单,考虑到数据(物料)的搜索涉及到多种条件,包括用户,时间,状态,还有物料本身的特定属性,还有根据某个属性进行范围,模糊和精确查询,对多个属性进行组合查询,使用mongo实现SQL较为复杂,而且响应时间不尽如人意,因此考虑使用ES来实现。
一、什么是ES
es是一款内部以Lucene 做索引与搜索的全文搜索引擎,底层数据结构使用倒排索引来实现全文搜索。他是一个分布式的实时文档存储中间件,每个字段都可以被索引与搜索,一个分布式实时分析搜索引擎。
二、ES的基本概念
1、倒排索引 :
es会对每一属性都创建一个倒排索引,每个不同的属性值都记录其所存在的文档的id,因此只要知道属性值,就能通过对应的id找到整个文档。
1)正常表结构
ID | Name | Age | Sex |
---|---|---|---|
1 | Kate | 24 | Female |
2 | John | 24 | Male |
3 | Bill | 29 | Male |
2)倒排索引表
①Name索引
Term | Posting List |
---|---|
Kate | 1 |
John | 2 |
Bill | 3 |
②Age索引
Term | Posting List |
---|---|
24 | [1, 2] |
29 | 3 |
③Sex索引
Term | Posting List |
---|---|
Female | 1 |
male | [2,3] |
2、索引(index)
相当于传统数据库里的表,也当做动词,表示为一个字段创建索引的过程。
3、映射(mapping)
定义一个索引里各个属性的数据结构,text、keyword、double、date、等等,除此之外,还定义各个属性使用的分析器。
1)text:text类型的属性,会被分词器进行分词然后对结果进行索引存储。
2)keyword:关键词类型,和text都是string类型,但不会被分词器进行分词,直接索引。
两种设置方式
方式一:
"releaseStatus": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
}
通过此方式设置的类型不会对字段进行分词并生成索引,只生成当前值的索引。
方式二:
"ownerName": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256,
"normalizer": "lowercase_normalizer"
}
}
}
通过此方式设置的keyword类型既对字段进行分词生成索引,也生成一个当前值的索引。
3) double、float、long:其他数字类型。
4、分析器(analyzer)
1)分析器负责将一段文本也即属性值分成适合于倒排索引的独立的词条。
2)分析器其实是由字符过滤器、分词器、token过滤器三个功能组成。
①字符过滤器:在进行分词前整理字符串,过滤掉HTML内容。
②分词器:对文本进行分词成词条。
③token过滤器:词条通过token过滤器,将单词小写,删除of等无用词。
3)两个常用的中文分词器:ik_max_word和ik_smart
①ik_max_word:将文本做最细粒度的拆分,如中华人民会被分为中华人民、中华、华人、人民。
②ik_smart:做最粗粒度的拆分,如中华人民只能分为中华人民。
4)调用es分析器对文本分词
①ik_max_word
POST localhost:9200/_analyze
{
"text": "中华人民",
"analyzer": "ik_max_word"
}
结果
{
"tokens" : [
{
"token" : "中华人民",
"start_offset" : 0,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "中华",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "华人",
"start_offset" : 1,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "人民",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 3
}
]
}
②ik_smart
POST localhost:9200/_analyze
{
"text": "中华人民",
"analyzer": "ik_smart"
}
结果
{
"tokens" : [
{
"token" : "中华人民",
"start_offset" : 0,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 0
}
]
}
4)通常对于中文的全文检索,创建索引映射时使用ik_max_word分词器,在进行搜索时使用ik_smart分词器,(前提下载ik_max_word和ik_smart插件并解压到Elasticsearch的插件目录中)如下:
①创建一个索引并设置映射
PUT /my_index/_mapping
{
"properties": {
"desc": {
"type": "text",
"analyzer": "ik_max_word" // 使用ik_max_word分词器
}
}
}
②构建查询语句
GET /my_index/_search
{
"query": {
"match": {
"desc": {
"query": "示例",
"analyzer": "ik_smart" // 在搜索时使用ik_smart分词器
}
}
}
}
5、文档(doc)
相当于数据库里的一条数据,一个索引下由多个文档组成。
三、ES的系统架构
1、集群(cluster)
一个集群有多个节点组成,以下是有一个节点组成的集群。
2、节点(node)
一个节点相当于一个运行中的es服务实例,节点在集群中会被选举其中一个作为主节点,主节点复制这个集群范围内的增加删除索引,或者增加删除节点等变更。
3、分片(shard)
索引其实是由多个分片组成,每个分片是一个基本的搜索引擎,他能使用在一个节点中的所有资源(相当于一个新的进程),文档被存储和索引在分片中。分片又有主分片和副本分片,其实副本分片只是主分片的一份拷贝。一个索引在创建的时候主分片已经确定不能修改(why?),而副本分片是可随时增减的。以下是一个集群中包含一个主节点,该主节点包含三个主分片,没有副节点也没有副分片,这就是单机版es。
综上:多个文档组成分片,多个主分片组成索引,多个主分片可能分布在不同的节点中,多个节点又组成一个集群。
4、扩展节点、分片
①增加节点,实现数据备份并且提高读效率:
以下为上图的记下扩展一个新的节点Node2,其中R0、R1、R2分别是P0、P1、P2的副本分片,此时如果主节点Node1挂了,数据也能完整保留。
②再增加一个节点,此时将每个分片分摊到每个节点之中,其中一对主副分片不能在同一个节点中,否则会导致节点挂了后改分片的数据丢失。该结构进一步提升了查询效率,因为每个节点只有两个分片,每个分片能占用的资源更多。
③每个分片再增加一份副本分片,此时并不会提升查询效率,因为每个分片从节点上获得的资源会变少,但数据安全性更强,即使丢失了两个节点,也不会丢失数据。
5、文档的创建、索引和搜索过程
1)文档会根据路由算法存储到某个主分片上,该算法相当于取主分片总数的余数,所以主分片数量在索引创建的时候就已经确定。
2)协调节点:当发起文档的创建、索引和搜索请求时,请求到的第一个节点被称为协调节点,他会轮询其他节点的分片,将请求均衡的分配到其他节点或自身节点。
3)文档的创建、索引过程:
①、请求打在node1上,此时node1为协调节点,经过路由算法,该文档分配到P0主分片上。
②文档存储到P0。
③文档拷贝到R0副本分片。
④所有副本拷贝完成,通知协调节点创建成功,协调节点返回成功给客户端
四、ES的查询语句DSL
1、match
GET /_search
{
"query": {
"match": {
"desc": "elasticsearch"
}
}
}
①会对查询内容elasticsearch进行分词elastic、search。
②根据分词结果与desc的倒排索引表(如下)进行匹配。
Term | Posting List |
---|---|
elastic | 1 |
search | [2,3] |
desc其他属性值的分词… | … |
③只要存在其中一个分词与之匹配,则返回对应的文档,以上返回文档d的id 1,2,3。
2、match_phrase
GET /_search
{
"query": {
"match_phrase": {
"desc": "elasticsearch"
}
}
}
①会对查询内容elasticsearch进行分词为elastic、search。
②根据分词结果与desc的倒排索引表进行匹配。
Term | Posting List |
---|---|
elastic | [1,3] |
search | [2,3] |
desc其他属性值的分词… | … |
③当所有分词结果都与之匹配,而且每个分词的位置与对内容的分词结果一样,则返回对应所有匹配成功的文档,如上返回文档的id为3,其中id为3的文档的desc值还必须为elasticsearch,而不能为searchelastic(因此个人认为和term有点类似)
3、term
1)不带keyword
{
"query": {
"term": {
"desc": "elasticsearch"
}
}
}
①不对查询内容elasticsearch进行分词。
②根elasticsearch与desc的倒排索引表进行匹配。
Term | Posting List |
---|---|
elastic | [1,3] |
search | [2,3] |
elasticsearch | 4 |
desc其他属性值的分词… | … |
③当elasticsearch与之匹配,返回对应所有匹配成功的文档,如上返回的文档id为4
2)带keyword
{
"query": {
"term": {
"desc.keyword":{
"value":"elastic"
}
}
}
}
①不对查询内容elastic进行分词。
②查找不到内容,因为带了keyword后只会匹配当时插入的完整词条elasticsearch。因此带了keyword是精确查询,不会进行分词匹配。
4、terms
{
"query": {
"terms": {
"desc": ["elasticsearch","mongoDB"]
}
}
}
①类似于在term的基础上进行in查询
②根elasticsearch、mongoDB与desc的倒排索引表进行匹配。
Term | Posting List |
---|---|
elastic | [1,3] |
mongo | [2,3] |
mongoDB | 5 |
elasticsearch | 4 |
desc其他属性值的分词… | … |
③如上返回的文档id为4,5
5、wildcard
例一、
{
"query": {
"wildcard" : { "user" : "ki*y" }
}
}
例二、
{
"query": {
"wildcard" : { "user" : "ki?y" }
}
}
①通配符查询:支持*(匹配多个字符)和?(匹配一个字符)查询,不对查询内容elasticsearch进行分词。
②如上分别返回[kittty、kity]和[kijy]
6、range
{
"query": {
"range": {
"age": {
"gte": 29,
"lt": 59
}
}
}
}
①范围查询,查询age属性值在[29,59)之间的文档。
7、bool
1)bool布尔查询是一个组合查询,其结构如下
解释:bool查询的下一层必须至少有must、shuold、must_not其中一个。
①must:类似于and,数组内的所有查询全都匹配才会匹配。
②shuold:类似于or,数组内至少有一条查询匹配了就会匹配。
③must_not:数组内的所有查询全都不匹配时才会匹配。
{
"bool" : {
"must" : []或{},
"should" : []或{},
"must_not" : []或{},
}
}
2)bool布尔查询嵌套
bool类型可以相互嵌套。
如下图,筛选desc包含kafka或者同时包含elasticsearch和mongoDB的文档
"query": {
"bool": {
"should": [
{
"term": {
"desc": "kafka"
}
},
{
"bool": {
"must": [
{
"term": {
"desc": "elasticsearch"
}
},
{
"term": {
"desc": "mongoDB"
}
}
]
}
}
]
}
}
8、nested
①对象数组查询,类似于对JsonArray里的符合条件的Json查询
如下数据结构
PUT /my_index/blogpost/1
{
"title": "Nest eggs",
"people": [
{
"name": "Alice Smith",
"age": 10
},
{
"name": "Alice White",
"age": 31
}
]
}
②当我们想查名字包含Alice且年龄为10岁的怎么办?
答案是
"query": {
"nested": {
"path": "people",
"query": {
"bool": {
"must": [
{
"match": {
"people.name": "Alice"
}
},
{
"term": {
"people.age": 10
}
}
]
}
}
}
}
③前提people的索引结构需要设置为
{
"properties": {
"people": {
"type": "nested",
"properties": {
"name": {
"type": "string"
},
"age": {
"type": "short"
}
}
}
}
}
五、ES在Java中的应用
1、配置
①maven依赖:版本需要与es的当前版本一致
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.2.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.2.0</version>
</dependency>
②配置类:
@Configuration
@ConfigurationProperties(prefix = "elasticsearch")
@Data
public class ElasticSearchConfig extends AbstractElasticsearchConfiguration {
private String host;
private String port;
private String username;
private String password;
@Override
public RestHighLevelClient elasticsearchClient() {
//es设置账户密码时
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
new UsernamePasswordCredentials(username, password)); //es账号
RestClientBuilder builder= RestClient.builder(new HttpHost(host, Integer.parseInt(port),"http"))
.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) {
httpClientBuilder.disableAuthCaching();
return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
}
});
//未设置账户密码时
// RestClientBuilder builder= RestClient.builder(new HttpHost(host, Integer.parseInt(port),"http"));
RestHighLevelClient restHighLevelClient=new RestHighLevelClient(builder);
return restHighLevelClient;
}
2、索引文档的增删改查
1)索引的增删改查
一般索引的增删改不在代码中实现,而是写好mapping和索引
①创建索引同时设置映射:/PUT请求 http://{ip}:9200/{index_name}
可以看到,以下部分属性有
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
}
}
{
"mappings": {
"properties": {
"attributes": {
"type": "nested",
"properties": {
"attributeName": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
}
},
"analyzer": "ik_max_word"
},
"attributeValue": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
}
},
"analyzer": "ik_max_word"
},
"unitAttributeValue": {
"type": "double"
},
"unit": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
},
"cidAttributes": {
"type": "nested",
"properties": {
"attributeName": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
}
},
"analyzer": "ik_max_word"
},
"attributeValue": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
}
},
"analyzer": "ik_max_word"
}
}
},
"createdTime": {
"type": "long"
},
"currentVersion": {
"type": "boolean"
},
"id": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256,
"normalizer": "lowercase_normalizer"
}
}
},
"idAttributes": {
"type": "nested",
"properties": {
"attributeName": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
}
},
"analyzer": "ik_max_word"
},
"attributeValue": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
}
},
"analyzer": "ik_max_word"
}
}
},
"material": {
"properties": {
"businessId": {
"type": "keyword",
"normalizer": "lowercase_normalizer",
"copy_to": [
"sug_objectName"
]
},
"classificationId": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
},
"classificationPath": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256,
"normalizer": "lowercase_normalizer"
}
}
},
"idGeneratorId": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
},
"isInProcess": {
"type": "boolean"
},
"materialTypeId": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
},
"uid": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
},
"ownerId": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"ownerName": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"libraryIds": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
}
}
},
"mdmState": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256,
"normalizer": "lowercase_normalizer"
}
}
},
"modifiedTime": {
"type": "long"
},
"modifierId": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256,
"normalizer": "lowercase_normalizer"
}
}
},
"modifierName": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256,
"normalizer": "lowercase_normalizer"
}
}
},
"objectDesc": {
"type": "text",
"fields": {
"raw": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
}
},
"copy_to": [
"sug_objectName"
],
"analyzer": "ik_max_word"
},
"objectName": {
"type": "text",
"fields": {
"raw": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
}
},
"copy_to": [
"sug_objectName"
],
"analyzer": "ik_max_word"
},
"ownerGroup": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256,
"normalizer": "lowercase_normalizer"
}
}
},
"ownerId": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256,
"normalizer": "lowercase_normalizer"
}
}
},
"ownerName": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256,
"normalizer": "lowercase_normalizer"
}
}
},
"failCause": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256,
"normalizer": "lowercase_normalizer"
}
}
},
"releaseStatus": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
},
"revId": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256,
"normalizer": "lowercase_normalizer"
}
}
},
"sug_objectName": {
"type": "completion",
"analyzer": "ik_max_word",
"preserve_separators": true,
"preserve_position_increments": true,
"max_input_length": 50
},
"uid": {
"type": "keyword",
"normalizer": "lowercase_normalizer"
}
}
},
"settings": {
"analysis": {
"normalizer": {
"lowercase_normalizer": {
"type": "custom",
"char_filter": [],
"filter": [
"lowercase"
]
}
}
}
}
}
②设置跳页最大数量: http://{ip}:9200/{index_name}/_settings
{
"index.max_result_window": 500000
}
解释:如果分页请求的from超过500000(默认为10000),就会报错,这个数量不是越大越好,越大容易引起jvm的栈溢出。
2) 文档的增删改查:
es一般不修改文档,而是直接将新的覆盖旧的,所有文档都只是数据库的一份备份。所以一般是数据库修改完,重新刷到es即可。
①文档的新增:
方式一、单条新增:
public void insertDocument(@NonNull MaterialRevision revision) {
try {
IndexRequest indexRequest = new IndexRequest(materialIndex);
indexRequest.id(revision.getId());
indexRequest.source(new Gson().toJson(doc), XContentType.JSON);
IndexResponse response = client.index(indexRequest, RequestOptions.DEFAULT);
// 20 开头的则表示执行成功
if (String.valueOf(response.status().getStatus()).startsWith(ElasticSearch.ES_STATUS_SUCCESS_PREFIX)) {
// log.info("insert document {} into elastic search", doc.keyInfo());
log.debug(new ObjectMapper().writeValueAsString(doc));
} else {
ObjectMapper mapper = new ObjectMapper();
log.warn("fail to update doc to elasticsearch: {}, response: {}", mapper.writeValueAsString(doc),
mapper.writeValueAsString(response));
}
} catch (Throwable e) {
}
}
方式二、批量新增:
@Autowired
private RestHighLevelClient client;
@Value("${elasticsearch.index.material}")
private String materialIndex;//索引
public void bulkInsertDocument(List<MaterialRevision> revisions) {
try {
long begin = System.currentTimeMillis();
BulkRequest bulkRequest = new BulkRequest();
for (MaterialRevision revision : revisions) {
MaterialRevisionDoc doc = MaterialRevisionDoc.buildESDoc(revision);
IndexRequest indexRequest = new IndexRequest(materialIndex);
indexRequest.id(doc.getId());//文档id
indexRequest.source(new Gson().toJson(doc), XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
long end = System.currentTimeMillis();
if (String.valueOf(bulkResponse.status().getStatus()).startsWith(ElasticSearch.ES_STATUS_SUCCESS_PREFIX)) {
log.info("批量插入成功,用时:" + (end - begin) / 1000 + "秒");
} else {
log.info("批量插入失败");
}
} catch (Throwable e) {
log.error("Elasticsearch insert error :{}", e);
}
}
②文档的删除:当数据库的数据删除后,同时要记得删除es的文档
public void delete(String id) {
try {
DeleteRequest deleteReq = new DeleteRequest(materialIndex);
deleteReq.id(id);
client.delete(deleteReq, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("Elasticsearch delete error :{}", e);
}
}
③文档的查找
private List<MaterialRevision> query(SearchCondition condition) throws IOException {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder rootBoolBuilder=new BoolQueryBuilder();
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("businessId",condition.getBusinessId());
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("desc", condition.getDesc());
WildcardQueryBuilder wildcardQueryBuilder = QueryBuilders.wildcardQuery("ownerName", "*"+condition.getOwner()+ "*");
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("createTime").gte(condition.getCreatedTimeFrom()).lte(condition.getCreatedTimeTo());
TermQueryBuilder subTermQueryBuilder = QueryBuilders.termQuery("attributes.attributeName.keyword",condition.getProperties().getName());
TermsQueryBuilder subTermsQueryBuilder = QueryBuilders.termsQuery("attributes.attributeValue.keyword", condition.getProperties().getValues());
NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attributes", QueryBuilders.boolQuery()
.must(subTermQueryBuilder).must(subTermsQueryBuilder), ScoreMode.None);
//组合所有条件为且
rootBoolBuilder.must(termQueryBuilder).must(matchQueryBuilder).must(wildcardQueryBuilder).must(rangeQueryBuilder).must(nestedQueryBuilder);
//查询
sourceBuilder.query(rootBoolBuilder);
//对结果根据businessId倒序
sourceBuilder.sort("businessId", SortOrder.DESC);
//分页
sourceBuilder.from(condition.getFrom()).size(condition.getSize());
//返回真实的命中条数
sourceBuilder.trackTotalHits(true);
//建立请求
SearchRequest request = new SearchRequest(materialIndex).source(sourceBuilder);
//发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//得到命中的字符串内容并转换成实体类
List<MaterialRevision> res = Arrays.stream(response.getHits().getHits()).map(hit ->
JSONObject.parseObject(hit.getSourceAsString(), MaterialRevision.class)
).collect(toList());
return res;
}
六、面试题
1、ES 的分布式架构原理能说一下么(ES 是如何实现分布式的啊)?
首先,ES 中存储数据的基本单位是索引,相当于mysql里的表,索引下面就是一条条数据称为文档。
你创建一个索引,这个索引可以拆分成多个分片 ,每个分片存储部分数据。拆分多个 分片是有好处的,一是支持横向扩展,比如你数据量是 3T,3 个 分片,每个 分片就 1T 的数据,若现在数据量增加到 4T,怎么扩展,很简单,重新建一个有 4 个 分片的索引,将数据导进去;二是提高性能,数据分布在多个 分片,即多台服务器上,所有的操作,都会在多台机器上并行分布式执行,提高了吞吐量和性能。
接着就是这个 分片的数据实际是有多个备份,就是说每个 分片都有一个 主分片,负责写入数据,但是还有几个 副本分片 。 主分片写入数据之后,会将数据同步到其他几个 副本分片上去。
通过这个 副本的方案,每个 分片的数据都有多个备份,如果某个机器宕机了,没关系,还有别的数据副本在别的机器上呢。高可用了吧。
ES 集群多个节点,会自动选举一个节点为 主节点,这个 主节点其实就是干一些管理的工作的,比如维护索引元数据、负责切换 主分片 和 副本分片身份等。要是 主节点宕机了,那么会重新选举一个节点为 主节点。
如果是非 主节点宕机了,那么会由 主节点,让那个宕机节点上的主分片的身份转移到其他机器上的 副本分片。接着你要是修复了那个宕机机器,重启了之后,主节点会控制将缺失的 副本分片 分配过去,同步后续修改的数据之类的,让集群恢复正常。
说得更简单一点,就是说如果某个非 主节点宕机了。那么此节点上的主分片 不就没了。那好,主机节点会让主分片对应的副本分片(在其他机器上)切换为主分片。如果宕机的机器修复了,修复后的节点也不再是主分片,而是副本分片。
其实上述就是 ElasticSearch 作为分布式搜索引擎最基本的一个架构设计。
2、ES 写入数据的工作原理是什么啊?ES 查询数据的工作原理是什么啊?底层的 Lucene 介绍一下呗?倒排索引了解吗?
1)es写数据过程
①首先客户端发送一个写请求,接收到请求的节点被称为协调节点。
②协调节点对文档id进行路由hash确定该文档所在分片,然后将请求转发给对应的节点上。
③对应节点的主分片处理完请求后,会同步到其他节点的副本分片上去。
④当协调节点发现主分片和副本分片都写完后,才会将结果返回。
2)ES读数据过程
①请求发送到任一个节点,该节点成为协调节点。
②协调节点对文档id通过hash计算后得出其所在的分片,使用随机轮询算法从主分片和副本分片找到某一个分片。
③被请求到的分片节点将结果返回给协调节点,协调节点返回内容。
3)ES查询过程
ES的查询过程分为两阶段,query then fetch
①查询请求发送到协调节点。
②由于查询时es不知道这个文档在哪个分片,因此协调节点调用每个主分片或副分片去查。
③每个分片将搜索结果(此时为文档id)返回给协调节点,协调节点对这些值进行排序合并分页等操作。
④最后协调节点再通过文档id到各节点上拉取数据,最终返回给客户端。