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
第一节到此结束。总结一下我们所讲的内容:
- 添加一个文档。隐式创建了一个索引(该索引 以前不存在)。
- 检索文档。
- 更新文档,包括更正一个错别字并检查是否已更正。
- 添加第二个文档。
- 执行搜索,包括隐式使用所有字段的搜索,以及 仅专注于一个字段的搜索。
- 建议开展几个搜索练习。
- 解释了同时在多个索引和类型中进行搜索的 基础知识。
- 建议同时在多个索引中进行搜索。
- 删除一个文档。
- 删除整个索引。
有关本节中主题的详细信息,请参阅以下链接:
玩转更多有趣的数据。
到目前为止,我们使用的都是一些虚构的数据。本节我们将探讨莎士比亚的戏剧。第一步是下载文件 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
聚合来找到特别常见的词语?
第二节到此结束。总结一下我们所讲的内容:
- 使用 Bulk API 添加莎士比亚戏剧。
- 简单搜索以检查通过 Query DSL 执行查询的 通用格式。
- 执行 leaf 搜索以在字段中搜索文本。
- 执行复合搜索以组合 2 个文本搜索。
- 建议增加第二个复合搜索。
- 建议测试请求主体中的不同选项。
- 介绍聚合的概念,并简要回顾 映射和字段类型。
- 计算数据集中有多少部戏剧。
- 检索哪些戏剧在数据集中出现频率 更高。
- 将几个聚合组合起来,看看 10 部最常出现的戏剧 每部有多少幕、场景和台词。
- 建议在 Elastic 中探索更多聚合。
一些额外的建议
在这些练习中,我们稍微运用了一下 Elasticsearch 中各种类型的概念。最后,类型只是一个内部的额外字段:值得一提的是,从版本 6 开始,我们只允许创建具有单一类型的索引,并且预计从版本 7 开始,将会移除类型。更多信息可以在 这篇博客文章 中找到。
结论
在这篇文章中,我们通过一些例子让大家对 Elasticsearch 有了一点了解。
与这篇简短文章中所展示的内容相比,Elasticsearch 和 Elastic Stack 中值得探索的内容要多得多。在我们结束之前值得一提的是 相关度。Elasticsearch 不仅关注 这个文档是否满足我的搜索需求,而且还涵盖了 这个文档满足我的搜索需求的程度,首先提供与查询最相关的搜索结果。文档内容广泛且包含大量示例。
在实现任何新的定制功能之前,建议先检查文档,看看我们是否已经实现了该功能,以便在您的项目中轻松利用它。您认为有用的功能或想法很可能已经发布了,因为我们的开发路线图在很大程度上受到用户和开发人员的影响,他们会告诉我们他们想要的内容!
如果需要身份验证/控制访问权限/加密/审核,可以在“安全”中使用这些功能。如果需要监测集群,可以在“监测”中实现。如果需要在结果中按需创建字段,我们已经允许通过 脚本字段 进行创建。如果需要通过电子邮件/Slack/Hipchat/等创建告警,可以通过“告警”实现。如果需要在图表中直观呈现数据和聚合,我们已经提供了一个丰富的环境 Kibana 来执行这一操作。如果您需要为数据库、日志文件、管理队列或几乎任何可以想象到的来源中的数据编制索引,可以通过 Logstash 和 Beats 实现。总之,不管您有任何需要,请先检查一下这项功能是否已经推出!