Elasticsearch系列---多字段搜索

偶尔善良 提交于 2020-03-27 00:41:24

概要

本篇介绍一下multi_match的best_fields、most_fields和cross_fields三种语法的场景和简单示例。

最佳字段

bool查询采取"more-matches-is-better"匹配越多分越高的方式,所以每条match语句的评分结果会被加在一起,从而为每个文档提供最终的分数_score。能与两条语句同时匹配的文档会比只与一条语句匹配的文档得分要高,但有时这样也会带来一些与期望不符合的情况,我们举个例子:

我们以英文儿歌为案例背景,我们这样搜索:

GET /music/children/_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "name":  "brush mouth" }},
        { "match": { "content": "you sunshine"   }}
      ]
    }
  }
}

结果响应(有删减)

{
  "hits": {
    "total": 2,
    "max_score": 1.7672573,
    "hits": [
      {
        "_id": "4",
        "_score": 1.7672573,
        "_source": {
          "name": "brush your teeth",
          "content": "When you wake up in the morning it's a quarter to one, and you want to have a little fun You brush your teeth"
        }
      },
      {
        "_id": "3",
        "_score": 0.7911257,
        "_source": {
          "name": "you are my sunshine",
          "content": "you are my sunshine, my only sunshine, you make me happy, when skies are gray"
        }
      }
    ]
  }
}

预期的结果是"you are my sunshine"要排在"brush you teeth"前面,实际结果却相反,为什么呢?

我们按照匹配的方式复原一下_score的评分过程:每个query的分数,乘以匹配的query的数量,除以总query的数量。

我们来看一下匹配情况:
文档4的name字段包含brush,content字段包含you,所以两个match都能得到评分。
文档3的name字段不匹配,但是content字段包含you和sunshine,命中一个match,只能得一项的分。
结果文档4的得分会高一些。

但我们仔细想一想,文档4虽然两个match都匹配了,但每个match只匹配了其中一个关键词,文档3只匹配了一个match,却是同时匹配了两个连续的关键词,按我们的预期,一个field上匹配了两个连续关键词的相关性应该高一些,简单的把多个match的得分加起来,虽然分高一些,但不是我们期望的首位。

我们探寻的是最佳字段匹配,某一个字段匹配到了尽可能多的关键词,让它排在前面;而不是更多的field匹配了关键词,就让它在前面。

我们使用dis_max语法查询,优先将最佳匹配的评分作为查询的评分结果返回,请求如下:

GET /music/children/_search
{
  "query": {
    "dis_max": {
      "queries": [
        { "match": { "name":  "brush mouth" }},
        { "match": { "content": "you sunshine"   }}
      ]
    }
  }
}

结果响应(有删减)

{
  "hits": {
    "total": 2,
    "max_score": 1.0310873,
    "hits": [
      {
        "_id": "4",
        "_score": 1.0310873,
        "_source": {
          "name": "brush your teeth",
          "content": "When you wake up in the morning it's a quarter to one, and you want to have a little fun You brush your teeth"
        }
      },
      {
        "_id": "3",
        "_score": 0.7911257,
        "_source": {
          "name": "you are my sunshine",
          "content": "you are my sunshine, my only sunshine, you make me happy, when skies are gray"
        }
      }
    ]
  }
}

呃,结果排序还是不理想,不过可以看到_id为4的评分由之前的1.7672573降为1.0310873,说明dis_max操作后,能够影响评分,只是案例取得不好,_id为3的记录评分实在太低了,只有0.7911257,仍然不能改变次序。

最佳字段查询调优

上一节的dis_max查询会采用单个最佳匹配字段,而忽略其他的匹配项,这对精准化搜索还是不够合理,我们需要其他匹配项的匹配结果按一定权重参与最后的评分,权重可以自己设置。

我们可以加一个tie_breaker参数,这样就可以把其他匹配项的结果也考虑进去,它的使用规则如下:

  1. tie_breaker的值介于0-1之间,是个小数,建议此值范围0.1-0.4.
  2. dis_max负责获取最佳匹配语句的分数_score,其他匹配语句的_score与tie_breaker相乘。
  3. 对评分求和并归一化处理。

所以说,加上了tie_breaker,会考虑所有的匹配条件,但最佳匹配语句仍然占大头。

请求示例:

GET /music/children/_search
{
  "query": {
    "dis_max": {
      "queries": [
        { "match": { "name":  "brush mouth" }},
        { "match": { "content": "you sunshine"   }}
      ],
      "tie_breaker": 0.3
    }
  }
}

multi_match查询

best_fields

best-fields策略:将某一个field匹配了尽可能多关键词的文档优先返回回来。

