上一篇文章中我们已经启动 Elasticsearch 了,接下来我们就可以与 Elasticsearch 进行通信,例如插入数据、检索数据、删除数据等等。Elasticsearch 提供了Java API和 RESTful API 两种方式来与之通信。
Java API
如果你使用的是 Java,Elasticsearch 内置了两个客户端,你可以在你的代码中使用:
节点客户端: 节点客户端以一个 无数据节点 的身份加入了一个集群。换句话说,它自身是没有任何数据的,但是他知道什么数据在集群中的哪一个节点上,然后就可以请求转发到正确的节点上并进行连接。
传输客户端: 更加轻量的传输客户端可以被用来向远程集群发送请求。他并不加入集群本身,而是把请求转发到集群中的节点。
这两个客户端都使用 Elasticsearch 的 传输 协议,通过 9300 端口与 java 客户端进行通信。集群中的各个节点也是通过 9300 端口进行通信。如果这个端口被禁止了,那么你的节点们将不能组成一个集群。
注意事项
Java 的客户端的版本号必须要与 Elasticsearch 节点所用的版本号一样,不然他们之间可能无法识别。
更多关于 Java API 的说明可以在这里找到 Guide
通过 HTTP 向 RESTful API 传送 json
其他的语言可以通过 9200 端口与 Elasticsearch 的 RESTful API 进行通信。事实上如你所见,你甚至可以使用行命令 curl 来与 Elasticsearch 通信。
向 Elasticsearch 发出的请求和其他所有的 HTTP 请求的组成部分是一致的。例如,计算集群中文件的数量,我们就可以使用:
curl -XGET 'http://localhost:9200/_count?pretty' -d '{ "query": { "match_all": {} }}'123456712345671234567
- 相应的 HTTP 请求方法 或者 变量 : GET, POST, PUT, HEAD 或者 DELETE。
- 集群中任意一个节点的访问协议、主机名以及端口。
- 请求的路径。
- 任意一个查询后再加上 ?pretty 就可以生成 更加美观 的 JSON 反馈,以增强可读性。
- 一个 JSON 编码的请求主体(如果需要的话)。
Elasticsearch 将会返回一个 HTTP 状态码类似于 ‘200 OK’,以及一个 JSON 格式的主体(除了单纯的 ‘HEAD’ 请求),上面的请求会得到下方的 JSON 主体:
{ "count" : 0, "_shards" : { "total" : 5, "successful" : 5, "failed" : 0 }}123456781234567812345678
在反馈中,我们并没有看见 HTTP 的头部信息,因为我们没有告知 curl 显示这些内容。如果你想看到头部信息,可以在使用 curl 命令的时候再加上 -i 这个参数:
curl -i -XGET 'http://localhost:9200/_count'
基本概念
Elasticsearch 是 面向文档型数据库,这意味着它存储的是整个对象或者 文档,它不但会存储它们,还会为他们建立索引,这样你就可以搜索他们了。你可以在 Elasticsearch 中索引、搜索、排序和过滤这些文档。不需要成行成列的数据。这将会是完全不同的一种面对数据的思考方式,这也是为什么 Elasticsearch 可以执行复杂的全文搜索的原因。
Elasticsearch 使用 JSON (或称作JavaScript Object Notation ) 作为文档序列化的格式。JSON 已经被大多数语言支持,也成为 NoSQL 领域的一个标准格式。它简单、简洁、易于阅读。
例如我们有一个 User 对象,我们可以构造这样的一个 JSON 对象:
{ "email": "john@smith.com", "first_name": "John", "last_name": "Smith", "about": { "bio": "Eco-warrior and defender of the weak", "age": 25, "interests": [ "dolphins", "whales" ] }, "join_date": "2014/05/01",}123456789101112345678910111234567891011
虽然 user 这个对象非常复杂,但是它的结构和含义都被保留到 JSON 中了。在 Elasticsearch 中,将对象转换为 JSON 并作为索引要比在表结构中做相同的事情简单多了。
在 Elasticsearch 中,文档属于一种 类型(type),各种各样的类型存在于一个 索引 中。你也可以通过类比传统的关系数据库得到一些大致的相似之处:
关系数据库 ⇒ 数据库 ⇒ 表 ⇒ 行 ⇒ 列(Columns)Elasticsearch ⇒ 索引 ⇒ 类型 ⇒ 文档 ⇒ 字段(Fields)121212
一个 Elasticsearch 集群可以包含多个 索引(数据库),也就是说其中包含了很多 类型(表)。这些类型中包含了很多的 文档(行),然后每个文档中又包含了很多的 字段(列)。
易混淆点
索引这个词汇已经被赋予了太多意义,所以在这里我们有必要澄清一下:
- 索引 (名词)
如上文所说,一个 索引 就类似于传统关系型数据库中的 数据库。这里就是存储相关文档的的地方。
- 索引 (动词)
为一个文档创建索引 是把一个文档存储到一个索引(名词)中的过程,这样它才能被检索。这个过程非常类似于 SQL 中的 INSERT
命令,如果已经存在文档,新的文档将会覆盖旧的文档。
- 反向索引
在关系数据库中的某列添加一个 索引,比如多路搜索树(B-Tree)索引,就可以加速数据的取回速度, Elasticsearch 以及
Lucene 使用的是一个叫做 反向索引(inverted index) 的结构来实现相同的功能。
示例
想象我们正在为一个名叫 megacorp 的公司的 HR 部门制作一个新的员工名单系统,这些名单应该可以满足实时协同工作,所以它应该可以满足以下要求:
- 数据可以包含多个值的标签、数字以及纯文本内容。
- 可以检索任何职员的所有数据。
- 允许结构化搜索。例如,查找 30 岁以上的员工。
- 允许简单的全文搜索以及相对复杂的短语搜索。
- 在返回的匹配文档中高亮关键字。
- 拥有数据统计与管理的后台。
数据存储
第一步就是存储员工的数据。这样你就需要一个“员工档案”的表单,这样每个文档都代表着一个员工。在 Elasticsearch 中,每个文档都属于一种 类型(type),各种各样的类型存在于一个 索引 中。
所以为了创建员工名单,我们需要进行如下操作:
- 为每一个员工的 文档 创建索引,每个 文档 都包含了一个员工的所有信息。
- 每个文档都会被标记为 employee 类型。
- 这种类型将存活在 megacorp 这个 索引 中。
- 这个索引将会存储在 Elasticsearch 的集群中
在实际的操作中,这些操作是非常简单的(即使看起来有这么多步骤)。我们可以把如此之多的操作通过一个命令来完成:
curl -XPUT 'http://localhost:9200/megacorp/employee/1' -d '{ "first_name": "John", "last_name": "Smith", "age": 25, "about": "I love to go rock climbing", "interests": [ "sports", "music" ], "join_time": "2014-11-24"}'123456789101112123456789101112123456789101112
注意在 /megacorp/employee/1 路径下,包含了三个部分:
名字 | 内容 |
---|---|
megacorp | 索引的名字 |
employee | 类型的名字 |
1 | 当前员工的 ID |
请求部分,也就是 JSON 文档,在这里包含了关于这名员工的所有信息。他的名字是 “Douglas Fir”,他已经 25 岁了,他很喜欢攀岩。
我们在操作前不需要进行任何管理操作,Elasticsearch 会自动检测数据结构和类型、创建索引 并让其能够被搜检索,所有的一切操作已经在后台被默默地完成了,非常简单吧!
在进行下一步之前,我们再为这个目录添加更多的员工信息吧:
curl -XPUT 'http://localhost:9200/megacorp/employee/2' -d '{ "first_name" : "Jane", "last_name" : "Smith", "age" : 32, "about" : "I like to collect rock albums", "interests": [ "music" ], "join_time":"2012-10-15"}123456789123456789123456789
curl -XPUT 'http://localhost:9200/megacorp/employee/3' -d '{ "first_name" : "Douglas", "last_name" : "Fir", "age" : 25, "about": "I like to build cabinets", "interests": [ "forestry" ], "join_time":"2016-01-24"}123456789123456789123456789
数据检索
现在,我们已经在 Elasticsearch 中存储了一些数据,我们可以开始根据这个项目的需求进行工作了。第一个需求就是要能搜索每一个员工的数据。
1.根据 id 请求
对于 Elasticsearch 来说,这是非常简单的。我们只需要执行一次 HTTP GET 请求,然后指出文档的地址,也就是索引、类型以及 ID 即可。通过这三个部分,我们就可以得到原始的 JSON 文档:
curl XGET 'http://localhost:9200/megacorp/employee/1'
返回的内容包含了这个文档的元数据信息,而 John Smith 的原始 JSON 文档也在 _source 字段中出现了:
{ "_index": "megacorp", "_type": "employee", "_id": "1", "_version": 2, "found": true, "_source": { "first_name": "John", "last_name": "Smith", "age": 25, "about": "I love to go rock climbing", "interests": [ "sports", "music" ], "join_time": "2014-11-24" }}123456789101112131415161718123456789101112131415161718123456789101112131415161718
2.简易检索
我们首先要完成一个最简单的搜索命令来搜索全部员工:
curl XGET 'http://localhost:9200/megacorp/employee/_search'
你可以发现我们正在使用 megacorp 索引,employee 类型,但是我们我们并没有指定文档的 ID,我们现在使用的是 _search 端口。你可以再返回的 hits 中发现我们录入的三个文档。搜索会默认返回最前的 10 个数值。
{ "took": 26, "timed_out": false, "_shards": { "total": 2, "successful": 2, "failed": 0 }, "hits": { "total": 3, "max_score": 1, "hits": [ { "_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" ], "join_time": "2014-11-24" } }, { "_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" ], "join_time": "2014-11-24" } }, { "_index": "megacorp", "_type": "employee", "_id": "5", "_score": 1, "_source": { "first_name": "Douglas", "last_name": "Fir", "age": 35, "about": "I like to build cabinets", "interests": [ "forestry" ], "join_time": "2012-12-24" } } ] }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263641234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636412345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
反馈值中不仅会告诉你匹配到哪些文档,同时也会把这个文档都会包含到其中:我们需要搜索的用户的所有信息。
接下来,我们将要尝试着实现搜索一下哪些员工的姓氏中包含 Smith。为了实现这个,我们需要使用一种轻量的搜索方法。这种方法经常被称做 查询字符串(query string) 搜索,因为我们通过 URL 来传递查询的关键字:
curl -XGET 'http://localhost:9200/megacorp/employee/_search?q=last_name:Smith'
我们依旧使用 _search 端口,然后可以将参数传入给 q=。这样我们就可以得到姓 Smith 的结果:
{ "took": 2, "timed_out": false, "_shards": { "total": 2, "successful": 2, "failed": 0 }, "hits": { "total": 2, "max_score": 1, "hits": [ { "_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" ], "join_time": "2014-11-24" } }, { "_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" ], "join_time": "2014-11-24" } } ] }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748123456789101112131415161718192021222324252627282930313233343536373839404142434445464748123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
3.使用 Query DSL 搜索
查询字符串是通过命令语句完成 点对点(ad hoc) 的搜索,但是这也有它的局限性(可参阅《搜索局限性》章节)。Elasticsearch 提供了更加丰富灵活的查询语言,它被称作 Query DSL,通过它你可以完成更加复杂、强大的搜索任务。
DSL (Domain Specific Language 领域特定语言) 需要使用 JSON 作为主体,我们还可以这样查询姓 Smith 的员工:
curl -XGET 'http://localhost:9200/megacorp/employee/_search' -d{ "query" : { "match" : { "last_name" : "Smith" } }}123456781234567812345678
这个请求会返回同样的结果。你会发现我们在这里没有使用 查询字符串,而是使用了一个由 JSON 构成的请求体,其中使用了 match 查询法,随后我们还将会学习到其他的查询类型。
更加复杂的搜索
接下来,我们再提高一点儿搜索的难度。我们依旧要寻找出姓 Smith 的员工,但是我们还将添加一个年龄大于 30 岁的限定条件。我们的查询语句将会有一些细微的调整来以识别结构化搜索的限定条件 filter(过滤器):
curl -XGET 'http://localhost:9200/megacorp/employee/_search' -d{ "query" : { "filtered" : { "filter" : { "range" : { "age" : { "gt" : 30 } <1> } }, "query" : { "match" : { "last_name" : "Smith" <2> } } } }}123456789101112131415161712345678910111213141516171234567891011121314151617
[root@w03 cluster_cn]# curl -XGET 'http://localhost:9200/megacorp/employee/_search' -d '{"query":{"filtered":{"filter":{"range":{"age":{"gt":30}}},"query":{"match":{"last_name":"Smith"}}}}}'
这一部分的语句是 range filter ,它可以查询所有超过 30 岁的数据 – gt 代表 greater than (大于)。
这一部分我们前一个操作的 match query 是一样的
先不要被这么多的语句吓到,我们将会在之后带你逐渐了解他们的用法。你现在只需要知道我们添加了一个 filter,可以在 match 的搜索基础上再来实现区间搜索。现在,我们的只会显示 32 岁的名为 Jane Smith 的员工了:
{ "took": 3, "timed_out": false, "_shards": { "total": 2, "successful": 2, "failed": 0 }, "hits": { "total": 1, "max_score": 1, "hits": [ { "_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" ], "join_time": "2014-11-24" } } ] }}123456789101112131415161718192021222324252627282930313212345678910111213141516171819202122232425262728293031321234567891011121314151617181920212223242526272829303132
全文搜索
上面的搜索都很简单:名字搜索、通过年龄过滤。接下来我们来学习一下更加复杂的搜索,全文搜索——一项在传统数据库很难实现的功能。 我们将会搜索所有喜欢 rock climbing 的员工:
curl -XGET ‘http://localhost:9200/megacorp/employee/_search’ -d
{
“query” : {
“match” : {
“about” : “rock climbing”
}
}
}
[root@w03 cluster_cn]# curl -XGET 'http://localhost:9200/megacorp/employee/_search' -d '{"query":{"match":{"about":"rock climbing"}}}'
你会发现我们同样使用了 match 查询来搜索 about 字段中的 rock climbing。我们会得到两个匹配的文档:
{ "took": 7, "timed_out": false, "_shards": { "total": 2, "successful": 2, "failed": 0 }, "hits": { "total": 2, "max_score": 0.6468432, "hits": [ { "_index": "megacorp", "_type": "employee", "_id": "1", "_score": 0.6468432, "_source": { "first_name": "John", "last_name": "Smith", "age": 25, "about": "I love to go rock climbing", "interests": [ "sports", "music" ], "join_time": "2014-11-24" } }, { "_index": "megacorp", "_type": "employee", "_id": "2", "_score": 0.108701006, "_source": { "first_name": "Jane", "last_name": "Smith", "age": 32, "about": "I like to collect rock albums", "interests": [ "music" ], "join_time": "2014-11-24" } } ] }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748123456789101112131415161718192021222324252627282930313233343536373839404142434445464748123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
相关评分
通常情况下,Elasticsearch 会通过相关性来排列顺序,第一个结果中,John Smith 的 about 字段中明确地写到 rock climbing。而在 Jane Smith 的 about 字段中,提及到了 rock,但是并没有提及到 climbing,所以后者的 _score 就要比前者的低。
这个例子很好地解释了 Elasticsearch 是如何执行全文搜索的。对于 Elasticsearch 来说,相关性的概念是很重要的,而这也是它与传统数据库在返回匹配数据时最大的不同之处。
段落搜索
能够找出每个字段中的独立单词固然很好,但是有的时候你可能还需要去匹配精确的短语或者 段落。例如,我们只需要查询到 about 字段只包含 rock climbing 的短语的员工。
为了实现这个效果,我们将对 match 查询变为 match_phrase 查询:
curl -XGET 'http://localhost:9200/megacorp/employee/_search' -d{ "query" : { "match_phrase" : { "about" : "rock climbing" } }}123456781234567812345678
[root@w03 cluster_cn]# curl -XGET 'http://localhost:9200/megacorp/employee/_search' -d '{"query":{"match_phrase":{"about":"rock climbing"}}}'
这样,系统会没有异议地返回 John Smith 的文档:
{ "took": 2, "timed_out": false, "_shards": { "total": 2, "successful": 2, "failed": 0 }, "hits": { "total": 1, "max_score": 0.9020494, "hits": [ { "_index": "megacorp", "_type": "employee", "_id": "1", "_score": 0.9020494, "_source": { "first_name": "John", "last_name": "Smith", "age": 25, "about": "I love to go rock climbing", "interests": [ "sports", "music" ], "join_time": "2014-11-24" } } ] }}123456789101112131415161718192021222324252627282930313212345678910111213141516171819202122232425262728293031321234567891011121314151617181920212223242526272829303132
OK,今天先到这里结束了,下一篇文章会介绍如何使用 ElasticSearch Java API来实现上述的功能。