Elasticsearch 学习:入门篇

Elasticsearch 是一个分布式搜索引擎,底层基于 Lucene 实现。Elasticsearch 屏蔽了 Lucene 的底层细节,提供了分布式特性,同时对外提供了 Restful API。Elasticsearch 以其易用性迅速赢得了许多用户,被用在网站搜索、日志分析等诸多方面。由于 ES 强大的横向扩展能力,甚至很多人也会直接把 ES 当做 NoSQL 来用。

本文主要记录了 ES 的一些必要的基础知识,也是自己在学习和使用 ES 的一些总结。当然,要系统和深入学习还是要依靠官方文档:Elasticsearch Reference 和不断地实践。

本文会涉及以下内容:

  1. ES 的基本概念讲解
  2. 如何通过 ES 增删数据以及批量修改
  3. ES 基本的查询和搜索功能、高亮关键词搜索以及多索引查询功能

基本概念

在正式学习,有一些名词和概念需要简单的了解下。

  • Document (文档)
  • Index (索引)
  • Type [已废弃]

Document (文档)

文档指的是用户提交给 ES 的一条数据。需要注意的是,这里的文档并非指的是一个纯字符串文本,在 ES 中文档指的是一条 JSON 数据。如果对 MongoDB 有了解的话,这里文档的含义和 MongoDB 中的基本类似。

JSON 数据中可以包含多个字段,这些字段可以类比为 MySQL 中每个表的字段。

例如:

{
  "message": "this is my blog",
  "author": "cyhone"
}

这样我们后期进行搜索和查询的时候,也可以分别针对 message 字段和 author 字段进行搜索。

Index (索引)

Index(索引) 可以理解为是文档的集合,同在一个索引中的文档共同建立倒排索引。

也有很多人会把索引类比于 MySQL 中 schema 的概念。但在 ES 中 Index 更加灵活,用起来也更加方便。

此外,提交给同一个索引中的文档,最好拥有相同的结构。这样对于 ES 来说,不管是存储还是查询,都更容易优化。

Type [已废弃]

Type 可以理解为是 Index 的子集,类似于 MySQL 中 schema 和 table 的关系。Type 原来存在的目的是为了在同一个 Index 存储异构数据。但其实 ES 中的索引用起来足够方便和灵活,对于异构数据,完全可以再建另外单独的 Index 存储。

所以在 Elasticsearch 的新版本中,已经逐步淡化和移除了 Type 的概念。在 7.0 版本中,对于每个 Index,ES 直接内置了一个 _doc 的 Type,且一个 Index 只能包含一个 Type。如果用户在添加数据时用到了其他 Type,则会报错。

所以不管是新旧版本,大家在使用 ES 的时候,也忘记 Type 这个存在就好,用 _doc 即可。


我们接下来看下如何在 ES 中存储和查询一个文档,也是常说的 CRUD 操作。

在 ES 中,用户的一切操作和行为都是围绕 REST 风格的 HTTP API 进行的。ES 中所有接口的语义都严格遵守 REST 规范。

新增 / 更新文档

要想搜索内容的前提肯定是先把内容交给 ES 进行存储和索引。

我们有两种方法向对应索引中新增文档:

通过 POST 新增文档

POST /es-test/_doc

{
  "message": "this is my blog",
  "author": "cyhone"
}

对于以上请求来说,我们通过 POST 把对应的数据存储在了索引 es-test 中。
这里需要注意的是,Index 并不需要提前建好。对于用户指定的 Index,如果不存在,ES 会自动建立对应的 Index。

通过 PUT 新增文档

PUT /es-test/_doc/1

{
  "message": "this is my blog",
  "author": "cyhone"
}

在上面例子里面,我们通过 PUT 在索引 es-test 中,新增了一条数据。与 POST 不一样的是,通过 PUT 新增数据需要手动指定该条数据的唯一 id。也就是上述的 /es-test/_doc/1 中的 1。这个唯一 id 不必要是数字,任何合法字符串均可。

POST 和 PUT 的行为都非常符合 REST 风格:

  1. PUT 保证幂等性。因此在提交的时候需要指定一个唯一 id,对于同一个唯一 id 来说,无论 PUT 多少次,ES 只会修改这个 id 对应文档的内容,而不会新增文档。
  2. POST 不保证幂等性。因此每次的 POST 请求都会在系统新增一条文档。对于新增的文档,系统会自动生成一个唯一 ID。

这也意味着,我们可以用 PUT + 指定唯一 id 的方式,来修改和更新文档。

删除文档

我们可以使用 DELETE 来删除一个文档。例如:

DELETE /es-test/_doc/1

DELETE 也是幂等性操作,在使用的时候也需要指定唯一 ID。

查询 / 搜索文档

查询和搜索文档相对来说非常复杂,不过这也是很多人使用 ES 的原因。作为一个搜索引擎,自然需要提供足够强大的查询功能。
本文仅介绍几种常用的查询方法,其他复杂的查询方式和聚合、分析等操作,以后会单独写一篇文章总结。

简单查询

我们可以通过以下语法,提供关键词,搜索所有字段进行查询。

GET /es-test/_search?q=blog

或者我们也可以指定查询某个字段,如下:

GET /es-test/_search
{
  "query":{
    "match": {
        "message": "elasticsearch"
    }
  }
}

以上例子中,我们指定查询 message 字段中包含有 elasticsearch 的文档。

分页查询

对于查询得到的结果,数目过多的情况下,es 默认会进行分页。分页主要有两个参数进行控制:

  • size 显示应该返回的结果数量,默认是 10
  • from 显示应该跳过的初始结果数量,默认是 0

我们可以通过直接在 url 中指定分页参数,如下:

GET /es-test/_search?size=5&from=10

也可以在请求体中指定分页参数,如下:

GET /es-test/_search
{
  "query":{
    "match": {
        "message": "elasticsearch"
    }
  },
  "size": 10,
  "from": 5
}

关键词高亮显示

我们通常自己开发搜索引擎的时候,往往需要对搜索结果中的关键词高亮这种功能。如下:
/img/es/highlight.png

ES 可以非常简单的实现关键词的高亮。我们可以构建如下请求体:

{
  "query": {
    "match": {
      "message": "blog"
    }
  },
  "highlight": {
    "fields": {
      "message": {}
    }
  }
}

其实就是增加一个 highlight 属性,里面指明了要高亮的字段。其返回的消息体如下:

{
  "took" : 41,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "es-test",
        "_type" : "_doc",
        "_id" : "PNyBzHABTSSzPOmql8i9",
        "_score" : 0.2876821,
        "_source" : {
          "message" : "this is my blog",
          "author" : "cyhone"
        },
        "highlight" : {
          "message" : [
            "this is my <em>blog</em>"
          ]
        }
      }
    ]
  }
}

在返回体中有一个 highlight 字段,里面对 message 字段进行高亮处理: 关键词使用了 <em></em> 标签包围了。

我们可以利用 css 修改对 <em> 标签的样式,以实现其关键词高亮效果。

多索引查询

在 ES 中可以非常方便地在多个索引中通过搜索文档。

例如你有两个索引: es-test-1es-test-2
你可以这样直接在 URL 中指明两个索引:

GET /es-test-1,es-test-2/_search

或者如下的模糊搜索的方式

GET /es-test-*/_search

如果有必要的话,甚至可以这样:

GET /a*, b*/_search

以上方式都可以在多个索引中同时搜索文档,把多个索引看做一个使用。

其实这也意味着,我们在存储的时候,没必要把所有的文档都存在一个 Index 中。
很常见的一个操作是,我们可以将文档按天分索引存储。例如: es-test-2020-03-11,es-test-2020-03-12 等,
在查询的时候,指定 es-test-* 查询即可,这样对外看来,文档似乎还是存储在一起,同时也减轻了 Index 的存储压力。(一个 ES 分片最多能存储 Integer.MAX_VALUE - 128 个文档)

批量操作

上文讲到的通过 POST、PUT 来新增或修改数据,都是基于单条数据的。但是我们知道网络 IO 是网络操作中最耗时的部分,对于大数据量写入的场景下,我们通常希望写入方可以提供批量修改的接口,以避免频繁的网络交互,更大限度地提升写入性能。

ES 当然也提供了批量修改的接口。在批量接口中,我们一次可以进行多个新增、更新和删除等修改行为的动作。例如:

POST _bulk
{"index" : { "_index" : "es-test"} }
{"message" : "this is my blog"}
{"create" : { "_index" : "es-test", "_id" : "3"} }
{"message" : "this is my blog"}
{"delete" : { "_index" : "es-test", "_id" : "2"} }
{"update" : {"_id" : "1", "_index" : "test"} }
{"message" : "this is my blog"}

以上这个批量操作有些复杂。里面包含了 4 种操作 indexcreatedeleteupdate
其中 indexcreateupdate 都包含两行,一行是具体的操作,一行是文档内容。

indexcreate 的区别在于,create 会携带一个唯一 id,如果该 id 存在,则插入失败。

动态映射

有一点值得注意的是,本文中的例子都是用了 message 字段来进行 match 搜索,如果换成字段名换成了其他,例如 content 可能就不行。

这是因为在我这边的 ES 有一个默认的动态映射,将长度低于 2048 的字符串认定为 keyword 类型。但是字段名是 message 的话,则为 text 类型。keyword 类型不进行分词处理,不适合进行关键词搜索处理。

这样就需要我们不得不关注 ES 的动态映射。此部分内容以后会再单独分一篇文章讲解。