Elasticsearch 入门介绍

292 阅读11分钟

是什么

Elasticsearch 使用 Java 开发并使用 Lucene 作为其核心来实现所有索引和搜索的功能,它的目的是通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。

特点:

  • 全文搜索
  • 分布式的实时文件存储,每个字段都被索引并可被搜索
  • 分布式的实时分析搜索引擎
  • 可以扩展到上百台服务器,处理PB级结构化或非结构化数据
  • 可以通过简单的 RESTful API 与各种开发语言交互
  • 上手简单,对初学者隐藏了复杂的搜索引擎理论
  • 配置灵活

安装

Elasticsearch 在安装之前要安装 Java ,然后从官网中下载 Elasticsearch 的压缩包,在本地的自定义的目录下进行解压即可。另外有一个 Elasticsearch 的管理和监控工具 Marvel ,它包含了一个叫做 Sense 的交互式控制台,使用户方便的通过浏览器直接与 Elasticsearch 进行交互。这个不是必须要安装的。

与Elasticsearch交互

  • 如果是用 Java 开发语言,那可以直接使用 Java API,Elasticsearch 为 Java 用户提供了两种内置客户端:节点客户端和传输客户端

  • 以 JSON 为数据交互格式的 RESTful API ,所有程序语言都可以使用 RESTful API ,通过默认的 9200 端口的与Elasticsearch 进行通信。向 Elasticsearch 发出的请求的组成部分与其它普通的HTTP请求是一样的:

      curl -X<VERB> '<PROTOCOL>://<HOST>:<PORT>/<PATH>?<QUERY_STRING>' -d '<BODY>'
      
      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": {}
          }
      }
      	'
    

面向文档

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

ELasticsearch 使用 JSON ,作为文档序列化格式。JSON 现在已经被大多语言所支持,而且已经成为NoSQL领域的标准格式。它简洁、简单且容易阅读。

JSON 文档来表示一个用户对象:
{
    "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"
}

索引

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

Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices   -> Types  -> Documents -> Fields
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" ]
}

其中 megacorp 是索引名,employee 是类型名, 1 是员工 ID。

然后可以继续试着添加若干个员工:

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" ]
}

搜索

  1. 执行 HTTP 的 GET 请求并指出文档的“地址”——索引、类型和 ID 既可:

     GET /megacorp/employee/1
    

    根据这三部分信息,我们就可以返回原始 JSON 文档,响应的内容中包含一些文档的元信息。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 一次。

  2. 尝试一个最简单的搜索全部员工的请求,与上一个命令不同指出是在结尾使用关键字 _search 来取代原来的文档 ID 。响应内容的 hits 数组中包含了我们所有的三个文档。默认情况下搜索会返回前 10 个结果:

     GET /megacorp/employee/_search
     
     {
        "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 的结果。

  3. 查询字符串搜索便于通过命令行完成特定的搜索,但是它也有局限性(参阅简单搜索章节)。Elasticsearch 提供丰富且灵活的查询语言叫做 DS L查询 (Query DSL) ,它允许你构建更加复杂、强大的查询。

    DSL (Domain Specific Language 特定领域语言) 以 JSON 请求体的形式出现。我们可以这样表示之前关于 Smith 的查询:

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

    返回的结果和之前查询的结果一样,但是这里不使用查询字符串 (query string) 做为参数,而是使用请求体代替。这个请求体使用 JSON 表示,其中使用了 match 语句。

  4. 如果需要结合更复杂条件进行搜索,则需要添加过滤器,如下:

     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)一致。

    结果中只显示了一个员工:

     {
        ...
        "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" ]
                 }
              }
           ]
        }
     }
    
  5. 到目前为止搜索都很简单:搜索特定的名字,通过年龄筛选。接下来可以使用一种更高级的搜索,全文搜索——一种传统数据库很难实现的功能。搜索所有喜欢 “rock climbing” 的员工:

     GET /megacorp/employee/_search
     {
         "query" : {
             "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" ]
                 }
              }
           ]
        }
     }
    

    默认情况下,Elasticsearch 根据结果相关性评分来对结果集进行排序,所谓的“结果相关性评分”就是文档与查询条件的匹配程度。排名第一的 John Smith 的 about 字段明确的写到 “rock climbing” 。但是为什么 Jane Smith 也会出现在结果里呢?原因是 “rock” 在她的 abuot 字段中被提及了。因为只有 “rock” 被提及而 “climbing” 没有,所以她的 _score 要低于 John 。 这个例子很好的解释了 Elasticsearch 如何在各种文本字段中进行全文搜索,并且返回相关性最大的结果集。相关性 (relevance) 的概念在 Elasticsearch 中非常重要,而这个概念在传统关系型数据库中是不可想象的,因为传统数据库对记录的查询只有匹配或者不匹配。

  6. 目前我们可以在字段中搜索单独的一个词,但是有时候你想要确切的匹配若干个单词或者短语 (phrases) 。例如我们想要查询同时包含 "rock" 和 "climbing" (并且是相邻的)的员工记录。 要做到这个,我们只要将 match 查询变更为 match_phrase 查询即可:

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

    毫无疑问,该查询返回John Smith的文档。

  7. 很多应用喜欢从每个搜索结果中高亮 (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 <em>rock</em> <em>climbing</em>" <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": "sports",
           "doc_count": 1,
           "avg_age": {
              "value": 25
           }
        }
     ]
  }

可以看出通过这个特性可以完成相当复杂的聚合工作,你可以处理任何类型的数据。

分布式的特性

Elasticsearch 在分布式概念上做了很大程度上的透明化,用户不需要知道任何关于分布式系统、分片、集群发现或者其他大量的分布式概念。既可以运行在你的笔记本上,也可以运行在拥有100个节点的集群上,其工作方式是一样的。

Elasticsearch 致力于隐藏分布式系统的复杂性。以下这些操作都是在底层自动完成的:

将你的文档分区到不同的容器或者分片 (shards) 中,它们可以存在于一个或多个节点中。
将分片均匀的分配到各个节点,对索引和搜索做负载均衡。
冗余每一个分片,防止硬件故障造成的数据丢失。
将集群中任意一个节点上的请求路由到相应数据所在的节点。
无论是增加节点,还是移除节点,分片都可以做到无缝的扩展和迁移。