elasticsearch-4-复杂检索

Query-string 搜索通过命令非常方便地进行临时性的即席搜索 ,但它有自身的局限性(参见 轻量 搜索 )。Elasticsearch 提供一个丰富灵活的查询语言叫做 查询表达式 , 它支持构建更加复杂和健壮的查询。
领域特定语言 (DSL), 指定了使用一个 JSON 请求。我们可以像这样重写之前的查询所有 Smith 的搜索 :
GET /megacorp/employee/_search
{
“query” : {
“match” : {
“last_name” : “Smith”
}
}
}
View in Sense
返回结果与之前的查询一样,但还是可以看到有一些变化。其中之一是,不再使用 query-string 参数,而是一个请求体替代。这个请求使用 JSON 构造,并使用了一个 match 查询(属于查询类型之一,后续将会了解)。

更复杂的搜索

现在尝试下更复杂的搜索。 同样搜索姓氏为 Smith 的雇员,但这次我们只需要年龄大于 30 的。查询需要稍作调整,使用过滤器 filter ,它支持高效地执行一个结构化查询。

GET /megacorp/employee/_search {     "query" : {         "bool": {             "must": {                 "match" : {                     "last_name" : "smith"                   }             },             "filter": {                 "range" : {                     "age" : { "gt" : 30 }                   }             }         }     } }

这部分与我们之前使用的 match 查询 一样。
这部分是一个 range 过滤器 , 它能找到年龄大于 30 的文档,其中 gt 表示_大于(_great than)。
目前无需太多担心语法问题,后续会更详细地介绍。只需明确我们添加了一个 过滤器 用于执行一个范围查询,并复用之前的 match 查询。现在结果只返回了一个雇员,叫 Jane Smith,32 岁。

{    ...    "hits": {       "total":      1,       "max_score":  0.30685282,       "hits": [          {             ...             "_source": {                "first_name":  "Jane",                "last_name":   "Smith",                "age":         32,                "about":       "I like to collect rock albums",                "interests": [ "music" ]             }          }       ]    } } 

bool简单介绍

首先,简单介绍下bool,它是一种复合查询方式,
(参考:https://www.elastic.co/guide/en/elasticsearch/reference/6.0/query-dsl-bool-query.html
与匹配其他查询的布尔组合的文档相匹配的查询。bool查询映射到Lucene BooleanQuery。它是使用一个或多个布尔子句构建的,每个子句都有一个类型化的事件。发生的类型是:

发生 描述
must 该条款(查询)必须出现在匹配的文件,并将有助于得分。
filter 子句(查询)必须出现在匹配的文档中。然而不像 must查询的分数将被忽略。Filter子句在过滤器上下文中执行,这意味着评分被忽略,子句被考虑用于高速缓存。

should 子句(查询)应该出现在匹配的文档中。如果 bool查询位于查询上下文中并且具有mustorfilter子句,那么bool即使没有 should查询匹配,文档也将匹配查询。在这种情况下,这些条款仅用于影响分数。如果bool查询是过滤器上下文 或者两者都不存在,must或者filter至少有一个should查询必须与文档相匹配才能与bool查询匹配。这种行为可以通过设置minimum_should_match参数来显式控制 。

must_not 子句(查询)不能出现在匹配的文档中。子句在过滤器上下文中执行,意味着评分被忽略,子句被考虑用于高速缓存。因为计分被忽略,0所有文件的分数被返回。

即,must:必须匹配,filter:匹配的结果过滤,should:至少有一个 must_not:不能匹配

Client程序演示bool查询

term

增加一个方法:

/*      * 简单运用一个bool查询,查询姓Smith且年龄大于的员工      * 查询姓Smith的员工      * 过滤为大于30岁的      */     private static void findEmployeeByAgeAndName(Client client) {         SearchRequestBuilder request = client.prepareSearch("megacorp1")                 .setTypes("employee1")                 .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)                  .setQuery(QueryBuilders.boolQuery().must(termQuery("last_name","Smith")).filter(rangeQuery("age").gt(30))); //      SearchResponse response = request.get();         printResponseHits(request.get());     }  

封装查看结果方法:

//查看结果     private static void printResponseHits(SearchResponse response) {         SearchHits searchHits = response.getHits();         Iterator<SearchHit> iterator = searchHits.iterator();         while(iterator.hasNext()) {             SearchHit hit = iterator.next();             String index = hit.getIndex();             String type = hit.getType();             String id = hit.getId();             float score = hit.getScore();             System.out.println("index="+index+" type="+type+" id="+id+" score="+score+" source-->"+hit.getSourceAsString());         }     }  

Main方法中增加调用

// 5.查询姓smith的雇员,过滤过滤器查询示例 bool查询 findEmployeeByAgeAndName(client);

结果显示:
index=megacorp1 type=employee1 id=2 score=1.2809339 source–>{“first_name”:”Jane”,”last_name”:”Smith”,”age”:”32”,”about”:”I like to collect rock albums”,”interests”:[“music”]}
有兴趣的可以自己debug到request查看bool的请求

Head插件示例

Elasticsearch(四)elasticsearch复杂检索

match

我们可以现在用match来写下:
将刚才的例子改为如下

SearchRequestBuilder request = client.prepareSearch("megacorp1")                 .setTypes("employee1")                 .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)                  .setQuery(QueryBuilders.boolQuery().must(matchQuery("last_name","Smith")).filter(rangeQuery("age").gt(30))); //      SearchResponse response = request.get();         printResponseHits(request.get());  

再次调用此方法返回结果为:
index=megacorp1 type=employee1 id=2 score=1.3862944 source–>{“first_name”:”Jane”,”last_name”:”Smith”,”age”:”32”,”about”:”I like to collect rock albums”,”interests”:[“music”]}

head插件示例

Elasticsearch(四)elasticsearch复杂检索
我们的结果没有区别,因为这里我们的索引不会进行分词解析。
我们去可以之前可以分词解析的索引megacorp中实验以下:

term:rock climbing

Elasticsearch(四)elasticsearch复杂检索

Match:rock climbing

Elasticsearch(四)elasticsearch复杂检索
可以看出档案的结果根据相关性评分排序。整个都匹配的在第一个,匹配其中一个的在后面。
Elasticsearch 默认按照相关性得分排序,即每个文档跟查询的匹配程度。第一个最高得分的结果很明显:John Smith 的 about 属性清楚地写着 “rock climbing” 。
但为什么 Jane Smith 也作为结果返回了呢?原因是她的 about 属性里提到了 “rock” 。因为只有 “rock” 而没有 “climbing” ,所以她的相关性得分低于 John 的。
这是一个很好的案例,阐明了 Elasticsearch 如何 在 全文属性上搜索并返回相关性最强的结果。Elasticsearch中的 相关性 概念非常重要,也是完全区别于传统关系型数据库的一个概念,数据库中的一条记录要么匹配要么不匹配。

至于之前的last_name为何查不出,还是一个疑问。
尝试将lastname增加这个没有下划线的字段,term依旧没有查出来。
尝试将内容改为Smith Smith中间空格形式也查不出来term
欢迎解惑。

短语搜索

找出一个属性中的独立单词是没有问题的,但有时候想要精确匹配一系列单词或者短语 。 比如, 我们想执行这样一个查询,仅匹配同时包含 “rock” 和 “climbing” ,并且 二者以短语 “rock climbing” 的形式紧挨着的雇员记录。
为此对 match 查询稍作调整,使用一个叫做 match_phrase 的查询:

GET /megacorp/employee/_search {     "query" : {         "match_phrase" : {             "about" : "rock climbing"         }     } }

毫无悬念,返回结果仅有 John Smith 的文档。

{    ...    "hits": {       "total":      1,       "max_score":  0.23013961,       "hits": [          {             ...             "_score":         0.23013961,             "_source": {                "first_name":  "John",                "last_name":   "Smith",                "age":         25,                "about":       "I love to go rock climbing",                "interests": [ "sports", "music" ]             }          }       ]    } } 

Client程序演示

增加一个方法

/**      * match phrase查询      * 仅匹配同时包含 “rock” 和 “climbing” ,并且 二者以短语 “rock climbing” 的形式紧挨着的雇员记录。      * @param client 客户端      * @param field 字段      * @param phrase 词语      */     private static void findEmployeesWithOneUniqueMatchPhrase(Client client, String field, String phrase) {         SearchRequestBuilder request = client.prepareSearch("megacorp")                 .setTypes("employee")                 .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)                  .setQuery(QueryBuilders.boolQuery().must(matchPhraseQuery(field, phrase)));         printResponseHits(request.get());     } 

Main方法中调用

// 6.match_phrase查询 仅匹配同时包含 “rock” 和 “climbing” ,并且 二者以短语 “rock climbing” 的形式紧挨着的雇员记录。             findEmployeesWithOneUniqueMatchPhrase(client,"about","rock climbing"); 

我增加了一些数据:
结果显示:
index=megacorp type=employee id=5 score=0.6449836 source–>{“first_name”:”John”,”last_name”:”Smith1”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}
index=megacorp type=employee id=8 score=0.6449836 source–>{“first_name”:”John”,”last_name”:”蜂蜜柚子”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}
index=megacorp type=employee id=9 score=0.6449836 source–>{“first_name”:”John”,”last_name”:”蜂蜜”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}
index=megacorp type=employee id=10 score=0.6449836 source–>{“first_name”:”John”,”last_name”:”Smith Smith”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}
index=megacorp type=employee id=6 score=0.6449836 source–>{“first_name”:”John”,”last_name”:”Smith 1”,”age”:26,”about”:”I love to go rock climbing”,”interests”:[“sports”,”art”]}
index=megacorp type=employee id=1 score=0.6449836 source–>{“first_name”:”John”,”last_name”:”Smith”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}
index=megacorp type=employee id=7 score=0.6449836 source–>{“first_name”:”John”,”last_name”:”蜂蜜柚子蜂蜜”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}

可以看出结果完全符合。

Head插件示例

{“query”:{“match_phrase”:{“about”:”rock climbing”}}}

抱歉图发不出!!!

elasticsearch轻量检索

一个 GET 是相当简单的,可以直接得到指定的文档。 现在尝试点儿稍微高级的功能,比如一个简单的搜索!

搜索所有雇员

第一个尝试的几乎是最简单的搜索了。我们使用下列请求来搜索所有雇员:

GET /megacorp/employee/_search

可以看到,我们仍然使用索引库 megacorp 以及类型 employee,但与指定一个文档 ID 不同,这次使用_search 。返回结果包括了所有三个文档,放在数组 hits 中。一个搜索默认返回十条结果。

