Elasticsearch是一个基于Apache Lucene™的开源搜索引擎。无论在开源还是专有领域,Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。
但是,Lucene只是一个库。想要使用它,必须使用Java来作为开发语言并将其直接集成到你的应用中,并且Lucene非常复杂,需要深入了解检索的相关知识来理解它是如何工作的。
Elasticsearch也使用Java开发并使用Lucene作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的 RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。
不过,Elasticsearch不仅仅是Lucene和全文搜索,同时还具备以下功能:
而且,所有的这些功能被集成到一个服务里面,你的应用可以通过简单的 RESTful API、各种语言的客户端甚至命令行与之交互。
Elasticsearch非常容易上手。它提供了许多合理的缺省值,并对初学者隐藏了复杂的搜索引擎理论。它开箱即用(安装即可使用),只需很少的学习既可在生产环境中使用。
同时可以根据不同的问题领域定制Elasticsearch的高级特性,这一切都是可配置的,并且配置非常灵活。
节点**(node)是一个运行着的Elasticsearch实例。集群(cluster)**是一组具有相同cluster.name的节点集合,他们协同工作,共享数据并提供故障转移和扩展功能,当然一个节点也可以组成一个集群。
最好找一个合适的名字来替代cluster.name的默认值,比如你自己的名字,这样可以防止一个新启动的节点加入到相同网络中的另一个同名的集群中。
可以通过修改config/目录下的elasticsearch.yml文件,然后重启ELasticsearch来做到这一点。当Elasticsearch在前台运行可以使用Ctrl-C快捷键终止,或者你可以调用shutdown API来关闭:
curl -XPOST 'http://localhost:9200/_shutdown'
有两种方式,一种是Java API,一种是基于HTTP协议的
Elasticsearch为Java用户提供了两种内置客户端:
节点客户端(node client):
节点客户端以无数据节点(none data node)身份加入集群,换言之,它自己不存储任何数据,但是它知道数据在集群中的具体位置,并且能够直接转发请求到对应的节点上。
传输客户端(Transport client):
这个更轻量的传输客户端能够发送请求到远程集群。它自己不加入集群,只是简单转发请求给集群中的节点。
两个Java客户端都通过9300端口与集群交互,使用Elasticsearch传输协议(Elasticsearch Transport Protocol)。
集群中的节点之间也通过9300端口进行通信。如果此端口未开放,你的节点将不能组成集群。
Java客户端所在的Elasticsearch版本必须与集群中其他节点一致,否则,它们可能互相无法识别。
更多细节请关注后续文章
其他所有程序语言都可以使用RESTful API,通过9200端口的与Elasticsearch进行通信,你可以使用你喜欢的WEB客户端,事实上,你甚至可以通过curl命令与Elasticsearch通信。
Elasticsearch官方提供了多种程序语言的客户端——Groovy,Javascript,.NET,PHP,Perl,Python,以及 Ruby ——还有很多由社区提供的客户端和插件,所有这些可以在文档中找到。
向Elasticsearch发出的请求的组成部分与其它普通的HTTP请求是一样的:
curl -X<VERB> ':///?' -d ''
请求:
curl -XGET 'http://localhost:9200/_count?pretty' -d '
{
"query": {
"match_all": {}
}
}'
Elasticsearch返回一个类似 200 OK的HTTP状态码和JSON格式的响应主体(除了 HEAD 请求)。上面的请求会得到如下的JSON格式的响应主体:
{
"count" : 0,
"_shards" : {
"total" : 5,
"successful" : 5,
"failed" : 0
}
}
看不到HTTP头是因为我们没有让 curl显示它们,如果要显示,使用curl命令后跟 -i 参数:
在后面以及后续文章中,将简写curl请求中重复的部分,例如主机名和端口,还有curl 命令本身。
比如一个完整的请求如下:
curl -XGET 'localhost:9200/_count?pretty' -d '
{
"query": {
"match_all": {}
}
}'
我们将简写成这样:
GET /_count
{
"query": {
"match_all": {}
}
}
应用中的对象很少只是简单的键值列表,更多时候它拥有复杂的数据结构,比如包含日期、地理位置、另一个对象或者数组。
最终会把这些对象存储到数据库中。将这些数据保存到由行和列组成的关系数据库中:不得不拆散对象以适应表模式(通常一列表示一个字段),然后又不得不在查询的时候重建它们。
Elasticsearch是面向文档(document oriented)的,这意味着它可以存储整个对象或文档(document)。然而它不仅仅是存储,还会索引(index)每个文档的内容使之可以被搜索。
在Elasticsearch中,你可以对文档(而非成行成列的数据)进行索引、搜索、排序、过滤。这和传统的关系型数据库是不一样的,这也是Elasticsearch能够执行复杂的全文搜索的原因之一。
ELasticsearch使用Javascript对象符号**(JavaScript Object Notation),也就是JSON**,作为文档序列化格式。JSON现在已经被大多语言所支持,而且已经成为NoSQL领域的标准格式。
以下使用JSON文档来表示一个用户对象:
{
"email": "[email protected]",
"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并做索引要比在表结构中做相同的事情简单的多。
关于ES中的索引,目录,文档,检索等概念,让我们通过一个例子来了解一下。
加入由于项目需要我们创建一个员工目录,这个目录用于促进人文关怀和用于实时协同工作,所以它有以下不同的需求:
我们首先要做的是存储员工数据,每个文档代表一个员工。
在Elasticsearch中存储数据的行为就叫做索引(indexing),不过在索引之前,我们需要明确数据应该存储在哪里。
在Elasticsearch中,文档归属于一种类型(type),而这些类型存在于索引(index)中,为了更好地理解这两个概念,下列表格是Es和关系型数据库的类比:
Elasticsearch | Relational DB |
---|---|
Indices | Databases |
Types | Tables |
Documents | Rows |
Fields | Columns |
Elasticsearch集群可以包含多个索引**(indices)(数据库),每一个索引可以包含多个类型(types)(表),每一个类型包含多 个文档(documents)(行),然后每个文档包含多个字段(Fields)**(列)。
索引含义的区分:
上文可以看出索引(index)这个词在Elasticsearch中有着不同的含义,这里做一下区分:
索引(名词): 如上文所述,一个索引(index)就像是传统关系数据库中的数据库,它是相关文档存储的地方,index的复数是indices或indexes。
索引(动词):「索引一个文档」表示把一个文档存储到索引(名词)里,以便它可以被检索或者查询。这很像SQL中的 INSERT 关键字,差别是,如果文档已经存在,新的文档将覆盖旧的文档。
倒排索引: 传统数据库为特定列增加一个索引(例如B-Tree索引)来加速检索。Elasticsearch和Lucene使用一种叫做倒排索引(inverted index)的数据结构来达到相同目的。
默认情况下,文档中的所有字段都会被索引(拥有一个倒排索引),只有这样他们才是可被搜索的。 我们将会在倒排索引章节中更详细的讨论。
所以为了创建员工目录,我们将进行如下操作:
实际上这些都是很容易的(尽管看起来有许多步骤)。可以通过一个命令执行完成的操作:
PUT /megacorp/employee/1
{
"first_name" : "John",
"last_name" : "Smith",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
我们看到path: /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" ]
}
现在Elasticsearch中已经存储了一些数据,我们可以根据业务需求开始工作了。
第一个需求是能够检索单个员工的信息。
这对于Elasticsearch来说非常简单。我们只要执行HTTP GET请求并指出文档的“地址”——索引、类型和ID即可。根据这三部分信息,我们就可以返回原始JSON文档:
GET /megacorp/employee/1
响应的内容中包含一些文档的元信息,John Smith的原始JSON文档包含在 _source 字段中。
{
"_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" ]
}
}
我们通过HTTP方法** GET 来检索文档**,同样的,我们可以使用 DELETE 方法删除文档,使用 HEAD 方法检查某文档是否存在。如果想更新已存在的文档,我们只需再PUT 一次。
我们尝试一个最简单的搜索全部员工的请求:
GET /megacorp/employee/_search
可以看到这里依然使用 megacorp 索引和 employee 类型,但在结尾使用关键字 _search 来取代原来的文档ID。响应内容的 hits 数组中包含了我们所有的三个文档。默认情况下搜索会返回前10个结果。
{
"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",
"_type": "employee",
"_id": "1",
"_score": 1,
"_source": {
"first_name" : "John",
"last_name" : "Smith",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
},
{
"_index": "megacorp",
"_type": "employee",
"_id": "2",
"_score": 1,
"_source": {
"first_name" : "Jane",
"last_name" : "Smith",
"age" : 32,
"about" : "I like to collect rock albums",
"interests": [ "music" ]
}
}
]
},
}
注意:
响应内容不仅会告诉我们哪些文档被匹配到,而且这些文档内容完整的被包含在其中——我们在给用户展示搜索结果时 需要用到的所有信息都有了。
接下来,让我们搜索姓氏中包含“Smith”的员工。要做到这一点,我们将在命令行中使用轻量级的搜索方法。这种方法常被称作查询字符串(query string)搜索,因为我们像传递URL参数一样去传递查询语句:
GET /megacorp/employee/_search?q=last_name:Smit
在请求中依旧**使用 _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" ]
}
}
]
}
}
查询字符串搜索便于通过命令行完成特定(ad hoc)的搜索,但是它也有局限性。
Elasticsearch提供丰富且灵活的查询语言叫做DSL查询(Query DSL),它允许你构建更加复杂、强大的查询。
**DSL(Domain Specific Language特定领域语言)**以JSON请求体的形式出现。我们可以这样表示之前关于“Smith”的查询:
GET /megacorp/employee/_search {
"query" : {
"match" : {
"last_name" : "Smith"
}
}
}
会返回与之前查询相同的结果。可以看到我们不再使用查询字符串(query string)做为参数,而是使用请求体代替。这个请求体使用JSON表示,其中使用了** match 语句**(查询类型之一,具体我们以后会学到)
让搜索稍微再变的复杂一些:依旧想要找到姓氏为“Smith”的员工,但是我们只想得到年龄大于30岁的员工。
我们的语句将添加过滤器(filter),它使得我们高效率的执行一个结构化搜索:
GET /megacorp/employee/_search {
"query" : {
"filtered" : {
"filter" : {
"range" : {
"age" : { "gt" : 30 } <1>
}
},
"query" : {
"match" : {
"last_name" : "smith" <2>
}
}
}
}
}
现在不要担心语法太多,后面会详细讨论。只需要知道这里添加了一个过滤器(filter)用于执行区间搜索,然后重 复利用了之前的 match 语句。现在我们的搜索结果只显示了一个32岁且名字是“Jane Smith”的员工:
{
...
"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" ]
}
}
]
}
}
到目前为止搜索都很简单:搜索特定的名字,并筛选年龄。让我们尝试一种更高级的搜索,全文搜索——一种传统数据库很难实现的功能。
我们将会搜索所有喜欢“rock climbing”的员工:
GET /megacorp/employee/_search {
"query" : {
"match" : {
"about" : "rock climbing"
}
}
}
你可以看到我们使用了之前的 match 查询,从 about 字段中搜索"rock climbing",我们得到了两个匹配文档:
{
...
"hits": {
"total": 2,
"max_score": 0.16273327,
"hits": [
{
...
"_score": 0.16273327, <1>
"_source": {
"first_name": "John",
"about": "I love to go rock climbing",
...
}
},
{
...
"_score": 0.016878016, <2>
"_source": {
"first_name": "Jane",
"about": "I like to collect rock albums",
...
}
}
]
}
可以看到返回结果中的<1><2> 是结果相关性评分。
默认情况下,Elasticsearch根据结果相关性评分来对结果集进行排序。
所谓的**「结果相关性评分」**就是文档与查询条件的匹配程度。很显然,排名第一的 John Smith 的 about 字段明确的写到“rock climbing”。
但是为什么 Jane Smith 也会出现在结果里呢?原因是“rock”在她的 abuot 字段中被提及了。因为只有“rock”被提及而“climbing”没有,所以她的 _score 要低于John。
这个例子很好的解释了Elasticsearch如何在各种文本字段中进行全文搜索,并且返回相关性最大的结果集。相关性 (relevance)的概念在Elasticsearch中非常重要,而这个概念在传统关系型数据库中是不可想象的,因为传统数据库对记录的 查询只有匹配或者不匹配。
目前我们可以在字段中搜索单独的一个词。但是有时候想要确切的匹配若干个单词或者短语(phrases)。
例如我们想要查询同时包含"rock"和"climbing"(并且是相邻的)的员工记录。 要做到这个,我们只要将 match 查询变更为 match_phrase 查询即可:
GET /megacorp/employee/_search {
"query" : {
"match_phrase" :
{
"about" : "rock climbing"
}
}
}
这次返回的结果只包含John
很多应用喜欢从每个搜索结果中高亮(highlight)匹配到的关键字,这样用户可以知道为什么这些文档和查询相匹配。在Elasticsearch中高亮片段是非常容易的。
让我们在之前的语句上增加 highlight 参数:
GET /megacorp/employee/_search {
"query" : {
"match_phrase" : {
"about" : "rock climbing"
}
},
"highlight": {
"fields" : {
"about" : {}
}
}
}
我们运行这个语句时,会命中与之前相同的结果,但是在返回结果中会有一个新的部分叫做 highlight ,这里包含了来自about 字段中的文本,并且用 来标识匹配到的单词。
{
...
"about": [
"I love to go rock climbing" <1>
]
}
最后,还有一个需求要完成:允许管理者在职员目录中进行一些分析。
Elasticsearch有一个功能叫做聚合(aggregations),它允许你在数据上生成复杂的分析统计。它很像SQL中的 GROUP BY 但是功能更强大。
比如找到所有职员中最大的共同点(兴趣爱好)是什么:
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"的人最大的共同点(兴趣爱好),我们只需要增加合适的语句即可:
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 }
]
}
}
聚合也允许分级汇总。例如,让我们统计每种兴趣下职员的平均年龄:
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": "forestry",
"doc_count": 1,
"avg_age": {
"value": 35
}
"key": "sports",
"doc_count": 1,
"avg_age": {
"value": 25
}
}
]
}
聚合结果比之前的聚合结果要更加丰富。我们依然得到了兴趣以及数量(指具有该兴趣的员工人数)的列表,但是现在每 个兴趣额外拥有 avg_age 字段来显示具有该兴趣员工的平均年龄。
参考这里哦