ES搜索(一)

Published: 16 Oct 2019 Category: ES

启动: elasticsearch &
测试: curl ‘http://localhost:9200/?pretty’

一、搜索

1.1 简单搜索

GET /megacorp/employee/1

能获取到ID为1的员工的文档。

GET /megacorp/employee/_search

在结尾使用关键字_search来取代原来的文档ID。响应内容的hits数组中包含了我们所有的三个文档。默认情况下搜索会返回前10个结果。

GET /megacorp/employee/_search?q=last_name:Smith

上例中,在请求中依旧使用_search关键字,然后将查询语句传递给参数q=。这样就可以得到所有姓氏为Smith的结果。

1.2 使用DSL语句查询

DSL(Domain Specific Language特定领域语言)以JSON请求体的形式出现。它允许你构建更加复杂、强大的查询。

GET /megacorp/employee/_search
{
    "query" : {
        "match" : {
            "last_name" : "Smith"
        }
    }
}

这样可以代替之前的关于“Smith”的查询。这个请求体使用JSON表示,其中使用了match语句。

1.3 全文搜索

全文搜索——一种传统数据库很难实现的功能。

下例搜索所有喜欢“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”。

相关性(relevance)的概念在Elasticsearch中非常重要,而这个概念在传统关系型数据库中是不可想象的,因为传统数据库对记录的查询只有匹配或者不匹配。

1.4 高亮搜索结果

很多应用喜欢从每个搜索结果中高亮(highlight)匹配到的关键字,这样用户可以知道为什么这些文档和查询相匹配。在Elasticsearch中高亮片段是非常容易的。

让我们在之前的语句上增加highlight参数:

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

当我们运行这个语句时,会命中与之前相同的结果,但是在返回结果中会有一个新的部分叫做highlight,这里包含了来自about字段中的文本,并且用<em></em>来标识匹配到的单词。

{
   ...
   "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>" <1>
               ]
            }
         }
      ]
   }
}

<1> 原有文本中高亮的片段.

二、索引

在Elasticsearch中,文档归属于一种类型(type),而这些类型存在于索引(index)中,我们可以画一些简单的对比图来类比传统关系型数据库:

Relational DB -> Databases -> Tables -> Rows -> Columns Elasticsearch -> Indices -> Types -> Documents -> Fields

形如:

PUT /{index}/{type}/{id}
{
  "field": "value",
  ...
} Elasticsearch的响应:

{
   "_index":    "website",
   "_type":     "blog",
   "_id":       "123",
   "_version":  1,
   "created":   true
}

这个索引中包含_index、_type和_id元数据,以及一个新元素:_version。Elasticsearch中每个文档都有版本号,每当文档变化(包括删除)都会使_version增加。

Elasticsearch集群可以包含多个索引(indices)(数据库),每一个索引可以包含多个类型(types)(表),每一个类型包含多个文档(documents)(行),然后每个文档包含多个字段(Fields)(列)。

索引是一个存储关联数据的地方。实际上,索引只是一个用来指向一个或多个分片(shards)的“逻辑命名空间(logical namespace)” .

一个分片(shard)是一个最小级别“工作单元(worker unit)” ,它只是保存了索引中所有数据的一部分。一个分片就是一个Lucene实例,并且它本身就是一个完整的搜索引擎。我们的文档存储在分片中,并且在分片中被索引,但是我们的应用程序不会直接与它们通信,取而代之的是,直接与索引通信。

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

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

复制分片只是主分片的一个副本,它可以防止硬件故障导致的数据丢失,同时可以提供读请求,比如搜索或者从别的shard取回文档。

当索引创建完成的时候,主分片的数量就固定了(默认情况下,一个索引被分配5个主分片),但是复制分片的数量可以随时调整。

三、聚合(aggregations)

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
            }
         ]
      }
   }
} 我们可以看到两个职员对音乐有兴趣,一个喜欢林学,一个喜欢运动。

四、分布式集群

Elasticsearch天生就是分布式的:它知道如何管理节点来提供高扩展和高可用。这意味着你的程序不需要关心这些。

只要第二个节点与第一个节点有相同的cluster.name(请看./config/elasticsearch.yml文件),它就能自动发现并加入第一个节点所在的集群。

在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
} status字段提供一个综合的指标来表示集群的的服务状况。三种颜色各自的含义:
颜色 意义
green 所有主要分片和复制分片都可用
yellow 所有主要分片可用,但不是所有复制分片都可用
red 不是所有的主要分片都可用