{    "took":      6,    "timed_out": false,    "_shards": { ... },    "hits": {       "total":      3,       "max_score":  1,       "hits": [          {             "_index":         "megacorp",             "_type":          "employee",             "_id":            "3",             "_score":         1,             "_source": {                "first_name":  "Douglas",                "last_name":   "Fir",                "age":         35,                "about":       "I like to build cabinets",                "interests": [ "forestry" ]             }          },          {             "_index":         "megacorp",         ...

注意:返回结果不仅告知匹配了哪些文档,还包含了整个文档本身:显示搜索结果给最终用户所需的全部信息。

Client程序演示

增加一个方法:

/*      * GET /megacorp/employee/_search      * 返回的文档放在hit[]中      * SearchResponse response5 = client.prepareSearch(index1, index2)                 .setTypes(type1, type2)                 .setSearchType(SearchType.DFS_QUERY_THEN_FETCH) // 就写这个就好了,虽然Java API定义了额外的搜索类型QUERY_AND_FETCH和DFS_QUERY_AND_FETCH,但这些模式是内部优化,不应该由API的用户明确指定。                 .setQuery(QueryBuilders.termQuery("brandNameNew", 2))                 // Query                  .setPostFilter(QueryBuilders.rangeQuery("useYears").from(2).to(5))     // Filter                 .setFrom(0).setSize(60).setExplain(true)                 .get();         //所有的参数都是可选的,也就是说,最简单的可以这样写,代表查询整个集群         SearchResponse response6 = client.prepareSearch().get();      * 此方面知识来源于 Search API 搜索API允许执行搜索查询并取回匹配查询的搜索匹配。      * 它可以跨越一个或多个索引并跨越一个或多个类型执行。查询可以使用查询Java API提供。      *  took:是查询花费的时间,毫秒单位         time_out:标识查询是否超时         _shards:描述了查询分片的信息,查询了多少个分片、成功的分片数量、失败的分片数量等         hits:搜索的结果,total是全部的满足的文档数目,hits是返回的实际数目(默认是10)         _score是文档的分数信息,与排名相关度有关,参考各大搜索引擎的搜索结果,就容易理解。      * !!!搜索请求的主体是使用SearchSourceBuilder。      */     private static void getEmployeesByIndexAndType(Client client,String[] indics,String[] types) {         System.out.println("集群中查询索引为"+Arrays.deepToString(indics)+"和类型为"+Arrays.deepToString(types)+"的所有数据,开始查询...");         //查询         SearchResponse response = client.prepareSearch(indics)                 .setTypes(types)                 .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)                 .get();         //分析查询结果 -- took暂不说明,它每次是变化的         // timed_out         boolean isTimedOut = response.isTimedOut();         System.out.println("timed_out:"+isTimedOut);         // _shards         int totalShards = response.getTotalShards();         int successfulShards = response.getSuccessfulShards();         int failedShards = response.getFailedShards();         System.out.println("_shards:{ total="+totalShards+" successful="+successfulShards+" failed="+failedShards+"}");         // 文档在hit数组中,更多方法使用请看API中SearchHits         SearchHits searchHits = response.getHits();         Iterator<SearchHit> iterator = searchHits.iterator();         while(iterator.hasNext()) {             SearchHit hit = iterator.next();             String index = hit.getIndex();             String type = hit.getType();             String id = hit.getId();             float score = hit.getScore();             System.out.println("index="+index+" type="+type+" id="+id+" score="+score+" source-->"+hit.getSourceAsString());         }         System.out.println("查询结束...");     } 

Main中增加一个调用(main方法见之前文档,其实只要获得client连接即可)

// 3.查询所有雇员文档  _search  getEmployeesByIndexAndType(client,new String[] {"megacorp"},new String[] {"employee"}); 

运行结果显示:
集群中查询索引为[megacorp]和类型为[employee]的所有数据,开始查询…
timed_out:false
_shards:{ total=5 successful=5 failed=0}
index=megacorp type=employee id=2 score=1.0 source–>{“first_name”:”Jane”,”last_name”:”Smith”,”age”:”32”,”about”:”I like to collect rock albums”,”interests”:[“music”]}
index=megacorp type=employee id=4 score=1.0 source–>{“first_name”:”Douglas1”,”last_name”:”Fir”,”age”:35,”about”:”I like to build cabinets”,”interests”:[“forestry”]}
index=megacorp type=employee id=1 score=1.0 source–>{“first_name”:”John”,”last_name”:”Smith”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}
index=megacorp type=employee id=3 score=1.0 source–>{“first_name”:”Douglas”,”last_name”:”Fir”,”age”:35,”about”:”I like to build cabinets”,”interests”:[“forestry”]}
查询结束…

Head插件示例

Elasticsearch(三)elasticsearch轻量检索

搜索姓中为smith的雇员

接下来,尝试下搜索姓氏为 Smith 的雇员。为此,我们将使用一个 高亮 搜索,很容易通过命令行完成。这个方法一般涉及到一个 查询字符串 (query-string) 搜索,因为我们通过一个URL参数来传递查询信息给搜索接口:
GET /megacorp/employee/_search?q=last_name:Smith
我们仍然在请求路径中使用 _search 端点,并将查询本身赋值给参数 q= 。返回结果给出了所有的 Smith:

{    ...    "hits": {       "total":      2,       "max_score":  0.30685282,       "hits": [          {             ...             "_source": {                "first_name":  "John",                "last_name":   "Smith",                "age":         25,                "about":       "I love to go rock climbing",                "interests": [ "sports", "music" ]             }          },          {             ...             "_source": {                "first_name":  "Jane",                "last_name":   "Smith",                "age":         32,                "about":       "I like to collect rock albums",                "interests": [ "music" ]             }          }       ]    } } 

Client程序演示

我们引入
import static org.elasticsearch.index.query.QueryBuilders.*;
类似第一个例子使用即可。
增加一个方法:

/*      * 根据一个字段的值查询        * GET /megacorp/employee/_search?q=last_name:Smith      *       * QueryBuilders的term查询 ,表全部匹配,不进行分词解析      */     private static void getEmployeesByFieldEqual(Client client, String field, String text) {         SearchResponse response = client.prepareSearch("megacorp")                 .setTypes("employee")                 .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)                 .setQuery(QueryBuilders.termQuery(field, text))                 .get();          //查看结果         SearchHits searchHits = response.getHits();         Iterator<SearchHit> iterator = searchHits.iterator();         while(iterator.hasNext()) {             SearchHit hit = iterator.next();             String index = hit.getIndex();             String type = hit.getType();             String id = hit.getId();             float score = hit.getScore();             System.out.println("index="+index+" type="+type+" id="+id+" score="+score+" source-->"+hit.getSourceAsString());         }     } 

主方法中增加调用:

// 4.查询姓smith的雇员 getEmployeesByFieldEqual(client,"last_name","Smith"); 

结果并没有显示。。。
我们先测试它运作吗?
getEmployeesByFieldEqual(client,”about”,”love”);
结果显示:
index=megacorp type=employee id=5 score=0.7884338 source–>{“first_name”:”John”,”last_name”:”Smith1”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}
index=megacorp type=employee id=1 score=0.7884338 source–>{“first_name”:”John”,”last_name”:”Smith”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}

即它把包含的也显示出来了,原来about这个字段是text类型的,也就是默认分析的,(analyzed:默认选项,以标准的全文索引方式,分析字符串,完成索引。)表示他将被分析器分析,也就是说如果一个文档的about字段是I love to go rock climbing,那么将被分析成[I,love,to,go,rock,climbing],如图
Elasticsearch(三)elasticsearch轻量检索
在匹配love词时只要about字段中有love这个词就会被匹配。所以会出现这个结果。参考:
https://www.elastic.co/guide/en/elasticsearch/reference/6.0/query-dsl-term-query.html

如果我们不想要这样的结果:

可以将此索引的类型改变成不被分析的类型。
(not_analyzed 索引时不进行分词分析,确切值形式)
查看一下索引mapping的内容:
Elasticsearch(三)elasticsearch轻量检索
发现他们都是默认text类型的。
我们已经存在的索引是不可以更改它的映射的,(为了使数据可查询,就需要知道每一个field包含的数据的数据类型以及它是如何索引的。如果你将一个field的数据类型从string修改为date,这这个字段所包含的数据将全部无用。你需要重建创建索引了!这条规则不仅仅针对es,任何一个可用于查询的数据库系统都是这样。如果不用索引,就是为灵活性牺牲速度。参考:http://blog.csdn.net/jingkyks/article/details/41513063
对于存在的索引,只有新字段出现时,Elasticsearch才会自动进行处理。如果确实需要修改映射,那么就使用reindex,采用重新导入数据的方式完成。
(参考:http://blog.csdn.net/u010994304/article/details/50454025
(如果想要执行重新导入的操作参考:
http://blog.csdn.net/jingkyks/article/details/41513063
http://blog.csdn.net/u010994304/article/details/50454025
http://blog.csdn.net/lengfeng92/article/details/38230521
http://www.cnblogs.com/Creator/p/3722408.html
所以要么建立的时候就将这个字段设置为不分析的字段(删除这个索引,重新增加)
要么重新导入数据

示例

现在我们举个栗子,重新新建一个索引,让他的映射都为no_analyzed
(你也可以先delete你现有的索引,我这里重建)
Elasticsearch(三)elasticsearch轻量检索
(参照原来的索引写)
(数据类型参考https://www.cnblogs.com/xing901022/p/5471419.html
放入和原来相同的数据

Elasticsearch(三)elasticsearch轻量检索
再调用刚才的方法:
getEmployeesByFieldEqual(client,”about”,”love”);
没有返回任何数据
调用:
getEmployeesByFieldEqual(client,”about”,”I love to go rock climbing”);
结果显示2条数据:
index=megacorp1 type=employee1 id=5 score=0.87546873 source–>{“first_name”:”John”,”last_name”:”Smith”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}
index=megacorp1 type=employee1 id=1 score=0.87546873 source–>{“first_name”:”John”,”last_name”:”Smith1”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}

增加一条如图的数据:
Elasticsearch(三)elasticsearch轻量检索
调用:
getEmployeesByFieldEqual(client,”last_name”,”Smith 1”);
显示:
index=megacorp1 type=employee1 id=6 score=1.5404451 source–>{“first_name”:”John”,”last_name”:”Smith 1”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}

调用:
getEmployeesByFieldEqual(client,”last_name”,”Smith”);
显示:
index=megacorp1 type=employee1 id=5 score=1.0296195 source–>{“first_name”:”John”,”last_name”:”Smith”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}
index=megacorp1 type=employee1 id=2 score=1.0296195 source–>{“first_name”:”Jane”,”last_name”:”Smith”,”age”:”32”,”about”:”I like to collect rock albums”,”interests”:[“music”]}
当然也可以用querystring来写

SearchRequestBuilder request = client.prepareSearch("megacorp")                 .setTypes("employee")         .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)                 .setQuery(new QueryStringQueryBuilder(text).field(field));   SearchResponse response = request.get(); 

问题解决!

疑问

至于为什么改了它的是否分析后就能够查询到了,这一点很迷惑,未找到原因。他们的tokens完全相同
Elasticsearch(三)elasticsearch轻量检索
请求的request串也完全相同。
{
“query” : {
“term” : {
“last_name” : {
“value” : “Smith”,
“boost” : 1.0
}
}
}
}
这个方法是运作的, last_name没有匹配任何值
{“took”:3,”timed_out”:false,”_shards”:{“total”:5,”successful”:5,”failed”:0},”hits”:{“total”:0,”max_score”:null,”hits”:[]}}
欢迎解惑。。。

Head插件示例

Elasticsearch(三)elasticsearch轻量检索

Elasticsearch-2-索引数据与简单检索获取doc

本文参考elasticsearch权威指南。
是一个学习笔记,按照里面的示例进行学习,在此记录跟踪。

与elasticsearch交互的两种方式

JAVA API

如果你和我一样,使用java,在代码中你可以使用 Elasticsearch 内置的两个客户端:

节点客户端(Node client)

节点客户端作为一个非数据节点加入到本地集群中。换句话说,它本身不保存任何数据,但是它知道数据在集群中的哪个节点中,并且可以把请求转发到正确的节点。

传输客户端(Transport client)

轻量级的传输客户端可以可以将请求发送到远程集群。它本身不加入集群,但是它可以将请求转发到集群中的一个节点上。

两个 Java 客户端都是通过 9300 端口并使用本地 Elasticsearch 传输 协议和集群交互。集群中的节点通过端口 9300 彼此通信。如果这个端口没有打开,节点将无法形成一个集群。
PS:Java 客户端作为节点必须和 Elasticsearch 有相同的 主要 版本;否则,它们之前将无法互相理解。即客户端使用的版本要与服务器相对应。

推荐使用传输客户端,下面是一个连接客户端的小例子:

Settings settings = Settings.builder().put("cluster.name", "bdrg")//集群名称 .put("client.transport.sniff", true)//sniff功能             .put("client.transport.ping_timeout","100s")//连接超时 .build();  TransportClient client = new PreBuiltTransportClient(settings).addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"),9300));//连接IP和PORT 

RESTFUL API WITH JSON OVER HTTP

所有其他语言可以使用 RESTful API 通过端口 9200 和 Elasticsearch 进行通信,你可以用你最喜爱的 web 客户端访问 Elasticsearch 。事实上,正如你所看到的,你甚至可以使用 curl 命令来和 Elasticsearch 交互。
Elasticsearch 为以下语言提供了官方客户端 –Groovy、JavaScript、.NET、 PHP、 Perl、 Python 和 Ruby–还有很多社区提供的客户端和插件,所有这些都可以在 Elasticsearch Clients 中找到。

一个 Elasticsearch 请求和任何 HTTP 请求一样由若干相同的部件组成:

curl -X<VERB> '<PROTOCOL>://<HOST>:<PORT>/<PATH>?<QUERY_STRING>' -d '<BODY>'

被 < > 标记的部件:
VERB :
适当的 HTTP 方法 或 谓词 : GETPOSTPUTHEAD 或者 DELETE
PROTOCOL:
http 或者 https(如果你在 Elasticsearch 前面有一个https 代理)
HOST :
Elasticsearch 集群中任意节点的主机名,或者用 localhost 代表本地机器上的节点。
PORT :
运行 Elasticsearch HTTP 服务的端口号,默认是 9200 。
PATH :
API 的终端路径(例如 _count 将返回集群中文档数量)。Path 可能包含多个组件,例如:_cluster/stats 和 _nodes/stats/jvm 。
QUERY_STRING :
任意可选的查询字符串参数 (例如 ?pretty 将格式化地输出 JSON 返回值,使其更容易阅读)
BODY :
一个 JSON 格式的请求体 (如果请求需要的话)

例如,计算集群中文档的数量,我们可以用这个:

curl -XGET 'http://localhost:9200/_count?pretty' -d ' {     "query": {         "match_all": {}     } } 

Elasticsearch 返回一个 HTTP 状态码(例如:200 OK)和(除HEAD请求)一个 JSON 格式的返回值。前面的curl 请求将返回一个像下面一样的 JSON 体:

{     "count" : 0,     "_shards" : {         "total" : 5,         "successful" : 5,         "failed" : 0     } } 

在返回结果中没有看到 HTTP 头信息是因为我们没有要求curl显示它们。想要看到头信息,需要结合-i 参数来使用 curl 命令:

curl -i -XGET 'localhost:9200/'

面向文档

假如一个User对象其中包含了另一个地址对象,关系型数据库不能放在一个地方,需要放在另一张表中。elasticsearch却吧User包含的地址也放在User当中,因为它根据文档来索引数据。

对文档进行索引、检索、排序和过滤–而不是对行列数据。这是一种完全不同的思考数据的方式,也是 Elasticsearch 能支持复杂全文检索的原因。

例如一个JSON 文档,它代表了一个 user 对象:

{     "email":      "john@smith.com",     "first_name": "John",     "last_name":  "Smith",     "info": {         "bio":         "Eco-warrior and defender of the weak",         "age":         25,         "interests": [ "dolphins", "whales" ]     },     "join_date": "2014/05/01" } 

虽然原始的 user 对象很复杂,但这个对象的结构和含义在 JSON 版本中都得到了体现和保留。在 Elasticsearch 中将对象转化为 JSON 并做索引要比在一个扁平的表结构中做相同的事情简单的多。

几乎所有的语言都有相应的模块可以将任意的数据结构或对象 转化成 JSON 格式,只是细节各不相同。具体请查看 serialization 或者 marshalling 这两个 处理 JSON 的模块。官方 Elasticsearch 客户端 自动为您提供 JSON 转化。

下面是一个elasticsearch自动提供json转化的示例:

XContentBuilder builder = XContentFactory.jsonBuilder()                 .startObject()                     .field("vehicleId", "BCAAAD0005")                 .endObject(); String json4 =  builder.string(); System.out.println(json4); 

显示如下:

{"vehicleId":"BCAAAD0005"}

接下来我们学习怎样操作数据,以一个雇员目录为例:
我们受雇于 Megacorp 公司,作为 HR 部门新的 “热爱无人机” (“We love our drones!”)激励项目的一部分,我们的任务是为此创建一个雇员目录。该目录应当能培养雇员认同感及支持实时、高效、动态协作,因此有一些业务需求:
• 支持包含多值标签、数值、以及全文本的数据
• 检索任一雇员的完整信息
• 允许结构化搜索,比如查询 30 岁以上的员工
• 允许简单的全文搜索以及较复杂的短语搜索
• 支持在匹配文档内容中高亮显示搜索片段
• 支持基于数据创建和管理分析仪表盘

创建雇员文档

也称索引雇员文档。
第一个业务需求就是存储雇员数据。 这将会以 雇员文档 的形式存储:一个文档代表一个雇员。存储数据到 Elasticsearch 的行为叫做 索引 ,但在索引一个文档之前,需要确定将文档存储在哪里。
一个 Elasticsearch 集群可以 包含多个 索引(类比数据库) ,相应的每个索引可以包含多个 类型 (类比表)。 这些不同的类型存储着多个 文档 (类比数据),每个文档又有 多个 属性 (类比字段)。

对于雇员目录,我们将做如下操作:
• 每个雇员索引一个文档,包含该雇员的所有信息。
• 每个文档都将是 employee 类型 。
• 该类型位于 索引 megacorp 内。
• 该索引保存在我们的 Elasticsearch 集群中。
实践中这非常简单(尽管看起来有很多步骤),我们可以通过一条命令完成所有这些动作:

PUT /megacorp/employee/1
{
“first_name” : “John”,
“last_name” : “Smith”,
“age” : 25,
“about” : “I love to go rock climbing”,
“interests”: [ “sports”, “music” ]
}

注意,路径 /megacorp/employee/1 包含了三部分的信息:
megacorp 索引名称
employee 类型名称
1 特定雇员的ID
请求体 —— JSON 文档 —— 包含了这位员工的所有详细信息,他的名字叫 John Smith ,今年 25 岁,喜欢攀岩。

很简单!无需进行执行管理任务,如创建一个索引或指定每个属性的数据类型之类的,可以直接只索引一个文档。Elasticsearch 默认地完成其他一切,因此所有必需的管理任务都在后台使用默认设置完成。
进行下一步前,让我们增加更多的员工信息到目录中:
PUT /megacorp/employee/2
{
“first_name” : “Jane”,
“last_name” : “Smith”,
“age” : 32,
“about” : “I like to collect rock albums”,
“interests”: [ “music” ]
}

PUT /megacorp/employee/3
{
“first_name” : “Douglas”,
“last_name” : “Fir”,
“age” : 35,
“about”: “I like to build cabinets”,
“interests”: [ “forestry” ]
}

用客户端连接的方式完成以上操作的示例

POM文件

<dependencies>     <dependency>       <groupId>junit</groupId>       <artifactId>junit</artifactId>       <version>3.8.1</version>       <scope>test</scope>     </dependency>          <!-- <dependency>              <groupId>org.elasticsearch</groupId>              <artifactId>elasticsearch</artifactId>              <version>2.2.0</version>         </dependency> -->           <dependency>           <groupId>org.elasticsearch.client</groupId>           <artifactId>transport</artifactId>           <version>5.3.0</version>         </dependency>           <!-- logger for client -->         <dependency>             <groupId>org.apache.logging.log4j</groupId>             <artifactId>log4j-core</artifactId>             <version>2.9.1</version>         </dependency>          <dependency>             <groupId>com.fasterxml.jackson.core</groupId>             <artifactId>jackson-core</artifactId>             <version>2.8.6</version>         </dependency>      <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->     <dependency>         <groupId>com.fasterxml.jackson.core</groupId>         <artifactId>jackson-databind</artifactId>         <version>2.8.6</version>     </dependency>     <dependency>         <groupId>com.fasterxml.jackson.core</groupId>         <artifactId>jackson-annotations</artifactId>         <version>2.8.6</version>     </dependency>      <dependency>         <groupId>com.alibaba</groupId>         <artifactId>fastjson</artifactId>         <version>1.2.8</version>     </dependency>      </dependencies> 

其中如果你的elasticsearch服务器没有对应的客户端传输org.elasticsearch.client,请使用类似于2.0.0版本的依赖。

initData.java–客户端连接

public class InitialData {     private static Logger logger = (Logger) LogManager.getLogger(InitialData.class);     /**      * 获得连接      * @param clusterName 集群名称      * @param sniff 是否增加嗅探功能      * @param time 设定超时时间      * @param ip 连接IP      * @param port 连接port,传输端口一般都是9300      * @return      * @throws UnknownHostException       */     public static Client connect(String clusterName,boolean sniff,int time,String ip,int port) throws UnknownHostException {          //集群名称 -- 默认"elasticsearch"         String cluster = null;         if(null == clusterName || ("").equals(clusterName.trim())) {             cluster = "elasticsearch";         }else {             cluster = clusterName;         }          //是否增加嗅探功能         //连接超时时间 -- 最小5s         if(time < 5) {             time = 5;         }          Settings settings = Settings.builder().put("cluster.name", cluster)//集群名称                 .put("client.transport.sniff", sniff)//sniff功能                 .put("client.transport.ping_timeout",time+"s")//连接超时时限                 .build();         TransportClient client = new PreBuiltTransportClient(settings)                 .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(ip),port));//连接IP和PORT          logger.info("连接成功...");         return client;     } } 

ClientDemo2 .java–模拟雇员示例

public class ClientDemo2 {      /*      * 索引雇员文档,相当于我们传统数据库的insert操作      * 对于雇员目录,我们将做如下操作:          每个雇员索引一个文档,包含该雇员的所有信息。  --doc          每个文档都将是  employee 类型 。  --type          该类型位于 索引 megacorp 内。  --index          该索引保存在我们的 Elasticsearch 集群中。       */     private static void insertEmployee (Client client) throws Exception{         //1.插入第一个员工,员工id为1,插在当前集群下的megacorp索引(类比数据库)employee类型(类比表)下         IndexResponse response = client.prepareIndex("megacorp","employee","1")//index  type  id(表特定雇员)                 .setSource(XContentFactory.jsonBuilder()                             .startObject()                                 .field("first_name","John")                                 .field("last_name","Smith")                                 .field("age",25)                                 .field("about","I love to go rock climbing")                                 .array("interests", new String[] {"sports","music"})                             .endObject()).get();         //索引名称         String _index = response.getIndex();         //键入名称         String _type = response.getType();         //文件ID(生成与否)         String _id = response.getId();         //版本(如果这是您首次索引此文档,您将获得:1)--- 每次执行版本数都会+1         long _version = response.getVersion();         // status has stored current instance statement。         RestStatus status = response.status();         System.out.println("index:"+_index+";type:"+_type+";id:"+_id+";version:"+_version+";status:"+status);//         System.out.println(response.getResult().toString());//第一次执行是CREATED,同一个id第二次开始是UPDATED          //2.插入第二个员工         //你也可以手工写入自己的json         String json ="{"+                 ""first_name":"Jane","+                 ""last_name":"Smith","+                 ""age":"32","+                 ""about":"I like to collect rock albums","+                 ""interests":["music"]"+            "}";          IndexResponse response2 = client.prepareIndex("megacorp","employee","2")             .setSource(json,XContentType.JSON)             .get();          System.out.println(response2.getResult().toString());//CREATED          //3.插入第三个员工,不想获得结果可以直接调用         client.prepareIndex("megacorp","employee","3")//index  type  id(表特定雇员)                 .setSource(XContentFactory.jsonBuilder()                     .startObject()                         .field("first_name","Douglas")                         .field("last_name","Fir")                         .field("age",35)                         .field("about","I like to build cabinets")                         .array("interests", "forestry")                     .endObject()).get();     }       public static void main(String[] args) {          // 获得客户端连接         Client client = null;         try {             client = InitialData.connect("bdrg", true, 100, "127.0.0.1", 9300);               // 1.索引雇员文档             insertEmployee (client);         } catch (UnknownHostException e) {             System.out.println("服务器地址错误:"+e.getMessage());         } catch (Exception e) {             System.out.println("客户端操作错误:"+e.getMessage());             e.printStackTrace();         } finally {             // 关闭客户端             if(null != client) {                 client.close();             }         }       }   } 

可以多次执行,插入操作第一次会执行插入,第二次或以上会执行为更新操作。

结果显示:
no modules loaded
loaded plugin [org.elasticsearch.index.reindex.ReindexPlugin]
loaded plugin [org.elasticsearch.percolator.PercolatorPlugin]
loaded plugin [org.elasticsearch.script.mustache.MustachePlugin]
loaded plugin [org.elasticsearch.transport.Netty3Plugin]
loaded plugin [org.elasticsearch.transport.Netty4Plugin]
连接成功…
index:megacorp;type:employee;id:1;version:5;status:OK
UPDATED
UPDATED

Head插件操作示例

Elasticsearch(二)elasticsearch索引数据与简单检索GET一个文档

再次点击:

Elasticsearch(二)elasticsearch索引数据与简单检索GET一个文档

看我们索引中的数据:

Elasticsearch(二)elasticsearch索引数据与简单检索GET一个文档

我们的interets数据呢?

Elasticsearch(二)elasticsearch索引数据与简单检索GET一个文档

(还可以选择json格式的,只有table格式的出不来)
它没有表示成一个字段显示出来。也很符合逻辑。

简单检索一个文档

目前我们已经在 Elasticsearch 中存储了一些数据, 接下来就能专注于实现应用的业务需求了。第一个需求是可以检索到单个雇员的数据。
这在 Elasticsearch 中很简单。简单地执行 一个 HTTP GET 请求并指定文档的地址——索引库、类型和ID。 使用这三个信息可以返回原始的 JSON 文档:

GET /megacorp/employee/1

返回结果包含了文档的一些元数据,以及 _source 属性,内容是 John Smith 雇员的原始 JSON 文档:

{   "_index" :   "megacorp",   "_type" :    "employee",   "_id" :      "1",   "_version" : 1,   "found" :    true,   "_source" :  {       "first_name" :  "John",       "last_name" :   "Smith",       "age" :         25,       "about" :       "I love to go rock climbing",       "interests":  [ "sports", "music" ]   } } 

用客户端连接的方式完成以上操作的示例

接着上一个示例中写,我们增加一个检索方法:

main方法中增加调用

// 1.索引雇员文档 //insertEmployee(client);  // 2.简单检索一个文档  GET getOneEmployee(client,"megacorp","employee","1"); 

增加的方法

/* 简单检索一个文档  GET      * 简单地执行  一个 HTTP GET 请求并指定文档的地址——索引库、类型和ID。    使用这三个信息可以返回原始的 JSON 文档:      * {           "_index" :   "megacorp",           "_type" :    "employee",           "_id" :      "1",           "_version" : 5,           "found" :    true,           "_source" :  {               "first_name" :  "John",               "last_name" :   "Smith",               "age" :         25,               "about" :       "I love to go rock climbing",               "interests":  [ "sports", "music" ]           }         }      */     private static void getOneEmployee(Client client,String index,String type,String id)throws Exception {         GetResponse response3 = client.prepareGet(index, type, id).execute().actionGet();         System.out.println(response3.getSourceAsString());//这是_source部分         //{"first_name":"John","last_name":"Smith","age":25,"about":"I love to go rock climbing","interests":["sports","music"]}         System.out.println(response3.getIndex()+"--"+response3.getType()+"--"+response3.getId()+"--"+response3.getVersion());     }

结果显示:
连接成功…
{“first_name”:”John”,”last_name”:”Smith”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}
megacorp–employee–1—5

Head插件操作示例

Elasticsearch(二)elasticsearch索引数据与简单检索GET一个文档

Elasticsearch-5-高亮搜索

许多应用都倾向于在每个搜索结果中 高亮 部分文本片段,以便让用户知道为何该文档符合查询条件。在 Elasticsearch 中检索出高亮片段也很容易。

[AdSense-A]
再次执行前面的查询,并增加一个新的 highlight 参数:

GET /megacorp/employee/_search {     "query" : {         "match_phrase" : {             "about" : "rock climbing"         }     },     "highlight": {         "fields" : {             "about" : {}         }     } } 

当执行该查询时,返回结果与之前一样,与此同时结果中还多了一个叫做 highlight 的部分。这个部分包含了 about 属性匹配的文本片段,并以 HTML 标签 封装:

{    ...    "hits": {       "total":      1,       "max_score":  0.23013961,       "hits": [          {             ...             "_score":         0.23013961,             "_source": {                "first_name":  "John",                "last_name":   "Smith",                "age":         25,                "about":       "I love to go rock climbing",                "interests": [ "sports", "music" ]             },             "highlight": {                "about": [                   "I love to go <em>rock</em> <em>climbing</em>"                  ]             }          }       ]    } } 

Client程序演示

增加一个方法:

/**      * 高亮搜索      * SearchRequestBuilder中的addHighlightedField()方法可以定制在哪个域值的检索结果的关键字上增加高亮      * @param client      */     private static void findEmployeesWithHighlight(Client client,String field,String phrase,String highlightField) {         HighlightBuilder highlightBuilder = new HighlightBuilder().field(highlightField);           highlightBuilder.preTags("<em>");           highlightBuilder.postTags("</em>");           SearchRequestBuilder request = client.prepareSearch("megacorp")                 .setTypes("employee")                 .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)                  .setQuery(QueryBuilders.boolQuery().must(matchPhraseQuery(field, phrase)))                 .highlighter(highlightBuilder);         SearchResponse response = request.get();          //处理结果         SearchHits hits = response.getHits();         Iterator<SearchHit> iterator = hits.iterator();         while(iterator.hasNext()) {             SearchHit hit = iterator.next();             Map<String, HighlightField> highlightFields = hit.getHighlightFields();               Map<String, Object> source = hit.getSource();              //获取高亮结果             Set<String> set = highlightFields.keySet();             for (String str : set) { //              System.out.println("key="+str+" value="+highlightFields.get(str));                  HighlightField highLight = highlightFields.get(str);                 String name = highLight.getName();                  Text[] texts = highLight.getFragments();                 for (int i = 0; i < texts.length; i++) {                     Text text = texts[i];                     System.out.println(name+"="+text);                 }              }          }       } 

主方法中增加调用:

// 7.highlight高亮搜索             findEmployeesWithHighlight(client, "about", "rock climbing", "about"); 

结果运行显示:

about=I love to go <em>rock</em> <em>climbing</em> about=I love to go <em>rock</em> <em>climbing</em> about=I love to go <em>rock</em> <em>climbing</em> about=I love to go <em>rock</em> <em>climbing</em> about=I love to go <em>rock</em> <em>climbing</em> about=I love to go <em>rock</em> <em>climbing</em> about=I love to go <em>rock</em> <em>climbing</em>

head插件示例

Elasticsearch(五)elasticsearch高亮搜索

SpringCloud -3- Eureka通信过程

 

基础架构

[AdSense-A]

服务注册中心:

Eureka提供的服务端,提供服务注册与发现的功能,也就是eureka-server

服务提供者:

提供服务的应用,可以使springboot应用,也可以是其他技术平台且遵循eureka通信机制的应用。它将自己提供的服务注册到Eureka,以供其他应用发现,即Hello-service应用

服务消费者:

从服务注册中心获取服务列表,从而使消费者可以知道去何处调用其所需要的服务,例如我们的实例使用了Ribbon来实现服务消费,后续会介绍Feign的消费方式。
很多时候,客户端即是服务提供者也是服务消费者。

服务治理机制

服务提供者

服务注册:

启动时发送REST请求方式注册到注册中心,注册中心接收到请求后,将元数据信息存储在一个双层结构Map中,第一层Key是服务名,第二层Key是实例名。
就如同我们实现的那样:
SpringCloud 笔记 (三)---- Eureka通信过程

服务同步:

如同之前的高可用注册中心那样,服务可以分别注册到不同的注册中心里。因为此时注册中心之间因互相注册为服务,当服务提供者发送注册请求到一个服务注册中心时,它会将该请求转发给集群中相连的其他注册中心,从而实现注册中心之间的服务同步。这样,同一个服务就可以被多个注册中心所维护。

服务续约:

注册完后,服务提供者会维护一个心跳用来告诉注册中心它还服务着,放置eureka-server的“剔除”将该服务实例从服务列表中排除出去,我们称该操作为服务续约。
可调整相关属性。例如:
eureka.instance.lease-renewal-interval-in-seconds=30
服务续约任务的调用间隔时间,默认30秒
eureka.instance.lease-expiration-duration-in-seconds=90
服务失效时间,默认90秒

服务消费者

获取服务

此时,服务注册中心已经注册了一个服务,并且该服务有两个实例。我们启动消费者的时候,它会发送一个REST请求给服务注册中心,获取上面注册的服务清单。为了性能考虑,Eureka Server会维护一份只读的服务清单返回给客户端,同时该缓存清单会每隔30秒更新一次
获取服务是消费者的基础,所以必须确保eureka.client.fetch-registry=true没有被修改成false,默认为true.
缓存清单的更新时间可以修改:
eureka.client.registry-fetch-interval-seconds=30

服务调用

获取清单后,通过服务名可以获得具体提供服务的实例名和该实例的元数据信息。因为有这些服务实例的详细信息,所以客户端可以根据自己的需要决定具体调用哪个实例,在ribbon中会默认采用轮询的方式进行调用,从而实现客户端的负载均衡。

服务下线

当服务实例进行正常的关闭操作时,它会触发一个服务下线的REST请求给EurekaServer,告诉服务注册中心它要下线,服务端接受到请求后,将该服务状态置为下线(DOWN),并把该下线事件传播出去。

服务注册中心

失效剔除

有些时候,服务实例并不一定会正常下线,可能由于内存溢出,网络故障灯原因使得服务不能正常工作,而注册中心并未收到服务下线的请求。为了从服务列表中将这些无法提供服务的实例剔除,EurekaServer在启动时候会创建一个定时任务,默认每隔一段时间默认60秒将当前清单中超时(默认90秒)没有续约的服务剔除出去。

自我保护

当我们在本地调试基于Eureka的程序时,基本上都会碰到这样一个问题,在服务注册中心面板中出现类似下面的红色警告信息:
SpringCloud 笔记 (三)---- Eureka通信过程
该警告就是出发了Eureka Server的自我保护机制。
服务注册到Eureka Server后,会维护一个心跳连接,告诉Eureka Server自己还服务着。在注册中心运行期间,会统计心跳失败的比例再15分钟之内是否低于85%,如果出现低于的情况(单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka
Server会将当前的实例注册信息保护起来,让这些实例不会过期,尽可能保护这些注册信息。但是,在这段保护期间内实例若出现问题,那么客户端很容易拿到实际已经不存在的服务实例,会出现调试失败的情况,所以客户端必须要有容错机制,比如可以使用请求重试,断路器等机制。
由于本地调试很容易触发注册中心的保护机制,会使得注册中心维护的服务实例不那么准确。所以,我们在本地进行开发的时候,可以使用eureka.server.enable-self-preservation=false参数来关闭保护机制,以确保注册中心可以将不可用的实例正确剔除。

Elasticsearce-6-elasticsearch聚合分析

终于到了最后一个业务需求:支持管理者对雇员目录做分析。 Elasticsearch 有一个功能叫聚合(aggregations),允许我们基于数据生成一些精细的分析结果。聚合与 SQL 中的 GROUP BY 类似但更强大。

[AdSense-A]

基本聚合

举个例子,挖掘出雇员中最受欢迎的兴趣爱好:
GET /megacorp/employee/_search
{
“aggs”: {
“all_interests”: {
“terms”: { “field”: “interests” }
}
}
}
暂时忽略掉语法,直接看看结果:
{

“hits”: { … },
“aggregations”: {
“all_interests”: {
“buckets”: [
{
“key”: “music”,
“doc_count”: 2
},
{
“key”: “forestry”,
“doc_count”: 1
},
{
“key”: “sports”,
“doc_count”: 1
}
]
}
}
}
可以看到,两位员工对音乐感兴趣,一位对林地感兴趣,一位对运动感兴趣。这些聚合并非预先统计,而是从匹配当前查询的文档中即时生成。如果想知道叫 Smith 的雇员中最受欢迎的兴趣爱好,可以直接添加适当的查询来组合查询:

Client程序演示

增加一个方法:

/**      * 挖掘出雇员中最受欢迎的兴趣爱好   聚合搜索using aggrefations      * @param client      */     private static void findInterestHobby(Client client) {         SearchRequestBuilder request = client.prepareSearch("megacorp1")                 .setTypes("employee1")                 .addAggregation(                         AggregationBuilders.terms("agg1").field("interests")                 );         SearchResponse response = request.get();         Aggregations aggs = response.getAggregations();         Map<String,Aggregation> map= aggs.asMap();         Set<String> set = map.keySet();         for (String str : set) {             System.out.println("agg name="+str);             Aggregation agg = map.get(str);             Map<String,Object> data = agg.getMetaData();             Set<String> dataSet = map.keySet();             for (String str2 : dataSet) {                 StringTerms obj = (StringTerms) map.get(str2);                 System.out.println("DocCountError="+obj.getDocCountError());                 System.out.println("SumOfOtherDocCounts="+obj.getSumOfOtherDocCounts());                 List<Bucket> buckes = obj.getBuckets();                 for (Iterator iterator = buckes.iterator(); iterator.hasNext();) {                     Bucket bucket = (Bucket) iterator.next();                     String key = bucket.getKeyAsString();                      System.out.println(key+"="+bucket.getDocCount());                 }             }         }   } 

主方法中增加调用:

// 8.挖掘出雇员中最受欢迎的兴趣爱好   聚合搜索using aggrefations findInterestHobby(client); 

运行后结果报错:

Caused by: RemoteTransportException[[111][127.0.0.1:9300][indices:data/read/search[phase/query]]]; nested: IllegalArgumentException[Fielddata is disabled on text fields by default. Set fielddata=true on [interests] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory.];  Caused by: java.lang.IllegalArgumentException: Fielddata is disabled on text fields by default. ...

fielddata

这里看下fielddata:
大多数字段默认都是索引的,这使得它们可以搜索。但是,在脚本中进行排序、聚合和访问字段值需要从搜索中获得不同的访问模式。

搜索需要回答“哪些文档包含这个术语?”排序和聚合需要回答一个不同的问题:“这个字段对这个文档的值是多少?”。

大多数字段可以使用索引时,找到值但是text文本字段不支持。
Text field使用fielddata的这种内存数据结构。它会在内存中存储反转整个索引的每个片段,包括文档关系。

因为它非常耗费内存所以默认是关闭的disabled,一般不必要设置的不要设置。
参考https://www.elastic.co/guide/en/elasticsearch/reference/current/fielddata.html

我们这里让interests这个字段设置为fielddata:true
让已存在的text field设能够fielddata:

Elasticsearch(六)elasticsearch聚合分析
再次调用,运行结果:
agg name=agg1
DocCountError=0
SumOfOtherDocCounts=0
music=11
sports=8
forestry=2

Head插件示例

结果太长了,只显示最后聚合的结果,hits返回的数据结果省略。(下同)
Elasticsearch(六)elasticsearch聚合分析
Elasticsearch(六)elasticsearch聚合分析

有查询条件的聚合

GET /megacorp/employee/_search
{
“query”: {
“match”: {
“last_name”: “smith”
}
},
“aggs”: {
“all_interests”: {
“terms”: {
“field”: “interests”
}
}
}
}
all_interests 聚合已经变为只包含匹配查询的文档:

“all_interests”: {
“buckets”: [
{
“key”: “music”,
“doc_count”: 2
},
{
“key”: “sports”,
“doc_count”: 1
}
]
}

Client程序演示

我们把刚才的方法请求部分加上查询条件,就如我们之前学习的那样:

SearchRequestBuilder request = client.prepareSearch("megacorp1")                 .setTypes("employee1")                 .setQuery(QueryBuilders.matchQuery("last_name","Smith"))                 .addAggregation(                         AggregationBuilders.terms("agg1").field("interests") 

其他部分相同
调用结果:
agg name=agg1
DocCountError=0
SumOfOtherDocCounts=0
music=2
sports=1

Head插件示例

Elasticsearch(六)elasticsearch聚合分析
Elasticsearch(六)elasticsearch聚合分析

聚合支持分级汇总

聚合还支持分级汇总 。比如,查询特定兴趣爱好员工的平均年龄:
GET /megacorp/employee/_search
{
“aggs” : {
“all_interests” : {
“terms” : { “field” : “interests” },
“aggs” : {
“avg_age” : {
“avg” : { “field” : “age” }
}
}
}
}
}

得到的聚合结果有点儿复杂,但理解起来还是很简单的:

“all_interests”: {
“buckets”: [
{
“key”: “music”,
“doc_count”: 2,
“avg_age”: {
“value”: 28.5
}
},
{
“key”: “forestry”,
“doc_count”: 1,
“avg_age”: {
“value”: 35
}
},
{
“key”: “sports”,
“doc_count”: 1,
“avg_age”: {
“value”: 25
}
}
]
}

输出基本是第一次聚合的加强版。依然有一个兴趣及数量的列表,只不过每个兴趣都有了一个附加的 avg_age 属性,代表有这个兴趣爱好的所有员工的平均年龄。
即使现在不太理解这些语法也没有关系,依然很容易了解到复杂聚合及分组通过 Elasticsearch 特性实现得很完美。可提取的数据类型毫无限制。

Client程序演示

此部分可以参考https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/_structuring_aggregations.html

通俗点说,你可以在一个聚合下面再次聚合

增加一个方法:

/**      * 子聚合      * @param client      */     private static void findAvgInterestHobby(Client client) {         SearchRequestBuilder request = client.prepareSearch("megacorp1")                 .setTypes("employee1")                 .addAggregation(                      AggregationBuilders.terms("agg1").field("interests")                      .subAggregation(AggregationBuilders.avg("avg_age").field("age"))                 );           SearchResponse response = request.execute().actionGet();         //为了方便直接返回string了,类似第一个例子可以分析         System.out.println(response.toString());     } 

main方法增加调用:

// 9.子聚合 findAvgInterestHobby(client); 

结果显示:
{“took”:8,”timed_out”:false,”_shards”:{“total”:5,”successful”:5,”failed”:0},”hits”:{“total”:13,”max_score”:1.0,”hits”:[{“_index”:”megacorp1”,”_type”:”employee1”,”_id”:”5”,”_score”:1.0,”_source”:{“first_name”:”John”,”last_name”:”Smith”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}},{“_index”:”megacorp1”,”_type”:”employee1”,”_id”:”8”,”_score”:1.0,”_source”:{“first_name”:”Jane”,”last_name”:”1 Smith”,”age”:”32”,”about”:”I like to collect rock albums”,”interests”:[“music”]}},{“_index”:”megacorp1”,”_type”:”employee1”,”_id”:”9”,”_score”:1.0,”_source”:{“first_name”:”John”,”last_name”:”SmithSmithSmith”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}},{“_index”:”megacorp1”,”_type”:”employee1”,”_id”:”10”,”_score”:1.0,”_source”:{“first_name”:”John”,”last_name”:”冬瓜核桃”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}},{“_index”:”megacorp1”,”_type”:”employee1”,”_id”:”12”,”_score”:1.0,”_source”:{“first_name”:”John”,”last_name”:”蜂蜜”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}},{“_index”:”megacorp1”,”_type”:”employee1”,”_id”:”2”,”_score”:1.0,”_source”:{“first_name”:”Jane”,”last_name”:”Smith”,”age”:”32”,”about”:”I like to collect rock albums”,”interests”:[“music”]}},{“_index”:”megacorp1”,”_type”:”employee1”,”_id”:”4”,”_score”:1.0,”_source”:{“first_name”:”Douglas1”,”last_name”:”Fir”,”age”:35,”about”:”I like to build cabinets”,”interests”:[“forestry”]}},{“_index”:”megacorp1”,”_type”:”employee1”,”_id”:”6”,”_score”:1.0,”_source”:{“first_name”:”John”,”last_name”:”Smith 1”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}},{“_index”:”megacorp1”,”_type”:”employee1”,”_id”:”1”,”_score”:1.0,”_source”:{“first_name”:”John”,”last_name”:”Smith1”,”age”:25,”about”:”I love to go rock climbing”,”interests”:[“sports”,”music”]}},{“_index”:”megacorp1”,”_type”:”employee1”,”_id”:”7”,”_score”:1.0,”_source”:{“first_name”:”Jane”,”last_name”:”1Smith”,”age”:”32”,”about”:”I like to collect rock albums”,”interests”:[“music”]}}]},”aggregations”:{“agg1”:{“doc_count_error_upper_bound”:0,”sum_other_doc_count”:0,”buckets”:[{“key”:”music”,”doc_count”:11,”avg_age”:{“value”:26.90909090909091}},{“key”:”sports”,”doc_count”:8,”avg_age”:{“value”:25.0}},{“key”:”forestry”,”doc_count”:2,”avg_age”:{“value”:35.0}}]}}}

Head插件示例

Elasticsearch(六)elasticsearch聚合分析
Elasticsearch(六)elasticsearch聚合分析

Elasticsearch–7–集群内原理

ElasticSearch 的主旨是随时可用和按需扩容。 而扩容可以通过购买性能更强大( 垂直扩容 ,或 纵向扩容 ) 或者数量更多的服务器( 水平扩容 ,或 横向扩容 )来实现。

[AdSense-A]
虽然 Elasticsearch 可以获益于更强大的硬件设备,但是垂直扩容是有极限的。 真正的扩容能力是来自于水平扩容–为集群添加更多的节点,并且将负载压力和稳定性分散到这些节点中。
对于大多数的数据库而言,通常需要对应用程序进行非常大的改动,才能利用上横向扩容的新增资源。 与之相反的是,ElastiSearch天生就是 分布式的 ,它知道如何通过管理多节点来提高扩容性和可用性。 这也意味着你的应用无需关注这个问题。
本章将讲述如何按需配置集群、节点和分片,并在硬件故障时确保数据安全。

空集群

如果我们启动了一个单独的节点,里面不包含任何的数据和 索引,那我们的集群看起来就是一个 Figure 1, “包含空内容节点的集群”。
Elasticsearch(七)elasticsearch集群内原理
一个运行中的 Elasticsearch 实例称为一个 节点,而集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。

当一个节点被选举成为 主 节点时, 它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。我们的示例集群就只有一个节点,所以它同时也成为了主节点。

作为用户,我们可以将请求发送到 集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。 Elasticsearch 对这一切的管理都是透明的。

集群健康

Elasticsearch 的集群监控信息中包含了许多的统计数据,其中最为重要的一项就是 集群健康 , 它在 status 字段中展示为 green 、 yellow 或者 red 。
GET /_cluster/health
在一个不包含任何索引的空集群中,它将会有一个类似于如下所示的返回内容:
{
“cluster_name”: “elasticsearch”,
“status”: “green”,
“timed_out”: false,
“number_of_nodes”: 1,
“number_of_data_nodes”: 1,
“active_primary_shards”: 0,
“active_shards”: 0,
“relocating_shards”: 0,
“initializing_shards”: 0,
“unassigned_shards”: 0
}
status 字段指示着当前集群在总体上是否工作正常。它的三种颜色含义如下:

green 所有的主分片和副本分片都正常运行。
yellow 所有的主分片都正常运行,但不是所有的副本分片都正常运行。
red 有主分片没能正常运行。
在本章节剩余的部分,我们将解释什么是 主分片和 副本分片,以及上面提到的这些颜色的实际意义。
Elasticsearch(七)elasticsearch集群内原理

添加索引

我们往 Elasticsearch 添加数据时需要用到 索引 —— 保存相关数据的地方。
索引实际上是指向一个或者多个物理 分片 的 逻辑命名空间 。

一个 分片 是一个底层的 工作单元 ,它仅保存了 全部数据中的一部分。 在分片内部机制中,我们将详细介绍分片是如何工作的,而现在我们只需知道一个分片是一个 Lucene 的实例,以及它本身就是一个完整的搜索引擎。 我们的文档被存储和索引到分片内,但是应用程序是直接与索引而不是与分片进行交互。

Elasticsearch 是利用分片将数据分发到集群内各处的。分片是数据的容器,文档保存在分片内,分片又被分配到集群内的各个节点里。 当你的集群规模扩大或者缩小时, Elasticsearch 会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里。

一个分片可以是 主 分片或者 副本 分片。 索引内任意一个文档都归属于一个主分片,所以主分片的数目决定着索引能够保存的最大数据量。

技术上来说,一个主分片最大能够存储 Integer.MAX_VALUE – 128 个文档,但是实际最大值还需要参考你的使用场景:包括你使用的硬件, 文档的大小和复杂程度,索引和查询文档的方式以及你期望的响应时长。

一个副本分片只是一个主分片的拷贝。 副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读操作提供服务。

在索引建立的时候就已经确定了主分片数,但是副本分片数可以随时修改。

让我们在包含一个空节点的集群内创建名为 blogs 的索引。 索引在默认情况下会被分配5个主分片, 但是为了演示目的,我们将分配3个主分片和一份副本(每个主分片拥有一个副本分片):

PUT /blogs
{
“settings” : {
“number_of_shards” : 3,
“number_of_replicas” : 1
}
}
我们的集群现在是Figure 2, “拥有一个索引的单节点集群”。所有3个主分片都被分配在 Node 1 。

Elasticsearch(七)elasticsearch集群内原理
如果我们现在查看集群健康, 我们将看到如下内容:
{
“cluster_name”: “elasticsearch”,
“status”: “yellow”,
“timed_out”: false,
“number_of_nodes”: 1,
“number_of_data_nodes”: 1,
“active_primary_shards”: 3,
“active_shards”: 3,
“relocating_shards”: 0,
“initializing_shards”: 0,
“unassigned_shards”: 3,
“delayed_unassigned_shards”: 0,
“number_of_pending_tasks”: 0,
“number_of_in_flight_fetch”: 0,
“task_max_waiting_in_queue_millis”: 0,
“active_shards_percent_as_number”: 50
}
集群 status 值为 yellow 。
“unassigned_shards”: 3 没有被分配到任何节点的副本数。

集群的健康状况为 yellow 则表示全部 主 分片都正常运行(集群可以正常服务所有请求),但是 副本 分片没有全部处在正常状态。 实际上,所有3个副本分片都是 unassigned —— 它们都没有被分配到任何节点。 在同一个节点上既保存原始数据又保存副本是没有意义的,因为一旦失去了那个节点,我们也将丢失该节点上的所有副本数据。

当前我们的集群是正常运行的,但是在硬件故障时有丢失数据的风险。

添加故障转移

当集群中只有一个节点在运行时,意味着会有一个单点故障问题——没有冗余。 幸运的是,我们只需再启动一个节点即可防止数据丢失。

如果启动了第二个节点,我们的集群将会如Figure 3, “拥有两个节点的集群——所有主分片和副本分片都已被分配”所示。
Elasticsearch(七)elasticsearch集群内原理
当第二个节点加入到集群后,3个 副本分片 将会分配到这个节点上——每个主分片对应一个副本分片。 这意味着当集群内任何一个节点出现问题时,我们的数据都完好无损。

所有新近被索引的文档都将会保存在主分片上,然后被并行的复制到对应的副本分片上。这就保证了我们既可以从主分片又可以从副本分片上获得文档。

cluster-health 现在展示的状态为 green ,这表示所有6个分片(包括3个主分片和3个副本分片)都在正常运行。
{
“cluster_name”: “elasticsearch”,
“status”: “green”,
“timed_out”: false,
“number_of_nodes”: 2,
“number_of_data_nodes”: 2,
“active_primary_shards”: 3,
“active_shards”: 6,
“relocating_shards”: 0,
“initializing_shards”: 0,
“unassigned_shards”: 0,
“delayed_unassigned_shards”: 0,
“number_of_pending_tasks”: 0,
“number_of_in_flight_fetch”: 0,
“task_max_waiting_in_queue_millis”: 0,
“active_shards_percent_as_number”: 100
}
集群 status 值为 green 。
我们的集群现在不仅仅是正常运行的,并且还处于 始终可用 的状态。

启动第二个节点

为了测试第二个节点启动后的情况,你可以在同一个目录内,完全依照启动第一个节点的方式来启动一个新节点(参考安装并运行 Elasticsearch)。多个节点可以共享同一个目录。

当你在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的 cluster.name 配置,它就会自动发现集群并加入到其中。 但是在不同机器上启动节点的时候,为了加入到同一集群,你需要配置一个可连接到的单播主机列表。

本地示例

完全按照第一个节点的方式启动一个新的节点

参考:
https://www.cnblogs.com/wxw16/p/6160186.html
将之前已安装好的elasticseach文件重新复制一份名为elasticsearch-5.3.0-node2
elasticsearch-5.3.0-node2的elasticsearch.yml中的节点名复制而来的111改为222(与第一个节点名不同即可)
以上做的原因是一个elasticsearch中只能有一个node,否则报错
elasticsearch-5.3.0-node2的data文件夹下所有内容清除
分别启动连接
Elasticsearch(七)elasticsearch集群内原理

本地集群健康查询现在显示:
Elasticsearch(七)elasticsearch集群内原理

配置一个可连接到的单播主机列表

最好使用单播代替组播
Elasticsearch 默认被配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。

虽然组播仍然 作为插件提供, 但它应该永远不被使用在生产环境了,否在你得到的结果就是一个节点意外的加入到了你的生产环境,仅仅是因为他们收到了一个错误的组播信号。 对于组播 本身 并没有错,组播会导致一些愚蠢的问题,并且导致集群变的脆弱(比如,一个网络工程师正在捣鼓网络,而没有告诉你,你会发现所有的节点突然发现不了对方了)。

使用单播,你可以为 Elasticsearch 提供一些它应该去尝试连接的节点列表。 当一个节点联系到单播列表中的成员时,它就会得到整个集群所有节点的状态,然后它会联系 master 节点,并加入集群。

这意味着你的单播列表不需要包含你的集群中的所有节点, 它只是需要足够的节点,当一个新节点联系上其中一个并且说上话就可以了。如果你使用 master 候选节点作为单播列表,你只要列出三个就可以了。 这个配置在 elasticsearch.yml 文件中:

discovery.zen.ping.unicast.hosts: [“host1”, “host2:port”]
这个功能有防火墙,暂未实验。

水平扩容

怎样为我们的正在增长中的应用程序按需扩容呢? 当启动了第三个节点,我们的集群将会看起来如Figure 4, “拥有三个节点的集群——为了分散负载而对分片进行重新分配”所示。
Elasticsearch(七)elasticsearch集群内原理
Node 1 和 Node 2 上各有一个分片被迁移到了新的 Node 3 节点,现在每个节点上都拥有2个分片,而不是之前的3个。 这表示每个节点的硬件资源(CPU, RAM, I/O)将被更少的分片所共享,每个分片的性能将会得到提升。
分片是一个功能完整的搜索引擎,它拥有使用一个节点上的所有资源的能力。 我们这个拥有6个分片(3个主分片和3个副本分片)的索引可以最大扩容到6个节点,每个节点上存在一个分片,并且每个分片拥有所在节点的全部资源。

更多的扩容

我们之前说过 Elasticsearch 可以应对节点故障,接下来让我们尝试下这个功能。 如果我们关闭第一个节点,这时集群的状态为Figure 6, “关闭了一个节点后的集群”。
Elasticsearch(七)elasticsearch集群内原理
但是如果我们想要扩容超过6个节点怎么办呢?

主分片的数目在索引创建时 就已经确定了下来。实际上,这个数目定义了这个索引能够 存储 的最大数据量。(实际大小取决于你的数据、硬件和使用场景。) 但是,读操作——搜索和返回数据——可以同时被主分片 或 副本分片所处理,所以当你拥有越多的副本分片时,也将拥有越高的吞吐量。

在运行中的集群上是可以动态调整副本分片数目的 ,我们可以按需伸缩集群。让我们把副本数从默认的 1 增加到 2 :

PUT /blogs/_settings
{
“number_of_replicas” : 2
}

View in Sense

如Figure 5, “将参数 number_of_replicas 调大到 2”所示, blogs 索引现在拥有9个分片:3个主分片和6个副本分片。 这意味着我们可以将集群扩容到9个节点,每个节点上一个分片。相比原来3个节点时,集群搜索性能可以提升 3 倍。

当然,如果只是在相同节点数目的集群上增加更多的副本分片并不能提高性能,因为每个分片从节点上获得的资源会变少。 你需要增加更多的硬件资源来提升吞吐量。

但是更多的副本分片数提高了数据冗余量:按照上面的节点配置,我们可以在失去2个节点的情况下不丢失任何数据

应对故障

我们之前说过 Elasticsearch 可以应对节点故障,接下来让我们尝试下这个功能。 如果我们关闭第一个节点,这时集群的状态为Figure 6, “关闭了一个节点后的集群”。

我们关闭的节点是一个主节点。而集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点: Node 2 。

在我们关闭 Node 1 的同时也失去了主分片 1 和 2 ,并且在缺失主分片的时候索引也不能正常工作。 如果此时来检查集群的状况,我们看到的状态将会为 red :不是所有主分片都在正常工作。

幸运的是,在其它节点上存在着这两个主分片的完整副本, 所以新的主节点立即将这些分片在 Node 2 和 Node 3 上对应的副本分片提升为主分片, 此时集群的状态将会为 yellow 。 这个提升主分片的过程是瞬间发生的,如同按下一个开关一般。

为什么我们集群状态是 yellow 而不是 green 呢? 虽然我们拥有所有的三个主分片,但是同时设置了每个主分片需要对应2份副本分片,而此时只存在一份副本分片。 所以集群不能为 green 的状态,不过我们不必过于担心:如果我们同样关闭了 Node 2 ,我们的程序 依然 可以保持在不丢任何数据的情况下运行,因为 Node 3 为每一个分片都保留着一份副本。

如果我们重新启动 Node 1 ,集群可以将缺失的副本分片再次进行分配,那么集群的状态也将如Figure 5, “将参数 number_of_replicas 调大到 2”所示。 如果 Node 1 依然拥有着之前的分片,它将尝试去重用它们,同时仅从主分片复制发生了修改的数据文件。

到目前为止,你应该对分片如何使得 Elasticsearch 进行水平扩容以及数据保障等知识有了一定了解。 接下来我们将讲述关于分片生命周期的更多细节。

总结

建立索引时,确定了主分片和副分片的数量,副分片数量可以修改。
假设新建blogs索引时设定它3个主分片,每个主分片1个副分片,即有3个主分片3个副分片。那么
1个节点: node1 3分片(yello)
2个节点: node1 3分片 node2 3分片 (green ,所有主分片与副分片同时运行)
3个节点: node1 2分片 node2 2分片 node3 2分片(green)
。。。。
6个节点: node1~node6每个节点1个分片
如果此时还想增加节点,可以增大副分片数量,即可以设定为每个主分片2个副分片,即有3个主分片6个副分片,那么
1个节点: node1 3分片(yello)
2个节点: node1 3分片 node2 3分片(yello)
3个节点: node1 3分片 node2 3分片 node3 3分片(green ,所有主分片与副分片同时运行)

。。。。
9个节点: node1~node6每个节点1个分片
这样做可以提高搜索性能,同时避免风险。其中一个节点宕机,所有分片会自动均匀分布在其他节点上,保证数据的完整。

Elasticsearch-11-es搜索–最基本的工具

我们已经学会了如何使用 Elasticsearch 作为一个简单的 NoSQL 风格的分布式文档存储系统。我们可以将一个 JSON 文档扔到 Elasticsearch 里,然后根据 ID 检索。但 Elasticsearch 真正强大之处在于可以从无规律的数据中找出有意义的信息——从“大数据”到“大信息”。

[AdSense-A]

Elasticsearch 不只会存储(stores) 文档,为了能被搜索到也会为文档添加索引(indexes) ,这也是为什么我们使用结构化的 JSON 文档,而不是无结构的二进制数据。

文档中的每个字段都将被索引并且可以被查询 。不仅如此,在简单查询时,Elasticsearch 可以使用 所有(all) 这些索引字段,以惊人的速度返回结果。这是你永远不会考虑用传统数据库去做的一些事情。

搜索(search) 可以做到:

• 在类似于 gender 或者 age 这样的字段 上使用结构化查询,join_date 这样的字段上使用排序,就像SQL的结构化查询一样。
• 全文检索,找出所有匹配关键字的文档并按照相关性(relevance) 排序后返回结果。
• 以上二者兼而有之。

很多搜索都是开箱即用的,为了充分挖掘 Elasticsearch 的潜力,你需要理解以下三个概念:

映射(Mapping)
描述数据在每个字段内如何存储

分析(Analysis)
全文是如何处理使之可以被搜索的

领域特定查询语言(Query DSL)
Elasticsearch 中强大灵活的查询语言

以上提到的每个点都是一个大话题,我们将在 深入搜索 一章详细阐述它们。本章节我们将介绍这三点的一些基本概念——仅仅帮助你大致了解搜索是如何工作的。

我们将使用最简单的形式开始介绍 search API。
这一节我们只做简单的介绍,使之前的例子能够更清晰。

空搜索

搜索API的最基础的形式是没有指定任何查询的空搜索 ,它简单地返回集群中所有索引下的所有文档:

GET /_search

返回的结果(为了界面简洁编辑过的)像这样:

{
“hits” : {
“total” : 14,
“hits” : [
{
“_index”: “us”,
“_type”: “tweet”,
“_id”: “7”,
“_score”: 1,
“_source”: {
“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
},
“took” : 4,
“_shards” : {
“failed” : 0,
“successful” : 10,
“total” : 10
},
“timed_out” : false
}

hits

返回结果中最重要的部分是 hits ,它 包含 total 字段来表示匹配到的文档总数,并且一个 hits 数组包含所查询结果的前十个文档。

在 hits 数组中每个结果包含文档的 _index 、 _type 、 _id ,加上 _source 字段。这意味着我们可以直接从返回的搜索结果中使用整个文档。这不像其他的搜索引擎,仅仅返回文档的ID,需要你单独去获取文档。

_score

每个结果还有一个 _score ,它衡量了文档与查询的匹配程度。默认情况下,首先返回最相关的文档结果,就是说,返回的文档是按照 _score 降序排列的。在这个例子中,我们没有指定任何查询,故所有的文档具有相同的相关性,因此对所有的结果而言 1 是中性的 _score 。

max_score

值是与查询所匹配文档的 _score 的最大值。

took

took 值告诉我们执行整个搜索请求耗费了多少毫秒。

shards

_shards 部分 告诉我们在查询中参与分片的总数,以及这些分片成功了多少个失败了多少个。正常情况下我们不希望分片失败,但是分片失败是可能发生的。如果我们遭遇到一种灾难级别的故障,在这个故障中丢失了相同分片的原始数据和副本,那么对这个分片将没有可用副本来对搜索请求作出响应。假若这样,Elasticsearch 将报告这个分片是失败的,但是会继续返回剩余分片的结果。

timeout

timed_out 值告诉我们查询是否超时。默认情况下,搜索请求不会超时。 如果低响应时间比完成结果更重要,你可以指定 timeout 为 10 或者 10ms(10毫秒),或者 1s(1秒):

GET /_search?timeout=10ms
在请求超时之前,Elasticsearch 将会返回已经成功从每个分片获取的结果。

Warning
应当注意的是 timeout 不是停止执行查询,它仅仅是告知正在协调的节点返回到目前为止收集的结果并且关闭连接。在后台,其他的分片可能仍在执行查询即使是结果已经被发送了。
使用超时是因为 SLA(服务等级协议)对你是很重要的,而不是因为想去中止长时间运行的查询。

Client程序演示

PS:在代码中引入静态
import static org.elasticsearch.index.query.QueryBuilders.*;
就能直接调用它的方法不用每次写出。例如
import static org.elasticsearch.index.query.QueryBuilders.*;
QueryBuilder qb = matchAllQuery();

不引入则:
import org.elasticsearch.index.query.QueryBuilders;
QueryBuilder qb = QueryBuilders.matchAllQuery();

在之前演示雇员例子中,我们已经演示过,这里可以感觉很容易的理解他们了。

/*      * 空索引      * 认识搜索响应      */     private static void showQueryAll(Client client) {         SearchResponse response = client.prepareSearch().get();         //分析查询结果         // took--执行整个搜索请求耗费了多少毫秒         long took = response.getTookInMillis();         System.out.println(took);         /*          * timed_out--告诉我们查询是否超时。默认情况下,搜索请求不会超时。 如果低响应时间比完成结果更重要,          * 你可以指定 timeout 为 10 或者 10ms(10毫秒),或者 1s(1秒):          * GET /_search?timeout=10ms          * 在请求超时之前,Elasticsearch 将会返回已经成功从每个分片获取的结果。          * Warning 应当注意的是 timeout  不是停止执行查询,它仅仅是告知正在协调的节点返回到目前为止收集的结果并且关闭连接。在后台,其他的分片可能仍在执行查询即使是结果已经被发送了。 使用超时是因为 SLA(服务等级协议)对你是很重要的,而不是因为想去中止长时间运行的查询。          */         boolean isTimedOut = response.isTimedOut();         System.out.println("timed_out:"+isTimedOut);         /*          *  _shards--告诉我们在查询中参与分片的总数,以及这些分片成功了多少个失败了多少个。          *  正常情况下我们不希望分片失败,但是分片失败是可能发生的。如果我们遭遇到一种灾难级别的故障,          *  在这个故障中丢失了相同分片的原始数据和副本,那么对这个分片将没有可用副本来对搜索请求作出响应。          *  假若这样,Elasticsearch 将报告这个分片是失败的,但是会继续返回剩余分片的结果。          */         int totalShards = response.getTotalShards();         int successfulShards = response.getSuccessfulShards();         int failedShards = response.getFailedShards();         System.out.println("_shards:{ total="+totalShards+" successful="+successfulShards+" failed="+failedShards+"}");           // hits总结果         SearchHits searchHits = response.getHits();         // max_score--与查询所匹配文档的 _score 的最大值         float maxScore = searchHits.getMaxScore();         // 一共文档数         long totalHits = searchHits.getTotalHits();         System.out.println("total="+totalHits+" max_score="+maxScore);          // 文档在hit数组中,默认返回前10条         Iterator<SearchHit> iterator = searchHits.iterator();         while(iterator.hasNext()) {             SearchHit hit = iterator.next();             //索引             String index = hit.getIndex();             //类型             String type = hit.getType();             //id             String id = hit.getId();             //每个结果还有一个 _score   ,它衡量了文档与查询的匹配程度。默认情况下,首先返回最相关的文档结果,就是说,返回的文档是按照 _score 降序排列的。             float score = hit.getScore();             System.out.println("index="+index+" type="+type+" id="+id+" score="+score+" source-->"+hit.getSourceAsString());         }         System.out.println("查询结束...");     } 

调用:

//1.空索引 showQueryAll(client);

结果显示:
连接成功…
连接成功…
3
timed_out:false
_shards:{ total=30 successful=30 failed=0}
total=53 max_score=1.0
index=logstash-car-msg type=carmsg id=AV9_t-4zDK8yG4k-MolH score=1.0 source–>{“host”:”YFCSPT-SUSE-86”,”vehicleId”:”BCAAAD0005”,”vehicleName”:”巴斯达BBL5054XJE1公共安全监测车”,”searchCode”:”BSD-BBL5054XJE1”,”vehicleClass”:”特种车类”,”vehicleClassPicc”:”C49”,”vehicleType”:”1”,”brandNameNew”:”巴斯达”}
index=logstash-car-msg type=carmsg id=BCAAAD0000 score=1.0 source–>{“host”:”YFCSPT-SUSE-86”,”vehicleId”:”BCAAAD0000”,”vehicleName”:”巴斯达BBL5054XJE1公共安全监测车0”,”searchCode”:”BSD-BBL5054XJE0”,”vehicleClass”:”特种车类”,”vehicleClassPicc”:”C49”,”vehicleType”:”1”,”brandNameNew”:”巴斯达0”,”useYears”:0,”makeDate”:”2017-01-01 11:23:01”}
index=logstash-car-msg type=carmsg id=BCAAAD0005 score=1.0 source–>{“host”:”YFCSPT-SUSE-86”,”vehicleId”:”BCAAAD0005”,”vehicleName”:”巴斯达BBL5054XJE1公共安全监测车5”,”searchCode”:”BSD-BBL5054XJE5”,”vehicleClass”:”特种车类11”,”vehicleClassPicc”:”C49”,”vehicleType”:”1”,”brandNameNew”:”巴斯达5”,”useYears”:5,”makeDate”:”2017-01-01 11:23:01”}
index=logstash-car-msg type=carmsg id=BCAAAD0008 score=1.0 source–>{“vehicleName”:”车88”,”searchCode”:”BSD-BBL5054XJE8”}
index=logstash-car-msg1 type=carmsg1 id=BCAAAD0000 score=1.0 source–>{“host”:”YFCSPT-SUSE-86”,”vehicleId”:”BCAAAD0000”,”vehicleName”:”巴斯达BBL5054XJE1公共安全监测车0”,”searchCode”:”BSD-BBL5054XJE0”,”vehicleClass”:”特种车类”,”vehicleClassPicc”:”C49”,”vehicleType”:”1”,”brandNameNew”:”巴斯达0”,”useYears”:0,”makeDate”:”2017-01-01 11:23:01”}
。。。。。。
查询结束…

Head插件示例

Elasticsearch(十一)elasticsearch搜索--最基本的工具

多索引,多类型

你有没有注意到之前的 empty search 的结果,不同类型的文档 — user 和 tweet 来自不同的索引— us 和 gb ?

如果不对某一特殊的索引或者类型做限制,就会搜索集群中的所有文档。Elasticsearch 转发搜索请求到每一个主分片或者副本分片,汇集查询出的前10个结果,并且返回给我们。

然而,经常的情况下,你 想在一个或多个特殊的索引并且在一个或者多个特殊的类型中进行搜索。我们可以通过在URL中指定特殊的索引和类型达到这种效果,如下所示:

/_search 在所有的索引中搜索所有的类型
/gb/_search 在 gb 索引中搜索所有的类型
/gb,us/_search 在 gb 和 us 索引中搜索所有的文档
/g*,u*/_search 在任何以 g 或者 u 开头的索引中搜索所有的类型
/gb/user/_search 在 gb 索引中搜索 user 类型
/gb,us/user,tweet/_search 在 gb 和 us 索引中搜索 user 和 tweet 类型 /_all/user,tweet/_search 在所有的索引中搜索 user 和 tweet 类型
当在单一的索引下进行搜索的时候,Elasticsearch 转发请求到索引的每个分片中,可以是主分片也可以是副本分片,然后从每个分片中收集结果。多索引搜索恰好也是用相同的方式工作的–只是会涉及到更多的分片。

Tip
搜索一个索引有五个主分片和搜索五个索引各有一个分片准确来所说是等价的。

Client演示示例

在之前演示雇员例子中,我们已经演示过,这里可以感觉很容易的理解他们了。

SearchResponse response = client.prepareSearch("logstash-car-msg","logstash-car-msg1")         .setTypes("carmsg", "carmsg1")         .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)         .get(); 

还有MultiGet

MultiGetRequestBuilder builder = client.prepareMultiGet()                 .add("website","blog","1")    //单一ID                        .add("website","blog","1","2") //多ID                 .add("logstash-car-msg", "carmsg","BCAAAD0005");   

等等,多获取的都能做到如此。

分页

在之前的 空搜索 中说明了集群中有 14 个文档匹配了(empty)query 。 但是在 hits 数组中只有 10 个文档。如何才能看到其他的文档?

和 SQL 使用 LIMIT 关键字返回单个 page 结果的方法相同,Elasticsearch 接受 from 和 size 参数:

size 显示应该返回的结果数量,默认是 10 from 显示应该跳过的初始结果数量,默认是 0
如果每页展示 5 条结果,可以用下面方式请求得到 1 到 3 页的结果:

GET /_search?size=5
GET /_search?size=5&from=5
GET /_search?size=5&from=10
考虑到分页过深以及一次请求太多结果的情况,结果集在返回之前先进行排序。 但请记住一个请求经常跨越多个分片,每个分片都产生自己的排序结果,这些结果需要进行集中排序以保证整体顺序是正确的。

在分布式系统中深度分页

理解为什么深度分页是有问题的,我们可以假设在一个有 5 个主分片的索引中搜索。 当我们请求结果的第一页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 50 个结果排序得到全部结果的前 10 个。

现在假设我们请求第 1000 页–结果从 10001 到 10010 。所有都以相同的方式工作除了每个分片不得不产生前10010个结果以外。 然后协调节点对全部 50050 个结果排序最后丢弃掉这些结果中的 50040 个结果。

可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。这就是 web 搜索引擎对任何查询都不要返回超过 1000 个结果的原因。

Tip
在 重新索引你的数据 中解释了如何 能够 有效获取大量的文档。

Client演示示例

/*      * 分页      */     private static void showScrollQuery(Client client) {         SearchResponse response = client.prepareSearch("megacorp1")                 .setQuery(termQuery("first_name", "John"))                 .setSize(5).setFrom(0).get(); //size每一页最多5条记录,from跳过多少个数据显示          SearchHits searchHits = response.getHits();         // 文档在hit数组中,默认返回前10条         Iterator<SearchHit> iterator = searchHits.iterator();         while(iterator.hasNext()) {             SearchHit hit = iterator.next();             //为了效果只打印id             String id = hit.getId();             System.out.println("id="+id);         }      } 

调用:

//3.分页 showScrollQuery(client);

结果显示:
连接成功…
id=6
id=11
id=1
id=13
id=5

改为setFrom(3):
id=13
id=5
id=9
id=10
id=12

Head插件示例

Elasticsearch(十一)elasticsearch搜索--最基本的工具

轻量搜索

有两种形式的 搜索 API:一种是 “轻量的” 查询字符串 版本,要求在查询字符串中传递所有的 参数,另一种是更完整的 请求体 版本,要求使用 JSON 格式和更丰富的查询表达式作为搜索语言。

查询字符串搜索非常适用于通过命令行做即席查询。例如,查询在 tweet 类型中 tweet 字段包含 elasticsearch 单词的所有文档:

GET /_all/tweet/_search?q=tweet:elasticsearch

下一个查询在 name 字段中包含 john 并且在 tweet 字段中包含 mary 的文档。实际的查询就是这样
+name:john +tweet:mary

但是查询字符串参数所需要的 百分比编码 (译者注:URL编码)实际上更加难懂:

GET /_search?q=%2Bname%3Ajohn+%2Btweet%3Amary

  • 前缀表示必须与查询条件匹配。类似地, – 前缀表示一定不与查询条件匹配。没有 + 或者 – 的所有其他条件都是可选的——匹配的越多,文档就越相关。

_all 字段

这个简单搜索返回包含 mary 的所有文档:

GET /_search?q=mary

之前的例子中,我们在 tweet 和 name 字段中搜索内容。然而,这个查询的结果在三个地方提到了 mary :

• 有一个用户叫做 Mary
• 6条微博发自 Mary
• 一条微博直接 @mary

Elasticsearch 是如何在三个不同的字段中查找到结果的呢?

当索引一个文档的时候,Elasticsearch 取出所有字段的值拼接成一个大的字符串,作为 _all 字段进行索引。例如,当索引这个文档时:

{
“tweet”: “However did I manage before Elasticsearch?”,
“date”: “2014-09-14”,
“name”: “Mary Jones”,
“user_id”: 1
}

这就好似增加了一个名叫 _all 的额外字段:

“However did I manage before Elasticsearch? 2014-09-14 Mary Jones 1”

除非设置特定字段,否则查询字符串就使用 _all 字段进行搜索。

Tip
在刚开始开发一个应用时,_all 字段是一个很实用的特性。之后,你会发现如果搜索时用指定字段来代替 _all 字段,将会更好控制搜索结果。当 _all 字段不再有用的时候,可以将它置为失效,正如在 元数据: _all 字段 中所解释的。

更复杂的查询

下面的查询针对tweents类型,并使用以下的条件:

• name 字段中包含 mary 或者 john
• date 值大于 2014-09-10
all 字段包含 aggregations 或者 geo

+name:(mary john) +date:>2014-09-10 +(aggregations geo)

View in Sense

查询字符串在做了适当的编码后,可读性很差:

?q=%2Bname%3A(mary+john)+%2Bdate%3A%3E2014-09-10+%2B(aggregations+geo)

从之前的例子中可以看出,这种 轻量 的查询字符串搜索效果还是挺让人惊喜的。 它的查询语法在相关参考文档中有详细解释,以便简洁的表达很复杂的查询。对于通过命令做一次性查询,或者是在开发阶段,都非常方便。

但同时也可以看到,这种精简让调试更加晦涩和困难。而且很脆弱,一些查询字符串中很小的语法错误,像 – , : , / 或者 ” 不匹配等,将会返回错误而不是搜索结果。

最后,查询字符串搜索允许任何用户在索引的任意字段上执行可能较慢且重量级的查询,这可能会暴露隐私信息,甚至将集群拖垮。

Tip

因为这些原因,不推荐直接向用户暴露查询字符串搜索功能,除非对于集群和数据来说非常信任他们。

相反,我们经常在生产环境中更多地使用功能全面的 request body 查询API,除了能完成以上所有功能,还有一些附加功能。但在到达那个阶段之前,首先需要了解数据在 Elasticsearch 中是如何被索引的。

这个很复杂,所以我们例子中一般是使用request body 查询API,这个不做研究

Elasticsearch-12-es映射和分析

[AdSense-A]

映射和分析

当摆弄索引里面的数据时,我们发现一些奇怪的事情。一些事情看起来被打乱了:在我们的索引中有12条推文,其中只有一条包含日期 2014-09-15 ,但是看一看下面查询命中的 总数 (total):

GET /_search?q=2014 # 12 results
GET /_search?q=2014-09-15 # 12 results !
GET /_search?q=date:2014-09-15 # 1 result
GET /_search?q=date:2014 # 0 results !

为什么在 _all 字段查询日期返回所有推文,而在 date 字段只查询年份却没有返回结果?为什么我们在 _all 字段和 date 字段的查询结果有差别?

推测起来,这是因为数据在 all 字段与 data 字段的索引方式不同。所以,通过请求 gb 索引中 tweet 类型的映射_(或模式定义),让我们看一看 Elasticsearch 是如何解释我们文档结构的:

GET /gb/_mapping/tweet

这将得到如下结果:

{
“gb”: {
“mappings”: {
“tweet”: {
“properties”: {
“date”: {
“type”: “date”,
“format”: “strict_date_optional_time||epoch_millis”
},
“name”: {
“type”: “string”
},
“tweet”: {
“type”: “string”
},
“user_id”: {
“type”: “long”
}
}
}
}
}
}

基于对字段类型的猜测, Elasticsearch 动态为我们产生了一个映射。这个响应告诉我们 date 字段被认为是 date 类型的。由于 _all 是默认字段,所以没有提及它。但是我们知道 _all 字段是 string 类型的。

所以 date 字段和 string 字段 索引方式不同,因此搜索结果也不一样。这完全不令人吃惊。你可能会认为 核心数据类型 strings、numbers、Booleans 和 dates 的索引方式有稍许不同。没错,他们确实稍有不同。

但是,到目前为止,最大的差异在于 代表 精确值 (它包括 string 字段)的字段和代表 全文 的字段。这个区别非常重要——它将搜索引擎和所有其他数据库区别开来。

本地演示

11
Elasticsearch(十二)elasticsearch映射和分析
12
Elasticsearch(十二)elasticsearch映射和分析
13
Elasticsearch(十二)elasticsearch映射和分析
14
Elasticsearch(十二)elasticsearch映射和分析
15
Elasticsearch(十二)elasticsearch映射和分析

16
Elasticsearch(十二)elasticsearch映射和分析

至于为什么这样,因为他们索引的类型不同所以效果不同,具体请看之前的博客,有提到过这一点。即term查询方式虽然表达的是查询精确值,但是它的意思是要根据索引类型,如果索引类型是精确值,查询的结果才对应。之前的疑惑解决。如果索引类型是text,即全文,那么即使查询方式是term它也会被分词和倒排索引,只有我们手动让他not_analyzed,才可以有相同的结果。(当然最主要的终结原因是索引类型)

精确值 VS 全文

Elasticsearch 中的数据可以概括的分为两类:精确值和全文。

精确值 如它们听起来那样精确。例如日期或者用户 ID,但字符串也可以表示精确值,例如用户名或邮箱地址。对于精确值来讲,Foo 和 foo 是不同的,2014 和 2014-09-15 也是不同的。

另一方面,全文 是指文本数据(通常以人类容易识别的语言书写),例如一个推文的内容或一封邮件的内容。

Note

全文通常是指非结构化的数据,但这里有一个误解:自然语言是高度结构化的。问题在于自然语言的规则是复杂的,导致计算机难以正确解析。例如,考虑这条语句:
May is fun but June bores me.

它指的是月份还是人?

精确值很容易查询。结果是二进制的:要么匹配查询,要么不匹配。这种查询很容易用 SQL 表示:

WHERE name = “John Smith”
AND user_id = 2
AND date > “2014-09-15”

查询全文数据要微妙的多。我们问的不只是“这个文档匹配查询吗”,而是“该文档匹配查询的程度有多大?”换句话说,该文档与给定查询的相关性如何?

我们很少对全文类型的域做精确匹配。相反,我们希望在文本类型的域中搜索。不仅如此,我们还希望搜索能够理解我们的 意图 :

• 搜索 UK ,会返回包含 United Kindom 的文档。
• 搜索 jump ,会匹配 jumped , jumps , jumping ,甚至是 leap 。
• 搜索 johnny walker 会匹配 Johnnie Walker , johnnie depp 应该匹配 Johnny Depp 。
• fox news hunting 应该返回福克斯新闻( Foxs News )中关于狩猎的故事,同时, fox hunting news 应该返回关于猎狐的故事。

为了促进这类在全文域中的查询,Elasticsearch 首先 分析 文档,之后根据结果创建 倒排索引 。在接下来的两节,我们会讨论倒排索引和分析过程。

总结

Elasticsearch对于全文这种类型的文档,会对他进行分析,尽量让相关度高的结果在前面,非常人性化。即:
对于精确值类型,查询的时候可以类似SQL语句中那样值是什么查出来的就表达的是什么。
对于全文类型,查询的时候会在文本域中进行分析,然后对分析的结果进行倒排索引,最后查出来的值是经过分析处理后相关性高在前的排序结果。

倒排索引

Elasticsearch 使用一种称为 倒排索引 的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。

例如,假设我们有两个文档,每个文档的 content 域包含如下内容:

  1. The quick brown fox jumped over the lazy dog
  2. Quick brown foxes leap over lazy dogs in summer

为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的 词(我们称它为 词条 或 tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示:

Term Doc_1 Doc_2

Quick | | X
The | X |
brown | X | X
dog | X |
dogs | | X
fox | X |
foxes | | X
in | | X
jumped | X |
lazy | X | X
leap | | X
over | X | X
quick | X |
summer | | X

the | X |

现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:

Term Doc_1 Doc_2

brown | X | X

quick | X |

Total | 2 | 1

两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单 相似性算法 ,那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。

但是,我们目前的倒排索引有一些问题:

• Quick 和 quick 以独立的词条出现,然而用户可能认为它们是相同的词。
• fox 和 foxes 非常相似, 就像 dog 和 dogs ;他们有相同的词根。
• jumped 和 leap, 尽管没有相同的词根,但他们的意思很相近。他们是同义词。

使用前面的索引搜索 +Quick +fox 不会得到任何匹配文档。(记住,+ 前缀表明这个词必须存在。)只有同时出现 Quick 和 fox 的文档才满足这个查询条件,但是第一个文档包含 quick fox ,第二个文档包含 Quick foxes 。

我们的用户可以合理的期望两个文档与查询匹配。我们可以做的更好。

如果我们将词条规范为标准模式,那么我们可以找到与用户搜索的词条不完全一致,但具有足够相关性的文档。例如:

• Quick 可以小写化为 quick 。
• foxes 可以 词干提取 –变为词根的格式– 为 fox 。类似的, dogs 可以为提取为 dog 。
• jumped 和 leap 是同义词,可以索引为相同的单词 jump 。

现在索引看上去像这样:

Term Doc_1 Doc_2

brown | X | X
dog | X | X
fox | X | X
in | | X
jump | X | X
lazy | X | X
over | X | X
quick | X | X
summer | | X

the | X | X

这还远远不够。我们搜索 +Quick +fox 仍然 会失败,因为在我们的索引中,已经没有 Quick 了。但是,如果我们对搜索的字符串使用与 content 域相同的标准化规则,会变成查询 +quick +fox ,这样两个文档都会匹配!

Note
这非常重要。你只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式。

分词和标准化的过程称为 分析 , 我们会在下个章节讨论。

总结

文档分析后倒排索引的结果去匹配查询的字符串若相关度高,会排序在前返回。
分析过程包括分词和标准化。

倒排索引可以这样想:
以前传统数据库我们是拿查询的字符串去找数据库中的数据,倒排索引是对文档进行分析,最后统计结果取匹配查询的字符串相关度是否高。

分析与分析器

分析与分析器

分析 包含下面的过程:

• 首先,将一块文本分成适合于倒排索引的独立的 词条 ,
• 之后,将这些词条统一化为标准格式以提高它们的“可搜索性”,或者 recall

分析器执行上面的工作。 分析器 实际上是将三个功能封装到了一个包里:

字符过滤器

首先,字符串按顺序通过每个 字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉HTML,或者将 & 转化成 and

分词器

其次,字符串被 分词器 分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。

Token 过滤器

最后,词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,小写化 Quick ),删除词条(例如, 像 aandthe 等无用词),或者增加词条(例如,像 jump 和 leap 这种同义词)。
Elasticsearch提供了开箱即用的字符过滤器、分词器和token 过滤器。 这些可以组合起来形成自定义的分析器以用于不同的目的。我们会在 自定义分析器 章节详细讨论。

内置分析器

但是, Elasticsearch还附带了可以直接使用的预包装的分析器。 接下来我们会列出最重要的分析器。为了证明它们的差异,我们看看每个分析器会从下面的字符串得到哪些词条:
“Set the shape to semi-transparent by calling set_trans(5)”

标准分析器 standard

标准分析器是Elasticsearch默认使用的分析器。它是分析各种语言文本最常用的选择。它根据 Unicode 联盟 定义的 单词边界 划分文本。删除绝大部分标点。最后,将词条小写。它会产生
set, the, shape, to, semi, transparent, by, calling, set_trans, 5

简单分析器 simple

简单分析器在任何不是字母的地方分隔文本,将词条小写。它会产生
set, the, shape, to, semi, transparent, by, calling, set, trans

空格分析器 whitespace

空格分析器在空格的地方划分文本。它会产生
Set, the, shape, to, semi-transparent, by, calling, set_trans(5)

语言分析器 例如english

特定语言分析器可用于 很多语言。它们可以考虑指定语言的特点。例如, 英语 分析器附带了一组英语无用词(常用单词,例如 and 或者 the ,它们对相关性没有多少影响),它们会被删除。 由于理解英语语法的规则,这个分词器可以提取英语单词的 词干 。

英语 分词器会产生下面的词条:
set, shape, semi, transpar, call, set_tran, 5

注意看 transparentcalling 和 set_trans 已经变为词根格式。

什么时候使用分析器

当我们 索引 一个文档,它的全文域被分析成词条以用来创建倒排索引。 但是,当我们在全文域 搜索 的时候,我们需要将查询字符串通过 相同的分析过程 ,以保证我们搜索的词条格式与索引中的词条格式一致。

全文查询,理解每个域是如何定义的,因此它们可以做 正确的事:
• 当你查询一个 全文 域时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。
• 当你查询一个 精确值 域时,不会分析查询字符串, 而是搜索你指定的精确值。

现在你可以理解在 开始章节 的查询为什么返回那样的结果:
• date 域包含一个精确值:单独的词条 2014-09-15
• _all 域是一个全文域,所以分词进程将日期转化为三个词条: 201409, 和 15

当我们在 _all 域查询 2014,它匹配所有的12条推文,因为它们都含有2014 :

GET /_search?q=2014 # 12 results
当我们在 _all 域查询 2014-09-15,它首先分析查询字符串,产生匹配201409, 或15 中 任意 词条的查询。这也会匹配所有12条推文,因为它们都含有 2014 :

GET /_search?q=2014-09-15 # 12 results !

当我们在 date 域查询 2014-09-15,它寻找 精确 日期,只找到一个推文:

GET /_search?q=date:2014-09-15 # 1 result

当我们在 date 域查询 2014,它找不到任何文档,因为没有文档含有这个精确日志:

GET /_search?q=date:2014 # 0 results !

测试分析器

有些时候很难理解分词的过程和实际被存储到索引中的词条,特别是你刚接触 Elasticsearch。为了理解发生了什么,你可以使用 analyze API 来看文本是如何被分析的。在消息体里,指定分析器和要分析的文本:

GET /_analyze
{
“analyzer”: “standard”,
“text”: “Text to analyze”
}

结果中每个元素代表一个单独的词条:

{
“tokens”: [
{
“token”: “text”,
“start_offset”: 0,
“end_offset”: 4,
“type”: “”,
“position”: 1
},
{
“token”: “to”,
“start_offset”: 5,
“end_offset”: 7,
“type”: “”,
“position”: 2
},
{
“token”: “analyze”,
“start_offset”: 8,
“end_offset”: 15,
“type”: “”,
“position”: 3
}
]
}

token 是实际存储到索引中的词条。 position 指明词条在原始文本中出现的位置。 start_offset 和 end_offset 指明字符在原始字符串中的位置。

Tip
每个分析器的 type 值都不一样,可以忽略它们。它们在Elasticsearch中的唯一作用在于keep_types token 过滤器。

analyze API 是一个有用的工具,它有助于我们理解Elasticsearch索引内部发生了什么,随着深入,我们会进一步讨论它。
17
Elasticsearch(十二)elasticsearch映射和分析

指定分析器

当Elasticsearch在你的文档中检测到一个新的字符串域 ,它会自动设置其为一个全文 字符串 域,使用 标准 分析器对它进行分析。

你不希望总是这样。可能你想使用一个不同的分析器,适用于你的数据使用的语言。有时候你想要一个字符串域就是一个字符串域–不使用分析,直接索引你传入的精确值,例如用户ID或者一个内部的状态域或标签。

要做到这一点,我们必须手动指定这些域的映射。

总结

如果我们不手动映射,字符串类型值默认在文档中是全文类型的,使用标准分析器分析,然后倒排索引匹配查询返回相关度高的文档。
这是Elasticsearch的默认分析配置,当然分析器除了默认分析器还有其他不同种类,可以根据效果进行选择。

映射

为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串, Elasticsearch 需要知道每个域中数据的类型。这个信息包含在映射中。

如 数据输入和输出 中解释的, 索引中每个文档都有 类型 。每种类型都有它自己的 映射 ,或者 模式定义 。映射定义了类型中的域,每个域的数据类型,以及Elasticsearch如何处理这些域。映射也用于配置与类型有关的元数据。

我们会在 类型和映射 详细讨论映射。本节,我们只讨论足够让你入门的内容。

核心简单域类型

Elasticsearch 支持 如下简单域类型:

• 字符串: string
• 整数 : byte, short, integer, long
• 浮点数: float, double
• 布尔型: boolean
• 日期: date

当你索引一个包含新域的文档–之前未曾出现– Elasticsearch 会使用 动态映射 ,通过JSON中基本数据类型,尝试猜测域类型,使用如下规则:

JSON type
域 type

布尔型: true 或者 false
boolean

整数: 123
long

浮点数: 123.45
double

字符串,有效日期: 2014-09-15
date

字符串: foo bar
string

Note
这意味着如果你通过引号( “123” )索引一个数字,它会被映射为 string 类型,而不是 long 。但是,如果这个域已经映射为 long ,那么 Elasticsearch 会尝试将这个字符串转化为 long ,如果无法转化,则抛出一个异常。

即,如果你不自己手动写映射,那么你put了一个”123”这样的值,elasticsearch会默认它是text字符串类型的,索引的时候会被分析,如果手动映射为long那么才是你想要的效果。

查看映射

通过 /_mapping ,我们可以查看 Elasticsearch 在一个或多个索引中的一个或多个类型的映射 。在 开始章节 ,我们已经取得索引 gb 中类型 tweet 的映射:

GET /gb/_mapping/tweet

Elasticsearch 根据我们索引的文档,为域(称为 属性 )动态生成的映射。

{
“gb”: {
“mappings”: {
“tweet”: {
“properties”: {
“date”: {
“type”: “date”,
“format”: “strict_date_optional_time||epoch_millis”
},
“name”: {
“type”: “string”
},
“tweet”: {
“type”: “string”
},
“user_id”: {
“type”: “long”
}
}
}
}
}
}

Tip
错误的映射,例如 将 age 域映射为 string 类型,而不是 integer ,会导致查询出现令人困惑的结果。

检查一下!而不是假设你的映射是正确的。

自定义域映射

尽管在很多情况下基本域数据类型 已经够用,但你经常需要为单独域自定义映射 ,特别是字符串域。自定义映射允许你执行下面的操作:

• 全文字符串域和精确值字符串域的区别
• 使用特定语言分析器
• 优化域以适应部分匹配
• 指定自定义数据格式
• 还有更多

域最重要的属性是 type 。对于不是 string 的域,你一般只需要设置 type :

{
“number_of_clicks”: {
“type”: “integer”
}
}

默认, string 类型域会被认为包含全文。就是说,它们的值在索引前,会通过 一个分析器,针对于这个域的查询在搜索前也会经过一个分析器。

string 域映射的两个最重要 属性是 index 和 analyzer 。

index

index 属性控制怎样索引字符串。它可以是下面三个值:
analyzed 首先分析字符串,然后索引它。换句话说,以全文索引这个域。
not_analyzed 索引这个域,所以可以搜索到它,但索引指定的精确值。不对它进行分析。
no Don’t index this field at all不索引这个域。这个域不会被搜索到。
string 域 index 属性默认是 analyzed 。如果我们想映射这个字段为一个精确值,我们需要设置它为 not_analyzed :

{
“tag”: {
“type”: “string”,
“index”: “not_analyzed”
}
}

Note
其他简单类型(例如 long , double , date 等)也接受 index 参数,但有意义的值只有 no 和 not_analyzed , 因为它们永远不会被分析。

analyzer

对于 analyzed 字符串域,用 analyzer 属性指定在搜索和索引时使用的分析器。默认, Elasticsearch 使用 standard 分析器, 但你可以指定一个内置的分析器替代它,例如 whitespace 、 simple 和 english

{
“tweet”: {
“type”: “string”,
“analyzer”: “english”
}
}

在 自定义分析器 ,我们会展示怎样定义和使用自定义分析器。

更新映射

当你首次 创建一个索引的时候,可以指定类型的映射。你也可以使用 /_mapping 为新类型(或者为存在的类型更新映射)增加映射。

Note
尽管你可以 增加_ 一个存在的映射,你不能 _修改 存在的域映射。如果一个域的映射已经存在,那么该域的数据可能已经被索引。如果你意图修改这个域的映射,索引的数据可能会出错,不能被正常的搜索。

我们可以更新一个映射来添加一个新域,但不能将一个存在的域从 analyzed 改为 not_analyzed 。

为了描述指定映射的两种方式,我们先删除 gd 索引:

DELETE /gb

然后创建一个新索引,指定 tweet 域使用 english 分析器:

PUT /gb
{
“mappings”: {
“tweet” : {
“properties” : {
“tweet” : {
“type” : “string”,
“analyzer”: “english”
},
“date” : {
“type” : “date”
},
“name” : {
“type” : “string”
},
“user_id” : {
“type” : “long”
}
}
}
}
}

通过消息体中指定的 mappings 创建了索引。

稍后,我们决定在 tweet 映射增加一个新的名为 tag 的 not_analyzed 的文本域,使用 _mapping :

PUT /gb/_mapping/tweet
{
“properties” : {
“tag” : {
“type” : “string”,
“index”: “not_analyzed”
}
}
}

注意,我们不需要再次列出所有已存在的域,因为无论如何我们都无法改变它们。新域已经被合并到存在的映射中。

测试映射

你可以使用 analyze API 测试字符串域的映射。比较下面两个请求的输出:

GET /gb/_analyze
{
“field”: “tweet”,
“text”: “Black-cats”
}

GET /gb/_analyze
{
“field”: “tag”,
“text”: “Black-cats”
}

消息体里面传输我们想要分析的文本。
tweet 域产生两个词条 black 和 cat , tag 域产生单独的词条 Black-cats 。换句话说,我们的映射正常工作。

总结

  1. 对于不是text类型的字段,Elasticsearch不会默认对他们分析,所以只需要指定正确他们的字段类型type即可。
  2. 对于是text类型的字段,Elasticsearch默认对他们进行分析,且使用的是标准分析器。如果希望其他的效果,可以对该字段的索引中index 和 analyzer属性进行配置。
    index 属性控制怎样索引字符串。
    analyzed 首先分析字符串,然后索引它。换句话说,以全文索引这个域。
    not_analyzed 索引这个域,所以可以搜索到它,但索引指定的精确值。不对它进行分析。
    no Don’t index this field at all不索引这个域。这个域不会被搜索到。

默认处理中text字段的index属性就是analyzed,如果我们希望它匹配查询的精确值而不是分词查询,可以吧index设为not_analyzed
. Analyzer 指定了如果分析此text字段使用的分析器类型
标准分析器standard,简单分析器simple,空格分析器whitespace,语言分析器,例如英语english

但是如果一个映射已经存在,不能更改,只能在一开始建立映射的时候或是第一次添加该字段的值之前指定该字段的索引类型,因为如果更改了数据类型,数字long可能存入不了date类型种。

第一次建立索引:
在决定使用解析器前可以先测试是不是你要的效果:
Elasticsearch(十二)elasticsearch映射和分析

然后谨慎建立,因为一旦建立就改不了咯~!

18
Elasticsearch(十二)elasticsearch映射和分析

插入一条数据后想要插入一个新字段,在新字段赋值前增加索引:
19
Elasticsearch(十二)elasticsearch映射和分析

20-26
Elasticsearch(十二)elasticsearch映射和分析

Elasticsearch(十二)elasticsearch映射和分析
查看映射:

Elasticsearch(十二)elasticsearch映射和分析

可以测试了:

Elasticsearch(十二)elasticsearch映射和分析

Elasticsearch(十二)elasticsearch映射和分析

Elasticsearch(十二)elasticsearch映射和分析

Elasticsearch(十二)elasticsearch映射和分析

Elasticsearch-10-elasticsearch分布式文档存储

在前面的章节,我们介绍了如何索引和查询数据,不过我们忽略了很多底层的技术细节, 例如文件是如何分布到集群的,又是如何从集群中获取的。 Elasticsearch 本意就是隐藏这些底层细节,让我们好专注在业务开发中,所以其实你不必了解这么深入也无妨。

[AdSense-A]

在这个章节中,我们将深入探索这些核心的技术细节,这能帮助你更好地理解数据如何被存储到这个分布式系统中。

注意

这个章节包含了一些高级话题,上面也提到过,就算你不记住和理解所有的细节仍然能正常使用 Elasticsearch。 如果你有兴趣的话,这个章节可以作为你的课外兴趣读物,扩展你的知识面。

如果你在阅读这个章节的时候感到很吃力,也不用担心。 这个章节仅仅只是用来告诉你 Elasticsearch 是如何工作的, 将来在工作中如果你需要用到这个章节提供的知识,可以再回过头来翻阅。

路由一个文档到一个分片中

之前我们说过,原始数据都存在主分片中,副分片只是主分片的一个副本,便于节点管理数据,规避宕机等丢失数据的风险。

问题?Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?当我们创建文档时,它如何决定这个文档应当被存储在分片 1 还是分片 2 中呢?
首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:
shard = hash(routing) % number_of_primary_shards
routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到 余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。

这就解释了为什么我们要在创建索引的时候就确定好主分片的数量 并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。

所有的文档 API( get 、 index 、 delete 、 bulk 、 update 以及 mget )都接受一个叫做 routing 的路由参数 ,通过这个参数我们可以自定义文档到分片的映射。一个自定义的路由参数可以用来确保所有相关的文档——例如所有属于同一个用户的文档——都被存储到同一个分片中。我们也会在扩容设计这一章中详细讨论为什么会有这样一种需求。

主分片和副本分片如何交互

为了说明目的, 我们假设有一个集群由三个节点组成。 它包含一个叫 blogs 的索引,有两个主分片,每个主分片有两个副本分片。相同分片的副本不会放在同一节点,所以我们的集群看起来像 Figure 8, “有三个节点和一个索引的集群”。

Elasticsearch(十)elasticsearch分布式文档存储

我们可以发送请求到集群中的任一节点。 每个节点都有能力处理任意请求。 每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。 在下面的例子中,将所有的请求发送到 Node 1 ,我们将其称为 协调节点(coordinating node) 。

Tip
当发送请求的时候, 为了扩展负载,更好的做法是轮询集群中所有的节点。

新建、索引和删除文档

新建、索引和删除 请求都是 写 操作, 必须在主分片上面完成之后才能被复制到相关的副本分片,如下图所示 Figure 9, “新建、索引和删除单个文档”.
Elasticsearch(十)elasticsearch分布式文档存储
以下是在主副分片和任何副本分片上面 成功新建,索引和删除文档所需要的步骤顺序:

  1. 客户端向 Node 1 发送新建、索引或者删除请求。
  2. 节点使用文档的 _id 确定文档属于分片 0。请求会被转发到Node 3,因为分片 0 的主分片目前被分配在Node 3 上。
  3. Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1 和 Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。

在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。
有一些可选的请求参数允许您影响这个过程,可能以数据安全为代价提升性能。这些选项很少使用,因为Elasticsearch已经很快,但是为了完整起见,在这里阐述如下:
一致性
默认情况下,主分片 需要 规定数量(quorum),或大多数的分片 (其中分片副本可以是主分片或者副本分片)在写入操作时可用。这是为了防止将数据写入到网络分区的‘`背面’’。规定的数量定义公式如下:
int( (primary + number_of_replicas) / 2 ) + 1

允许的 一致性 值是 一个 (只是主分片)或者 所有(主分片和副本分片), 或者默认的规定数量或者大多数的副本分片。

注意 number_of_replicas 是在索引中的设置指定的分片数,不是当前处理活动状态的副本分片数。如果你指定索引应该有三个副本分片,那规定数量计算公式是:
int( (primary + 3 replicas) / 2 ) + 1 = 3

但是如果只启动两个节点,则活动分片副本无法满足规定数量,并且您将无法索引和删除任何文档。
超时 如果没有足够的副本分片会发生什么? Elasticsearch会等待,希望更多的分片出现。默认情况下,它最多等待1分钟。 如果你需要,你可以使用 timeout 参数 使它更早终止: 100 100毫秒,30s 是30秒。

Note
新索引默认有 1 个副本分片,这意味着为满足 规定数量 应该 需要两个活动的分片副本。 但是,这些默认的设置会阻止我们在单一节点上做任何事情。为了避免这个问题,要求只有当 number_of_replicas 大于1的时候,规定数量才会执行。

总结

在增删改的时候,也就是说如果像主例子那样,我们新建了一个索引,规定这个索引的主分片是2个,每个主分片有2个副分片,那么它能保证数据一致性的要求就是它有3 =(2+2)/2 + 1 个活动的副分片,如果我们这个时候只启动了两个节点,也就是这样子的运行:
Node1:R0 P1
Node2:R1 P0
此时只有2个副分片,这个时候无法保证在操作的时候保证主分片的值和所有副分片一致。
即最好启动3个节点。

取回一个文档

可以从主分片或者从其它任意副本分片检索检索文档 ,如下图所示 Figure 10, “取回单个文档”.
Elasticsearch(十)elasticsearch分布式文档存储

以下是从主分片或者副本分片检索文档的步骤顺序:

1、客户端向 Node 1 发送获取请求。

2、节点使用文档的 _id 来确定文档属于分片 0 。分片 0 的副本分片存在于所有的三个节点上。 在这种情况下,它将请求转发到 Node 2 。(为了读取请求,协调节点在每次请求的时候将选择不同的副本分片来达到负载均衡;通过轮询所有的副本分片。

3、Node 2 将文档返回给 Node 1 ,然后将文档返回给客户端。

在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。

总结:

在查询时,也就是说不像增删改操作那样必须到主分片执行,可以轮询访问所有的包含文档的主副分片,如果副分片此时不存在,也会再去访问主分片返回文档。此时如果返回了文档,此文档在副分片也是可查的了。

局部更新文档

如 Figure 11, “局部更新文档” 所示,update API 结合了先前说明的读取和写入模式 。

Elasticsearch(十)elasticsearch分布式文档存储

以下是部分更新一个文档的步骤:

  1. 客户端向 Node 1 发送更新请求。
  2. 它将请求转发到主分片所在的 Node 3 。
  3. Node 3 从主分片检索文档,修改 _source 字段中的 JSON ,并且尝试重新索引主分片的文档。 如果文档已经被另一个进程修改,它会重试步骤 3 ,超过 retry_on_conflict 次后放弃。
  4. 如果 Node 3 成功地更新文档,它将新版本的文档并行转发到 Node 1 和 Node 2 上的副本分片,重新建立索引。 一旦所有副本分片都返回成功, Node 3 向协调节点也返回成功,协调节点向客户端返回成功。

update API 还接受在 新建、索引和删除文档 章节中介绍的 routing 、 replication 、 consistency 和 timeout 参数。

基于文档的复制

当主分片把更改转发到副本分片时, 它不会转发更新请求。 相反,它转发完整文档的新版本。请记住,这些更改将会异步转发到副本分片,并且不能保证它们以发送它们相同的顺序到达。 如果Elasticsearch仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档。

多文档模式

mget 和 bulk API 的 模式类似于单文档模式。区别在于协调节点知道每个文档存在于哪个分片中。 它将整个多文档请求分解成 每个分片 的多文档请求,并且将这些请求并行转发到每个参与节点。

mget

协调节点一旦收到来自每个节点的应答,就将每个节点的响应收集整理成单个响应,返回给客户端,如 Figure 12, “使用 mget 取回多个文档” 所示。
Elasticsearch(十)elasticsearch分布式文档存储

以下是使用单个 mget 请求取回多个文档所需的步骤顺序:
1. 客户端向 Node 1 发送 mget 请求。
2. Node 1 为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上。一旦收到所有答复, Node 1 构建响应并将其返回给客户端。
可以对 docs 数组中每个文档设置 routing 参数。

bulk

Elasticsearch(十)elasticsearch分布式文档存储

bulk API 按如下步骤顺序执行:

  1. 客户端向 Node 1 发送 bulk 请求。
  2. Node 1 为每个节点创建一个批量请求,并将这些请求并行转发到每个包含主分片的节点主机。
  3. 主分片一个接一个按顺序执行每个操作。当每个操作成功时,主分片并行转发新文档(或删除)到副本分片,然后执行下一个操作。 一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功,协调节点将这些响应收集整理并返回给客户端。

bulk API 还可以在整个批量请求的最顶层使用 consistency 参数,以及在每个请求中的元数据中使用 routing 参数。

为什么是有趣的格式?

当我们早些时候在代价较小的批量操作章节了解批量请求时, 您可能会问自己, “为什么 bulk API 需要有换行符的有趣格式,而不是发送包装在 JSON 数组中的请求,例如 mget API?” 。
为了回答这一点,我们需要解释一点背景:在批量请求中引用的每个文档可能属于不同的主分片, 每个文档可能被分配给集群中的任何节点。这意味着批量请求 bulk 中的每个 操作 都需要被转发到正确节点上的正确分片。
如果单个请求被包装在 JSON 数组中,那就意味着我们需要执行以下操作:
• 将 JSON 解析为数组(包括文档数据,可以非常大)
• 查看每个请求以确定应该去哪个分片
• 为每个分片创建一个请求数组
• 将这些数组序列化为内部传输格式
• 将请求发送到每个分片
这是可行的,但需要大量的 RAM 来存储原本相同的数据的副本,并将创建更多的数据结构,Java虚拟机(JVM)将不得不花费时间进行垃圾回收。
相反,Elasticsearch可以直接读取被网络缓冲区接收的原始数据。 它使用换行符字符来识别和解析小的 action/metadata 行来决定哪个分片应该处理每个请求。
这些原始请求会被直接转发到正确的分片。没有冗余的数据复制,没有浪费的数据结构。整个请求尽可能在最小的内存中处理。