Elasticsearch 的核心功能是搜索,我们可以使用 Search API 来搜索存储在一个或多个 Elasticsearch 索引中的数据,Search API 有如下两种类型:
URI Search:查询是通过查询参数提供的,功能往往比较简单,适用于测试
Request Body Search:查询是通过请求的 JSON 主体提供的,即 Query DSL 编写的
这两种方式都支持对多个索引进行操作,支持逗号分隔的索引列表、正则匹配、搜索全部索引则用 *、_all。下面就来介绍 Elasticsearch 提供的丰富的 Search API 及其用法。
URI Search
通过提供请求参数,可以只使用 URI 执行搜索请求。当使用这种模式执行搜索时,并不是所有的搜索选项都会被公开,但对于测试来说它很方便。URI 中支持的搜索参数如下所示:
- q:指定查询字符串,使用 Query String Syntax 格式
- df:默认字段,不指定时会对所有字段进行查询
- analyzer:查询时使用的分析器名称
- default_operator:使用的默认操作符,取值为 AND 或 OR,默认为 OR
- lenient:如果为 true 则忽略搜索提供的字段值与文档实际字段类型不匹配的错误
- sort:按指定字段排序,取值为 fieldname、fieldname:asc、fieldname:desc
- timeout:搜索的超时时间
- from:分页查询参数,默认为 0,即从 0 开始
- size:分页查询参数,默认为 10,即一次返回 10 条
下面示例用于查询 user 字段的值包含 kimchy 的文档:
curl -X GET "localhost:9200/twitter/_search?df=user&q=kimchy&pretty"
# 或者
curl -X GET "localhost:9200/twitter/_search?q=user:kimchy&pretty"
返回结果如下:
{
"took" : 104,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 0.35667494,
"hits" : [
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "pOwcJXoBg2C-dN53ZQUN",
"_score" : 0.35667494,
"_source" : {
"user" : "kimchy",
"post_date" : "2009-11-15T14:12:12",
"message" : "trying out Elasticsearch"
}
},
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.35667494,
"_source" : {
"user" : "kimchy",
"post_date" : "2009-11-15T14:12:12",
"message" : "trying out Elasticsearch"
}
}
]
}
}
- took:索引数据花费的时间
- timed_out:查询是否超时
- _shards:搜索了多少个分片,以及有多少分片搜索成功、失败或跳过
- hits:结果集
- total
- value:搜索到的匹配文档的数量
- max_score:搜索到的文档的最大相关性算分
- hits
- _index:文档所在索引名称
- _type:文档类型
- _id:文档 ID
- _score:文档的相关性得分
- _source:文档源数据
- total
下面讲解下 Query String Syntax 在 URI Search 中的用法:
1. TermQuery、PhraseQuery
TermQuery 会将句子拆分成单词然后分别进行查询,此时需要将句子用括号括起来,默认采用 OR 操作符,我们也可以指定要执行的布尔操作:
- 布尔操作:AND、OR、NOT(必须大写)或者 &&、||、! 符号
- 加减操作:+ 表示 must、- 表示 must_not
使用示例如下:
# 查询包含 Beautiful 或 Mind 单词的文档
http://localhost:9200/twitter/_search?q=title:(Beautiful Mind)&pretty
# 查询包含 Beautiful 和 Mind 单词的文档
http://localhost:9200/twitter/_search?q=title:(Beautiful AND Mind)&pretty
# 查询包含 Beautiful 但不包含 Mind 单词的文档
http://localhost:9200/twitter/_search?q=title:(Beautiful NOT Mind)&pretty
# 查询包含 Beautiful 但不包含 Mind 单词的文档
http://localhost:9200/twitter/_search?q=title:(+Beautiful -Mind)&pretty
使用 PhraseQuery 查询时会将整个句子进行查询,并且要求单词的前后顺序需保持一致。此时需要将句子用引号引起来。使用示例如下:
# 查询包含 Beautiful Mind 的文档
http://localhost:9200/twitter/_search?q=title:"Beautiful Mind"&pretty
2. 范围查询
我们可以通过区间来为日期、数字或字符串字段指定范围,具体如下:
- [] 表示闭区间,如 date:[2012-01-01 TO 2012-12-31]、count:[10 TO *]
- {} 表示开区间,如 date:{* TO 2012-01-01}
此外,还可以通过算数符号确定范围(支持布尔操作和加减操作),具体如下:
- age:>10
- age:>=10
- age:<10
- age:<=10
- age:(>=10 AND <20)
- age:(+>=10 +<20)
3. 其他
通配符查询(效率低、占用内存大,不建议使用)
- ?代表 1 个字符,* 代表 0 或多个字符
正则表达式
- 如:title:[bt]oy、title:*oy
模糊匹配与近似查询
- 如:title:beautify~1,用于 TermQuery 查询,代表允许有一个字母可以和 beautify 有差别
- 如:title:”lord rings”~2,用于 PhraseQuery 查询,代表 lord 和 rings 之间允许有 2 个单词
Request Body Search
使用 Elasticsearch 提供的,基于 JSON 格式的一种查询语言(Query Domain Specific Language,DSL)来执行搜索请求,提供了非常完备的搜索参数,下面详细介绍:
1. query
query 元素允许我们通过 Query DSL 进行查询,Query DSL 的详细内容后面再讲。如下示例是一个基本的查询语句:
curl -X GET "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query" : {
"match_phrase" : { "user" : "kimchy" }
}
}'
2. form/size
Elasticsearch 默认返回第 1 页的前 10 条结果,可以通过使用 from 和 size 参数来配置分页。from 定义了想要获取的第一个结果的偏移量,size 参数允许配置要返回的最大命中量。
如下示例,表示从第二个文档开始搜索,最多返回 5 条命中的文档:
curl -X GET "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
"from" : 1,
"size" : 5,
"query" : {
"term" : { "user" : "kimchy" }
}
}'
使用时要注意,分页的深度不能超过 index.max._result_window 配置的值,该值默认为 10000。因为由于搜索请求通常跨越多个分片的,每个分片必须生成自己的排序结果,然后再将这些单独的结果组合起来进行排序,以确保总体排序顺序是正确的。因此越向后翻页,其消耗的内存成本会越高。
作为深度分页的替代方案,我们建议使用 Scroll 或 Search After API 获得更有效的深度滚动方法。
3. sort
通过 sort 参数可以对搜索结果按指定字段排序,Elasticsearch 默认按照文档的相关性得分降序排序,对于 match_all 查询而言,由于只返回所有文档,不需要评分,文档的顺序为添加文档的顺序。
最好在数字型与日期型字段上使用排序。此外通过 _score 还可以按文档的相关性得分进行排序,以及通过 _doc 按索引顺序排序。排序顺序通过以下参数控制:
- asc:按升序排序(默认)
- desc:按降序排序,对 _score 排序时默认降序
sort 使用示例如下:
curl -X GET "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
"sort" : [
{ "post_date" : "desc"}
],
"query" : {
"term" : { "user" : "kimchy" }
}
}'
Elasticsearch 支持对数组或多值字段排序,可以通过 mode 选项来控制选择哪一个数组值来对它所属的文档进行排序。mode 可以有以下值:
- max:最大值(desc 的默认值)
- min:最小值(asc 的默认值)
- sum:使用所有值的和作为排序值,仅适用于基于数字的数组字段。
- avg:使用所有值的平均值作为排序值,仅适用于基于数字的数组字段。
- median:使用所有值的中位数作为排序值,仅适用于基于数字的数组字段。
对数组字段的排序示例如下:
curl -X PUT "localhost:9200/my_index/_doc/1?refresh&pretty" -H 'Content-Type: application/json' -d'
{
"product": "chocolate",
"price": [20, 4]
}'
curl -X POST "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query" : {
"term" : { "product" : "chocolate" }
},
"sort" : [
{"price" : {"order" : "asc", "mode" : "avg"}}
]
}'
Elasticsearch 还支持根据一个或多个 nested 对象中的字段进行排序。按嵌套字段排序支持具有以下属性的嵌套排序选项:
- path:定义要对哪个嵌套对象排序,实际的排序字段必须是这个嵌套对象中的一个直接字段。当按嵌套字段排序时,该字段是必需的。
- filter:过滤器,嵌套路径内的内部对象应该与之匹配,以便通过排序将其字段值考虑在内。
对 nested 字段的排序示例如下,offer 为 nested 字段类型:
curl -X POST "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query" : {
"term" : { "product" : "chocolate" }
},
"sort" : [
{
"offer.price" : {
"mode" : "avg",
"order" : "asc",
"nested": {
"path": "offer",
"filter": {
"term" : { "offer.color" : "blue" }
}
}
}
}
]
}'
排序是针对原始内容进行的,倒排索引无法发挥作用,需要用到正排索引,即通过文档 ID 和字段快速得到字段的原始内容。对此 Elasticsearch 有两种实现方式:
- Fielddata
- Doc Values(列式存储,对 Text 类型无效)
Doc Values | Field data | |
---|---|---|
何时创建 | 索引时,和倒排索引一起创建 | 搜索时动态创建 |
创建位置 | 磁盘文件 | JVM Heap |
优点 | 避免大量内存占用 | 索引速度快,不占用额外磁盘空间 |
缺点 | 降低索引速度,占用额外磁盘空间 | 文档过多时,动态创建开销大,占用过多 JVM Heap |
缺省值 | ES 2.x 之后默认方式 | ES 1.x 之前 |
我们可以在 Mapping 设置中显示关闭 doc_values 以增加索引的速度、减少磁盘占用。但前提是明确不需要做排序及聚合分析的字段,如果重新打开,则需要重建索引。
4. _source
默认情况下,搜索请求将返回 _source 字段的全部内容。如果 _source 元字段没有被存储,那就只返回匹配的文档的元数据。我们可以在搜索时通过 _source 参数不让元字段返回或只返回部分字段。
禁用源字段返回,只返回匹配的文档的元数据:
curl -X GET "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
"_source": false,
"query" : {
"term" : { "user" : "kimchy" }
}
}'
返回 _source 字段中的部分数据,支持使用通配符:
curl -X GET "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
"_source": ["obj.*", "message"],
"query" : {
"term" : { "user" : "kimchy" }
}
}'
5. highlight
通过 highlight 参数能够从搜索结果中的一个或多个字段中获得高亮显示的片段,以便向用户显示查询匹配的位置。当请求高亮显示时,响应为每个搜索命中的文档包含一个额外的 highlight 元素,其中包括高亮显示的字段和高亮显示的片段。
curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query" : {
"match": { "message": "Elasticsearch" }
},
"highlight" : {
"fields" : {
"message" : {}
}
}
}'
返回结果如下:
{
"took": 58,
"timed_out": false,
"_shards": {
"total": 13,
"successful": 13,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 0.10536051,
"hits": [
{
"_index": "twitter",
"_type": "_doc",
"_id": "pOwcJXoBg2C-dN53ZQUN",
"_score": 0.10536051,
"_source": {
"user": "kimchy",
"post_date": "2009-11-15T14:12:12",
"message": "trying out Elasticsearch"
},
"highlight": {
"message": [
"trying out <em>Elasticsearch</em>"
]
}
}
]
}
}
在返回结果中,Elasticsearch 默认会用 标签标记关键字。如果我们想使用自定义标签,在高亮属性中给需要高亮的字段加上 pre_tags 和 post_tags 即可。使用示例如下:
curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query" : {
"match": { "message": "Elasticsearch" }
},
"highlight" : {
"fields" : {
"message" : {
"pre_tags" : ["<strong>"],
"post_tags" : ["</strong>"]
}
}
}
}'
Elasticsearch 提供了三种高亮器,分别是默认的 highlighter 高亮器、postings-highlighter 高亮器和 fast-vector-highlighter 高亮器。
默认的 highlighter 是最基本的高亮器,highlighter 高亮器实现高亮功能需要对 _source 中保存的原始文档进行二次分析,其速度在三种高亮器里是最慢的,但优点是不需要额外的存储空间。
postings-highighlighter 高亮器实现高亮功能不需要二次分析,但需要在字段的映射中设置 index_options 参数的取值为 offsets,即保存关键词的偏移量,速度快于默认的 highlighter 高亮器。
fast-vector-highlighter 高亮器实现高亮功能速度最快,但是需要在字段的映射中设置 term_vector 参数的取值为 with_positions_offsets,即保存关键词的位置和偏移信息,占用的存储空间最大,是典型的空间换时间。
6. scroll
Scroll API 可用于从单个搜索请求检索大量结果甚至所有结果,其方式与传统数据库的游标使用非常相似。滚动请求返回的结果反映了初始搜索请求发出时索引的状态,就像即时的快照一样。对文档的后续更改(索引、更新或删除)只会影响后续的搜索请求。
为了使用滚动请求,初始搜索请求应该在查询字符串中指定滚动参数,用来告诉 Elasticsearch 应该保持搜索上下文要存活多长时间,如下示例:
curl -X POST "localhost:9200/twitter/_search?scroll=1m&pretty" -H 'Content-Type: application/json' -d'
{
"size": 100,
"query": {
"match" : {
"message" : "elasticsearch"
}
}
}'
返回结果会包含一个 _scroll_id 字段,可以将该值传递给 Scroll API 以搜索下一批结果。在搜索下一批结果时我们还可以再指定 scroll 参数以延长上下文保存时间。如果搜索间隔超过了 scroll 设置的时间后,将自动删除搜索上下文,此时再执行滚动则抛出异常。
curl -X POST "localhost:9200/_search/scroll?pretty" -H 'Content-Type: application/json' -d'
{
"scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFlRseGEtbko4UWJXVURiRE4yek1NV3cAAAAAAAAAOBY3Zi1iTzhLWlFqLWJqeGE2V3hsdDBB"
}'
# 将上下文保存时间延长1分钟
curl -X POST "localhost:9200/_search/scroll?pretty" -H 'Content-Type: application/json' -d'
{
"scroll" : "1m",
"scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFlRseGEtbko4UWJXVURiRE4yek1NV3cAAAAAAAAAOBY3Zi1iTzhLWlFqLWJqeGE2V3hsdDBB"
}'
通过 size 参数可以配置每批结果返回的最大命中数。每次对 Scroll API 的调用都会返回下一批结果,直到没有更多的结果可以返回,即 hits 数组为空。
7. search_after
前面讲到过,如果要对命中结果进行分页可以通过使用 from 和 size 来完成,但是当达到深层分页时,成本就变得令人望而却步,且搜索请求占用的堆内存和时间与分页的大小是成正比的。
对于有效的深度滚动,推荐使用 Scroll API,但是滚动上下文代表的是某个时刻的快照,因此不推荐用于实时的用户请求。而 search_after 参数可以绕过使用游标的方式,使用前一页的结果来帮助检索下一页,因此比较适用于深度分页,scroll 则适合导出全部文档数据的场景。
假设检索第一个页面的查询示例如下:
curl -X GET "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
"size": 10,
"query": {
"match" : {
"user" : "kimchy"
}
},
"sort": [
{"post_date": "asc"}
]
}'
该请求返回的结果中包含了每个文档的排序值数组,即 sort 元素。
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "pOwcJXoBg2C-dN53ZQUN",
"_score" : null,
"_source" : {
"user" : "kimchy",
"post_date" : "2009-11-15T14:12:12",
"message" : "trying out Elasticsearch"
},
"sort" : [
1258294332000
]
},
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_score" : null,
"_source" : {
"user" : "kimchy",
"post_date" : "2009-11-15T14:12:12",
"message" : "trying out Elasticsearch"
},
"sort" : [
1258294332000
]
}
]
}
}
这些排序值可以与参数 search_after 一起使用。例如,我们可以使用上一个文档的排序值,并将其传递给之后的搜索请求,以检索结果的下一页(内部先按 search_after 过滤再分页以达到滚动分页的效果):
curl -X GET "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
"size": 10,
"query": {
"match" : {
"user" : "kimchy"
}
},
"search_after": [1258294332000],
"sort": [
{"post_date": "asc"}
]
}'
在使用 search_after 时,我们应该选择具有唯一值的字段作为排序字段,否则,具有相同排序值的文档的排序顺序将是未定义的,有可能导致丢失或结果重复。当使用 search_after 时,from 参数必须设置为 0 或 -1。
它与 scroll API 的区别在于,search_after 是无状态的,它始终针对最新版本的搜索器进行解析。 因此,排序顺序可能会在分页搜索期间发生变化,具体取决于索引的更新和删除。
8. explain
对搜索请求进行解释,说明文档的相关性算分是如何计算的:
curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
"explain": true,
"query" : {
"term" : { "user" : "kimchy" }
}
}'
9. profile
可以查看搜索请求是如何执行的,采用的是什么匹配查询算法:
curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
"profile": true,
"query" : {
"term" : { "user" : "kimchy" }
}
}'
Suggesters
建议功能会根据提供的文本,使用提示符(suggest)来建议外观相似的术语。原理是将输入的文本分解为多个 token,然后在搜索的字典里根据编辑距离查找相似的 term 并返回。
Suggester 就是一种特殊类型的搜索,通常与 _search 请求中的查询部分一起定义,如果没有查询部分,则只返回建议信息。
1. Term Suggester
对建议文本进行分词,对每个词分别进行建议。如下示例,query 部分用于执行搜索请求,suggest 部分用于请求建议,通过 field 参数指定在特定字段中搜索建议项:
curl -X POST "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
"size" : 1,
"query" : {
"match": {
"message": "tring out Elasticsearch"
}
},
"suggest" : {
"my-suggestion" : {
"text" : "tring out Elasticsearch",
"term" : {
"field" : "message"
}
}
}
}'
由于 message 字段里有 trying 这个 term,所以会对 tring 这个词返回 trying 的建议:
{
"suggest" : {
"my-suggestion" : [
{
"text" : "tring",
"offset" : 0,
"length" : 5,
"options" : [
{
"text" : "trying",
"score" : 0.8,
"freq" : 16
}
]
},
{
"text" : "out",
"offset" : 6,
"length" : 3,
"options" : [ ]
},
{
"text" : "elasticsearch",
"offset" : 10,
"length" : 13,
"options" : [ ]
}
]
}
}
从返回中可以看到,每个建议都包含了一个算分,表示建议文本与原文本的相似程度。其通过 Levenshtein Edit Distance 算法实现,核心思想就是一个词改动了多少字符就可以和另外一个词一致。Elasticsearch 提供了很多参数来控制相似性的模糊程度:
- max_edits:最大编辑距离,只能是 1 到 2 之间的值,默认为 2。
- prefix_length:必须匹配的最小前缀字符的数量,默认为 1,增加该值能提高拼写检查性能。
- min_word_length:建议词必须包含的最小长度,默认为 4。
2. Phrase Suggester
Term Suggester 提供了非常方便的 API 来访问特定字符串距离内每个 token 的代替者,Phrase Suggester 在其基础上增加了一些额外的逻辑以选择整个正确的语句而非词项。
curl -X POST "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
"size" : 1,
"query" : {
"match": {
"message": "tring out Elasticsearch"
}
},
"suggest" : {
"my-suggestion" : {
"text" : "tring out Elasticsearch",
"phrase" : {
"field" : "message"
}
}
}
}'
3. Complete Suggester
Completion Suggester 提供了自动完成的功能,用户每输入一个字符,就需要即时发送一个查询请求到后端查找匹配项。该功能可以在用户输入时引导他们找到相关的结果,提高搜索精度,并不会对词进行纠正。
理想情况下,自动完成功能应该与用户输入的速度是一样快的,以便提供与用户输入内容相关的即时反馈。因此 Elasticsearch 采用了不同的数据结构,没有使用倒排索引,而是将 Analyze 的数据编码成 FST 和索引存放在一起,而 FST 会被整个加载进内存,因此速度非常快,不过 FST 只能用于前缀查找。
由于对需要自动补全的字段进行了特殊处理,因此在 Mapping 中需要显示指定字段类型为 completion:
curl -X PUT "localhost:9200/music?pretty" -H 'Content-Type: application/json' -d'
{
"mappings": {
"properties" : {
"title_completion" : {
"type" : "completion"
}
}
}
}'
执行查询:
curl -X POST "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
"size" : 1,
"suggest" : {
"my-suggestion" : {
"prefix" : "e",
"completion" : {
"field" : "title_completion"
}
}
}
}'
此外,还提供了以下参数:
- size:返回建议的数量
- skip_duplicates:跳过重复文档