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.webp

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-1 和 es-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 种操作 index、create、delete 和 update。 其中 index、create 和 update 都包含两行,一行是具体的操作,一行是文档内容。

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

动态映射

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

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

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