Elasticsearch(三)——Es搜索(简单使用、全文查询、复合查询)、地理位置查询、特殊查询、聚合操作、桶聚合、管道聚合
一、Es搜索
这里的 Es 数据博主自己上网找的,为了练习 Es 搜索。
1、Elasticsearch 搜索入门
搜索分为两个过程:
- 当向索引中保存文档时,默认情况下,es 会保存两份内容,一份是 _source 中的数据,另一份则是通过分词、排序等一系列过程生成的倒排索引文件,倒排索引中保存了词项和文档之间的对应关系。
- 搜索时,当 es 接收到用户的搜索请求之后,就会去倒排索引中查询,通过的倒排索引中维护的倒排记录表找到关键词对应的文档集合,然后对文档进行评分、排序、高亮等处理,处理完成后返回文档。
2、简单搜索
a、match_all——查询所有
简写:
结果:
因为没有设置查询条件,所有最大的得分是 1.0。
这里并没有把所有的数据都展示出来,因为默认是有分页功能的。
b、词项查询
即 term 查询,就是根据词去查询,查询指定字段中包含给定单词的文档,term 查询不被解析,只有搜索的词和文档中的词精确匹配,才会返回文档。应用场景如:人名、地名等等。
查询包含化学的:
结果:
这里是按照查询出来的分数从高到低排序。最上面是得分最高的,也就是匹配度最高的。
c、分页
默认返回前 10 条数据,es 中也可以像关系型数据库一样,给一个分页参数:
from:从第几条开始。
size:多少条数据。
d、过滤返回字段
如果返回的字段比较多,又不需要这么多字段,此时可以指定返回的字段:
这里指定返回 name 字段:
效果:
e、最小评分
有的文档得分特别低,说明这个文档和我们查询的关键字相关度很低。我们可以设置一个最低分,只有得分超过最低分的文档才会被返回。
最低分要高于 1.78 :
结果:
f、高亮
查询关键字高亮:
3、Es 全文查询
a、match query
match query 会对查询语句进行分词,分词后,如果查询语句中的任何一个词项被匹配,则文档就会被索引到。
前面都是输入词进行查询,这里可以输入一句话来查询了:
效果:
这里是传一句话进去,只要能有一个词能匹配,这条记录就算是相关记录会返回来。
如果想要两个词都包含,那么可以使用 operator 的 and (默认是 or):
b、match_phrase query
match_phrase query 也会对查询的关键字进行分词,但是它分词后有两个特点:
- 分词后的词项顺序必须和文档中词项的顺序一致
- 所有的词都必须出现在文档中
示例如下:
结果:
结果是什么都没有搜到。
因为这两个词分词之后是挨着的。但现在想要查询十一五和计算机这两个词不要挨在一起,中间可以隔着其他东西。所以这里可以通过 slop 配置中间可以隔多少字符:
效果:
query 是查询的关键字,会被分词器进行分解,分解之后去倒排索引中进行匹配。
slop 是指关键字之间的最小距离,但是注意不是关键之间间隔的字数。文档中的字段被分词器解析之后,解析出来的词项都包含一个 position 字段表示词项的位置,查询短语分词之后 的 position 之间的间隔要满足 slop 的要求。
c、match_phrase_prefix query(效率低,了解即可)
这个类似于 match_phrase query,只不过这里多了一个通配符,match_phrase_prefix 支持最后一个词项的前缀匹配,但是由于这种匹配方式效率较低,因此大家作为了解即可。
这个查询过程,会自动进行单词匹配,会自动查找以计开始的单词,默认是 50 个,可以自己控制:
match_phrase_prefix 是针对分片级别的查询,假设 max_expansions 为 1,可能返回多个文档,但是只有一个词,这是我们预期的结果。有的时候实际返回结果和我们预期结果并不一致,原因在于这个查询是分片级别的,不同的分片确实只返回了一个词,但是结果可能来自不同的分片,所以最终会看到多个词。
d、multi_match query
match 查询的升级版,可以指定多个查询域(意思就是查询多个字段):
效果:
这种查询方式还可以指定字段的权重:
意思就是更在意 name 中是否有 java ;加到其他字段也就是更在意哪些字段有 java。
结果:
e、query_string query
query_string 是一种紧密结合 Lucene 的查询方式,在一个查询语句中可以用到 Lucene 的一些查询语法:
f、simple_query_string
这个是 query_string 的升级,可以直接使用 +、|、- 代替 AND、OR、NOT 等。
g、term query
词项查询。词项查询不会分析查询字符,直接拿查询字符去倒排索引中比对。
h、terms query
词项查询,但是可以给多个关键词。
i、range query
范围查询,可以按照日期范围、数字范围等查询。
range query 中的参数主要有四个:
- gt
- lt
- gte
- lte
查询价格大于等于 10,小于等于 20 的书:
效果:
因为都在价格范围内,没有说谁分高分低。
根据价格排序:
效果:
j、exists query
exists query 会返回指定字段中至少有一个非空值的文档:
注意,空字符串也是有值。null 是空值。
k、prefix query
前缀查询,效率略低,除非必要,一般不太建议使用。
给定关键词的前缀去查询:
l、wildcard query
wildcard query 即通配符查询。支持单字符和多字符通配符:
- ? 表示一个任意字符。
- * 表示零个或者多个字符。
查询所有姓张的作者的书:
查询所有姓张并且名字只有两个字的作者的书:
m、regexp query
支持正则表达式查询。
查询所有姓张并且名字只有两个字的作者的书:
n、 fuzzy query
在实际搜索中,有时我们可能会打错字,从而导致搜索不到,在 match query 中,可以通过 fuzziness属性实现模糊查询。
fuzzy query 返回与搜索关键字相似的文档。怎么样就算相似?以LevenShtein 编辑距离为准。编辑距离是指将一个字符变为另一个字符所需要更改字符的次数,更改主要包括四种:
- 更改字符( javb–〉java )
- 删除字符( javva–〉java )
- 插入字符( jaa–〉java )
- 转置字符( ajva–〉java )
为了找到相似的词,模糊查询会在指定的编辑距离中创建搜索关键词的所有可能变化或者扩展的集合,然后进行搜索匹配:
o、 ids query
根据指定的 id 查询:
4、Es 复合查询
a、constant_score query
当我们不关心检索词项的频率(TF)对搜索结果排序的影响时,可以使用 constant_score 将查询语句或者过滤语句包裹起来。
比如:java 出现 10 词跟出现 1 词是一样的,那么就可以这么做:
b、bool query
bool query 可以将任意多个简单查询组装在一起,有四个关键字可供选择,四个关键字所描述的条件可以有一个或者多个。
- must:文档必须匹配 must 选项下的查询条件。
- should:文档可以匹配 should 下的查询条件,也可以不匹配。
- must_not:文档必须不满足 must_not 选项下的查询条件。
- filter:类似于 must,但是 filter 不评分,只是过滤数据。
例如查询 name 属性中必须包含 java,同时书价不在 [0,35] 区间内,info 属性可以包含 程序设计 也可以不包含程序设计:
效果:
这里还涉及到一个关键字, minmum_should_match 参数。
minmum_should_match 参数在 es 官网上称作最小匹配度。在之前学习的 multi_match 或者这里的should 查询中,都可以设置 minmum_should_match 参数。
假设我们要做一次查询,查询 name 中包含 语言程序设计 关键字的文档:
在这个查询过程中,首先会进行分词,分词结果如下:
分词后的 term 会构造成一个 should 的 bool query,每一个 term 都会变成一个 term query 的子句。换句话说,上面的查询和下面的查询等价。
在这两个查询语句中,都是文档只需要包含词项中的任意一项即可,文档就回被返回,在 match 查询中,可以通过 operator 参数设置文档必须匹配所有词项。
如果想匹配一部分词项,就涉及到一个参数,就是 minmum_should_match ,即最小匹配度。即至少匹配多少个词。
50% 表示词项个数的 50%。就是说至少有一般的词量被匹配才会返回。
如下两个查询等价(参数 4 是因为查询关键字分词后有 4 项):
调整数字,数字越多,意味着要匹配的词汇越多,结果越少。
c、dis_max query
假设现在有两本书:
肉眼观察,第二个的 comtent 语句有两个关键字,第一个只是 title 和 content 各有一个关键字,感觉第二个和查询关键字相似度更高。但是实际查询结果并非这样。
要理解这个原因,我们需要来看下 should query 中的评分策略:
- 首先会执行 should 中的两个查询。
- 对两个查询结果的评分求和。
- 对求和结果乘以匹配语句总数。
- 在对第三步的结果除以所有语句总数。
反映到具体的查询中:
前者:
- title 中 包含 java,假设评分是 1.1
- content 中包含解决方案,假设评分是 1.2
- 有得分的 query 数量,这里是 2
- 总的 query 数量也是 2
最终结果: (1.1+1.2)*2/2=2.3
后者:
- title 中 不包含查询关键字,没有得分
- content 中包含解决方案和 java,假设评分是 2
- 有得分的 query 数量,这里是 1
- 总的 query 数量也是 2
最终结果: 2*1/2=1
在这种查询中,title 和 content 相当于是相互竞争的关系,所以我们需要找到一个最佳匹配字段。
为了解决这一问题,就需要用到 dis_max query(disjunction max query,分离最大化查询):匹配的文档依然返回,但是只将最佳匹配的评分作为查询的评分。(这里就是:跟 title 和 content 都去匹配,谁的得分高就用谁;不在给这两个去算综合的分数):
查询结果如下:
这一次的结果就符合心中的预期了。
在 dis_max query 中,还有一个参数 tie_breaker (取值在0~1),在 dis_max query 中,是完全不考虑其他 query 的分数,只是将最佳匹配的字段的评分返回。但是,有的时候,我们又不得不考虑一下其他 query 的分数,此时,可以通过 tie_breaker 来优化 dis_max query。 tie_breaker 会将其他 query 的分数,乘以 tie_breaker ,然后和分数最高的 query 进行一个综合计算:
如果这里设置回 1,结果又会变得跟原来一样。
d、function_score query
场景:例如想要搜索附近的肯德基,搜索的关键字是肯德基,但是我希望能够将评分较高的肯德基优先展示出来。但是默认的评分策略是没有办法考虑到餐厅评分的,他只是考虑相关性,这个时候可以通过 function_score query 来实现。
准备两条测试数据:
现在搜索标题中包含 java 关键字的文档:
查询结果如下:
默认情况下,id 为 2 的记录得分较高,因为他的 title 中包含两个 java。
如果我们在查询中,希望能够充分考虑 votes 字段,将 votes 较高的文档优先展示,就可以通过function_score 来实现。
具体的思路,就是在旧的得分基础上,根据 votes 的数值进行综合运算,重新得出一个新的评分。
具体有几种不同的计算方式:
- weight
- random_score
- script_score
- field_value_factor
(1)weight
weight 可以对评分设置权重,就是在旧的评分基础上乘以 weight,他其实无法解决我们上面所说的问题。具体用法如下:
(2)random_score
random_score 会根据 uid 字段进行 hash 运算,生成分数,使用 random_score 时可以配置一个种子,如果不配置,默认使用当前时间。
(3)script_score
自定义评分脚本。假设每个文档的最终得分是旧的分数加上votes。查询方式如下:
现在,最终得分是 (oldScore+votes)*oldScore 。
如果不想乘以 oldScore,查询方式如下:
通过 boost_mode 参数,可以设置最终的计算方式。该参数还有其他取值:
- multiply:分数相乘
- sum:分数相加
- avg:求平均数
- max:最大分
- min:最小分
- replace:不进行二次计算
(4)field_value_factor
这个的功能类似于 script_score ,但是不用自己写脚本。
假设每个文档的最终得分是旧的分数乘以votes。查询方式如下:
默认的得分就是 oldScore * votes 。
还可以利用 es 内置的函数进行一些更复杂的运算:
此时,最终的得分是(sqrt(votes))。
modifier 中可以设置内置函数,其他的内置函数还有:
另外还有个参数 factor ,影响因子。字段值先乘以影响因子,然后再进行计算。以 sqrt 为例,计算方式为 sqrt ( factor * votes) :
还有一个参数 max_boost ,控制计算结果的范围:
max_boost 参数表示 functions 模块中,最终的计算结果上限。如果超过上限,就按照上线计算。
e、boosting query
boosting query 中包含三部分:
- positive:得分不变
- negative:降低得分
- negative_boost:降低的权重
可以看到,id 为 86 的文档满足条件,因此它的最终得分在旧的分数上 *0.5 。
关系型数据库中有表的关联关系,在 es 中,我们也有类似的需求,例如订单表和商品表,在 es 中,这样的一对多一般来说有两种方式:
- 嵌套文档(nested)
- 父子文档
f、嵌套文档
假设:有一个电影文档,每个电影都有演员信息:
注意 actors 类型要是 nested。
这是因为 nested 文档在 es 内部其实也是独立的 lucene 文档,只是在我们查询的时候,es 内部帮我们做了 join 处理,所以最终看起来就像一个独立文档一样。因此这种方案性能并不是特别好。
g、嵌套查询
这个用来查询嵌套文档:
h、父子文档
相比于嵌套文档,父子文档主要有如下优势:
- 更新父文档时,不会重新索引子文档
- 创建、修改或者删除子文档时,不会影响父文档或者其他的子文档。
- 子文档可以作为搜索结果独立返回。
例如学生和班级的关系:
s_c 表示父子文档关系的名字,可以自定义。join 表示这是一个父子文档。relations 里边,class 这个位置是 parent,student 这个位置是 child。
接下来,插入两个父文档:
再来添加三个子文档:
首先大家可以看到,子文档都是独立的文档。特别需要注意的地方是,子文档需要和父文档在同一个分片上,所以 routing 关键字的值为父文档的 id。另外,name 属性表明这是一个子文档。
父子文档需要注意的地方:
- 每个索引只能定义一个 join filed
- 父子文档需要在同一个分片上(查询,修改需要routing)
- 可以向一个已经存在的 join filed 上新增关系
i、 has_child query
通过子文档查询父文档使用 has_child query。
查询 wangwu 所属的班级。
j、has_parent query
通过父文档查询子文档:
查询二班的学生。但是大家注意,这种查询没有评分。
可以使用 parent id 查询子文档:
通过 parent id 查询,默认情况下使用相关性计算分数。
k、小结
整体上来说:
- 普通子对象实现一对多,会损失子文档的边界,子对象之间的属性关系丢失。
- nested 可以解决第 1 点的问题,但是 nested 有两个缺点:更新主文档的时候要全部更新,不支持子文档属于多个主文档。
- 父子文档解决 1、2 点的问题,但是它主要适用于写多读少的场景。
二、Es 地理位置查询
1、数据准备
创建一个索引:
PUT geo
{
"mappings": {
"properties": {
"name":{
"type": "keyword"
},
"location":{
"type": "geo_point"
}
}
}
}
准备一个 geo.json 文件:
{"index":{"_index":"geo","_id":1}}
{"name":"西安","location":"34.288991865037524,108.9404296875"}
{"index":{"_index":"geo","_id":2}}
{"name":"北京","location":"39.926588421909436,116.43310546875"}
{"index":{"_index":"geo","_id":3}}
{"name":"上海","location":"31.240985378021307,121.53076171875"}
{"index":{"_index":"geo","_id":4}}
{"name":"天津","location":"39.13006024213511,117.20214843749999"}
{"index":{"_index":"geo","_id":5}}
{"name":"杭州","location":"30.259067203213018,120.21240234375001"}
{"index":{"_index":"geo","_id":6}}
{"name":"武汉","location":"30.581179257386985,114.3017578125"}
{"index":{"_index":"geo","_id":7}}
{"name":"合肥","location":"31.840232667909365,117.20214843749999"}
{"index":{"_index":"geo","_id":8}}
{"name":"重庆","location":"29.592565403314087,106.5673828125"}
最后,执行如下命令,批量导入 geo.json 数据:
curl -XPOST "http://localhost:9200/geo/_bulk?pretty" -H "content-type:application/json" --data-binary @geo.json
可能用到的工具网站(主要用来验证搜索的结果对不对):
http://geojson.io/#map=6/32.741/116.521
2、geo_distance query
给出一个中心点,查询距离该中心点指定范围内的文档:
以(34.288991865037524,108.9404296875) 为圆心,以 600KM 为半径,这个范围内的数据。
效果:
3、geo_bounding_box query
在某一个矩形内的点,通过两个点锁定一个矩形:
以南京经纬度作为矩形的左上角,以舟山经纬度作为矩形的右下角,构造出来的矩形中,包含上海和杭州两个城市。
4、geo_polygon query
在某一个多边形范围内的查询。
给定多个点,由多个点组成的多边形中的数据。
上图是三个点,也就是三角形。
5、geo_shape query
geo_shape 用来查询图形,针对 geo_shape ,两个图形之间的关系有:相交、包含、不相交。
新建索引:
然后添加一条线:
接下来查询某一个图形中是否包含该线:
relation 属性表示两个图形的关系:
- within:包含
- intersects:相交
- disjoint:不相交
三、Es 特殊查询
1、more_like_this query
more_like_this query 可以实现基于内容的推荐,给定一篇文章,可以查询出和该文章相似的内容。
- fields:要匹配的字段,可以有多个。
- like:要匹配的文本。
- min_term_freq:词项的最低频率,默认是 2。特别注意,这个是指词项在要匹配的文本中的频率,而不是 es 文档中的频率。
- max_query_terms:query 中包含的最大词项数目。
- min_doc_freq:最小的文档频率,搜索的词,至少在多少个文档中出现,少于指定数目,该词会被忽略。
- max_doc_freq:最大文档频率。
- analyzer:分词器,默认使用字段的分词器。
- stop_words:停用词列表。
- minmum_should_match
2、script query——脚本查询(用的少)
3、 percolate query(用的少)
4、搜索高亮
普通高亮,默认会自动添加 em 标签:
5、排序
排序很简单,默认是按照查询文档的相关度来排序的,即( _score 字段):
四、聚合操作
1、Max Aggregation——统计最大值
也可以通过脚本来查询最大值:
2、 Min / Avg / Sum Aggregation——统计最小值、平均值、求和
Min Aggregation:统计最小值,用法和 Max Aggregation 基本一致。
Avg Aggregation:统计平均值,用法和 Max Aggregation 基本一致。
Sum Aggregation:求和,用法和 Max Aggregation 基本一致。
3、Cardinality Aggregation——基数统计
cardinality aggregation 用于基数统计。类似于 SQL 中的 distinct count(0):
text 类型是分析型类型,默认是不允许进行聚合操作的,如果相对 text 类型进行聚合操作,需要设置其 fielddata 属性为 true,这种方式虽然可以使 text 类型进行聚合操作,但是无法满足精准聚合,如果需要精准聚合,可以设置字段的子域为 keyword。
a、方式一(不推荐):
重新定义 books 索引:
定义完成后,重新插入数据。
接下来就可以查询出版社的总数量:
这种聚合方式可能会不准确。可以将 publish 设置为 keyword 类型或者设置子域为 keyword。
b、方式二
上面那种聚合方式可能会不准确。可以将 publish 设置为 keyword 类型或者设置子域为 keyword:
查询结果如下:
4、Stats Aggregation——基本统计
基本统计,一次性返回 count、max、min、avg、sum:
5、Extends Stats Aggregation——高级统计
高级统计,比 stats 多出来:平方和、方差、标准差、平均值加减两个标准差的区间:
6、Percentiles Aggregation——百分位统计
百分位统计:
效果:
这个百分位统计意思就是:在书的总数量的百分之一的时候,这个书是多少钱,总数量的百分之五的时候,又是多少钱;以此类推。
7、Value Count Aggregatio——按照字段统计文档数量
可以按照字段统计文档数量(包含指定字段的文档数量):
五、桶聚合(bucket)
桶聚合有点像 mysql 的分组查询。
1、Terms Aggregation——分组聚合
Terms Aggregation 用于分组聚合,例如,统计各个出版社出版的图书总数量:
统计结果如下:
在 terms 分桶的基础上,还可以对每个桶进行指标聚合。
统计不同出版社所出版的图书的平均价格:
统计结果如下:
2、 Filter Aggregation——过滤器聚合
过滤器聚合。可以将符合过滤器中条件的文档分到一个桶中,然后可以求其平均值。
例如查询书名中包含 java 的图书的平均价格:
3、Filters Aggregation——多过滤器聚合
多过滤器聚合。过滤条件可以有多个。
例如查询书名中包含 java 或者 office 的图书的平均价格:
4、Range Aggregation——范围聚合
按照范围聚合,在某一个范围内的文档数统计。
例如统计图书价格在 0-50、50-100、100-150、150以上的图书数量:
5、 Date Range Aggregation——统计日期
Range Aggregation 也可以用来统计日期,但是也可以使用 Date Range Aggregation,后者的优势在于可以使用日期表达式。
6、Date Histogram Aggregation——时间直方图聚合
时间直方图聚合。
例如统计各个月份的博客数量:
7、 Missing Aggregation——空值聚合
空值聚合。
统计所有没有 price 字段的文档:
8、Children Aggregation——根据父子关系分桶
可以根据父子文档关系进行分桶。
查询子类型为 student 的文档数量:
9、Geo Distance Aggregation
10、IP Range Aggregation
六、管道聚合
管道聚合相当于在之前聚合的基础上,再次聚合。
1、Avg Bucket Aggregation
2、Max Bucket Aggregation
统计每个出版社所出版图书的平均值,然后再统计平均值中的最大值:
3、Min Bucket Aggregation
统计每个出版社所出版图书的平均值,然后再统计平均值中的最小值。
4、Sum Bucket Aggregation
统计每个出版社所出版图书的平均值,然后再统计平均值之和: