Elasticsearch深入理解(一)

[x] Elasticsearch是一个实时分布式搜索和分析引擎。

  • 维基百科使用Elasticsearch提供全文搜索并高亮关键字,以及输入实时搜索(search-as-you-type)和搜索纠错(did-you-mean)等搜索建议功能。
  • StackOverflow结合全文搜索与地理位置查询,以及more-like-this功能来找到相关的问题和答案。
  • Github使用Elasticsearch检索1300亿行的代码。

    现在大部分数据库在提取可用知识方面显得异常无能。的确,它们能够通过时间戳或者精确匹配做过滤,但是它们能够进行全文搜索,处理同义词和根据相关性给文档打分吗?它们能根据同一份数据生成分析和聚合的结果吗?最重要的是,它们在没有大量工作进程(线程)的情况下能做到对数据的实时处理吗?
    这就是Elasticsearch存在的理由:Elasticsearch鼓励你浏览并利用你的数据,而不是让它烂在数据库里,因为在数据库里实在太难查询了。

[x] Elasticsearch也使用Java开发并使用Lucene作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。
[x] 不过,Elasticsearch不仅仅是Lucene和全文搜索,我们还能这样去描述它:
* 分布式的实时文件存储,每个字段都被索引并可被搜索
* 分布式的实时分析搜索引擎
* 可以扩展到上百台服务器,处理PB级结构化或非结构化数据

[ ] 集群和节点:节点(node)是一个运行着的Elasticsearch实例。集群(cluster)是一组具有相同cluster.name的节点集合,他们协同工作,共享数据并提供故障转移和扩展功能,当然一个节点也可以组成一个集群。

[ ] 与elasticsearch进行交互:
Elasticsearch为Java用户提供了两种内置客户端:
节点客户端(node client):节点客户端以无数据节点(none data node)身份加入集群,换言之,它自己不存储任何数据,但是它知道数据在集群中的具体位置,并且能够直接转发请求到对应的节点上。

传输客户端(Transport client):

这个更轻量的传输客户端能够发送请求到远程集群。它自己不加入集群,只是简单转发请求给集群中的节点。
两个Java客户端都通过9300端口与集群交互,使用Elasticsearch传输协议(Elasticsearch Transport Protocol)。集群中的节点之间也通过9300端口进行通信。如果此端口未开放,你的节点将不能组成集群。
spring.data.elasticsearch.cluster-name = cluster
elasticsearch.ip.nodes[0] = 10.118.29.158:9300
elasticsearch.ip.nodes[1] = 10.118.29.157:9300
elasticsearch.index = voicewords
elasticsearch.type = keywords

注意:Java客户端所在的Elasticsearch版本必须与集群中其他节点一致,否则,它们可能互相无法识别。

基于HTTP协议,以JSON为数据交互格式的RESTful API

其他所有程序语言都可以使用RESTful API,通过9200端口的与Elasticsearch进行通信,你可以使用你喜欢的WEB客户端,事实上,如你所见,你甚至可以通过curl命令与Elasticsearch通信。
curl -X ‘://:/?’ -d ‘’

  • VERB HTTP方法:GET, POST, PUT, HEAD, 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”: {}
}
}

[x] Elasticsearch是面向文档(document oriented)的,这意味着它可以存储整个对象或文档(document)。然而它不仅仅是存储,还会索引(index)每个文档的内容使之可以被搜索。在Elasticsearch中,你可以对文档(而非成行成列的数据)进行索引、搜索、排序、过滤。这种理解数据的方式与以往完全不同,这也是Elasticsearch能够执行复杂的全文搜索的原因之一。

ELasticsearch使用Javascript对象符号(JavaScript Object Notation),也就是JSON,作为文档序列化格式。JSON现在已经被大多语言所支持,而且已经成为NoSQL领域的标准格式。它简洁、简单且容易阅读。

[ ] 我们首先要做的是存储员工数据,每个文档代表一个员工。在Elasticsearch中存储数据的行为就叫做索引(indexing),不过在索引之前,我们需要明确数据应该存储在哪里。
在Elasticsearch中,文档归属于一种类型(type),而这些类型存在于索引(index)中,我们可以画一些简单的对比图来类比传统
关系型数据库:
Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices -> Types -> Documents -> Fields

