目录
1 ElasticSearch是什么
Elasticsearch(简称ES)是一个建立在开源库Lucene(基于Java的检索库lucene.apache.org/)之上的开源搜索引擎。ES隐藏了Lucene的复杂性,将所有功能封装到一个单独的服务,对外提供了一套简单且通用的Restful接口服务,同时也支持命令行格式作为客户端,其底层都是基于JAVA语言的。
但是Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎,我们这样定义它:
- 一个分布式的实时文档存储,每个字段 可以被索引与搜索
- 一个分布式实时分析搜索引擎
- 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据
Elasticsearch最早是用做日志搜索服务,我司的日志搜索中心LogCenter就是基于Kibana+Elasticsearch的。
2 ES相关概念
2.1 基本结构
集群、节点
2.2 基本概念
| ES | 描述 | 用法 | RDBMS |
|---|---|---|---|
| 分片(Shard) | 为了支持Index数据量非常大的情况,ES允许将Index水平分割为若干个Shard,这种机制还能够支持对不同Shard进行并行操作来提高吞吐量。在Index创建时指定Shard数量,之后不能再更改。 | 一个索引分成N个shard,每一个Shard的内容就是这个完整索引内容的1/N | 在数据库里没有类似的概念 |
| 索引(index) | 用于描述多个行记录的集合 | 例如把所有的商户放在一个索引里 | Table |
| 文档(document) | 文档是可搜索的结构化数据单元,用于描述一整条记录,由多个字段组成 | 例如上海天山西路的KFC商户我们可以作为一个文档 | Row |
| 字段(Field) | 用于表述每一个列的名字,字段是文档的组成单元,包含字段名称、字段属性和字段内容 | 例如商户名,城市名就分别是一个字段 | Column |
| Term | 它是搜索的基本单位,其表现形式为文本中的一个词。es中的不同字段类型,分词逻辑不一样。 | 例如上海天山西路的KFC商户,分词后term为:上海、天山西路、的、KFC、商户 | |
| DSL | Elasticsearch 提供的一个丰富灵活的查询语言叫做 查询表达式 ,即 领域特定语言 (DSL),它支持构建更加复杂和健壮的查询。使用 JSON 构造了一个请求。 | SQL | |
| 正排 | 文档到字段对应关系组成的链表,勾选可过滤后会构建正排链表。doc1->id,type,create_time… | 行记录 | |
| 倒排 | 词组到文档的对应关系组成的链表,勾选可搜索后会构建倒排链表。term1->doc1,doc2,doc3;term2->doc1,doc2见 目录3.3 | 类似B+树索引 |
3 初识索引
索引 这个词在 Elasticsearch 语境中有多种含义, 这里有必要做一些说明:
索引(名词):
如前所述,一个 索引 类似于传统关系数据库中的一个 数据库 ,是一个存储关系型文档的地方。 索引 (index) 的复数词为 indices 或 indexes 。
索引(动词):
索引一个文档 就是存储一个文档到一个 索引 (名词)中以便被检索和查询。这非常类似于 SQL 语句中的 INSERT 关键词,除了文档已存在时,新文档会替换旧文档情况之外。
倒排索引:
关系型数据库通过增加一个 索引 比如一个 B树(B-tree)索引 到指定的列上,以便提升数据检索速度。Elasticsearch 和 Lucene 使用了一个叫做 倒排索引 的结构来达到相同的目的。
默认的,一个文档中的每一个属性都是 被索引 的(有一个倒排索引)和可搜索的。一个没有倒排索引的属性是不能被搜索到的。
3.1 索引结构
在 ES 早期版本,一个索引下是可以有多个 Type 的,从 7.0 开始,一个索引只有一个 Type(名字唯一定义为_doc),也可以说一个 Type 有一个 Mapping 定义
索引定义
SQL
#创建一个shop的索引,副本数设置为1,分片数为2
curl -XPUT 'http://localhost:8080/shop'
-d '
{
//字段配置
"mappings":{ ### 描述索引的字段,以及该字段的各种属性;
"dynamic":true|false|strict, ### 动态映射:通过用户的输入数据,是否需要动态映射。true:动态添加新的字段—缺省;false:忽略新的字段;strict:如果遇到新字段抛出异常
"_metadata":{ ### 元数据域
"_source":true, ### 一个doc的原生的json数据存储
"_id":true, ### 文档的唯一标识
...
},
"properties":{### 字段定义
"address":{
"type":"text",
"analyzer":"ik_smart" ##指定分词器
},
"avgprice":{
"type":"long"
},
"cityid":{
"type":"integer"
},
"shopid":{
"type":"integer"
},
"email":{
"type":"keyword" ##keyword类型适用于索引结构化的字段,比如email地址、主机名、状态码和标签
"ignore_malformed":true ##ignore_malformed可以忽略不规则数据,对于login字段,有人可能填写的是date类型,也有人填写的是邮件格式。
"doc_values":true ##doc_values是为了加快排序、聚合操作,在建立倒排索引的时候,额外增加一个列式存储映射,是一个空间换时间的做法。默认是开启的,对于确定不需要聚合或者排序的字段可以关闭。
},
"shoppoi":{
"type":"geo_point" ##空间数据类型
}
}
},
//索引的一些配置
"settings":{ ### 描述该索引的全局配置,包括副本数、分片数等
"index":{
"number_of_shards":2, ##指定索引的分片数
"number_of_replicas":1 ##每个主分片拥有的副本数。在索引建立的时候就已经确定了主分片数,但是副本分片数可以随时修改
"max_result_window":10000 ##在搜索的时候,from:决定要返回的文档从哪里开始,size:决定返回多少条。
##假如from+size很大的话,将会消耗很多的内存和时间;这个设置就是为了防止内存不够用的情况。默认是:10000,也就是说from+size不能大于10000;
##如果需要获取更多的数据,请看 Scroll 和 Search After
}
},
//别名配置
"aliases":{ ### 索引别名配置
}
}'
3.2 字段类型
3.2.1 简单域(Field)类型
-
字符串: string
string 类型域会被认为包含全文。就是说,它们的值在索引前,会通过一个分析器,针对于这个域的查询在搜索前也会经过一个分析器。
string 域映射的两个最重要属性是 index(控制怎样索引字符串。analyzed: 以全文索引这个域;not_analyzed: 索引这个域,所以它能够被搜索,但索引的是精确值。不会对它进行分析;no: 不索引这个域。这个域不会被搜索到。) 和 analyzer (对于 analyzed 字符串域,用 analyzer 属性指定在搜索和索引时使用的分析器)。
string 域 index 属性默认是 analyzed 。如果我们想映射这个字段为一个精确值,我们需要设置它为 not_analyzed
-
整数 : byte, short, integer, long
其他简单类型(例如 long , double , date 等)也接受 index 参数,但有意义的值只有 no 和 not_analyzed , 因为它们永远不会被分析。
-
浮点数: float, double
-
布尔型: boolean
-
日期: date
3.2.2 复杂核心域类型
JSON 还有 null 值,数组,和对象,这些 Elasticsearch 都是支持的。(不做介绍)
3.3 倒排索引
Elasticsearch 使用一种称为 倒排索引 的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。(这种数据结构把索引中的每个Term与相应的Document映射起来)
假定我们的Document只有title域(Field)被编入索引,Document如下:
| 文档编号 | 文档内容 |
|---|---|
| 1 | ElasticSearch Server |
| 2 | Mastering ElasticSearch |
| 3 | Apache Solr 4 Cookbook |
所以索引(以一种直观的形式)展现如下:
| Term | count | Docs ID |
|---|---|---|
| 4 | 1 | < 3 > |
| Apache | 1 | < 3 > |
| Cookbook | 1 | < 3 > |
| ElasticSearch | 2 | < 1 > , < 2 > |
| Mastering | 1 | < 1 > |
| Server | 1 | < 1 > |
| Solr | 1 | < 1 > |
正如所看到的那样,每个词都指向它所在的文档号(Document Number/Document ID)。这样的存储方式使得高效的信息检索成为可能,比如基于词的检索(term-based query)。此外,每个词映射着一个数值(Count),它代表着Term在文档集中出现的频繁程度。
4 基础常用查询
4.0 空搜索
GET /_search
搜索结果
{
"hits" : {
"total" : 14, ### 表示匹配到的文档总数
"hits" : [ ### 一个 hits 数组包含所查询结果的前十个文档
{
"_index": "us", ### 索引名称
"_type": "tweet", ### 类型名称(7之后,一个index只包含一个type)
"_id": "7", ### doc ID
"_score": 1, ### 代表文档与查询的匹配程度。默认情况下,首先返回最相关的文档结果,就是说,返回的文档是按照 _score 降序排列的
"_source": { ### 源数据JSON格式
"date": "2014-09-17",
"name": "John Smith",
"tweet": "The Query DSL is really powerful and flexible",
"user_id": 2
}
},
... 9 RESULTS REMOVED ...
],
"max_score" : 1 ### 与查询所匹配文档的 _score 的最大值
},
"took" : 4, ### 执行整个搜索请求耗费了多少毫秒
"_shards" : { ### 在查询中参与分片的总数,以及这些分片成功了多少个失败了多少个
"failed" : 0,
"successful" : 10,
"total" : 10
},
"timed_out" : false ### 查询是否超时。默认情况下,搜索请求不会超时。如果低响应时间比完成结果更重要,你可以指定 timeout 为 10 或者 10ms(10毫秒),或者 1s(1秒)
}
4.1 精确查询(TermQuery)
作用:Term Query用来查询跟特定字段相关的数据,作精确查询,他不会对查询的词语进行分词。 例如商户索引里有cityid字段,我们要搜索cityid=1的有哪些商户,我们可以这样定义query:
- HTTP语法
代码块
Shell
#http请求 curl -X Get /shop/_doc
{"query":{"term":{"cityid":1}}}
- Java API语法
代码块
Java
public static void termQuery(RestHighLevelClient porosClient) throws IOException {
SearchRequest request = new SearchRequest("shop"); ///后续我们将这些通用语句省略,主要展示SearchSourceBuilder相关
SearchSourceBuilder builder = new SearchSourceBuilder();
request.source(builder);
builder.query(QueryBuilders.termQuery("cityid",1));
SearchResponse response = porosClient.search(request,RequestOptions.DEFAULT);//后续我们将这些通用语句省略
System.out.printf("结果为:"+response);
}
针对一个Term输入太少的情况,ES支持在term里使用多个条件进行召回的TermsQuery。
4.1.1 TermsQuery
terms 查询和 term 查询一样,但它允许你指定多值进行匹配。如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件。和 term 查询一样,terms 查询对于输入的文本不分析。它查询那些精确匹配的值。
-
HTTP语法
代码块
Shell
#http请求 ,与term query类似,将term改成terms,单值改成数组的形式curl -X Get /shop/_doc{"query":{"terms":{"cityid":[1,2,3]}}} -
Java API 语法
代码块
Shell
public static void termsQuery(RestHighLevelClient porosClient) throws IOException {SearchRequest request = new SearchRequest("shop");SearchSourceBuilder builder = new SearchSourceBuilder();int[] cityids = new int[]{1,2,3};TermsQueryBuilder termsQuery = QueryBuilders.termsQuery("cityid",cityids );builder.query(termsQuery);request.source(builder);SearchResponse response = porosClient.search(request, RequestOptions.DEFAULT);System.out.printf("结果为:" + response);}
4.2 范围查询(Range Query)
-
HTTP 语法
代码块
Shell
#http请求curl -X Get /shop/_doc{"from":0,"size":10,"query":{"range":{"avgprice":{"from":20,"to":90,"include_lower":true,"include_upper":false,"boost":1.0}}}} -
Java API 语法
代码块
Java
SearchRequest request = new SearchRequest("shop");SearchSourceBuilder builder = new SearchSourceBuilder();RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("avgprice").from(20,true).to(90,false);builder.query(rangeQueryBuilder);request.source(builder);builder.from(0);builder.size(10);System.out.printf("request:"+request);SearchResponse response = porosClient.search(request,RequestOptions.DEFAULT);System.out.printf("\nresponse:"+response);
4.3 布尔查询(Bool Query)
bool查询ES上通过must,shoud,mustnot来描述and/or/not等查询逻辑,在bool查询里还有一个filter,filer子句:要求查询的doc必须要满足filter的query,与must的区别是:filter不参与相关分计算,且可以缓存,如果我们不想用相关性分计算,直接用filter替换must,性能会有一定的提升。
-
HTTP语法
代码块
Shell
#select * from tableA where (columnA='' and columnB='') and (columnA='' or columnB='')#http请求curl -X Get /shop/_doc{"from":0,"size":10,"query":{"bool":{"filter":[{"term":{"address":{"value":"kfc","boost":1.0}}}],"should":[{"term":{"cityid":{"value":1,"boost":1.0}}}],"adjust_pure_negative":true,"boost":1.0}}} -
Java API 语法
代码块
Shell
SearchRequest request = new SearchRequest("shop");SearchSourceBuilder builder = new SearchSourceBuilder();BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();TermQueryBuilder termQuery = QueryBuilders.termQuery("address","kfc");boolQuery.should(QueryBuilders.termQuery("cityid",1));boolQuery.filter(termQuery);builder.query(boolQuery);request.source(builder);builder.from(0);builder.size(10);System.out.printf("request:"+request);SearchResponse response = porosClient.search(request,RequestOptions.DEFAULT);System.out.printf("\nresponse:"+response);
4.4 Match查询(Match Query)
match查询是ES里面的最基础的全文相关性查询,它既能处理全文字段,又能处理精确字段。match查询会有多个参数,分别解释各参数:
①operator:针对需要匹配的文本,通过分词器分析,将matchquery转化成term query,那么分词器分词后的这些term query之间的关系是什么呢?在ES中默认是or,如果想定义成and,需要修改operator,举例:输入"full text"进行分词匹配,变成term:full和term:text,进行召回,会把域字段中只要存在full或者text 的文档召回,如果你想字段中必须同时出现full和text,那么需要修改operator 为and。
②analyzer:指在对查询文本分析时的分析器,如果没有指定则会使用字段mapping 时指定的分析器,如果字段在 mapping 时也没有明显指定,则会使用默认的 search analyzer。
③lenient:默认值是 false , 表示用来在查询时如果数据类型不匹配且无法转换时会报错。如果设置成 true 会忽略错误。
④fuzzniess:类似FuzzyQuery中的fuzzniess,指定的编辑距离可以是0,1,2,Auto,Auto是按照给定的查询term自动适配编辑距离,如果term的长度<=3(lowDistance),则编辑距离是0,3<term长度<=6(highDistance),编辑距离是1,长度>6(highDistance)则是2,如果需要强烈建议使用Auto,默认为0,即进行精确查找。
⑤prefix_length:指定匹配的字符串前缀至少prefixLength个字符要一致,才进行后续编辑距离计算,prefix_length 必须结合 fuzziness 参数使用
⑥zero_terms_query:可选值为ZeroTermsQuery.NONE,ZeroTermsQuery.*ALL, *默认值为ZeroTermsQuery.NONE,ZeroTermsQuery.NONE:不搜索停用词;ZeroTermsQuery.ALL:可以搜索停用词 。
⑦autoGenerateSynonymsPhraseQuery:是否产生同义词查询,默认是true。
一个简单的match示例:
-
HTTP 语法
代码块
Shell
#http请求curl -X Get /shop/_doc{"from":0,"size":10,"query":{"bool":{"filter":[{"match":{"address":{"query":"kfc food","operator":"OR","fuzziness":"0","prefix_length":1,"max_expansions":50,"fuzzy_transpositions":true,"lenient":false,"zero_terms_query":"ALL","auto_generate_synonyms_phrase_query":true,"boost":1.0}}}],"adjust_pure_negative":true,"boost":1.0}}} -
Java API语法
代码块
Java
SearchRequest request = new SearchRequest("shop");SearchSourceBuilder builder = new SearchSourceBuilder();BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("address","kfc food").lenient(false).fuzziness(0).prefixLength(1).zeroTermsQuery(MatchQuery.ZeroTermsQuery.ALL).autoGenerateSynonymsPhraseQuery(true);boolQuery.filter(matchQuery);builder.query(boolQuery);request.source(builder);builder.from(0);builder.size(10);System.out.printf("request:"+request);SearchResponse response = porosClient.search(request,RequestOptions.DEFAULT);System.out.printf("\nresponse:"+response);
4.5 主键查询(Ids Query)
输入数据记录主键_id,返回对应的数据。
-
HTTP 语法
代码块
Shell
#http请求curl -X Get /shop/_doc{"from":0,"size":10,"query":{"ids":{"values":["1001","1000"],"boost":1.0}}} -
Java API语法
代码块
Shell
SearchRequest request = new SearchRequest("shop");SearchSourceBuilder builder = new SearchSourceBuilder();IdsQueryBuilder idsQueryBuilder = QueryBuilders.idsQuery().addIds("1000","1001");builder.query(idsQueryBuilder);request.source(builder);builder.from(0);builder.size(10);System.out.printf("request:"+request);SearchResponse response = porosClient.search(request,RequestOptions.DEFAULT);System.out.printf("\nresponse:"+response);
4.6 全局遍历查询(Scroll Query)
对于一些具有深度翻页的查询,甚至需要进行全局遍历的查询,虽然可以使用传统的from-size语法解决部分问题,但是带来的系统开销有时候是我们无法能接受的(大量GC,系统IO/CPU飙升等),ES提供了Scroll查询,让我们可以进行全局遍历查询,它类似mysql的游标查询,在执行完成后,一定要清除Scroll,否则,很容易出问题。 注:Scroll查询不适合实时查询,即在scroll执行过程中,新增或者修改的数据,在scroll中是感知不到的,另外不建议用户大量使用Scroll(太耗费内存)。
-
Java API语法
代码块
Java
SearchRequest request = new SearchRequest("shop");SearchSourceBuilder builder = new SearchSourceBuilder();//构造queryMatchAllQueryBuilder matchQuery = QueryBuilders.matchAllQuery();builder.query(matchQuery);builder.size(2);//设置每次scroll获取2个记录request.source(builder);//设置scrollid缓存的时间request.scroll(new Scroll(TimeValue.MINUS_ONE));SearchResponse response = porosClient.search(request, RequestOptions.DEFAULT);//对第一次获取的结果进行处理for (SearchHit hit : response.getHits().getHits()) {System.out.printf("hit:" + hit);}SearchScrollRequest scrollRequest = null;//对余下的数据进行游标遍历处理do {String scrollId = response.getScrollId();scrollRequest = new SearchScrollRequest(scrollId);//设置此次的保留时间scrollRequest.scroll(new Scroll(TimeValue.MINUS_ONE));response = porosClient.scroll(scrollRequest, RequestOptions.DEFAULT);for (SearchHit hit : response.getHits().getHits()) {System.out.printf("hit:" + hit);}}while (response.getHits().getHits().length != 0);//等到最后获取的结果数为0,即表示所有的结果全部遍历完成//scroll完成后,需要立即将缓存清除ClearScrollRequest clearScrollRequest = new ClearScrollRequest();clearScrollRequest.addScrollId(response.getScrollId());ClearScrollResponse clearScrollResponse = porosClient.clearScroll(clearScrollRequest,RequestOptions.DEFAULT);System.out.printf("\nresponse:" + clearScrollResponse.toString());
4.7 search after查询
在旧版本中,ES为深度分页有scroll search 的方式,官方的建议并不是用于实时的请求,因为每一个 scroll_id 不仅会占用大量的资源(特别是排序的请求),而且是生成的历史快照,对于数据的变更不会反映到快照上。这种方式往往用于非实时处理大量数据的情况,比如要进行数据迁移或者索引变更之类的。那么在实时情况下如果处理深度分页的问题呢?es 给出了 search_after 的方式,这是在 >= 5.0 版本才提供的功能。
基本思想: searchAfter的方式通过维护一个实时游标来避免scroll的缺点,它可以用于实时请求和高并发场景。
必须保证搜索排序字段是唯一的,才能保证每次检索的顺序是相同的。
每一次请求都会为每一个文档返回一个包含sort排序值的数组。这些sort排序值可以被用于 search_after 参数里以便抓取下一页的数。
第一次查询
GET twitter/_search
{
"size": 2,
"query": {
"match": {
"city": "北京"
}
},
"sort": [
{
"DOB": {
"order": "asc"
}
},
{
"user.keyword": {
"order": "asc"
}
}
]
}
查询结果
{
"took" : 29,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 5,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_score" : null,
"_source" : {
"user" : "双榆树-张三",
"DOB" : "1980-01-01",
"message" : "今儿天气不错啊,出去转转去",
"uid" : 2,
"age" : 20,
"city" : "北京",
"province" : "北京",
"country" : "中国",
"address" : "中国北京市海淀区",
"location" : {
"lat" : "39.970718",
"lon" : "116.325747"
}
},
"sort" : [
315532800000,
"双榆树-张三"
]
},
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "2",
"_score" : null,
"_source" : {
"user" : "东城区-老刘",
"DOB" : "1981-01-01",
"message" : "出发,下一站云南!",
"uid" : 3,
"age" : 30,
"city" : "北京",
"province" : "北京",
"country" : "中国",
"address" : "中国北京市东城区台基厂三条3号",
"location" : {
"lat" : "39.904313",
"lon" : "116.412754"
}
},
"sort" : [
347155200000,
"东城区-老刘"
]
}
]
}
}
第二次查询
GET twitter/_search
{
"size": 2,
"query": {
"match": {
"city": "北京"
}
},
"search_after": [
347155200000,
"东城区-老刘"
],
"sort": [
{
"DOB": {
"order": "asc"
}
},
{
"user.keyword": {
"order": "asc"
}
}
]
}
5 搜索原理
5.1 索引建立
5.1.1 数据类型
搜索的前提是索引已经建立好,ES中的数据分为2类
- 精确值:如id,ip等,精确值只能精确匹配,适用于term查询,查询的时候是根据二进制来比较
- 全文:指文本内容,比如日志,邮件内容,url等,适用于match查询,只能查出看起来像的结果
以下对五条doc建立索引
Name Age Address
- Alan 33 West Street Ca USA
- Alice 13 East Street La USA
- Brad 19 Suzhou JiangSu China
- Alice 15 Nanjing JiangSu China
- Alan 11 Changning Shanghai China
5.1.2 索引建立流程
索引结构如下:
5.2 执行搜索
- 如果是精确值搜索,比如搜索Id为20002,直接去正排和倒排索引中查找匹配的文档
- 如果是全文查询,则需要先对检索内容进行分析,产生token词条,再根据token词条去正排和倒排索引中匹配相应的文档
5.3 搜索类型
5.3.1 Query then fetch
如果你搜索时,没有指定搜索方式,就是使用的这种搜索方式。这种搜索方式,大概分两个步骤,第一步,先向所有的shard发出请求,各分片只返回排序和排名相关的信息(注意,不包括文档document),然后按照各分片返回的分数进行重新排序和排名,取前size个文档。
然后进行第二步,去相关的shard取document。这种方式返回的document与用户要求的size是相等的。
-
Query阶段:得到目标结果对应的doc Id和排序信息,并且做聚合
在初始 查询阶段 时, 查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的 优先队列。
优先队列
一个 优先队列 仅仅是一个存有 top-n 匹配文档的有序列表。优先队列的大小取决于分页参数 from 和 size 。例如,如下搜索请求将需要足够大的优先队列来放入100条文档。
GET /_search { "from": 90, "size": 10 }
查询阶段包含以下三个步骤:
- 客户端发送一个 search 请求到 Node 3 , Node 3 会创建一个大小为 from + size 的空优先队列。
- Node 3 将查询请求转发到索引的每个主分片或副本分片中。每个分片在本地执行查询并添加结果到大小为 from + size 的本地有序优先队列中。
- 每个分片返回各自优先队列中所有文档的 ID 和排序值给协调节点,也就是 Node 3 ,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。
- Fetch阶段:根据doc Id列表查找对应的数据内容
分布式阶段由以下步骤构成:
- 协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。
- 每个分片加载并 丰富 文档,如果有需要的话,接着返回文档给协调节点。
- 一旦所有的文档都被取回了,协调节点返回结果给客户端。
5.3.2 Query and fetch
向索引的所有分片(shard)都发出查询请求,各分片返回的时候把查询时指定的size元素文档(document)和计算后的排名信息一起返回。这种搜索方式是最快的。因为相比下面的几种搜索方式,这种查询方法只需要去shard查询一次。但是各个shard返回的结果的数量之和可能是用户要求的size的n倍。
5.3.3 深分页(Deep Pagination)
我们可以假设在一个有 5 个主分片的索引中搜索。 当我们请求结果的第一页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 50 个结果排序得到全部结果的前 10 个。
现在假设我们请求第 1000 页—结果从 10001 到 10010 。所有都以相同的方式工作除了每个分片不得不产生前10010个结果以外。 然后协调节点对全部 50050 个结果排序最后丢弃掉这些结果中的 50040 个结果。可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。这就是 web 搜索引擎对任何查询都不要返回超过 10000 个结果的原因。
6 分片原理
提交一个新的Document的过程原理:
倒排索引被写入磁盘后是 不可改变 的:它永远不会修改。所以,新增一个文档时,要想让一个新的文档能够被搜索,你需要动态更新索引。通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到—从最早的开始—查询完后再对结果进行合并。Elasticsearch 基于 Lucene, 这个 java 库引入了 按段搜索 的概念。 每一 段 本身都是一个倒排索引, 但 索引 在 Lucene 中除表示所有 段 的集合外, 还增加了 提交点 的概念 — 一个列出了所有已知段的文件。
第一步:先提交到内存索引缓冲区,此时还不能被检索。并将变更插入到Translog日志中。
第二步:缓冲区的内容每秒会自动被写入或打开到一个新的段中(一个可被搜索的段中,文件系统),但还没有进行提交。此过程称为Refresh操作。刷新(refresh)完成后, 缓存被清空但是事务日志不会
写入和打开一个新段的轻量的过程叫做 refresh 。 默认情况下每个分片会每秒自动刷新一次。 这就是为什么我们说 Elasticsearch 是 近 实时搜索: 文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。
第三步:每隔一段时间— translog 变得越来越大—索引被刷新(flush);一个新的 translog 被创建,并且一个全量提交被执行。
- 所有在内存缓冲区的文档都被写入一个新的段。
- 缓冲区被清空。
- 一个提交点被写入硬盘。
- 文件系统缓存通过 fsync 被刷新(flush)。
- 老的 translog 被删除。
当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。
分片每30分钟被自动刷新(flush),或者在 translog 太大的时候也会刷新
7 重新索引
7.1 重新索引
尽管可以增加新的字段到类型中,但是不能添加新的分析器或者对现有的字段做改动。 如果你那么做的话,结果就是那些已经被索引的数据就不正确, 搜索也不能正常工作。
对现有数据的这类改变最简单的办法就是重新索引:用新的设置创建新的索引并把文档从旧的索引复制到新的索引。
字段 _source 的一个优点是在Elasticsearch中已经有整个文档。你不必从源数据中重建索引,而且那样通常比较慢。
为了有效的重新索引所有在旧的索引中的文档,用 scroll 从旧的索引检索批量文档 , 然后用 bulk API 把文档推送到新的索引中。
从Elasticsearch v2.3.0开始, Reindex API 被引入。它能够对文档重建索引而不需要任何插件或外部工具。
7.2 索引别名:小功能,大作用!!
做好准备:在你的应用中使用别名而不是索引名。然后你就可以在任何时候重建索引
一个别名可以指向多个索引,所以我们在添加别名到新索引的同时必须从旧的索引中删除它。这个操作需要原子化,这意味着我们需要使用 _aliases 操作:
代码块
curl -X POST "localhost:9200/_aliases?pretty" -H 'Content-Type: application/json' -d'
{
"actions": [
{ "remove": { "index": "my_index_v1", "alias": "my_index" }},
{ "add": { "index": "my_index_v2", "alias": "my_index" }}
]
}
'
这样,你的应用就可以在零停机的情况下从旧索引迁移到新索引了!
8 参考文档
主分片的数目在索引创建时就已经确定了下来。实际上,这个数目定义了这个索引能够 存储 的最大数据量。(实际大小取决于你的数据、硬件和使用场景。) 但是,读操作——搜索和返回数据——可以同时被主分片 或 副本分片所处理,所以当你拥有越多的副本分片时,也将拥有越高的吞吐量。