工程

Elasticsearch 实用介绍

编者按(2021 年 8 月 3 日):这篇文章使用了已弃用的功能。有关当前说明,请参阅使用反向地理编码映射定制区域文档。

为什么要写这篇文章?

我最近有幸在拉科鲁尼亚大学教授一门硕士课程,课程名称是信息检索和语义网。这门课程的重点是让学生对 Elasticsearch 有个总体认识,以便他们能够在完成课程作业时开始使用 Elasticsearch。课程学员既有熟悉 Lucene 的人,也有第一次接触信息检索概念的人。由于上课时间很晚(晚上 7:30 才开始),如何让学员保持专注(或者换句话说,不让他们睡着!)就成了一个问题。有两种基本方法可以在教学时保持注意力,那就是向学员分发巧克力(但我今天忘记带了)以及让课程尽可能实用一些。

这就是今天这篇文章所提到的内容:我们将学习这门课程的实践部分。学习 Elasticsearch 中的每一个命令或每一个请求不是我们的目标(不然要文档有什么用)。相反,我们的目标是通过 30-60 分钟的指导教程,让您体验在没有先验知识的情况下使用 Elasticsearch 的乐趣。您只需复制粘贴每个请求来查看结果,并尝试找出所提出问题的解决方案。

谁将从这篇文章中受益?

我将通过展示 Elastic 的基本功能来介绍一些主要概念,有时还会介绍技术性更强的复杂概念,并添加相关文档链接以供进一步参考(但请记住:链接是为了进一步参考。您可以继续研读示例,稍后再看文档)。如果您以前没有使用过 Elasticsearch,想看看它的实际操作,还想成为实际负责人,这篇文章正好适合您。如果您已经有过使用 Elasticsearch 的经验,不妨看一下我们将要使用的数据集:当朋友问起 Elasticsearch 的功能时,用搜索莎士比亚戏剧的例子来说明会更加容易!

我们将介绍和不介绍哪些内容?

首先,我们会添加一些文档并对它们进行搜索和移除。之后,再使用莎士比亚的数据集来提供更多关于搜索和聚合的见解。这是一篇“我想现在就看到它开始发挥作用”的实操文章。

请注意,我们不会介绍与生产部署中的配置或最佳实践相关的任何内容。因此,请使用文中的信息来了解 Elasticsearch 提供的功能,毕竟这是一个起点,可以据此想象它如何满足您的需求。

设置

首先,您需要 Elasticsearch。请按照 文档说明 下载最新版本,然后安装并启动。说白了,您需要一个最新版本的 Java,下载并安装适用于您的操作系统的 Elasticsearch,最后使用默认值 bin/elasticsearch 启动它。在本课中,我们将使用目前可用的最新版本 5.5.0。

接下来,您需要与 Elasticsearch 通信:对 REST API 发出 HTTP 请求即可。Elastic 默认在端口 9200 中启动。如要访问,您可以使用最适合自己专业知识的工具:有命令行工具(如适用于 Linux 的 curl)、适用于 Chrome 或 Firefox 的 Web 浏览器 REST 插件,您也可以安装 Kibana 并使用 控制台插件。每个请求由一个 HTTP 动词(GET、POST、PUT)、一个 URL 终端和一个可选的主体组成;在大多数情况下,主体是一个 JSON 对象。

举个例子,为了确认 Elasticsearch 已启动,我们对基本 URL 执行一个 GET 操作以访问基本终端(不需要主体):

GET localhost:9200

响应应该类似于以下内容。由于我们没有配置任何内容,我们的实例名称将是一个随机由 7 个字母组成的字符串:

{
    "name": "t9mGYu5",
    "cluster_name": "elasticsearch",
    "cluster_uuid": "xq-6d4QpSDa-kiNE4Ph-Cg",
    "version": {
        "number":"5.5.0",
        "build_hash":"260387d",
        "build_date":"2017-06-30T23:16:05.735Z",
        "build_snapshot": false,
        "lucene_version":"6.6.0"
    },
    "tagline":"You Know, for Search"
}

一些基本示例

我们已经初始化并运行了一个干净的 Elasticsearch 实例。我们要做的第一件事是添加文档并对它们进行检索。Elasticsearch 中的文档以 JSON 格式表示。此外,文档会被添加到索引中,并且文档会有一个类型。我们现在将 ID 为 1 的 person 类型的文档添加到名为 accounts 的索引中;由于该索引尚不存在,Elasticsearch 会自动进行创建。

POST localhost:9200/accounts/person/1 
{
    "name" :"John",
    "lastname" :"Doe",
    "job_description" :"Systems administrator and Linux specialit"
}

响应将返回有关文档创建的信息:

{
    "_index": "accounts",
    "_type": "person",
    "_id":"1",
    "_version":1,
    "result": "created",
    "_shards": {
        "total":2,
        "successful":1,
        "failed":0
    },
    "created": true
}

现在文档已经存在,我们可以进行检索了:

GET localhost:9200/accounts/person/1 

结果将包含元数据和完整文档(在 _source 字段中显示):

{
    "_index": "accounts",
    "_type": "person",
    "_id":"1",
    "_version":1,
    "found": true,
    "_source": {
        "name":"John",
        "lastname":"Doe",
        "job_description":"Systems administrator and Linux specialit"
    }
}

敏锐的读者已经意识到我们在作业描述 (specialit) 中打错了一个词,让我们通过更新文档 (_update) 来纠正它:

POST localhost:9200/accounts/person/1/_update
{
      "doc":{
          "job_description" :"Systems administrator and Linux specialist"
       }
}

操作成功后,会对文档进行修改。让我们再次检索文档并查看响应:

{
    "_index": "accounts",
    "_type": "person",
    "_id":"1",
    "_version":2,
    "found": true,
    "_source": {
        "name":"John",
        "lastname":"Doe",
        "job_description":"Systems administrator and Linux specialist"
    }
}

为了准备接下来的操作,我们再添加一个 ID 为 2 的文档:

POST localhost:9200/accounts/person/2
{
    "name" :"John",
    "lastname" :"Smith",
    "job_description" :"Systems administrator"
}

到目前为止,我们按 ID 检索了文档,但没有进行搜索。使用 REST API 进行查询时,我们可以在请求主体中传递查询,也可以使用特定语法直接在 URL 中传递查询。在本节中,我们将以 /_search?q=something 的格式直接在 URL 中进行搜索:

GET localhost:9200/_search?q=john

这个搜索将返回两个文档,因为它们都包含 john

{
    "took":58,
    "timed_out": false,
    "_shards": {
        "total":5,
        "successful":5,
        "failed":0
    },
    "hits": {
        "total":2,
        "max_score":0.2876821,
        "hits": [
            {
                "_index": "accounts",
                "_type": "person",
                "_id":"2",
                "_score":0.2876821,
                "_source": {
                    "name":"John",
                    "lastname":"Smith",
                    "job_description":"Systems administrator"
                }
            },
            {
                "_index": "accounts",
                "_type": "person",
                "_id":"1",
                "_score":0.28582606,
                "_source": {
                    "name":"John",
                    "lastname":"Doe",
                    "job_description":"Systems administrator and Linux specialist"
                }
            }
        ]
    }
}

在这个结果中,我们可以看到匹配的文档和一些元数据,例如查询的结果总数。我们继续进行更多的搜索。在运行搜索之前,请试着自己弄清楚将检索哪些文档(响应发生在命令之后):

GET localhost:9200/_search?q=smith

这个搜索将只返回我们添加的最后一个文档,也就是唯一包含 smith 的文档。

GET localhost:9200/_search?q=job_description:john

这个搜索不会返回任何文档。在本例中,我们将搜索的范围限制为不包含该词的字段 job_description。在读者练习中,请尝试:

  • 在该字段中进行搜索并仅返回 ID 为 1 的文档
  • 在该字段中进行搜索并返回两个文档
  • 在该字段中进行搜索并仅返回 ID 为 2 的文档。温馨提示:“q”参数使用的语法与 查询字符串 相同。

最后一个例子带来了一个相关的问题:我们可以在特定的字段中进行搜索,那么是否可以只在特定索引内搜索呢?答案是肯定的:我们可以在 URL 中指定索引和类型。试试这个:

GET localhost:9200/accounts/person/_search?q=job_description:linux

除了在一个索引中搜索之外,我们还可以通过提供一个以逗号分隔的索引名称列表,同时在多个索引中进行搜索,并且也可以对类型执行相同的操作。还有更多选项:有关这些选项的信息可以在 多索引、多类型 中找到。在读者练习中,请将文档添加到第二个(不同的)索引中,并同时在两个索引中进行搜索。

在本节末尾,我们将删除一个文档,然后删除整个索引。删除文档后,请尝试在搜索中检索或查找该文档。

DELETE localhost:9200/accounts/person/1

响应将是确认:

{
    "found": true,
    "_index": "accounts",
    "_type": "person",
    "_id":"1",
    "_version":3,
    "result": "deleted",
    "_shards": {
        "total":2,
        "successful":1,
        "failed":0
    }
}

最后,我们可以删除整个索引。

DELETE localhost:9200/accounts

第一节到此结束。总结一下我们所讲的内容:

  1. 添加一个文档。隐式创建了一个索引(该索引 以前不存在)。
  2. 检索文档。
  3. 更新文档,包括更正一个错别字并检查是否已更正。
  4. 添加第二个文档。
  5. 执行搜索,包括隐式使用所有字段的搜索,以及 仅专注于一个字段的搜索。
  6. 建议开展几个搜索练习。
  7. 解释了同时在多个索引和类型中进行搜索的 基础知识。
  8. 建议同时在多个索引中进行搜索。
  9. 删除一个文档。
  10. 删除整个索引。

有关本节中主题的详细信息,请参阅以下链接:

玩转更多有趣的数据。

到目前为止,我们使用的都是一些虚构的数据。本节我们将探讨莎士比亚的戏剧。第一步是下载文件 shakespeare.json,该文件可从 Kibana:加载示例数据 中获取。Elasticsearch 提供了一个 Bulk API,用于批量执行添加、删除、更新和创建操作,即一次执行很多操作。该文件包含可以使用这个 API 采集的数据,后续准备将这些数据编入一个名为“Shakespeare”的索引中,该索引包含类型为 act、scene 和 line 的文档。Bulk API 的请求主体每行包含 1 个 JSON 对象;对于添加操作,比如在文件中,有一个 JSON 对象表示关于添加操作的元数据,下一行中有第二个 JSON 对象,包含要添加的文档:

{"index":{"_index":"shakespeare","_type":"act","_id":0}}
{"line_id":1,"play_name":"Henry IV","speech_number":"","line_number":"","speaker":"","text_entry":"ACT I"}

我们不会深入研究 Bulk API:如果读者感兴趣,请参阅 批量文档

接下来,我们把这些数据全导入到 Elasticsearch 中。由于这个请求的主体相当大(超过 200,000 行),因此建议通过允许从文件加载请求主体的工具来完成这一操作,例如使用 curl:

curl -XPOST "localhost:9200/shakespeare/_bulk?pretty" --data-binary @shakespeare.json

加载数据后,我们可以开始进行一些搜索。在上一节中,我们通过在 URL 中传递查询进行了搜索。在本节中,我们将介绍 Query DSL,它通过指定要在搜索请求主体中使用的 JSON 格式来定义查询。根据操作的类型,可以使用 GET 和 POST 动词发出查询。 让我们从最简单的开始:获取所有文档。为此,我们在主体中指定一个 query 键,并为值指定 match_all 查询。

GET localhost:9200/shakespeare/_search
{
    "query": {
            "match_all": {}
    }
}

结果将显示 10 个文档,部分输出如下:

{
    "took":7,
    "timed_out": false,
    "_shards": {
        "total":5,
        "successful":5,
        "failed":0
    },
    "hits": {
        "total":111393,
        "max_score":1,
        "hits": [
              ...         
            {
                "_index": "shakespeare",
                "_type": "line",
                "_id":"19",
                "_score":1,
                "_source": {
                    "line_id":20,
                    "play_name":"Henry IV",
                    "speech_number":1,
                    "line_number":"1.1.17",
                    "speaker":"KING HENRY IV",
                    "text_entry":"The edge of war, like an ill-sheathed knife,"
                }
            },
            ...         

搜索格式非常简单。有许多不同类型的搜索可用:Elastic 支持直接搜索(“搜索这个词”、“搜索这个范围内的元素”等)和复合查询(“a 和 b”、“a 或 b”等)。完整的参考资料可以在 Query DSL 文档 中找到;我们将在这里举一些例子来熟悉如何使用搜索。

POST localhost:9200/shakespeare/scene/_search/
{
    "query":{
        "match" : {
            "play_name" :"Antony"
        }
    }
}

在前面的查询中,我们搜索了戏剧名称包含 Antony 的所有场景(请参阅 URL)。我们可以细化这个搜索,选择说话人是 Demetrius 的场景:

POST localhost:9200/shakespeare/scene/_search/
{
    "query":{
        "bool": {
            "must" : [
                {
                    "match" : {
                        "play_name" :"Antony"
                    }
                },
                {
                    "match" : {
                        "speaker" :"Demetrius"
                    }
                }
            ]
        }
    }
}

在第一个读者练习中,请修改前面的查询,让搜索不仅返回说话人是 Demetrius 的场景,还返回说话人是 Antony 的场景。温馨提示:请检查布尔 should 子句。 在第二个读者练习中,我们将探索在搜索时 请求主体 中可以使用的各种选项,例如,选择从结果中的哪个位置开始,以及我们想要检索多少结果来进行分页。

到目前为止,我们使用 Query DSL 进行了一些查询。如果除了检索要查找的内容之外,我们还想进行一些分析该怎么办?这时就轮到聚合上场了。通过聚合,我们能够更深入地了解数据:例如,当前数据集中存在多少部戏剧?每部作品平均有多少个场景?哪些作品的场景较多?

在介绍实际示例之前,我们先回顾一下创建 Shakespeare 索引时的情况,因为不了解一点理论就继续学习也是浪费时间。在 Elastic 中,我们可以创建索引来定义索引可以采用的不同字段的数据类型:数字字段、关键字字段、文本字段等等,有很多 数据类型。索引可以采用的数据类型是通过 映射 定义的。在本例中,我们在为文档编制索引之前没有创建任何索引,因此由 Elastic 决定每个字段的类型(它创建了索引的映射)。为文本字段选择 text 类型:对这种类型进行分析,这样我们只需搜索 Antony 即可找到 play_name Antony 和 Cleopatra。默认情况下,我们不能在已分析的字段中进行聚合。 如果字段无效,我们将如何显示聚合?Elastic 在决定每个字段的类型时,还添加了文本字段的非分析版本(称为 keyword),以防我们想要进行聚合/排序/脚本:我们可以在聚合中使用 play_name.keyword。至于如何检查 当前映射,就留给读者练习。

在这节相对较小且相对理论化的课程之后,让我们回到键盘和聚合上来!要检查数据,可以先看看我们有多少部不同的戏剧:

GET localhost:9200/shakespeare/_search
{
    "size":0,
    "aggs" : {
        "Total plays" : {
            "cardinality" : {
                "field" : "play_name.keyword"
            }
        }
    }
}

请注意,由于我们对文档不感兴趣,所以我们决定显示 0 个结果。此外,由于我们要探索整个索引,因此并没有查询部分:我们将使用满足查询的所有文档计算聚合,在本例中默认为 match_all。最后,我们决定使用 cardinality 聚合,这可让我们知道 play_name 字段有多少唯一值。

{
    "took":67,
    "timed_out": false,
    "_shards": {
        "total":5,
        "successful":5,
        "failed":0
    },
    "hits": {
        "total":111393,
        "max_score":0,
        "hits": []
    },
    "aggregations": {
        "Total plays": {
            "value":36
        }
    }
}

现在,我们列出数据集中出现频率更高的戏剧:

GET localhost:9200/shakespeare/_search
{
    "size":0,
    "aggs" : {
        "Popular plays" : {
            "terms" : {
                "field" : "play_name.keyword"
            }
        }
    }
}

结果如下:

{
    "took":35,
    "timed_out": false,
    "_shards": {
        "total":5,
        "successful":5,
        "failed":0
    },
    "hits": {
        "total":111393,
        "max_score":0,
        "hits": []
    },
    "aggregations": {
        "Popular plays": {
            "doc_count_error_upper_bound":2763,
            "sum_other_doc_count":73249,
            "buckets": [
                {
                    "key":"Hamlet",
                    "doc_count":4244
                },
                {
                    "key":"Coriolanus",
                    "doc_count":3992
                },
                {
                    "key":"Cymbeline",
                    "doc_count":3958
                },
                {
                    "key":"Richard III",
                    "doc_count":3941
                },
                {
                    "key":"Antony and Cleopatra",
                    "doc_count":3862
                },
                {
                    "key":"King Lear",
                    "doc_count":3766
                },
                {
                    "key":"Othello",
                    "doc_count":3762
                },
                {
                    "key":"Troilus and Cressida",
                    "doc_count":3711
                },
                {
                    "key":"A Winters Tale",
                    "doc_count":3489
                },
                {
                    "key":"Henry VIII",
                    "doc_count":3419
                }
            ]
        }
    }
}

我们可以看到 play_name 中 10 个最受欢迎的值。读者可以在文档中深入了解如何在聚合中显示更多或更少的值。

如果您已经掌握了,学会下一步肯定不成问题:组合聚合。我们可能会想知道索引中有多少场景、幕和台词,而且我们可能会对每部戏剧相同的值感兴趣。我们可以通过在聚合中嵌套聚合来实现这一点:

GET localhost:9200/shakespeare/_search
{
    "size":0,
    "aggs" : {
        "Total plays" : {
            "terms" : {
                "field" : "play_name.keyword"
            },
            "aggs" : {
                "Per type" : {
                    "terms" : {
                        "field" : "_type"
                     }
                }
            }
        }
    }
}

部分响应如下所示:

    "aggregations": {
        "Total plays": {
            "doc_count_error_upper_bound":2763,
            "sum_other_doc_count":73249,
            "buckets": [
                {
                    "key":"Hamlet",
                    "doc_count":4244,
                    "Per type": {
                        "doc_count_error_upper_bound":0,
                        "sum_other_doc_count":0,
                        "buckets": [
                            {
                                "key": "line",
                                "doc_count":4219
                            },
                            {
                                "key": "scene",
                                "doc_count":20
                            },
                            {
                                "key": "act",
                                "doc_count":5
                            }
                        ]
                    }
                },
                ...

Elasticsearch 中有很多不同的 聚合:使用聚合结果的聚合、像 cardinality 这样的指标聚合、terms 这样的存储桶聚合等等。读者可以查看这个列表,决定哪种聚合适合您可能已经想到的特定用例!也许是通过 Significant terms 聚合来找到特别常见的词语?

第二节到此结束。总结一下我们所讲的内容:

  1. 使用 Bulk API 添加莎士比亚戏剧。
  2. 简单搜索以检查通过 Query DSL 执行查询的 通用格式。
  3. 执行 leaf 搜索以在字段中搜索文本。
  4. 执行复合搜索以组合 2 个文本搜索。
  5. 建议增加第二个复合搜索。
  6. 建议测试请求主体中的不同选项。
  7. 介绍聚合的概念,并简要回顾 映射和字段类型。
  8. 计算数据集中有多少部戏剧。
  9. 检索哪些戏剧在数据集中出现频率 更高。
  10. 将几个聚合组合起来,看看 10 部最常出现的戏剧 每部有多少幕、场景和台词。
  11. 建议在 Elastic 中探索更多聚合。

一些额外的建议

在这些练习中,我们稍微运用了一下 Elasticsearch 中各种类型的概念。最后,类型只是一个内部的额外字段:值得一提的是,从版本 6 开始,我们只允许创建具有单一类型的索引,并且预计从版本 7 开始,将会移除类型。更多信息可以在 这篇博客文章 中找到。

结论

在这篇文章中,我们通过一些例子让大家对 Elasticsearch 有了一点了解。

与这篇简短文章中所展示的内容相比,Elasticsearch 和 Elastic Stack 中值得探索的内容要多得多。在我们结束之前值得一提的是 相关度。Elasticsearch 不仅关注 这个文档是否满足我的搜索需求,而且还涵盖了 这个文档满足我的搜索需求的程度,首先提供与查询最相关的搜索结果。文档内容广泛且包含大量示例。

在实现任何新的定制功能之前,建议先检查文档,看看我们是否已经实现了该功能,以便在您的项目中轻松利用它。您认为有用的功能或想法很可能已经发布了,因为我们的开发路线图在很大程度上受到用户和开发人员的影响,他们会告诉我们他们想要的内容!

如果需要身份验证/控制访问权限/加密/审核,可以在“安全”中使用这些功能。如果需要监测集群,可以在“监测”中实现。如果需要在结果中按需创建字段,我们已经允许通过 脚本字段 进行创建。如果需要通过电子邮件/Slack/Hipchat/等创建告警,可以通过“告警”实现。如果需要在图表中直观呈现数据和聚合,我们已经提供了一个丰富的环境 Kibana 来执行这一操作。如果您需要为数据库、日志文件、管理队列或几乎任何可以想象到的来源中的数据编制索引,可以通过 LogstashBeats 实现。总之,不管您有任何需要,请先检查一下这项功能是否已经推出!