Elasticsearch集群可以包含多个索引(indices)(数据库),每一个索引可以包含多个类型(types)(表),每一个类型包含多个文档(documents)(行),然后每个文档包含多个字段(Fields)(列)。
* 索引(名词) 如上文所述,一个索引(index)就像是传统关系数据库中的数据库,它是相关文档存储的地方,index的复数是indices 或indexes。
* 索引(动词) 「索引一个文档」表示把一个文档存储到索引(名词)里,以便它可以被检索或者查询。这很像SQL中的INSERT关键字,差别是,如果文档已经存在,新的文档将覆盖旧的文档。
* 倒排索引 传统数据库为特定列增加一个索引,例如B-Tree索引来加速检索。Elasticsearch和Lucene使用一种叫做倒排索引(inverted index)的数据结构来达到相同目的。
文档中的所有字段都会被索引(拥有一个倒排索引),只有这样他们才是可被搜索的。

所以为了创建员工目录,我们将进行如下操作:
* 为每个员工的文档(document)建立索引,每个文档包含了相应员工的所有信息。
* 每个文档的类型为employee。
* employee类型归属于索引megacorp。
* megacorp索引存储在Elasticsearch集群中。
插入员工id为1的员工信息:
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
查询员工id为1的员工信息:
GET /megacorp/employee/1
我们尝试一个最简单的搜索全部员工的请求:
+
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: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” ]
}
}
]
}
}
使用DSL语句查询

查询字符串搜索便于通过命令行完成特定(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>
}
}
}
}
}
* <1> 这部分查询属于区间过滤器(range filter),它用于查找所有年龄大于30岁的数据——gt为”greater than”的缩写。
* <2> 这部分查询与之前的match语句(query)一致。

全文搜索

到目前为止搜索都很简单:搜索特定的名字,通过年龄筛选。让我们尝试一种更高级的搜索,全文搜索——一种传统数据库很难实现的功能。
我们将会搜索所有喜欢“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”,
“last_name”: “Smith”,
“age”: 25,
“about”: “I love to go rock climbing”,
“interests”: [ “sports”, “music” ]
}
},
{

“_score”: 0.016878016, <2>
“_source”: {
“first_name”: “Jane”,
“last_name”: “Smith”,
“age”: 32,
“about”: “I like to collect rock albums”,
“interests”: [ “music” ]
}
}
]
}
}
* <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 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” ]
}
}
]
}
}

高亮我们的搜索

很多应用喜欢从每个搜索结果中高亮(highlight)匹配到的关键字,这样用户可以知道为什么这些文档和查询相匹配。在Elasticsearch中高亮片段是非常容易的。
让我们在之前的语句上增加highlight参数:
GET /megacorp/employee/_search
{
“query” : {
“match_phrase” : {
“about” : “rock climbing”
}
},
“highlight”: {
“fields” : {
“about” : {}
}
}
}
当我们运行这个语句时,会命中与之前相同的结果,但是在返回结果中会有一个新的部分叫做highlight,这里包含了来自about字段中的文本,并且用来标识匹配到的单词。
{

“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 rock climbing” <1>
]
}
}
]
}
}
* <1> 原有文本中高亮的片段
你可以在高亮章节阅读更多关于搜索高亮的部分。

Elasticsearch致力于隐藏分布式系统的复杂性。以下这些操作都是在底层自动完成的:
* 将你的文档分区到不同的容器或者分片(shards)中,它们可以存在于一个或多个节点中。
* 将分片均匀的分配到各个节点,对索引和搜索做负载均衡。
* 冗余每一个分片,防止硬件故障造成的数据丢失。
* 将集群中任意一个节点上的请求路由到相应数据所在的节点。
* 无论是增加节点,还是移除节点,分片都可以做到无缝的扩展和迁移。

集群内部工作方式