如果我们在多个字段上使用相同的搜索字符串进行搜索,请求语法可以冗长一些:

GET /music/children/_search
{
  "query": {
    "dis_max": {
      "queries": [
        {
          "match": {
            "name": {
              "query": "you sunshine",
              "boost": 2,
              "minimum_should_match": "50%"
            }
          }
        },
        {
          "match": {
            "content": "you sunshine"
          }
        }
      ],
      "tie_breaker": 0.3
    }
  }
}

可以用multi_match将搜索请求简化,multi_match支持boost、minimum_should_match、tie_breaker参数的设置:

GET /music/children/_search
{
  "query": {
    "multi_match": {
      "query": "you sunshine",
      "type": "best_fields", 
      "fields": ["name^2","content"],
      "minimum_should_match": "50%",
      "tie_breaker": 0.3
    }
  }
}

而boost、minimum_should_match、tie_breaker参数的一个显著作用就是去长尾,长尾数据比如说我们搜索4个关键词,但很多文档只匹配1个,也显示出来了,这些文档其实不是我们想要的,可以通过这几个参数的设置,将门槛提高,过滤掉长尾数据。

most_fields

most-fields策略:尽可能返回更多field匹配到某个关键词的doc,优先返回回来。

常用方式是我们为同一文本字段,建立多种方式的索引,词干提取分析处理的和原文存储的都做一份,这样能提高匹配的精准度。

我们拿music索引举个例子(摘抄mapping片断信息)。我们做一点小修改:

PUT /music
{
  "mappings": {
      "children": {
        "properties": {
          "name": {
            "type": "text",
            "analyzer": "english"
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "content": {
            "type": "text",
            "analyzer": "english"
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    }
}

比如name和content字段,我们除了有text类型的字段,还有keyword类型的子字段,text会做分词、英文词干处理,keywork则保持原样,搜索内容的时候,我们可以使用name或name.keyword两个字段同时进行搜索,示例:

GET /music/children/_search
{
  "query": {
    "multi_match": {
      "query": "brushed",
      "type": "most_fields", 
      "fields": ["name","name.keyword"]
    }
  }
}

我们搜索name及name.keyword两个字段,由于name字段的分词器是english,搜索字符串brushed经过提取词干后变成brush,是能匹配到结果的,name.keyword则无法匹配,最终还是有文档结果返回。如果只对name.keyword字段搜索,则不会有结果返回。

这个就是most_fields的策略,希望对同一个文本进行多种索引,搜索时各种索引的结果都参与,这样就能尽可能地多返回结果。

与best_fields区别

  1. best_fields,是对多个field进行搜索,挑选某个field匹配度最高的那个分数,同时在多个query最高分相同的情况下,在一定程度上考虑其他query的分数。简单来说,你对多个field进行搜索,就想搜索到某一个field尽可能包含更多关键字的数据
  • 优点:通过best_fields策略,以及综合考虑其他field,还有minimum_should_match支持,可以尽可能精准地将匹配的结果推送到最前面
  • 缺点:除了那些精准匹配的结果,其他差不多大的结果,排序结果不是太均匀,没有什么区分度了

实际的例子:百度之类的搜索引擎,最匹配的到最前面,但是其他的就没什么区分度了

  1. most_fields,综合多个field一起进行搜索,尽可能多地让所有field的query参与到总分数的计算中来,此时就会是个大杂烩,出现类似best_fields案例最开始的那个结果,结果不一定精准,某一个document的一个field包含更多的关键字,但是因为其他document有更多field匹配到了,所以排在了前面;所以需要建立更多类似name.keyword,name.std这样的field,尽可能让某一个field精准匹配query string,贡献更高的分数,将更精准匹配的数据排到前面
  • 优点:将尽可能匹配更多field的结果推送到最前面,整个排序结果是比较均匀的
  • 缺点:可能那些精准匹配的结果,无法推送到最前面

实际的例子:wiki,明显的most_fields策略,搜索结果比较均匀,但是的确要翻好几页才能找到最匹配的结果

cross_fields

有些实体对象在设计中,可能会使用多个字段来标识一个信息,如地址,常见存储方案可以是省、市、区、街道四个字段,分别存储,合起来才是完整的地址信息。再如人名,国外有first name和last name之分。

遇到针对这种字段的搜索,我们叫做跨字段实体搜索,我们要注意哪些问题呢?

我们回顾music索引的author字段,就是设计成了author_first_name和author_last_name的结构,我们试着对它来演示一下跨字段实体搜索。

使用most_fields查询

GET /music/children/_search
{
  "query": {
    "multi_match": {
      "query":       "Peter Raffi",
      "type":        "most_fields",
      "fields":      [ "author_first_name", "author_last_name" ]
    }
  }
}

响应的结果:

{
  "hits": {
    "total": 2,
    "max_score": 1.3862944,
    "hits": [
      {
        "_id": "4",
        "_score": 1.3862944,
        "_source": {
          "id": "55fa74f7-35f3-4313-a678-18c19c918a78",
          "author_first_name": "Peter",
          "author_last_name": "Raffi",
          "author": "Peter Raffi",
          "name": "brush your teeth",
          "content": "When you wake up in the morning it's a quarter to one, and you want to have a little fun You brush your teeth"
        }
      },
      {
        "_id": "1",
        "_score": 0.2876821,
        "_source": {
          "author_first_name": "Peter",
          "author_last_name": "Gymbo",
          "author": "Peter Gymbo",
          "name": "gymbo",
          "content": "I hava a friend who loves smile, gymbo is his name"
        }
      }
    ]
  }
}

看起来结果是对的,"Peter Raffi"按预期排在首位,但Peter Gymbo也出来的,这不是我们想要的结果,只是由于数据量太少的原因,长尾数据没有显示出来,most_fields查询引出的问题有如下3个:

  1. 只是找到尽可能多的field匹配的doc,而不是某个field完全匹配的doc
  2. most_fields,没办法用minimum_should_match去掉长尾数据,就是匹配的特别少的结果
  3. TF/IDF算法,比如Peter Raffi和Peter Gymbo,搜索Peter Raffi的时候,由于first_name中很少有Raffi的,所以query在所有document中的频率很低,得到的分数很高,可能会出现非预期的次序。

使用copy_to合并字段

copy_to语法可以将多个字段合并在一起,这样就可以解决跨实体字段的问题,带来的副面影响就是占用更多的存储空间,copy_to的示例如下:

PUT /music/_mapping/children
{
  "properties": {
      "author_first_name": {
          "type":     "text",
          "copy_to":  "author_full_name" 
      },
      "author_last_name": {
          "type":     "text",
          "copy_to":  "author_full_name" 
      },
      "author_full_name": {
          "type":     "text"
      }
  }
}

注意这个请求需要在建立索引时执行,局限性比较大。
所以案例设计时,专门有一个author字段,存储完整的名称的。

GET /music/children/_search
{
  "query": {
    "match": {
      "author_full_name": {
        "query": "Peter Raffi",
        "operator": "and"
      }
    }
  }
}

单字段的查询,就可以随心所欲的指定operator或minimum_should_match来控制精度了。

我们看一下前面提到的3个问题能否解决

  1. 匹配问题

解决,最匹配的数据优先返回。

  1. 长尾问题

解决,可以指定operator或minimum_should_match来控制精度。

  1. 评分不准的问题

解决,所有信息在一个字段里,IDF计算时次数是均匀的,不会有极端的误差。

缺点:
需要前期设计时冗余字段,占用的存储会多一些。
copy_to拼接字段时,会遇到顺序问题,如英文名称名前姓后,而地址顺序则不固定,有的从省到街道由大到小,有的是反的,这也是局限性之一。

原生cross_fields语法

multi_match有原生的cross_fields语法解决跨字段实体搜索问题,请求如下:

GET /music/children/_search
{
  "query": {
    "multi_match": {
      "query": "Peter Raffi",
      "type": "cross_fields", 
      "operator": "and",
      "fields": ["author_first_name", "author_last_name"]
    }
  }
}

这次cross_fields的含义是要求:

  • Peter必须在author_first_name或author_last_name中出现
  • Raffi必须在author_first_name或author_last_name中出现

看看上面提及的3个问题解决情况:

  1. 匹配问题

解决,cross_fields要求每个term都必须在任何一个field中出现

  1. 长尾问题

解决,参见上一条,每个term都必须匹配,长尾问题自然迎刃而解。

  1. 评分不准的问题

解决,cross_fields通过混合不同字段逆向索引文档频率的方式解决词频的问题,具体来说,Peter在first_name中频率会高一些,在last_name中频率会低一些,在两个字段得到的IDF值,会取小的那个,Raffi也是同样处理,这样得到的IDF值就比较正常,不会偏高。

小结

我们可以花一点时间了解一下多字段搜索的场景,和要注意的细节点,精准搜索是一个非常大的话题,优化的空间没有上限,可以先从最基础的场景和调整语法开始尝试。

专注Java高并发、分布式架构,更多技术干货分享与心得,请关注公众号:Java架构社区
可以扫左边二维码添加好友,邀请你加入Java架构社区微信群共同探讨技术
Java架构社区

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!