深入搜索编辑
在 基础入门 中涵盖了基本工具并对它们有足够详细的描述,这让我们能够开始用 Elasticsearch 搜索数据。 用不了多长时间,就会发现我们想要的更多:希望查询匹配更灵活,排名结果更精确,不同问题域下搜索更具体。
想要进阶,只知道如何使用 match
查询是不够的,我们需要理解数据以及如何能够搜索到它们。本章会解释如何索引和查询我们的数据让我们能利用词的相似度(word proximity)、部分匹配(partial matching)、模糊匹配(fuzzy matching)以及语言感知(language awareness)这些优势。
理解每个查询如何贡献相关度评分 _score
有助于调试我们的查询:确保我们认为的最佳匹配文档出现在结果首页,以及削减结果中几乎不相关的 “长尾(long tail)”。
搜索不仅仅是全文搜索:我们很大一部分数据都是结构化的,如日期和数字。 我们会以说明结构化搜索与全文搜索最高效的结合方式开始本章的内容。
探索你的数据
样本数据集
现在我们已经看到了一些基本知识,让我们尝试一下更加真实的数据集。我准备了一个关于客户银行账户信息的虚构的JSON文档样本。每个文档都有以下模式:
{ "account_number": 0, "balance": 16623, "firstname": "Bradshaw", "lastname": "Mckenzie", "age": 29, "gender": "F", "address": "244 Columbus Place", "employer": "Euron", "email": "bradshawmckenzie@euron.com", "city": "Hobucken", "state": "CO" }
为了好奇,这个数据是使用生成的www.json-generator.com/
,所以请忽略数据的实际值和语义,因为这些都是随机生成的。
加载示例数据集
您可以从这里下载示例数据集(accounts.json)。将它解压到我们当前的目录下,并将其加载到我们的集群中,如下所示:
curl -H "Content-Type: application/json" -XPOST 'localhost:9200/bank/account/_bulk?pretty&refresh' --data-binary "@accounts.json" curl 'localhost:9200/_cat/indices?v'
答案是:
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size yellow open bank l7sSYV2cQXmu6_4rJWVIww 5 1 1000 0 128.6kb 128.6kb
这意味着我们只是成功将1000个文件批量索引到'_bank'索引(在account类型下)。
搜索API
现在让我们开始一些简单的搜索。有运行检索两种基本方式:一种是通过发送搜索参数REST请求URI和其他通过发送他们REST请求主体。请求主体方法允许您更具表现力,并以更易读的JSON格式定义您的搜索。我们将尝试请求URI方法的一个例子,但在本教程的剩余部分中,我们将专门使用请求主体方法。
用于搜索的REST API可以从_search
端点访问。本示例返回银行索引中的所有文档:
GET / bank / _search?q = *&sort = account_number:asc&pretty
我们首先解析搜索调用。我们_search
在银行索引中搜索(端点),并且该q=*
参数指示Elasticsearch匹配索引中的所有文档。该sort=account_number:asc
参数指示account_number
按升序对每个文档的字段进行排序。这个pretty
参数再次告诉Elasticsearch返回漂亮的JSON结果。
和响应(部分显示):
{ "took" : 63, "timed_out" : false, "_shards" : { "total" : 5, "successful" : 5, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : 1000, "max_score" : null, "hits" : [ { "_index" : "bank", "_type" : "account", "_id" : "0", "sort": [0], "_score" : null, "_source" : {"account_number":0,"balance":16623,"firstname":"Bradshaw","lastname":"Mckenzie","age":29,"gender":"F","address":"244 Columbus Place","employer":"Euron","email":"bradshawmckenzie@euron.com","city":"Hobucken","state":"CO"} }, { "_index" : "bank", "_type" : "account", "_id" : "1", "sort": [1], "_score" : null, "_source" : {"account_number":1,"balance":39225,"firstname":"Amber","lastname":"Duke","age":32,"gender":"M","address":"880 Holmes Lane","employer":"Pyrami","email":"amberduke@pyrami.com","city":"Brogan","state":"IL"} }, ... ] } }
至于回应,我们看到以下部分:
took
- Elasticsearch执行搜索的时间(以毫秒为单位)timed_out
- 搜索是否超时_shards
- 搜索了多少碎片,以及搜索碎片成功/失败的次数hits
- 搜索结果hits.total
- 符合我们搜索条件的文件总数hits.hits
- 实际的搜索结果数组(默认为前10个文档)hits.sort
- 对结果进行排序的键(按分数排序时丢失)hits._score
并且max_score
- 现在忽略这些字段
以上是使用替代请求主体方法的上述相同的确切搜索:
GET /bank/ _搜索 { "query":{"match_all":{}}, "排序":[ {"account_number":"asc"} ] }
这里的区别在于,我们没有传入q=*
URI,而是将一个JSON风格的查询请求体发送到_search
API。我们将在下一节讨论这个JSON查询。
重要的是要明白,一旦你得到你的搜索结果,Elasticsearch完成与请求,并没有维护任何种类的服务器端资源或打开游标到你的结果。这与许多其他平台(如SQL)形成鲜明对比,其中您最初可能会首先获得查询结果的部分子集,然后如果要获取(或翻阅)其余部分,则必须连续返回到服务器的结果使用某种有状态的服务器端游标。
介绍查询语言
Elasticsearch提供了一种可用于执行查询的JSON式特定于域的语言。这被称为Query DSL。查询语言非常全面,可以乍一看吓人,但实际学习的最好方法是从几个基本的例子开始。
回到我们的最后一个例子,我们执行了这个查询:
GET /bank/_search { "query": { "match_all": {} } }
解剖上述,query
部分告诉我们我们的查询定义是什么,match_all
部分只是我们想要运行的查询的类型。该match_all
查询仅仅是在指定索引的所有文件进行搜索。
除了query
参数之外,我们还可以传递其他参数来影响搜索结果。在上面的例子中 sort
,我们通过了size
:
GET /bank/_search { "query": { "match_all": {} }, "size": 1 }
请注意,如果size
未指定,则默认为10。
这个例子执行match_all
并返回文档11到20:
GET /bank/_search { "query": { "match_all": {} }, "from": 10, "size": 10 }
在from
(从0开始)参数规定了从启动该文件的索引和size
参数指定了多少文件,返回从参数开始的。在实现分页搜索结果时,此功能非常有用。请注意,如果from
未指定,则默认为0。
此示例match_all
按帐户余额(banlance)降序(desc)对结果进行排序并返回前10个(默认大小)文档。
GET /bank/_search { "query": { "match_all": {} }, "sort": { "balance": { "order": "desc" } } }
执行搜索
现在我们已经看到了一些基本的搜索参数,让我们再深入QUERY DSL。我们先来看看返回的文档字段。默认情况下,完整的JSON文档作为所有搜索的一部分返回。这被称为源(_source
搜索命中字段)。如果我们不希望整个源文档返回,我们有能力只需要返回源内的几个字段。
这个例子展示了如何从搜索中返回两个字段account_number
和balance
(在_source
)之内:
GET /bank/_search { "query": { "match_all": {} }, "_source": ["account_number", "balance"] }
请注意,上面的例子简单地减少了_source
字段。它仍然只会返回一个字段,_source
但在其中,只有字段account_number
,balance
并包括在内。
如果你有SQL基础,上面在概念上有点类似于SQL SELECT FROM
字段列表。
现在我们来看看查询部分。以前,我们已经看过match_all
查询是如何用来匹配所有文档的。现在我们来介绍一个叫做match
查询的新查询,它可以被看作是一个基本的搜索查询(即针对特定字段或者字段集合进行的搜索)。
此示例返回account_number为20的帐户:
GET /bank/_search { "query": { "match": { "account_number": 20 } } }
此示例返回address中包含“mill”的所有帐户:
GET /bank/_search { "query": { "match": { "address": "mill" } } }
此示例返回address中包含“mill lane”的所有帐户:
GET /bank/_search { "query": { "match": { "address": "mill lane" } } }
这个例子是match
(match_phrase
)的一个变体,返回在地址中包含短语“mill lane”的所有账户:
GET /bank/_search { "query": { "match_phrase": { "address": "mill lane" } } }
现在我们来介绍一下这个bool
Query。该bool
查询允许我们撰写较小的查询到使用布尔逻辑更大的查询。
此示例组成两个match
查询,并返回address中包含“mill”和“lane”的所有帐户:
GET /bank/_search { "query": { "bool": { "must": [ { "match": { "address": "mill" } }, { "match": { "address": "lane" } } ] } } }
在上面的例子中,bool must
子句指定了一个文件被认为是匹配的所有查询。
相反,这个例子组成两个match
查询,并返回地址中包含“mill”或“lane”的所有帐户:
GET /bank/_search { "query": { "bool": { "should": [ { "match": { "address": "mill" } }, { "match": { "address": "lane" } } ] } } }
在上面的例子中,bool should
子句指定了一个查询列表,其中任何一个查询都必须是真的才能被视为匹配的文档。
此示例组成两个match
查询,并返回address中既不包含“mill”也不包含“lane”的所有帐户:
GET /bank/_search { "query": { "bool": { "must_not": [ { "match": { "address": "mill" } }, { "match": { "address": "lane" } } ] } } }
在上面的例子中,bool must_not
子句指定了一个查询列表,其中任何一个查询都不可以被认为是匹配的。
我们可以在一个查询中同时结合must
,should
和must_not
子句bool
。此外,我们可以bool
在任何这些bool
子句中编写查询来模拟任何复杂的多级布尔逻辑。
这个例子返回所有40岁但不住ID(aho)的人的账号:
GET /bank/_search { "query": { "bool": { "must": [ { "match": { "age": "40" } } ], "must_not": [ { "match": { "state": "ID" } } ] } } }
执行过滤器
在上一节中,我们跳过了一个叫做文档分数(_score
搜索结果中的字段)的细节。得分是一个数字值,它是文档与我们指定的搜索查询匹配度的相对度量。分数越高,文档越相关,分数越低,文档就越不相关。
但查询并不总是需要产生分数,特别是当它们仅用于“过滤”文档集时。Elasticsearch检测这些情况并自动优化查询执行,以便不计算无用分数。
该bool
查询,我们在上一节中介绍还支持filter
条款允许使用查询来限制将被其他条款相匹配的文件,无需转换分数的计算方式。作为一个例子,我们来介绍一下range
查询,它允许我们通过一系列值来过滤文档。这通常用于数字或日期过滤。
本示例使用bool查询返回余额在20000和30000之间的所有帐户。换句话说,我们要查找大于或等于20000且小于等于30000的帐户。
GET /bank/_search { "query": { "bool": { "must": { "match_all": {} }, "filter": { "range": { "balance": { "gte": 20000, "lte": 30000 } } } } } }
解析上述,bool查询包含match_all
查询(查询部分)和range
查询(过滤器部分)。我们可以将其他查询替换为查询和过滤器部分。在上述情况下,范围查询是非常有意义的,因为落入该范围的文档全部匹配“平等”,即没有文档比另一个文档更相关。
除了match_all
,match
,bool
,和range
查询,有很多可用的其他查询类型的,我们不会进入他们在这里。由于我们已经对其工作原理有了一个基本的了解,所以将这些知识应用于其他查询类型的学习和实验并不难。
执行聚合
聚合提供了从数据中分组和提取统计数据的能力。考虑聚合的最简单方法是将其大致等同于SQL GROUP BY和SQL聚合函数。在Elasticsearch中,您可以执行搜索并返回匹配,同时还可以在一个响应中返回与匹配不同的聚合结果。这是非常强大和高效的,因为您可以运行查询和多个聚合,并且一次性获得两个(或两个)操作的结果,避免使用简洁和简化的API来实现网络往返。
首先,本示例按状态对所有帐户进行分组,然后返回按降序(也是默认值)排序的前10个(默认)状态:
GET /bank/_search { "size": 0, "aggs": { "group_by_state": { "terms": { "field": "state.keyword" } } } }
在SQL中,上面的聚合在概念上类似于:
SELECT state, COUNT(*) FROM bank GROUP BY state ORDER BY COUNT(*) DESC
和响应(部分显示):
{ "took": 29, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped" : 0, "failed": 0 }, "hits" : { "total" : 1000, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "group_by_state" : { "doc_count_error_upper_bound": 20, "sum_other_doc_count": 770, "buckets" : [ { "key" : "ID", "doc_count" : 27 }, { "key" : "TX", "doc_count" : 27 }, { "key" : "AL", "doc_count" : 25 }, { "key" : "MD", "doc_count" : 25 }, { "key" : "TN", "doc_count" : 23 }, { "key" : "MA", "doc_count" : 21 }, { "key" : "NC", "doc_count" : 21 }, { "key" : "ND", "doc_count" : 21 }, { "key" : "ME", "doc_count" : 20 }, { "key" : "MO", "doc_count" : 20 } ] } } }
我们可以看到,ID
(Idaho)有27个账户,其次是TX
(Texas)27个账户,其次是AL
(Alabama)25个账户,等等。
请注意,我们设置size=0
为不显示搜索匹配,因为我们只想看到响应中的聚合结果。
在前面的基础上,本示例按状态计算平均账户余额(再次仅按前几位按降序排列的状态):
GET /bank/_search { "size": 0, "aggs": { "group_by_state": { "terms": { "field": "state.keyword" }, "aggs": { "average_balance": { "avg": { "field": "balance" } } } } } }
注意我们如何在average_balance
聚合内部嵌套group_by_state
聚合。这是所有聚合的通用模式。您可以任意嵌套聚合内的聚合,以便从数据中提取所需。
建立在以前聚合的基础上,现在让我们按降序对平均余额进行排序:
GET /bank/_search { "size": 0, "aggs": { "group_by_state": { "terms": { "field": "state.keyword", "order": { "average_balance": "desc" } }, "aggs": { "average_balance": { "avg": { "field": "balance" } } } } } }
这个例子演示了我们如何按年龄段(20-29岁,30-39岁和40-49岁)进行分组,然后按性别进行分组,然后最终得到每个年龄段的平均账户平衡:
GET /bank/_search { "size": 0, "aggs": { "group_by_age": { "range": { "field": "age", "ranges": [ { "from": 20, "to": 30 }, { "from": 30, "to": 40 }, { "from": 40, "to": 50 } ] }, "aggs": { "group_by_gender": { "terms": { "field": "gender.keyword" }, "aggs": { "average_balance": { "avg": { "field": "balance" } } } } } } } }
还有很多其他聚合功能,我们在这里不会详细介绍。如果你想要做进一步的学习,请参考聚合参考指南。