你在使用Elasticsearch的时候可以长时间甚至永远都不必担心分片、复制和故障转移——但是它会帮助你理解Elasticsearch内部的工作流程。
Elasticsearch用于构建高可用和可扩展的系统。扩展的方式可以是购买更好的服务器(纵向扩展(vertical scale or scaling up))或者购买更多的服务器(横向扩展(horizontal scale or scaling out))
    对于大多数数据库而言,横向扩展意味着你的程序将做非常大的改动才能利用这些新添加的设备。对比来说,Elasticsearch天生就是分布式的:它知道如何管理节点来提供高扩展和高可用。这意味着你的程序不需要关心这些。

空集群

如果我们启动一个单独的节点,它还没有数据和索引,这个集群看起来:
一个节点(node)就是一个Elasticsearch实例,而一个集群(cluster)由一个或多个节点组成,它们具有相同的cluster.name,它们协同工作,分享数据和负载。当加入新的节点或者删除一个节点时,集群就会感知到并平衡数据。
集群中一个节点会被选举为主节点(master),它将临时管理集群级别的一些变更,例如新建或删除索引、增加或移除节点等。主节点不参与文档级别的变更或搜索,这意味着在流量增长的时候,该主节点不会成为集群的瓶颈。任何节点都可以成为主节点。我们例子中的集群只有一个节点,所以它会充当主节点的角色。
+
做为用户,我们能够与集群中的任何节点通信,包括主节点。每一个节点都知道文档存在于哪个节点上,它们可以转发请求到相应的节点上。我们访问的节点负责收集各节点返回的数据,最后一起返回给客户端。这一切都由Elasticsearch处理。
集群健康

在Elasticsearch集群中可以监控统计很多信息,但是只有一个是最重要的:集群健康(cluster health)。集群健康有三种状态:green、yellow或red。
GET /_cluster/health
{
“cluster_name”: “elasticsearch”,
“status”: “green”, <1>
“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
}
* <1> status 是我们最感兴趣的字段
status字段提供一个综合的指标来表示集群的的服务状况。三种颜色各自的含义:
颜色
意义
green
所有主要分片和复制分片都可用
yellow
所有主要分片可用,但不是所有复制分片都可用
red
不是所有的主要分片都可用
添加索引

为了将数据添加到Elasticsearch,我们需要索引(index)——一个存储关联数据的地方。实际上,索引只是一个用来指向一个或多个分片(shards)的“逻辑命名空间(logical namespace)”.

一个分片(shard)是一个最小级别“工作单元(worker unit)”,它只是保存了索引中所有数据的一部分。在接下来的《深入分片》一章,我们将详细说明分片的工作原理,但是现在我们只要知道分片就是一个Lucene实例,并且它本身就是一个完整的搜索引擎。我们的文档存储在分片中,并且在分片中被索引,但是我们的应用程序不会直接与它们通信,取而代之的是,直接与索引通信。
+

分片是Elasticsearch在集群中分发数据的关键。把分片想象成数据的容器。文档存储在分片中,然后分片分配到你集群中的节点上。当你的集群扩容或缩小,Elasticsearch将会自动在你的节点间迁移分片,以使集群保持平衡。

分片可以是主分片(primary shard)或者是复制分片(replica shard)。你索引中的每个文档属于一个单独的主分片,所以主分片的数量决定了索引最多能存储多少数据。

理论上主分片能存储的数据大小是没有限制的,限制取决于你实际的使用情况。分片的最大容量完全取决于你的使用状况:硬件存储的大小、文档的大小和复杂度、如何索引和查询你的文档,以及你期望的响应时间。
复制分片只是主分片的一个副本,它可以防止硬件故障导致的数据丢失,同时可以提供读请求,比如搜索或者从别的shard取回文档。

当索引创建完成的时候,主分片的数量就固定了,但是复制分片的数量可以随时调整。

让我们在集群中唯一一个空节点上创建一个叫做blogs的索引。默认情况下,一个索引被分配5个主分片,但是为了演示的目的,我们只分配3个主分片和一个复制分片(每个主分片都有一个复制分片):

PUT /blogs
{
“settings” : {
“number_of_shards” : 3,
“number_of_replicas” : 1
}
}
附带索引的单一节点集群:
我们的集群现在看起来就像上图——三个主分片都被分配到Node 1。如果我们现在检查集群健康(cluster-health),我们将见到以下信息:
{
“cluster_name”: “elasticsearch”,
“status”: “yellow”, <1>
“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 <2>
}
* <1> 集群的状态现在是 yellow
* <2> 我们的三个复制分片还没有被分配到节点上
集群的健康状态yellow表示所有的主分片(primary shards)启动并且正常运行了——集群已经可以正常处理任何请求——但是复制分片(replica shards)还没有全部可用。事实上所有的三个复制分片现在都是unassigned状态——它们还未被分配给节点。在同一个节点上保存相同的数据副本是没有必要的,如果这个节点故障了,那所有的数据副本也会丢失。
增加故障转移

在单一节点上运行意味着有单点故障的风险——没有数据备份。幸运的是,要防止单点故障,我们唯一需要做的就是启动另一个节点。
启动第二个节点

为了测试在增加第二个节点后发生了什么,你可以使用与第一个节点相同的方式启动第二个节点(《运行Elasticsearch》一章),而且命令行在同一个目录——一个节点可以启动多个Elasticsearch实例。
只要第二个节点与第一个节点有相同的cluster.name(请看./config/elasticsearch.yml文件),它就能自动发现并加入第一个节点所在的集群。如果没有,检查日志找出哪里出了问题。这可能是网络广播被禁用,或者防火墙阻止了节点通信。
如果我们启动了第二个节点:
双节点集群——所有的主分片和复制分片都已分配:
第二个节点已经加入集群,三个复制分片(replica shards)也已经被分配了——分别对应三个主分片,这意味着在丢失任意一个节点的情况下依旧可以保证数据的完整性。
文档的索引将首先被存储在主分片中,然后并发复制到对应的复制节点上。这可以确保我们的数据在主节点和复制节点上都可以被检索。
cluster-health现在的状态是green,这意味着所有的6个分片(三个主分片和三个复制分片)都已可用:
{
“cluster_name”: “elasticsearch”,
“status”: “green”, <1>
“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
}
* <1> 集群的状态是green.
我们的集群不仅是功能完备的,而且是高可用的。

横向扩展

随着应用需求的增长,我们该如何扩展?如果我们启动第三个节点,我们的集群会重新组织自己,就像包含3个节点的集群——分片已经被重新分配以平衡负载:
Node3包含了分别来自Node 1和Node 2的一个分片,这样每个节点就有两个分片,和之前相比少了一个,这意味着每个节点上的分片将获得更多的硬件资源(CPU、RAM、I/O)。
分片本身就是一个完整的搜索引擎,它可以使用单一节点的所有资源。我们拥有6个分片(3个主分片和三个复制分片),最多可以扩展到6个节点,每个节点上有一个分片,每个分片可以100%使用这个节点的资源。

继续扩展

如果我们要扩展到6个以上的节点,要怎么做?
主分片的数量在创建索引时已经确定。实际上,这个数量定义了能存储到索引里数据的最大数量(实际的数量取决于你的数据、硬件和应用场景)。然而,主分片或者复制分片都可以处理读请求——搜索或文档检索,所以数据的冗余越多,我们能处理的搜索吞吐量就越大。
复制分片的数量可以在运行中的集群中动态地变更,这允许我们可以根据需求扩大或者缩小规模。让我们把复制分片的数量从原来的1增加到2:
PUT /blogs/_settings
{
“number_of_replicas” : 2
}
https://raw.githubusercontent.com/looly/elasticsearch-definitive-guide-cn/master/images/elas_0205.png
图:增加number_of_replicas到2:
从图中可以看出,blogs索引现在有9个分片:3个主分片和6个复制分片。这意味着我们能够扩展到9个节点,再次变成每个节点一个分片。这样使我们的搜索性能相比原始的三节点集群增加三倍。
当然,在同样数量的节点上增加更多的复制分片并不能提高性能,因为这样做的话平均每个分片的所占有的硬件资源就减少了(译者注:大部分请求都聚集到了分片少的节点,导致一个节点吞吐量太大,反而降低性能),你需要增加硬件来提高吞吐量。
不过这些额外的复制节点使我们有更多的冗余:通过以上对节点的设置,我们能够承受两个节点故障而不丢失数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值