目录
1、搜索数据简介
搜索是Elasticsearch的核心功能,Elasticsearch提供了多种多样的搜索方式来满足不同使用场景的需求。Elasticsearch提供了领域特定语言(Domain Specific Language,DSL)查询语句,使用JSON字符串来定义每个查询请求。文章将要介绍的查询类型包含以下几种。
- Match all查询:直接查询索引的全部数据,默认返回前10个文档,每个文档的得分被设置为1.0,这是很简单的查询类型。
- 精准级查询:查询对象大多数是非text类型字段,直接匹配字段中的完整内容,在这个过程中不会对搜索内容进行文本分析。
- 全文检索:查询对象一般是text类型字段,搜索内容和索引数据都会进行文本分析,可以通过传参改变搜索时采用的分析器。
- 经纬度搜索:针对经纬度字段geo_point的搜索,搜索范围可以是圆形、矩形或多边形。
- 父子关联搜索:针对索引间的父子关系进行的查询,可以以父搜子、以子搜父。
- 复合搜索:复合搜索允许按照某种逻辑组织多个单一搜索语句,从而使搜索结果合并得到最终的结果。
测试数据准备:
DELETE user
PUT user
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"userid":{
"type": "long"
},
"username":{
"type": "text",
"fields": {
"keyword":{
"type":"keyword",
"ignore_above":256
}
}
},
"age":{
"type": "integer"
},
"sex":{
"type": "boolean"
},
"born":{
"type": "date",
"format": ["yyyy-MM-dd HH:mm:ss"]
},
"address":{
"type": "geo_point"
}
}
}
}
POST user/_bulk
{"index":{"_id":"1"}}
{"userid":"1","username":"张三","age":"18","sex":"true","born":"2000-01-26 19:00:00"}
{"index":{"_id":"2"}}
{"userid":"2","username":"李四","age":"28","sex":"false","born":"2000-02-26 19:00:00"}
{"index":{"_id":"3"}}
{"userid":"3","username":"王五","age":"48","sex":"true","born":"2000-04-26 19:00:00"}
{"index":{"_id":"4"}}
{"userid":"4","username":"马六","age":"9","sex":"false","born":"2000-05-26 19:00:00"}
{"index":{"_id":"5"}}
{"userid":"5","username":"李大头","age":"26","sex":"true","born":"2000-07-26 19:00:00","address": {"lat": 30.12,"lon": -31.34}}
{"index":{"_id":"6"}}
{"userid":"6","username":"李二狗","age":"88","sex":"true","born":"2000-08-26 19:00:00","address": {"lat": 20.12,"lon": -21.34}}
{"index":{"_id":"7"}}
{"userid":"7","username":"赵四","age":"18","sex":"true","born":"2000-09-26 19:00:00","address": {"lat": 10.12,"lon": -11.34}}
{"index":{"_id":"8"}}
{"userid":"8","username":"宋小宝","age":"28","sex":"false","born":"2000-10-26 19:00:00","address": {"lat": 40.12,"lon": -71.34}}
2、精准级查询
所谓精准级查询,指的是搜索内容不经过文本分析直接用于文本匹配,这个过程类似于数据库的SQL查询,搜索的对象大多是索引的非text类型字段。
2.1、术语查询
术语查询直接返回包含搜索内容的文档,常用来查询索引中某个类型为keyword的文本字段,类似于SQL的“=”查询,使用十分普遍。
下面的请求会直接向索引user发起术语查询。
GET user/_search
{
"query": {
"term": {
"username.keyword": {
"value": "张三"
}
}
}
}
这里的term query直接查询索引中username.keyword字段的value为“张三”的数据,会成功返回对应的结果,如下所示。
{
"took" : 490,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.9808291,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.9808291,
"_source" : {
"userid" : "1",
"username" : "张三",
"age" : "18",
"sex" : "true",
"born" : "2000-01-26 19:00:00"
}
}
]
}
}
返回的结果中,took表示搜索耗费的毫秒数,_shards中的total代表本次搜索一共使用了多少个分片,该值一般等于索引主分片数。hits里面的total代表一共搜索到多少结果;max_score代表搜索结果中相关度得分的最大值,默认搜索结果会按照相关度得分降序排列;_score代表单个文档的相关度得分;_source是数据的原始JSON内容。
注意:最好不要在精准级查询的字段中使用text字段,因为text字段会被分词,这样做既没有意义,还很有可能什么也查不到。
2.2、多术语查询
Terms query的功能与term query的基本一样,只是多术语查询允许在参数中传递多个查询词,被任意一个查询词匹配到的结果都会被搜索出来。例如:
GET user/_search
{
"query": {
"terms": {
"username.keyword": [
"马六",
"赵四",
"李二狗"
]
}
}
}
此时可以得到三个查询结果。
{
"took" : 1068,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "7",
"_score" : 1.0,
"_source" : {
"userid" : "7",
"username" : "赵四",
"age" : "18",
"sex" : "true",
"born" : "2000-09-26 19:00:00",
"address" : {
"lat" : 10.12,
"lon" : -11.34
}
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "4",
"_score" : 1.0,
"_source" : {
"userid" : "4",
"username" : "马六",
"age" : "9",
"sex" : "false",
"born" : "2000-05-26 19:00:00"
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "6",
"_score" : 1.0,
"_source" : {
"userid" : "6",
"username" : "李二狗",
"age" : "88",
"sex" : "true",
"born" : "2000-08-26 19:00:00",
"address" : {
"lat" : 20.12,
"lon" : -21.34
}
}
}
]
}
}
2.3、主键查询
使用主键查询一个文档,这里的IDs主键查询允许你传递多个主键同时查询多个文档。例如:
GET user/_search
{
"query": {
"ids": {
"values": ["3","5"]
}
}
}
从以下代码可以看到,主键为3和5的两条数据被成功返回。
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "5",
"_score" : 1.0,
"_source" : {
"userid" : "5",
"username" : "李大头",
"age" : "26",
"sex" : "true",
"born" : "2000-07-26 19:00:00",
"address" : {
"lat" : 30.12,
"lon" : -31.34
}
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"userid" : "3",
"username" : "王五",
"age" : "48",
"sex" : "true",
"born" : "2000-04-26 19:00:00"
}
}
]
2.4、范围查询
范围查询也很简单,可以返回某个数值或日期字段处于某一区间的数据。区间筛选参数gt表示大于,gte表示大于等于,lt表示小于,lte表示小于等于。由于索引中保存的时间是UTC时间,以下查询表示查询born日期处于2000/04/11 00:00:00(UTC)至2000/09/11 00:00:00(UTC)范围内的数据,可以使用format参数自定义查询的日期格式。
GET user/_search
{
"query": {
"range": {
"born": {
"gte": "2000/04/11 00:00:00",
"lte": "2000/09/11 00:00:00",
"format": "yyyy/MM/dd HH:mm:ss"
}
}
}
}
从以下结果可以看到born日期在筛选范围内的数据被查询出来了。
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "5",
"_score" : 1.0,
"_source" : {
"userid" : "5",
"username" : "李大头",
"age" : "26",
"sex" : "true",
"born" : "2000-07-26 19:00:00",
"address" : {
"lat" : 30.12,
"lon" : -31.34
}
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"userid" : "3",
"username" : "王五",
"age" : "48",
"sex" : "true",
"born" : "2000-04-26 19:00:00"
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "4",
"_score" : 1.0,
"_source" : {
"userid" : "4",
"username" : "马六",
"age" : "9",
"sex" : "false",
"born" : "2000-05-26 19:00:00"
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "6",
"_score" : 1.0,
"_source" : {
"userid" : "6",
"username" : "李二狗",
"age" : "88",
"sex" : "true",
"born" : "2000-08-26 19:00:00",
"address" : {
"lat" : 20.12,
"lon" : -21.34
}
}
}
]
注意:在实际中进行日期范围查询时,通常索引数据和查询条件要么都用北京时间,要么都用UTC时间,要么都用时间戳,不要混着用,否则容易产生错误。但是存在一种可能,就是索引中保存的是UTC时间,但是查询条件又想用北京时间来实现筛选,此时需要在查询中添加参数“time_zone”: “+08:00”,它表示查询条件的时间对应的时区是东八区。
GET user/_search
{
"query": {
"range": {
"born": {
"gte": "2000/04/11 08:00:00",
"lte": "2000/09/11 08:00:00",
"format": "yyyy/MM/dd HH:mm:ss",
"time_zone": "+08:00"
}
}
}
}
注意:有些人喜欢在日期区间中使用日期计算表达式,例如使用now表示当前时间,使用now/d代表当前时区的起点时间,而time_zone参数对now无影响但是对now/d有影响,这个时候用起来很容易出错,因此最好还是把时间范围计算好输进去。
2.5、存在查询
存在(exists)查询用于筛选某个字段不为空的文档,其作用类似于SQL的“is not null”语句的作用。
使用exists查询查找address字段不为空的数据。
GET user/_search
{
"query": {
"exists": {
"field": "address"
}
}
}
从以下结果可以发现,只有含有address字段的数据被查出来了。
{
"took" : 50,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "5",
"_score" : 1.0,
"_source" : {
"userid" : "5",
"username" : "李大头",
"age" : "26",
"sex" : "true",
"born" : "2000-07-26 19:00:00",
"address" : {
"lat" : 30.12,
"lon" : -31.34
}
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "7",
"_score" : 1.0,
"_source" : {
"userid" : "7",
"username" : "赵四",
"age" : "18",
"sex" : "true",
"born" : "2000-09-26 19:00:00",
"address" : {
"lat" : 10.12,
"lon" : -11.34
}
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "6",
"_score" : 1.0,
"_source" : {
"userid" : "6",
"username" : "李二狗",
"age" : "88",
"sex" : "true",
"born" : "2000-08-26 19:00:00",
"address" : {
"lat" : 20.12,
"lon" : -21.34
}
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "8",
"_score" : 1.0,
"_source" : {
"userid" : "8",
"username" : "宋小宝",
"age" : "28",
"sex" : "false",
"born" : "2000-10-26 19:00:00",
"address" : {
"lat" : 40.12,
"lon" : -71.34
}
}
}
]
}
}
2.6、前缀查询
前缀查询用于搜索某个字段的前缀与搜索内容匹配的文档,前缀查询比较耗费性能,如果是text字段,你可以在映射中配置index_prefixes参数,它会把每个分词的前缀字符写入索引,从而大大加快前缀查询的速度。先新建一个索引prefix-demo。
PUT prefix-demo
{
"mappings": {
"properties": {
"address": {
"type": "text",
"index_prefixes": {
"min_chars" : 1,
"max_chars" : 3
}
}
}
}
}
上面的请求定义了一个索引,它只包含一个text类型的字段,除了索引数据的每个分词,还会把每个分词的长度为1~3的前缀保存到索引中。进行前缀搜索时,输入任何一个分词的前缀都可以将数据查询出来。先添加一些数据到索引中。
PUT prefix-demo/_bulk
{"index":{"_id":"1"}}
{"id":"1","address":"hello elasticsearch"}
{"index":{"_id":"2"}}
{"id":"2","address":"acknowledged geopoint"}
{"index":{"_id":"3"}}
{"id":"3","address":"address type"}
然后进行前缀搜索
GET prefix-demo/_search
{
"query": {
"prefix": {
"address": {
"value": "a"
}
}
}
}
这个请求能搜索到id为2和3的数据,结果如下。
"hits" : [
{
"_index" : "prefix-demo",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"id" : "2",
"address" : "acknowledged geopoint"
}
},
{
"_index" : "prefix-demo",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"id" : "3",
"address" : "address type"
}
}
]
注意:如果前缀搜索的字段类型不是text而是keyword,就不能使用index_prefixes参数,keyword字段的前缀搜索会比较耗费性能,不宜大量使用。
2.7、正则查询
正则查询允许查询内容是正则表达式,它会查询出某个字段符合正则表达式的所有文档。例如:
GET user/_search
{
"query": {
"regexp": {
"username.keyword": ".*四.*"
}
}
}
上述查询代码中传入的正则表达式是“.*四.*”,可以匹配字段username.keyword包含“四”字的文本,所以查出的结果如下。
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "7",
"_score" : 1.0,
"_source" : {
"userid" : "7",
"username" : "赵四",
"age" : "18",
"sex" : "true",
"born" : "2000-09-26 19:00:00",
"address" : {
"lat" : 10.12,
"lon" : -11.34
}
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"userid" : "2",
"username" : "李四",
"age" : "28",
"sex" : "false",
"born" : "2000-02-26 19:00:00"
}
}
]
2.8、通配符查询
通配符查询允许在查询代码中添加两种通配符,“*”可匹配任意长度的任意字符串,“?”可匹配任意单个字符。例如:
GET user/_search
{
"query": {
"wildcard": {
"username.keyword": {
"value": "?三"
}
}
}
}
这里使用了“?三”匹配任意以“三”字结束的两个字符,结果如下。
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"userid" : "1",
"username" : "张三",
"age" : "18",
"sex" : "true",
"born" : "2000-01-26 19:00:00"
}
}
]
注意:正则查询和通配符查询虽然使用简便,但是其性能开销较大,大量使用时需谨慎。
3、全文检索
全文检索是Elasticsearch的重要功能,它对检索内容和检索字段都会进行文本分析,分析器的选择对搜索结果会产生重要的影响。
3.1、匹配搜索
匹配搜索(match query)和术语查询(term query)不一样,匹配搜索会比较搜索词和每个文档的相似度,只要搜索词能命中文档的分词就会被搜索到,而term query要么搜不到,要么搜到的内容就和索引内容一模一样。Match query主要用于对指定的text类型的字段做全文检索。
创建一个索引,并添加几条数据
PUT poem
{
"mappings": {
"properties": {
"content":{
"type": "text",
"analyzer": "ik_smart"
}
}
}
}
PUT poem/_doc/1
{
"content":"床前明月光"
}
PUT poem/_doc/2
{
"content":"疑是地上霜"
}
PUT poem/_doc/3
{
"content":"举头望明月"
}
PUT poem/_doc/4
{
"content":"低头思故乡"
}
用IK分词器来测试match query的效果。
GET poem/_search
{
"query": {
"match": {
"content": {
"query": "月",
"analyzer": "ik_smart"
}
}
}
}
如下述结果所示,搜索结果是空的,原因是IK分词器把“床前明月光”切分成了“床“ ”前”和“明月光”三个词,搜索“月”当然是搜不到的。
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 0,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
}
}
使用_analyze查看ik分词结果,加以证明上面的返回结果
POST _analyze
{
"analyzer": "ik_smart",
"text":"床前明月光"
}
分词结果为
{
"tokens" : [
{
"token" : "床",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "前",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "明月光",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
}
]
}
再尝试用"明月光"搜索文档
GET poem/_search
{
"query": {
"match": {
"content": {
"query": "明月光",
"analyzer": "ik_smart"
}
}
}
}
可以看到数据被成功搜索出来了,返回结果如下。
{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.2876821,
"hits" : [
{
"_index" : "poem",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.2876821,
"_source" : {
"content" : "床前明月光"
}
}
]
}
}
Match搜索允许添加多个搜索词,中间用空格隔开,默认的逻辑连接词是“or”,文档只要匹配了任意一个搜索词就能被搜到。如果你想实现只有文档匹配全部的搜索词才能被搜到,可以配置逻辑连接词为“and”。
GET poem/_search
{
"query": {
"match": {
"content": {
"query": "地上 霜",
"analyzer": "ik_smart",
"operator": "and"
}
}
}
}
3.2、布尔前缀匹配搜索
布尔前缀匹配搜索(match bool prefix query)会在搜索文本经过分析后,将前面的每个分词转化为进行Term query,最后一个分词转化为进行Prefix query。多个查询之间的布尔逻辑连接关系是“should”,这意味着任何一个子查询能够匹配的文档都会成为搜索结果。例如:
GET poem/_search
{
"query": {
"match_bool_prefix":{
"content":"明月 地上 床"
}
}
}
这个查询实际上被转化为了布尔查询,代码如下。
GET poem/_search
{
"query": {
"bool": {
"should": [
{"term": {
"content": {
"value": "明月"
}
}},
{
"term": {
"content": {
"value": "地上"
}
}
},
{
"term": {
"content": {
"value": "床"
}
}
}
]
}
}
}
搜索的结果如下
"hits" : [
{
"_index" : "poem",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.2039728,
"_source" : {
"content" : "疑是地上霜"
}
},
{
"_index" : "poem",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.2039728,
"_source" : {
"content" : "举头望明月"
}
},
{
"_index" : "poem",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"content" : "床前明月光"
}
}
]
3.3、短语搜索
短语搜索(match phrase)会对搜索文本进行文本分析,然后到索引中寻找搜索的每个分词并要求分词相邻,你可以通过调整slop参数设置分词出现的最大间隔距离。
使用短语搜索,先将slop设置为默认值0。
GET poem/_search
{
"query": {
"match_phrase": {
"content": {
"query": "床明月光",
"slop": 0
}
}
}
}
此时并不能搜索到刚才的测试数据,原因是床和明月光这两个分词在索引中并不相邻,可以增大slop的值让数据能被搜到。
GET poem/_search
{
"query": {
"match_phrase": {
"content": {
"query": "床明月光",
"slop":1
}
}
}
}
由于建索引时文本“床前明月光”会被切分为“床”“前”和“明月光”,所以分词“床”和“明月光”的距离其实是1。
3.4、短语前缀匹配索引
短语前缀匹配搜索(match phrase prefix)会首先对搜索文本进行分词,然后将前面的分词作为短语进行搜索,将最后一个分词作为前缀进行搜索。
GET poem/_search
{
"query": {
"match_phrase_prefix": {
"content": {
"query": "故乡低头",
"analyzer": "ik_smart"
}
}
}
}
以上代码会寻找content字段中包含“故乡”这个短语并且后面以“低头”开头的文本,它可以检索到id为4的文档,如果把搜索文本改为“低头故乡”就搜不到,因为最后一个分词前缀匹配失败。这种搜索方式可以用来实现文本输入时的搜索提示功能,当用户输入搜索词时将匹配的文本以下拉条的方式进行呈现。
3.5、多字段匹配搜索
多字段匹配搜索(multi_match)可以非常方便地用同一段文本同时检索多个字段,如果不指定字段名,将默认搜索全部字段。多字段匹配搜索有6种类型,选择的类型会影响搜索方式和相关度打分机制,这6种类型如表所示。
类型 | 说明 |
---|---|
best fields | 默认的搜索方式。搜索文本与哪个字段相关度最高,就最容易排名靠前,即使匹配的字段数目很少 |
most fields | 搜索文本与文档的相关度即使不高,但只要匹配的字段数目越多,排名就会越靠前 |
cross fields | 把全部的字段看成一个合并的大字段,搜索文本与这个大字段越匹配,排名越靠前 |
phrase | 将搜索文本在每个字段上做短语搜索,文档的最终得分采用best fields方式计算 |
phrase_prefix | 将搜索文本在每个字段上做短语前缀匹配搜索,文档的最终得分采用best fields方式计算 |
bool prefix | 将搜索文本在每个字段上做布尔前缀匹配搜索,文档的最终得分采用most fields方式计算 |
新增一条数据
POST poem/_doc/5
{
"content":"明月几时有"
}
典型的多字段匹配搜索的请求如下。
GET poem/_search
{
"query": {
"multi_match": {
"query": "明月 举头",
"fields": ["content"],
"operator": "or",
"type": "best_fields"
}
}
}
其中,operator操作符设置为“or”表示文本分析后任意一个分词在搜索结果的文档中出现即可,type用于指定搜索的类型,可选择表中介绍的6种类型中的一种。
3.6、查询字符串搜索
查询字符串搜索(query string)是Elasticsearch为开发人员提供的功能较为强大的全文检索方法,它可以传入一个复杂的字符串,可以包含逻辑表达式、通配符、正则表达式,也可以通过fields参数指定检索字段。
先来看一个简单的实例,使用前面的最细粒度分析器做查询字符串搜索。
GET poem/_search
{
"query": {
"query_string": {
"query": "举头 AND 明月",
"analyzer": "ik_smart"
}
}
}
上述搜索字符串的意思是必须在搜索结果中包含“举头”和“明月”这两个短语,但该请求会得到下面的搜索结果。
{
"took" : 4,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 2.261763,
"hits" : [
{
"_index" : "poem",
"_type" : "_doc",
"_id" : "3",
"_score" : 2.261763,
"_source" : {
"content" : "举头望明月"
}
}
]
}
}
4、经纬度搜索
经纬度搜索在GIS开发中较为常见,比如你想在地图上搜索有哪些坐标点落在某个圆形、矩形或者多边形的区域内,这时经纬度搜索就会特别管用。
4.1、圆形搜索
圆形搜索(geo-distance)用于搜索距离某个圆心一定长度的检索半径之内的全部数据,传参时需要传入圆心坐标和检索半径。
先新建一个索引geo-shop,并添加一些测试数据。
PUT geo-shop
{
"mappings": {
"properties": {
"name":{
"type": "keyword"
},
"location": {
"type": "geo_point"
}
}
}
}
PUT geo-shop/_bulk
{"index":{"_id":"1"}}
{"name":"北京","location":[116.4072154982,39.9047253699]}
{"index":{"_id":"2"}}
{"name":"上海","location":[121.4737919321,31.2304324029]}
{"index":{"_id":"3"}}
{"name":"天津","location":[117.1993482089,39.0850853357]}
{"index":{"_id":"4"}}
{"name":"顺义","location":[116.6569478577,40.1299127031]}
{"index":{"_id":"5"}}
{"name":"石家庄","location":[114.52,38.05]}
{"index":{"_id":"6"}}
{"name":"香港","location":[114.10000,22.20000]}
{"index":{"_id":"7"}}
{"name":"杭州","location":[120.20000,30.26667]}
{"index":{"_id":"8"}}
{"name":"青岛","location":[120.33333,36.06667]}
下面的请求会创建一个圆形搜索,它会搜索以经纬度[116.4107, 39.96820]为圆心,以100km为检索半径的城市列表。
POST geo-shop/_search
{
"query": {
"geo_distance": {
"distance": "100km",
"location": {
"lat": 39.96820,
"lon": 116.4107
}
}
}
}
得到的结果如下,如果你加大检索半径,搜到的城市会更多。
{
"took" : 2042,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "geo-shop",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"name" : "北京",
"location" : [
116.4072154982,
39.9047253699
]
}
},
{
"_index" : "geo-shop",
"_type" : "_doc",
"_id" : "4",
"_score" : 1.0,
"_source" : {
"name" : "顺义",
"location" : [
116.6569478577,
40.1299127031
]
}
}
]
}
}
4.2、矩形搜索
与圆形搜索不同,矩形搜索(geo-bounding box)需要提供左上角(top_left)和右下角(bottom_right)的经纬度坐标,这样才能查出所有的矩形范围内的数据。例如:
POST geo-shop/_search
{
"query": {
"geo_bounding_box": {
"location": {
"top_left": {
"lat": 40.82,
"lon": 111.65
},
"bottom_right": {
"lat": 36.07,
"lon": 120.33
}
}
}
}
}
在上面的矩形搜索参数中,top_left中传入了呼和浩特市的坐标,bottom_right中传入了青岛市的坐标,于是成功得到以下搜索结果。
"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "geo-shop",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"name" : "北京",
"location" : [
116.4072154982,
39.9047253699
]
}
},
{
"_index" : "geo-shop",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"name" : "天津",
"location" : [
117.1993482089,
39.0850853357
]
}
},
{
"_index" : "geo-shop",
"_type" : "_doc",
"_id" : "4",
"_score" : 1.0,
"_source" : {
"name" : "顺义",
"location" : [
116.6569478577,
40.1299127031
]
}
},
{
"_index" : "geo-shop",
"_type" : "_doc",
"_id" : "5",
"_score" : 1.0,
"_source" : {
"name" : "石家庄",
"location" : [
114.52,
38.05
]
}
}
]
}
4.3、多边形搜索
多边形搜索(geo-polygon)需要传入至少3个坐标点的数据,geo-polygon会搜索出坐标点围成的多边形范围内的数据。例如:
POST geo-shop/_search
{
"query": {
"geo_polygon": {
"location": {
"points": [
{
"lat": 39.9,
"lon": 116.4
},
{
"lat": 41.8,
"lon": 123.38
},
{
"lat": 30.52,
"lon": 114.31
}
]
}
}
}
}
可以得到以下搜索结果。
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "geo-shop",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"name" : "天津",
"location" : [
117.1993482089,
39.0850853357
]
}
}
]
}
5、复合搜索
前面介绍的每种搜索方法都是提供单一的功能,接下来介绍的搜索方法可以按照一定的方式组织多条不同的搜索语句,这样的搜索就是复合搜索。
5.1、布尔查询
布尔查询应该是项目开发中应用得很多的复合搜索方法了,它可以按照布尔逻辑条件组织多条查询语句,只有符合整个布尔条件的文档才会被搜索出来。在布尔条件中,可以包含两种不同的上下文。
(1) 搜索上下文(query context):使用搜索上下文时,Elasticsearch需要计算每个文档与搜索条件的相关度得分,这个得分的计算需使用一套复杂的计算公式,有一定的性能开销,带文本分析的全文检索的查询语句很适合放在搜索上下文中。
(2) 过滤上下文(filter context):使用过滤上下文时,Elasticsearch只需要判断搜索条件跟文档数据是否匹配,例如使用Term query判断一个值是否跟搜索内容一致,使用Range query判断某数据是否位于某个区间等。过滤上下文的查询不需要进行相关度得分计算,还可以使用缓存加快响应速度,很多术语级查询语句都适合放在过滤上下文中。
布尔查询一共支持4种组合类型,它们的使用说明如表所示。
类型 | 说明 |
---|---|
must | 可包含多个查询条件,每个条件均满足的文档才能被搜索到,每次查询需要计算相关度得分,属于搜索上下文 |
should | 可包含多个查询条件,不存在mustfflter条件时,至少要满足多个查询条件中的一个,文档才能被搜索到,否则需满足的条件数量不受限制,匹配到的查询越多相关度越高,也属于搜索上下文 |
filter | 可包含多个过滤条件,每个条件均满足的文档才能被搜索到,每个过滤条件不计算相关度得分,结果在一定条件下会被缓存,属于过滤上下文 |
must not | 可包含多个过滤条件,每个条件均不满足的文档才能被搜索到,每个过滤条件不计算相关度得分,结果在一定条件下会被缓存,属于过滤上下文 |
下面发起一个布尔查询请求,如下所示。
GET user/_search
{
"query": {
"bool": {
"must": [
{"match": {
"username": "赵 李"
}}
],
"should": [
{"range": {
"age": {
"gte": 20
}
}}
],
"filter": [
{"term": {
"sex": "true"
}}
],
"must_not": [
{"term": {
"born": {
"value": "2000-09-26 19:00:00"
}
}}
]
}
}
}
这个布尔查询请求中,must部分使用match查询姓名包含“赵 李”的文档,should部分进一步筛选出年龄大于等于20的文档,filter部分只保留sex字段为true的文档,must_not部分去掉了出生日期为2000-09-26 19:00:00的文档。实际上在大部分的开发场景中,must和filter是必不可少的,你可以使用下面的通用查询结构模板来完成多条件查询语句的组织。
// 通用查询结构模板
{
"query": {
"bool": {
"must": [
{ "match": { "title":"hello"}},
{ "match": { "content": "world" }}
],
"filter": [
{ "term": { "status": "ok" }},
{ "range": { "born_date": { "gte": "2011-01-01" }}}
]
}
}
}
注意:虽然你可以把match放在filter里面,但是这样不会计算相关度得分,可能导致搜索结果的排序并不理想;你也可以把term放在must里面,但是这样就无法用到缓存,还要计算相关度得分,会导致查询变慢。因此,请在使用时养成好的搜索习惯,把需要文本分词的检索条件放到must里面,把不需要分词的检索条件放到filter里面。
你可以给布尔查询添加参数minimum_should_match来控制should条件至少需要匹配的数量。如果布尔查询存在must或filter子句,则该值默认为1;否则,该值默认为0。例如:
GET user/_search
{
"query": {
"bool": {
"should": [
{"match": {
"username": "张"
}},
{"match": {
"age": "28"
}},
{"match": {
"username": "李"
}}
],
"minimum_should_match": 2
}
}
}
在这个布尔查询请求中,should子句的minimum_should_match设置为2,表示搜索结果必须匹配3个查询子句的2个以上。这个参数还可以设置为百分比形式,表示should子句至少需要匹配的比例(结果为小数则向下取整)。
结果如下
{
"took" : 108,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.9808291,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.9808291,
"_source" : {
"userid" : "2",
"username" : "李四",
"age" : "28",
"sex" : "false",
"born" : "2000-02-26 19:00:00"
}
}
]
}
}
5.2、常量得分查询
常量得分(constant score)查询本质上是过滤上下文,不会对文档计算相关度得分,每个搜索结果的得分会被赋值成请求传入的常数,默认每个文档的得分都是1.0。例如:
GET user/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"age": "28"
}
},
"boost": 1.2
}
}
}
由于上面的请求设置了boost参数为1.2,这样搜出的文档得分就被赋值成1.2。
{
"took" : 528,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 1.2,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.2,
"_source" : {
"userid" : "2",
"username" : "李四",
"age" : "28",
"sex" : "false",
"born" : "2000-02-26 19:00:00"
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "8",
"_score" : 1.2,
"_source" : {
"userid" : "8",
"username" : "宋小宝",
"age" : "28",
"sex" : "false",
"born" : "2000-10-26 19:00:00",
"address" : {
"lat" : 40.12,
"lon" : -71.34
}
}
}
]
}
}
5.3、析取最大查询
析取最大查询(disjunction max)允许添加多个查询条件,符合任意条件的文档就会被搜索到,每个文档的相关度得分取匹配搜索条件的最大的那一个值。某些文档可能会同时匹配多个搜索条件,但可能由于得分均不高而无法排名靠前,这时可以在查询中添加tie_breaker参数,将其他匹配的查询得分也计入在内。例如:
GET user/_search
{
"query": {
"dis_max": {
"tie_breaker": 0.7,
"boost": 1.2,
"queries": [
{"match": {
"username": "李"
}},
{
"match": {
"age": "28"
}
}
]
}
},
"from": 0,
"size": 10
}
这个请求添加了两个match查询条件,还设置了tie_breaker为0.7,你可以把它的值设置为0~1范围内的任意小数,这个值越大,文档匹配的查询条件越多,排名就可以越靠前。该请求的搜索结果会返回至少匹配了一个match查询条件的文档。
5.4、相关度增强查询
相关度增强查词(boosting query)包含一个positive查询条件和一个negative查询条件,该查询会返回所有匹配positive查询条件的相关文档,在这些返回的文档中,匹配negative查询条件的文档相关度得分会降低,得分降低的程度可以使用negative_boost参数进行控制。例如:
GET user/_search
{
"query": {
"boosting": {
"positive": {
"match": {
"username": "李"
}
},
"negative": {
"range": {
"age": {
"lte": 50
}
}
},
"negative_boost": 0.5
}
}
}
上述代码会返回所有username字段包含“李”的文档,但是对于age字段小于等于50的文档会将相关度乘0.5作为最后得分。上述代码会使符合negative查询条件的搜索结果的文档排序靠后,而其他文档的排序则靠前。
6、搜索结果的总数
从Elasticsearch 7.x开始,搜索结果中的total值会带有value和relation两个字段,当搜索结果总数小于等于10000时,返回的relation为“eq”,表示此时的搜索结果的总数是准确的。一旦搜索结果的总数超过10000,就默认会返回下面的内容。
…
"hits" : {
"total" : {
"value" : 10000,
"relation" : "gte"
},
…
发现此时搜索结果的relation变成了“gte”,表示实际的总数值比现在返回的值10000要大。默认最大只能返回10000,这样设置是为了在不需要搜索结果的精确总数时可以提高查询性能。如果想获得搜索结果的准确总数,需要在查询中将参数track_total_hits设置为true。
…
"query": {
"match_all": {}
},
"track_total_hits": true
…
从以下返回结果中可以发现此时就能正确返回搜索结果总数了。
…
"hits" : {
"total" : {
"value" : 10001,
"relation" : "eq"
},
…
所以,在实际使用中,如果你需要用到搜索结果的总数,就应该主动把track_total_hits设置为true,如果不需要用到它,则设置为false。
7、搜索结果的分页
对搜索结果进行分页是极为常用的操作,Elasticsearch支持的分页方式有3种:普通分页、滚动分页和search after分页。普通分页类似于关系数据库的分页方法,需要指定页面大小和起始位置的偏移量,它只适合页面数较少的场景,如果起始位置的偏移量太大,分页速度会变得很慢;滚动分页会在开始分页时产生一个数据快照,每个分页请求会返回一个scroll_id,使用scroll_id就能获取下一页的数据;search after可以用于深度分页,原理是利用上次分页的最后一条记录获取下一页的数据。下面就一一介绍每种分页方式的具体使用方法。
7.1、普通分页
普通分页需要在查询时提供两个参数,size表示页面大小,from表示分页记录起始位置,第一条数据的from值为0。例如:
GET user/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 2,
"track_total_hits": true
}
这个match all查询请求会获取搜索结果的前2条记录,这是很简单的分页方式,但是在from+size的值超过10000的时候会报错,原因是Elasticsearch不推荐使用这种方式进行深度分页。如果一定要用这种方式分页,需要在索引配置中调大index.max_result_window的值。
PUT _settings
{
"index.max_result_window" : "2000000000"
}
7.2、 滚动分页
发起滚动分页请求时,需要用size指定每次滚动的页面大小,在请求url中加上一个scroll参数代表滚动分页上下文保存的时间,如果超过指定的时间不往下滚动,则会因上下文过期而无法拉取到下一页的数据。例如:
GET user/_search?scroll=1m
{
"query": {
"match_all": {}
},
"size": 2,
"track_total_hits": true
}
在上面的请求中,设置了每次滚动可拉取2条数据,查询后会得到一个scroll_id。
{
"_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoAxRFRnJIaElzQnVuVUUyY0NVNmZZSwAAAAAAACZGFkNrb2tXYVNtVDNlQWlzd0VFZ0NCNXcURWxySGhJc0J1blVFMmNDVTZmWUwAAAAAAAAmSBZDa29rV2FTbVQzZUFpc3dFRWdDQjV3FEVWckhoSXNCdW5VRTJjQ1U2ZllLAAAAAAAAJkcWQ2tva1dhU21UM2VBaXN3RUVnQ0I1dw==",
"took" : 372,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
这个scroll_id在1min以内有效,你可以使用它发起请求以得到下一条数据。
POST _search/scroll
{
"scroll":"1m",
"scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoAxRFRnJIaElzQnVuVUUyY0NVNmZZSwAAAAAAACZGFkNrb2tXYVNtVDNlQWlzd0VFZ0NCNXcURWxySGhJc0J1blVFMmNDVTZmWUwAAAAAAAAmSBZDa29rV2FTbVQzZUFpc3dFRWdDQjV3FEVWckhoSXNCdW5VRTJjQ1U2ZllLAAAAAAAAJkcWQ2tva1dhU21UM2VBaXN3RUVnQ0I1dw=="
}
在这个滚动下一页的请求中,传入了上次请求得到的scroll_id,并且再一次设置了分页上下文的保存时间是1min,这意味着分页上下文的时间又被延长了1min,只要在1min以内发起请求以得到下一页数据,滚动分页的过程就不会中断。
滚动分页虽然可以用于深度分页,但是一旦开始分页,新的请求对数据的改变就无法被查询到,所以它不适合实时的分页查询请求,只适合用于导出某个时间点的大量数据。
注意:在某一次滚动分页的查询过程中,每个请求返回的scroll_id可能会有变化,所以使用时一定要用最后一次请求得到的那个scroll_id,否则可能会导致请求失败。
滚动分页产生的分页上下文会占用一些系统资源,超过期限后会被系统自动回收。为了减少资源占用,最好在某个scroll_id不需要使用后立即手动回收。例如:
DELETE _search/scroll
{
"scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoAxRFRnJIaElzQnVuVUUyY0NVNmZZSwAAAAAAACZGFkNrb2tXYVNtVDNlQWlzd0VFZ0NCNXcURWxySGhJc0J1blVFMmNDVTZmWUwAAAAAAAAmSBZDa29rV2FTbVQzZUFpc3dFRWdDQjV3FEVWckhoSXNCdW5VRTJjQ1U2ZllLAAAAAAAAJkcWQ2tva1dhU21UM2VBaXN3RUVnQ0I1dw=="
}
如果需要清空全部的滚动分页上下文,可以使用如下代码。
DELETE _search/scroll/_all
7.3、Search after分页
滚动分页无法实时显示最新的数据,Search after分页则可以有效避免这个问题,它的原理是每次根据上一次分页的最后一条记录的位置来寻找下一页的记录。
使用时先发起一个查询请求获取第一页的数据,与普通分页的唯一区别在于,这个查询请求必须添加一个唯一字段来进行排序,如果你的映射中没有唯一字段,可以使用文档自带的主键字段“_id”。例如:
GET user/_search
{
"query": {
"match_all": {}
},
"track_total_hits": true,
"from": 0,
"size": 2,
"sort": [
{
"userid": {
"order": "asc"
}
}
]
}
上述代码会取出搜索结果的前2条记录,数据按照唯一的字段userid进行升序排列,在以下结果中你只需要关注最后一条数据的sort值。
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "1",
"_score" : null,
"_source" : {
"userid" : "1",
"username" : "张三",
"age" : "18",
"sex" : "true",
"born" : "2000-01-26 19:00:00"
},
"sort" : [
1
]
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "2",
"_score" : null,
"_source" : {
"userid" : "2",
"username" : "李四",
"age" : "28",
"sex" : "false",
"born" : "2000-02-26 19:00:00"
},
"sort" : [
2
]
}
]
再把这个sort值2作为search_after分页的参数传入下一次分页的请求,就能得到下一页的数据了。
GET user/_search
{
"query": {
"match_all": {}
},
"track_total_hits": true,
"from": 0,
"size": 2,
"sort": [
{
"userid": {
"order": "asc"
}
}
],
"search_after":[2]
}
注意:在使用search_after参数的查询中,from参数必须为0,否则会报错。另外,排序选择的字段必须是唯一的,不然会因为该字段相同的数据无法决定先后顺序导致分页的结果不正确。
8、搜索结果的排序
你已经知道了如何给搜索结果添加排序功能,实际上排序的字段可以有多个,可以设置为升序或降序排列,其作用类似于SQL的order by语句的作用。例如:
GET user/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"username.keyword": {
"order": "desc"
},
"age":{
"missing": "_last"
}
}
]
}
上述代码使用了username.keyword和age这两个字段进行排序,先对username.keyword按照降序排列,姓名相同时再对age使用默认的升序排列,missing参数表示字段age为null时把数据排到最后。
Elasticsearch还支持按照某个数组字段来排序,你可以配置以数组的最大值、最小值、平均值、总和、中间值作为排序依据。下面的请求将mode排序参数设置为max,表示取数组字段tags.keyword的最大值作为排序依据。
"sort": [
{
"tags.keyword": {
"mode": "max"
}
}
]
注意:在指定排序字段时,不可以直接将分词的text类型字段作为排序依据,因为text类型字段没有在磁盘上保存doc value值,而且这样做在业务上没有任何意义。通常用来排序的字段是日期类型的字段、数值类型的字段、关键字类型的字段,还可以是某些元数据字段,比如_id、_score等。默认情况下,_score字段按照降序排列,其他字段按照升序排列。
9、筛选搜索结果返回的字段
有时候索引的字段数目比较多,而前端并不需要展示或者导出这么多字段,这时候就需要对搜索结果返回的字段进行筛选。以下请求用_source参数筛选出需要返回的字段列表“username”和“age”。
GET user/_search
{
"query": {
"match_all": {}
},
"_source": ["username","age"],
"from": 0,
"size": 1,
"track_total_hits": true
}
如果需要保留的字段太多,可以用excludes参数排除掉不需要的字段,字段中可以有通配符。
GET user/_search
{
"query": {
"match_all": {}
},
"_source": {
"excludes": "age"
},
"from": 0,
"size": 1,
"track_total_hits": true
}
注意:_source中不能包含映射的fields参数中附带的字段,例如username.keyword就不能出现在_source的字段列表中。
你还可以实现在返回的结果中查询出字段的doc value值,所谓的doc value值,就是字段本身的数据内容,它与_source的值是一样的。在构建索引时,索引字段的doc value值会生成并存放在磁盘上,这个值主要用于排序和聚集统计,text类型字段不支持doc value值。
GET user/_search
{
"query": {
"match_all": {}
},
"docvalue_fields": ["username.keyword"]
}
上述请求使用了docvalue_fields查看username.keyword字段的doc value值,可以发现它和_source中的username字段的内容是一样的,返回结果如下。
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "5",
"_score" : 1.0,
"_source" : {
"userid" : "5",
"username" : "李大头",
"age" : "26",
"sex" : "true",
"born" : "2000-07-26 19:00:00",
"address" : {
"lat" : 30.12,
"lon" : -31.34
}
},
"fields" : {
"username.keyword" : [
"李大头"
]
}
},
.......
10、高亮搜索结果中的关键词
高亮是搜索引擎中很常见的功能,使用百度或者谷歌搜索引擎搜索内容的时候,被搜索的关键词会以高亮形式出现在搜索结果中。
高亮功能的使用比较简单,只需要在highlight中传入需要高亮显示的字段和高亮标签,默认的高亮标签是<em></em>标签,你可以通过参数来自定义。例如:
GET user/_search
{
"query": {
"query_string": {
"query": "李 狗"
}
},
"highlight": {
"pre_tags" : ["<tag1>"],
"post_tags" : ["</tag1>"],
"fields": {
"username": {}
}
}
}
这个请求表示在query_string查询的结果中,对username字段进行关键词高亮,使用了pre_tags和post_tags设置高亮标签。该请求会得到以下结果。
{
"took" : 248,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : 1.8662264,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "6",
"_score" : 1.8662264,
"_source" : {
"userid" : "6",
"username" : "李二狗",
"age" : "88",
"sex" : "true",
"born" : "2000-08-26 19:00:00",
"address" : {
"lat" : 20.12,
"lon" : -21.34
}
},
"highlight" : {
"username" : [
"<tag1>李</tag1>二<tag1>狗</tag1>"
]
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.9808291,
"_source" : {
"userid" : "2",
"username" : "李四",
"age" : "28",
"sex" : "false",
"born" : "2000-02-26 19:00:00"
},
"highlight" : {
"username" : [
"<tag1>李</tag1>四"
]
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "5",
"_score" : 0.6407243,
"_source" : {
"userid" : "5",
"username" : "李大头",
"age" : "26",
"sex" : "true",
"born" : "2000-07-26 19:00:00",
"address" : {
"lat" : 30.12,
"lon" : -31.34
}
},
"highlight" : {
"username" : [
"<tag1>李</tag1>大头"
]
}
}
]
}
}
可以看到,搜索结果的高亮文本在highlight字段中进行了展示,如果你想直接对所有字段进行高亮显示,可以把字段名设置为*。
11、折叠搜索结果
如果你想知道索引中某个字段存在哪些不同的数据,就可以使用搜索的折叠功能,它的效果类似于SQL中的“select distinct”的效果,只保留某个字段不重复的数据。
在查询中添加一个collapse参数,选择在age字段上进行数据折叠。
GET user/_search
{
"query": {
"match_all": {}
},
"collapse": {
"field": "age"
}
}
结果如下,每个age会选择一条数据进行展示。
{
"took" : 190,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 8,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "5",
"_score" : 1.0,
"_source" : {
"userid" : "5",
"username" : "李大头",
"age" : "26",
"sex" : "true",
"born" : "2000-07-26 19:00:00",
"address" : {
"lat" : 30.12,
"lon" : -31.34
}
},
"fields" : {
"age" : [
26
]
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "7",
"_score" : 1.0,
"_source" : {
"userid" : "7",
"username" : "赵四",
"age" : "18",
"sex" : "true",
"born" : "2000-09-26 19:00:00",
"address" : {
"lat" : 10.12,
"lon" : -11.34
}
},
"fields" : {
"age" : [
18
]
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"userid" : "2",
"username" : "李四",
"age" : "28",
"sex" : "false",
"born" : "2000-02-26 19:00:00"
},
"fields" : {
"age" : [
28
]
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"userid" : "3",
"username" : "王五",
"age" : "48",
"sex" : "true",
"born" : "2000-04-26 19:00:00"
},
"fields" : {
"age" : [
48
]
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "4",
"_score" : 1.0,
"_source" : {
"userid" : "4",
"username" : "马六",
"age" : "9",
"sex" : "false",
"born" : "2000-05-26 19:00:00"
},
"fields" : {
"age" : [
9
]
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "6",
"_score" : 1.0,
"_source" : {
"userid" : "6",
"username" : "李二狗",
"age" : "88",
"sex" : "true",
"born" : "2000-08-26 19:00:00",
"address" : {
"lat" : 20.12,
"lon" : -21.34
}
},
"fields" : {
"age" : [
88
]
}
}
]
}
}
可以看到age字段重复的数据都被折叠了,多个age各显示了一条数据。如果想展示每个age所包含的详细数据,可以使用inner_hits参数来获取。
GET user/_search
{
"query": {
"match_all": {}
},
"collapse": {
"field": "age",
"inner_hits":{
"name":"by_age",
"size":2,
"sort":[{"born":"desc"}]
}
}
}
上面的请求在inner_hits中配置了每个相同的age数据最多展示两条,并且按照born字段降序排列,结果如下。
{
"took" : 220,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 8,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "5",
"_score" : 1.0,
"_source" : {
"userid" : "5",
"username" : "李大头",
"age" : "26",
"sex" : "true",
"born" : "2000-07-26 19:00:00",
"address" : {
"lat" : 30.12,
"lon" : -31.34
}
},
"fields" : {
"age" : [
26
]
},
"inner_hits" : {
"by_age" : {
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "5",
"_score" : null,
"_source" : {
"userid" : "5",
"username" : "李大头",
"age" : "26",
"sex" : "true",
"born" : "2000-07-26 19:00:00",
"address" : {
"lat" : 30.12,
"lon" : -31.34
}
},
"sort" : [
964638000000
]
}
]
}
}
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "7",
"_score" : 1.0,
"_source" : {
"userid" : "7",
"username" : "赵四",
"age" : "18",
"sex" : "true",
"born" : "2000-09-26 19:00:00",
"address" : {
"lat" : 10.12,
"lon" : -11.34
}
},
"fields" : {
"age" : [
18
]
},
"inner_hits" : {
"by_age" : {
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "7",
"_score" : null,
"_source" : {
"userid" : "7",
"username" : "赵四",
"age" : "18",
"sex" : "true",
"born" : "2000-09-26 19:00:00",
"address" : {
"lat" : 10.12,
"lon" : -11.34
}
},
"sort" : [
969994800000
]
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "1",
"_score" : null,
"_source" : {
"userid" : "1",
"username" : "张三",
"age" : "18",
"sex" : "true",
"born" : "2000-01-26 19:00:00"
},
"sort" : [
948913200000
]
}
]
}
}
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"userid" : "2",
"username" : "李四",
"age" : "28",
"sex" : "false",
"born" : "2000-02-26 19:00:00"
},
"fields" : {
"age" : [
28
]
},
"inner_hits" : {
"by_age" : {
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "8",
"_score" : null,
"_source" : {
"userid" : "8",
"username" : "宋小宝",
"age" : "28",
"sex" : "false",
"born" : "2000-10-26 19:00:00",
"address" : {
"lat" : 40.12,
"lon" : -71.34
}
},
"sort" : [
972586800000
]
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "2",
"_score" : null,
"_source" : {
"userid" : "2",
"username" : "李四",
"age" : "28",
"sex" : "false",
"born" : "2000-02-26 19:00:00"
},
"sort" : [
951591600000
]
}
]
}
}
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"userid" : "3",
"username" : "王五",
"age" : "48",
"sex" : "true",
"born" : "2000-04-26 19:00:00"
},
"fields" : {
"age" : [
48
]
},
"inner_hits" : {
"by_age" : {
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "3",
"_score" : null,
"_source" : {
"userid" : "3",
"username" : "王五",
"age" : "48",
"sex" : "true",
"born" : "2000-04-26 19:00:00"
},
"sort" : [
956775600000
]
}
]
}
}
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "4",
"_score" : 1.0,
"_source" : {
"userid" : "4",
"username" : "马六",
"age" : "9",
"sex" : "false",
"born" : "2000-05-26 19:00:00"
},
"fields" : {
"age" : [
9
]
},
"inner_hits" : {
"by_age" : {
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "4",
"_score" : null,
"_source" : {
"userid" : "4",
"username" : "马六",
"age" : "9",
"sex" : "false",
"born" : "2000-05-26 19:00:00"
},
"sort" : [
959367600000
]
}
]
}
}
}
},
{
"_index" : "user",
"_type" : "_doc",
"_id" : "6",
"_score" : 1.0,
"_source" : {
"userid" : "6",
"username" : "李二狗",
"age" : "88",
"sex" : "true",
"born" : "2000-08-26 19:00:00",
"address" : {
"lat" : 20.12,
"lon" : -21.34
}
},
"fields" : {
"age" : [
88
]
},
"inner_hits" : {
"by_age" : {
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "user",
"_type" : "_doc",
"_id" : "6",
"_score" : null,
"_source" : {
"userid" : "6",
"username" : "李二狗",
"age" : "88",
"sex" : "true",
"born" : "2000-08-26 19:00:00",
"address" : {
"lat" : 20.12,
"lon" : -21.34
}
},
"sort" : [
967316400000
]
}
]
}
}
}
}
]
}
}
注意:使用折叠功能时,选择的字段类型必须是关键字类型或者数值类型,否则请求会失败。
12、解释搜索结果
开发人员或许很想知道为什么一个文档在搜索结果中没有出现,或者为什么它能够出现,这时就可以使用搜索结果的解释API来显示原因,例如,下面的请求进行了range查询,想想看为什么主键为2的文档可以被检索到。
POST user/_explain/2
{
"query": {
"range": {
"born": {
"gte": "2000-02-11 08:00:00",
"lte": "now/d",
"time_zone": "+08:00"
}
}
}
}
上面的请求在url中传入了文档的主键2,指明需要解释的文档主键。请求体中是搜索的内容,会得到以下结果。
{
"_index" : "user",
"_type" : "_doc",
"_id" : "2",
"matched" : true,
"explanation" : {
"value" : 1.0,
"description" : "ConstantScore(DocValuesFieldExistsQuery [field=born])",
"details" : [ ]
}
}
其中,matched为true表示该文档能被搜到,原因是该文档的born值在范围查询的时间区间内,其中区间上界now/d参数被转换为时间戳1603987199999。如果不能被搜索到也能根据搜索结果看出原因,这个工具还能查看文档相关度得分的计算过程,对于开发人员调试一些检索结果比较有用,大家可以多多尝试使用。
13、小结
本文介绍的是Elasticsearch功能的核心内容,讲述了各种常用的搜索方法以及对搜索结果的控制方法,本文的主要内容总结如下。
- 使用布尔查询可以构成常用的查询结构模板,它包括过滤上下文和搜索上下文,你可以根据逻辑的需要拼接多个检索条件。
- 精准级查询通常用于精确地查询或匹配某个字段,这个过程大多针对的是不做文本分析的字段,对搜索内容也不进行文本分析。这类查询通常需要放在布尔查询的过滤上下文中,这样可以直接跳过计算相关度得分并使用缓存加快响应速度。
- 全文检索意味着需要对搜索文本和检索字段的文本进行文本分析,通过调节文本分析器可以改变搜索结果,这些查询语句往往出现在布尔查询的搜索上下文中。
- 经纬度搜索支持检索索引的经纬度坐标在指定圆形、矩形、多边形范围内的点。
- 要准确获取搜索结果的总数,需要配置track_total_hits参数,否则在搜索结果超过10000时,total值不准确。
- Elasticsearch支持的分页方式有3种,普通分页只适合数据量小的场景,滚动分页和search after都可以用于深度分页。但滚动分页一旦开始,后续的查询无法显示索引数据最新的内容,search after则没有这个问题,它是基于最后一次分页的结果查询下一页的数据,在使用它时要求要对搜索结果按照某个唯一的字段排序。
- 搜索结果可以使用折叠功能去掉某个关键字类型或数值类型字段的重复数据,折叠最多嵌套到第二级,使用折叠功能可以查看索引中某个字段的每种数据包含的文档列表。
- 可以使用explain端点来解释某个搜索条件能或者不能搜到一个文档的原因,这个工具常用来进行搜索结果的调试。