集群的健康状态yellow表示所有的主分片(primary shards)启动并且正常运行了——集群已经可以正常处理任何请求——但是复制分片(replica shards)还没有全部可用。事实上所有的三个复制分片现在都是unassigned状态——它们还未被分配给节点。在同一个节点上保存相同的数据副本是没有必要的,如果这个节点故障了,那所有的数据副本也会丢失。

扩展

主分片的数量在创建索引时已经确定。实际上,这个数量定义了能存储到索引里数据的最大数量(实际的数量取决于你的数据、硬件和应用场景)。然而,主分片或者复制分片都可以处理读请求——搜索或文档检索,所以数据的冗余越多,我们能处理的搜索吞吐量就越大。

复制分片的数量可以在运行中的集群中动态地变更,这允许我们可以根据需求扩大或者缩小规模。让我们把复制分片的数量从原来的1增加到2:

PUT /blogs/_settings
{
   "number_of_replicas" : 2
}

(在blogs这个索引上,为每一个主分片创建两个复分片)

五、数据

请记住_index、_type、_id三者唯一确定一个文档。

5.1 检查文档是否存在

如果你想做的只是检查文档是否存在——你对内容完全不感兴趣——使用HEAD方法来代替GET。HEAD请求不会返回响应体,只有HTTP头:

curl -i -XHEAD http://localhost:9200/website/blog/123

Elasticsearch将会返回200 OK状态如果你的文档存在:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Content-Length: 0 如果不存在返回404 Not Found:

curl -i -XHEAD http://localhost:9200/website/blog/124

HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=UTF-8
Content-Length: 0

当然,这只表示你在查询的那一刻文档不存在,但并不表示几毫秒后依旧不存在。另一个进程在这期间可能创建新文档。

5.2 更新整个文档

文档在Elasticsearch中是不可变的——我们不能修改他们。如果需要更新已存在的文档,我们可以使用《索引文档》章节提到的index API 重建索引(reindex) 或者替换掉它。

在内部,Elasticsearch已经标记旧文档为删除,并添加了一个完整的新文档。旧版本文档不会立即消失,但你也不能去访问它。Elasticsearch会在你继续索引更多数据时清理被删除的文档。

5.3 创建一个新文档

要想保证文档是新加入的,最简单的方式是使用POST方法让Elasticsearch自动生成唯一_id. (因为_index、_type、_id三者唯一确定一个文档)

有两种方式来检验是否创建成功。 第一种方法使用op_type查询参数:

PUT /website/blog/123?op_type=create
{ ... }

或者第二种方法是在URL后加/_create做为端点:

PUT /website/blog/123/_create
{ ... }

如果请求成功的创建了一个新文档,Elasticsearch将返回正常的元数据且响应状态码是201 Created。 如果没有创建成功,Elasticsearch将返回409 Conflict响应状态码,错误信息类似如下:

{
  "error" : "DocumentAlreadyExistsException[[website][4] [blog][123]:
             document already exists]",
  "status" : 409
}

5.4 删除文件

删除文档的语法模式与之前基本一致,只不过要使用DELETE方法:

DELETE /website/blog/123

如果文档未找到,我们将得到一个404 Not Found状态码。而且_version依然会增加。(说是为了确保在多节点间不同操作可以有正确的顺序。)

删除一个文档也不会立即从磁盘上移除,它只是被标记成已删除。Elasticsearch将会在你之后添加更多索引的时候才会在后台进行删除内容的清理。

5.5 检索多个文档

像Elasticsearch一样,检索多个文档依旧非常快。合并多个请求可以避免每个请求单独的网络开销。如果你需要从Elasticsearch中检索多个文档,相对于一个一个的检索,更快的方式是在一个请求中使用multi-get或者mgetAPI。

POST /_mget
{
   "docs" : [
      {
         "_index" : "website",
         "_type" :  "blog",
         "_id" :    2
      },
      {
         "_index" : "website",
         "_type" :  "pageviews",
         "_id" :    1,
         "_source": "views"
      }
   ]
}

响应体也包含一个docs数组,每个文档还包含一个响应,它们按照请求定义的顺序排列。

事实上,如果所有文档具有相同_index和_type,你可以通过简单的ids数组来代替完整的docs数组:

POST /website/blog/_mget
{
   "ids" : [ "2", "1" ]
}

#REF