一、关于elasticsearch7.8的搜索方式与实景应用(上)
本文是在elasticsearch7.8的环境下操作的,最近也很忙没时间去整理,终于肝了一篇出来。关于ES,接触的很早,了解他却很迟,有种相恨见晚的感觉,最近在项目上又一次碰见了它,不由得再来感叹一遍。
1、问题初始
1.1、应用场景:
这是某网站的一个搜索,在左侧有一个分类的过滤字段。当时我们公司的业务网站也是出现了这样的一个多条件过滤搜索的情况。第一时间想到的是MySQL的条件查询…后来发现数据量很大,还得要有多字段查询,精准过滤的同时,还要支持全文搜索等等。“全文搜索”,那就不得不提一下ES。
1.2、组合查询-bool查询
bool查询是本次在项目里主要用到的查询方式,首先是与4种操作符的组合查询,类似于and,or,not。布尔查询允许我们利用布尔逻辑将较小的查询组合成较大的查询。
must:字面意思就是必须匹配,相当于and,贡献算分。
must_not:过滤子句,必须不匹配,相当于not,这个相对来说用的比较少,不贡献算分。
should:选择性满足一个条件即可,相当于or,贡献算分。
filter:过滤子句,必须匹配,不贡献算分。
官方的说法是:bool
查询会为每个文档计算相关度评分 _score
,再将所有匹配的 must
和 should
语句的分数 _score
求和,最后除以 must
和 should
语句的总数。
must_not
语句不会影响评分;它的作用只是将不相关的文档排除。
所有 must
语句必须匹配,所有 must_not
语句都必须不匹配,但有多少 should
语句应该匹配呢?默认情况下,没有 should
语句是必须匹配的,只有一个例外:那就是当没有 must
语句的时候,至少有一个 should
语句必须匹配。
就像我们能控制 match
查询的精度 一样,我们可以通过 minimum_should_match
参数控制需要匹配的 should
语句的数量,它既可以是一个绝对的数字,又可以是个百分比:
知道了这层关系,就可以接着往下写了。具体格式的写法有好几种。最简单的是如下这种。举例上述图形中的过滤条件。
body={
"query":{
"bool":{
"must":[
{
"match":{
"行业分类":"建筑工程"
}
},
{
"match":{
"省份地区":"广东省"
}
},
{
"match":{
"招标结果":"开标"
}
},
{
"range":{
"发布时间":{
#近三天,当前日期2020-07-20
"lte":"2020-07-20",
"gte":"2020-07-17"
}
}
},
{
"range":{
"预算金额":{
"lte":"50",
"gte":"20"
}
}
},
#这里添加正常的字段搜索
{
"bool":{
"should":[
{"match":{"某个字段":"某个关键词"}},
{"match":{"某个字段":"某个关键词"}},
{"match":{"某个字段":"某个关键词"}},
......
]
}
}
]
}
}
}
理解:
对于普通的过滤条件就直接must \ must_not -match,对于范围的过滤条件就要用到range \lte(小于等于)\gte(大于等于)
这就是经典的and | or 复合查询。
通常情况下,需要对结果进行过滤条件的符合查询时,可以通过must和must_not,再获取匹配的文档,偶尔情况下,我们呢,可能不需要完全剔除这样的一个过滤,而是进行分数降低。这个时候就需要使用boosting query
对于过滤条件的处理思路:
查询的时候,如果没有进行条件筛选就是正常的搜索,所以这里的条件要进行判空处理的,然后拼接成上述我们需要的格式。就可以了。总体来说上述的应用场景实际操作起来还是比较简单的。
2、其他常见查询汇总
常见的查询有term\match\bool\filter
2.1、match查询
match 查询主要的应用场景就是进行全文检索,它既能处理全文字段,又能处理精确字段。
可能运用到的其他参数
- operator:用来控制match查询匹配词条的逻辑条件,默认值是or,如果设置为and,表示查询满足所有条件;
- minimum_should_match:当operator参数设置为or时,该参数用来控制应该匹配的分词的最少数量;
#正常的match查询
body = {
"query":{
"match":{
"title":"quick"
}
}
}
#添加控制参数
body = {
"query":{
"match":{
"title":{
"query":"About Search",
"operator":"or",
"minimum_should_match":2
#分词最少为2
}
}
}
}
过程分析:
-
检查字段类型 。
标题
title
字段是一个string
类型(analyzed
)已分析的全文字段,这意味着查询字符串quick本身也应该被分析。 -
分析查询字符串 。
将查询的字符串quick传入标准分析器中,输出的结果是单个项 quick 。因为只有一个单词项,所以
match
查询执行的是单个底层term
查询。 -
查找匹配文档 。
用
term
查询在倒排索引中查找quick
然后获取一组包含该项的文档,本例的结果是文档:1、2 和 3 。 -
为每个文档评分。
用
term
查询计算每个文档相关度评分_score
,这是种将词频(term frequency,即词quick
在相关文档的title
字段中出现的频率)和反向文档频率(inverse document frequency,即词quick
在所有文档的title
字段中出现的频率),以及字段的长度(即字段越短相关度越高)相结合的计算方式。
对于我们的中文分词器,不管是官方的标准分词器还是我们可能会引入的IK、jieba中文分词来说,当我们在进行字段mapping时,给title字段指定分词器后,我们的每一次对于title字段查询的字符串也会对其进行分词,例如:无人工厂
推荐大家对于中文的分词还是用IK或者jieba中文分词器。具体安装步骤,可以百度,或者私信我。
官方的分词器对于中文来说分的很散,这里会被分成,无人和工厂。ik_smart则会把无人工厂当作一个完整的词去对待。ik的分词包含ik_max_word和ik_smart俩种,个人还是觉得ik_smart用起来比较舒服。对于数据量很大的ES来说,如何提高用户搜索结果的精确度,最重要的就是分词,对于用户来说分词多了,就会出现很多垃圾搜索结果。对于专业领域的搜索,个人建议是做一个专业领域的词库,可参考百度词库以及搜狗词库,有关于专业领域的一些分词下载可直接用。这里又有一个点就是自定义分词器,说好的讲搜索又开始说分词了,回头可以详细再出一篇关于ES自定义中文分词器的博客。
继续我们的搜索!
2.2、multi_match查询
如果我们希望在多个字段上执行匹配相同的查询,就要用multi_match查询。
ES共有五种多字段匹配查询:best_fields,most_fields,cross_fields,phrase和phrase_prefix。就是对于我们多字段查询的结果进行结果筛选返回。
默认情况下,查询的类型是best_fileds
2.2.1、best_fileds
以下是写法:
# 通常默认情况下的multi_match查询
body = {
"multi_match":{
"query":"无人工厂",
# 指定多个字段
"fields":["title","subject","..."]
}
}
# 指定多字段查询的类型
body = {
"multi_match":{
"query":"无人工厂",
"type":"best_fields",
"fields":["title","subject","..."]
}
}
理解:
best_fields类型与dis_max查询相同,字母dis是单词“Disjunction”的简写,意思是分离,dis_max 分离最大化查询。具体来说,是表示把同一个文档中每个字段上的查询分离,分别计算出分数,只取任一字段中最高的评分结果,作为该文档的返回结果。
实例:
# 假设ES库中的内容为
{
#文档1
"字段A":"我爱中国,哈哈哈哈",0.25
"字段B":"中国强大"0.3 ==》 0.3
}
{
#文档2
"字段A":"中国加油,哈哈哈哈。",0.55 ==》 0.55
"字段B":"哈哈哈哈"0
}
# dis_max查询方式
body = {
"query" : {
"dis_max":{
"queries":[
{
"match":{
"字段A":"中国加油",
}
},
{
"match":{
"字段B":"中国加油"
}
}
]
}
}
}
# 等同与以下查询
# multi_match查询。记住哦,默认为best_fields。
body = {
"query" : {
"multi_match" : {
"query" : "中国加油",
"fields":["字段A","字段B"]
}
}
}
# tie_breaker 的参与
body = {
"query" : {
"multi_match" : {
"query" : "中国加油",
"fields":["字段A","字段B"],
"tie_breaker": 0.2
}
}
}
理解:
上述的只是个假设的例子让各位去理解,实际可自行去体验。
执行上述搜索时
对于字符串进行分词结果为:中国,加油。俩个词。
文档1,不管是字段A,还是字段B,都含有中国,但是文档1的分值,只取字段A或者字段B中的最大值,作为该文档的返回值,而不是字段A和字段B进行累加分值返回。文档2虽然字段B没有匹配项,分值为0,但是字段A的分值却很高,所以,在返回时,文档2的评分是比文档1高。
现在7.8版本的ES的文档分值计算已经从TF/IDF,升级为BM25算法,这个算法部分感兴趣的可以自行研究,东西太多了,我也不会。
补充:
对于这样的一个搜索结果,很多人可能觉得不公平,只取最高分,那其他字段就没有任何参与权了,这样的不平衡,文档1表示不服气。
因此有了tie_breaker:
假设tie_breaker=0.2
当然最佳匹配的结果还是评分最高,但是,文档1的评分却会有所提高,0.3 *(1 + 0.2)=0.36。
2.2.2、most_fields
解释好best_fields,most_fields就很好解释了。most_fields就是把每个字段的计算分数相加求每个文档的平均数,作为该文档的分值作为返回值。写法上没有区别,鉴于上述
2.2.3、phrase 和 phrase_prefix
phrase:在每个字段上运行match_phrase查询并和每个字段的_score组合。
phrase_prefix:在每个字段上运行match_phrase_prefix查询并和每个字段的_score组合。
上文中我们提到best_fields类型的查询,其类型在执行时,执行的是dis_max的match查询。
而这里的phrase 和 phrase_prefix,就是将执行时的match换成了match_phrase和match_phrase_prefix。
写法上不变
body = {
"query": {
"multi_match" : {
"query": "中国加油",
"type": "phrase_prefix",
"fields": [ "字段A", "字段B" ]
}
}
}
#等同于如下
#执行的是match_phrase_prefix
body = {
"query": {
"dis_max": {
"queries": [
{ "match_phrase_prefix": { "字段A": "中国加油" }},
{ "match_phrase_prefix": { "字段B": "中国加油" }}
]
}
}
}
match,match_phrase,match_phrase_prefix的区别
这一部分简单解释就是,match查询会分词,而match_phrase则是会将搜索的短语当作一个整体去查询,也就是完全匹配,当然标点符号除外。
这样的话match_phrase就会显得很孬。所以官网上有一句话是这么说的:
Also, accepts analyzer
, boost
, lenient
and zero_terms_query
as explained in Match, as well as slop
which is explained in Match phrase. Type phrase_prefix
additionally accepts。
因此也接受analyzer,boost,lenient,slop 和zero_terms_query作为在match query中的解释。phrase_prefix类型此外接受max_expansions。
所以slop参数就是用来控制查询词条之间最大词间距。
例如:
# 原文是:南京市长江大桥。
body = {
"query": {
"match_phrase": {
"message": {
"query": "南京大桥",
"slop": 3,
#"analyzer" : "my_analyzer"
}
}
}
}
#match是一定可以匹配到的。match_phrase只有在slop:3,最大词间距为3时,才可以匹配到结果的
analyzer参数:
这个参数就不用多说了,用来定义查询语句时对其中词条执行的分析过程。
zero_terms_query参数:
zero_terms_query默认为none,其意义为在搜索时,对于停止词就过滤了,停止词就是一些没有意义的词,不一定是结尾词,例如:了,的,嘛,英文就是 and、or、not、to等等。区分的标准具体看各个分词器的,不同分词器有一定区别。这个参数不常用。有兴趣的可以单独去了解一下。
2.2.4、cross_fields
cross_fields类型对于多个字段应该匹配的结构文档特别有用。例如,当为“Will Smith”查询first_name和last_name字段时,最佳匹配应该是"Will"在一个字段中并且"Smith"在另外一个字段中。`
这听起来像most_fields的工作,但这种方法有两个问题。第一个问题是operator和minimum_should_match在每个前缀字段中作用,以代替前缀项(请参考explanation above)。` `第二个问题是与关联性有关:在first_name和last_name字段中不同的项频率可能导致不可预期的结果。` `例如,想像我们有两个人,“Will Smith”和``"Smith Jones"``。“Smith”作为姓是非常常见的(所以重要性很低),但是“Smith”作为名字是非常不常见的(所以重要性很高)。` `假如我们搜索“Will Smith”,则“Smith Jones”文档可能显示在更加匹配的``"Will Smith"``上,因为first_name:smith的得分已经胜过first_name:will加last_name:smith的总分。
处理该种类型查询的一种方式是简单的将first_name和last_name索引字段放入单个full_name字段中。当然,这只能在索引时间完成。
cross_field类型尝试通过采用term-centric方法在查询时解决这些问题。首先把查询字符串分解成当个项,然后在任何字段中查询每个项,就好像它们是一个大的字段。
查询就像这样:
body = {
"query": {
"multi_match" : {
"query": "Will Smith",
"type": "cross_fields",
"fields": [ "first_name", "last_name" ],
"operator": "and"
}
}
}
被执行为:
+(first_name:will last_name:will)
+(first_name:smith last_name:smith)
换一种说法,所有的项必须至少在匹配文档中一个字段中出现(比较the logic used for best_fields and most_fields)。
解决了两个问题中的一个。通过混合所有字段项的频率解决不同项匹配的问题,以便平衡差异。
在实践中,first_name:smith将被视为和last_name:smith具有相同的频率,加1。这将使得在first_name和last_name上的匹配具有可比较的分数,对于last_name具有微小的优势,因为它是最有可能包含simth的字段。
注意,cross_fields通常仅作用与得到1提升的短字符串字段。 否则增加,项频率和长度正常化有助于得分, 使得项统计的混合不再有任何意义。
假如你通过Validata API运行上面的查询,将返回这样的解释:
+blended(``"will"``, fields: [first_name, last_name])
+blended(``"smith"``, fields: [first_name, last_name])
也接受analyzer
, boost
, operator
, minimum_should_match
, lenient
, zero_terms_query
和cutoff_frequency
,作为match query的解释。
cross_field
and analysis
cross_field类型只能在具有相同分析器的字段上以term-centric模式工作。具有相同分析器的字段如上述实例组合在一起。假如有多个组,则他们使用bool查询相结合。
例如,假如我们有相同分析器的first和last字段,增加一个同时使用edge_ngram分析器的first.edge和last.edge,该查询:
body = {
"query": {
"multi_match" : {
"query": "Jon",
"type": "cross_fields",
"fields": [
"first", "first.edge",
"last", "last.edge"
]
}
}
}
可能被执行为:
blended("jon", fields: [first, last])
| (
blended("j", fields: [first.edge, last.edge])
blended("jo", fields: [first.edge, last.edge])
blended("jon", fields: [first.edge, last.edge])
)
换句话说,first和last可能被组合在一起并被当做一个字段来对待,同时first.edge和last.edge可能被组合在一起并当做一个字段来对待。
具有多个组是好的,当使用operator或者minimum_should_match关联的时候,它可能遭受和most_fields和best_fields相同的问题。
你可以容易的将该查询重写为两个独立的cross_fields查询与bool查询相结合,并将minimum_should_match参数应用于其中一个:
body = {
"query": {
"bool": {
"should": [
{
"multi_match" : {
"query": "Will Smith",
"type": "cross_fields",
"fields": [ "first", "last" ],
"minimum_should_match": "50%" 【1】
}
},
{
"multi_match" : {
"query": "Will Smith",
"type": "cross_fields",
"fields": [ "*.edge" ]
}
}
]
}
}
}
【1】will或smith必须存在于first或last字段。
你可以通过在查询中指定analyzer参数强制把所有字段放入相同组中。
body = {
"query": {
"multi_match" : {
"query": "Jon",
"type": "cross_fields",
"analyzer": "standard", 【1】
"fields": [ "first", "last", "*.edge" ]
}
}
}
【1】对所有字段使用standard分析器
将执行如下:
blended("will", fields: [first, first.edge, last.edge, last])
blended("smith", fields: [first, first.edge, last.edge, last])
tie_breaker
默认情况,每一个per-term混合查询将使用组中任何字段的最佳分数,然后将这些分数相加,以得出最终分数。tie_breaker参数可以改变per-term混合查询的默认行为,它接受:
0.0 获取最好的分数(举例)first_name:will和last_name:will(default)
1.0 所有分数相加(举例)first_name:will和last_name:will
0.0 < n < 1.0 将单个最佳分数加上tie_breaker乘以其它每个匹配字段的分数。
重要:cross_fields
and fuzziness
fuzziness参数不能被cross_fields类型使用。
参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html