概要

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

最佳字段

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

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

  1. GET /music/children/_search
  2. {
  3. "query": {
  4. "bool": {
  5. "should": [
  6. { "match": { "name": "brush mouth" }},
  7. { "match": { "content": "you sunshine" }}
  8. ]
  9. }
  10. }
  11. }

结果响应(有删减)

  1. {
  2. "hits": {
  3. "total": 2,
  4. "max_score": 1.7672573,
  5. "hits": [
  6. {
  7. "_id": "4",
  8. "_score": 1.7672573,
  9. "_source": {
  10. "name": "brush your teeth",
  11. "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"
  12. }
  13. },
  14. {
  15. "_id": "3",
  16. "_score": 0.7911257,
  17. "_source": {
  18. "name": "you are my sunshine",
  19. "content": "you are my sunshine, my only sunshine, you make me happy, when skies are gray"
  20. }
  21. }
  22. ]
  23. }
  24. }

预期的结果是"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语法查询,优先将最佳匹配的评分作为查询的评分结果返回,请求如下:

  1. GET /music/children/_search
  2. {
  3. "query": {
  4. "dis_max": {
  5. "queries": [
  6. { "match": { "name": "brush mouth" }},
  7. { "match": { "content": "you sunshine" }}
  8. ]
  9. }
  10. }
  11. }

结果响应(有删减)

  1. {
  2. "hits": {
  3. "total": 2,
  4. "max_score": 1.0310873,
  5. "hits": [
  6. {
  7. "_id": "4",
  8. "_score": 1.0310873,
  9. "_source": {
  10. "name": "brush your teeth",
  11. "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"
  12. }
  13. },
  14. {
  15. "_id": "3",
  16. "_score": 0.7911257,
  17. "_source": {
  18. "name": "you are my sunshine",
  19. "content": "you are my sunshine, my only sunshine, you make me happy, when skies are gray"
  20. }
  21. }
  22. ]
  23. }
  24. }

呃,结果排序还是不理想,不过可以看到_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,会考虑所有的匹配条件,但最佳匹配语句仍然占大头。

请求示例:

  1. GET /music/children/_search
  2. {
  3. "query": {
  4. "dis_max": {
  5. "queries": [
  6. { "match": { "name": "brush mouth" }},
  7. { "match": { "content": "you sunshine" }}
  8. ],
  9. "tie_breaker": 0.3
  10. }
  11. }
  12. }

multi_match查询

best_fields

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

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

  1. GET /music/children/_search
  2. {
  3. "query": {
  4. "dis_max": {
  5. "queries": [
  6. {
  7. "match": {
  8. "name": {
  9. "query": "you sunshine",
  10. "boost": 2,
  11. "minimum_should_match": "50%"
  12. }
  13. }
  14. },
  15. {
  16. "match": {
  17. "content": "you sunshine"
  18. }
  19. }
  20. ],
  21. "tie_breaker": 0.3
  22. }
  23. }
  24. }

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

  1. GET /music/children/_search
  2. {
  3. "query": {
  4. "multi_match": {
  5. "query": "you sunshine",
  6. "type": "best_fields",
  7. "fields": ["name^2","content"],
  8. "minimum_should_match": "50%",
  9. "tie_breaker": 0.3
  10. }
  11. }
  12. }

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

most_fields

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

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

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

  1. PUT /music
  2. {
  3. "mappings": {
  4. "children": {
  5. "properties": {
  6. "name": {
  7. "type": "text",
  8. "analyzer": "english"
  9. "fields": {
  10. "keyword": {
  11. "type": "keyword",
  12. "ignore_above": 256
  13. }
  14. }
  15. },
  16. "content": {
  17. "type": "text",
  18. "analyzer": "english"
  19. "fields": {
  20. "keyword": {
  21. "type": "keyword",
  22. "ignore_above": 256
  23. }
  24. }
  25. }
  26. }
  27. }
  28. }
  29. }

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

  1. GET /music/children/_search
  2. {
  3. "query": {
  4. "multi_match": {
  5. "query": "brushed",
  6. "type": "most_fields",
  7. "fields": ["name","name.keyword"]
  8. }
  9. }
  10. }

我们搜索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查询

  1. GET /music/children/_search
  2. {
  3. "query": {
  4. "multi_match": {
  5. "query": "Peter Raffi",
  6. "type": "most_fields",
  7. "fields": [ "author_first_name", "author_last_name" ]
  8. }
  9. }
  10. }

响应的结果:

  1. {
  2. "hits": {
  3. "total": 2,
  4. "max_score": 1.3862944,
  5. "hits": [
  6. {
  7. "_id": "4",
  8. "_score": 1.3862944,
  9. "_source": {
  10. "id": "55fa74f7-35f3-4313-a678-18c19c918a78",
  11. "author_first_name": "Peter",
  12. "author_last_name": "Raffi",
  13. "author": "Peter Raffi",
  14. "name": "brush your teeth",
  15. "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"
  16. }
  17. },
  18. {
  19. "_id": "1",
  20. "_score": 0.2876821,
  21. "_source": {
  22. "author_first_name": "Peter",
  23. "author_last_name": "Gymbo",
  24. "author": "Peter Gymbo",
  25. "name": "gymbo",
  26. "content": "I hava a friend who loves smile, gymbo is his name"
  27. }
  28. }
  29. ]
  30. }
  31. }

看起来结果是对的,"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的示例如下:

  1. PUT /music/_mapping/children
  2. {
  3. "properties": {
  4. "author_first_name": {
  5. "type": "text",
  6. "copy_to": "author_full_name"
  7. },
  8. "author_last_name": {
  9. "type": "text",
  10. "copy_to": "author_full_name"
  11. },
  12. "author_full_name": {
  13. "type": "text"
  14. }
  15. }
  16. }

注意这个请求需要在建立索引时执行,局限性比较大。

所以案例设计时,专门有一个author字段,存储完整的名称的。

  1. GET /music/children/_search
  2. {
  3. "query": {
  4. "match": {
  5. "author_full_name": {
  6. "query": "Peter Raffi",
  7. "operator": "and"
  8. }
  9. }
  10. }
  11. }

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

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

  1. 匹配问题

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

  1. 长尾问题

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

  1. 评分不准的问题

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

缺点:

需要前期设计时冗余字段,占用的存储会多一些。

copy_to拼接字段时,会遇到顺序问题,如英文名称名前姓后,而地址顺序则不固定,有的从省到街道由大到小,有的是反的,这也是局限性之一。

原生cross_fields语法

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

  1. GET /music/children/_search
  2. {
  3. "query": {
  4. "multi_match": {
  5. "query": "Peter Raffi",
  6. "type": "cross_fields",
  7. "operator": "and",
  8. "fields": ["author_first_name", "author_last_name"]
  9. }
  10. }
  11. }

这次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架构社区微信群共同探讨技术

Elasticsearch系列---多字段搜索的更多相关文章

  1. elasticsearch系列四:搜索详解(搜索API、Query DSL)

    一.搜索API 1. 搜索API 端点地址 从索引tweet里面搜索字段user为kimchy的记录 GET /twitter/_search?q=user:kimchy 从索引tweet,user里 ...

  2. Elasticsearch系列---结构化搜索

    概要 结构化搜索针对日期.时间.数字等结构化数据的搜索,它们有自己的格式,我们可以对它们进行范围,比较大小等逻辑操作,这些逻辑操作得到的结果非黑即白,要么符合条件在结果集里,要么不符合条件在结果集之外 ...

  3. Elasticsearch系列---深入全文搜索

    概要 本篇介绍怎样在全文字段中搜索到最相关的文档,包含手动控制搜索的精准度,搜索条件权重控制. 手动控制搜索的精准度 搜索的两个重要维度:相关性(Relevance)和分析(Analysis). 相关 ...

  4. elasticsearch系列五:搜索详解(查询建议介绍、Suggester 介绍)

    一.查询建议介绍 1. 查询建议是什么? 查询建议,为用户提供良好的使用体验.主要包括: 拼写检查: 自动建议查询词(自动补全) 拼写检查如图: 自动建议查询词(自动补全): 2. ES中查询建议的A ...

  5. ElasticSearch 2 (15) - 深入搜索系列之多字段搜索

    ElasticSearch 2 (15) - 深入搜索系列之多字段搜索 摘要 查询很少是简单的一句话匹配(one-clause match)查询.很多时候,我们需要用相同或不同的字符串查询1个或多个字 ...

  6. ElasticSearch 2 (18) - 深入搜索系列之控制相关度

    ElasticSearch 2 (18) - 深入搜索系列之控制相关度 摘要 处理结构化数据(比如:时间.数字.字符串.枚举)的数据库只需要检查一个文档(或行,在关系数据库)是否与查询匹配. 布尔是/ ...

  7. ElasticSearch 2 (17) - 深入搜索系列之部分匹配

    ElasticSearch 2 (17) - 深入搜索系列之部分匹配 摘要 到目前为止,我们介绍的所有查询都是基于完整术语的,为了匹配,最小的单元为单个术语,我们只能查找反向索引中存在的术语. 但是, ...

  8. ElasticSearch 2 (16) - 深入搜索系列之近似度匹配

    ElasticSearch 2 (16) - 深入搜索系列之近似度匹配 摘要 标准的全文搜索使用TF/IDF处理文档.文档里的每个字段或一袋子词.match 查询可以告诉我们哪个袋子里面包含我们搜索的 ...

  9. ElasticSearch 2 (14) - 深入搜索系列之全文搜索

    ElasticSearch 2 (14) - 深入搜索系列之全文搜索 摘要 在看过结构化搜索之后,我们看看怎样在全文字段中查找相关度最高的文档. 全文搜索两个最重要的方面是: 相关(relevance ...

随机推荐

  1. JStorm:任务调度

    前一篇文章 JStorm:概念与编程模型 介绍了JStorm的基本概念以及编程模型方面的知识,本篇主要介绍自己对JStorm的任务调度方面的认识,主要从三个方面介绍: 调度角色 调度方法 自定义调度 ...

  2. 使用Connector / Python连接MySQL/查询数据

    使用Connector / Python连接MySQL connect()构造函数创建到MySQL服务器的连接并返回一个 MySQLConnection对象 在python中有以下几种方法可以连接到M ...

  3. <NOI2002>银河英雄传说の思路

    emm并没有什么好说的.毕竟我这个蒟蒻都能yy出来 #include<cstring> #include<cstdio> #include<iostream> #i ...

  4. Redis从出门到高可用--Redis复制原理与优化

    Redis从出门到高可用–Redis复制原理与优化 单机有什么问题? 1.单机故障; 2.单机容量有瓶颈 3.单机有QPS瓶颈 主从复制:主机数据更新后根据配置和策略,自动同步到备机的master/s ...

  5. Python-控制语句及函数

    if-elif-else for while 函数 函数定义 空函数 pass 返回多个值 可变参数 * 关键字参数 ** 控制语句 if - elif - else 比如,输入用户年龄,根据年龄打印 ...

  6. Hexo之旅(四):文章编写技巧

    hexo 编写文章可以使用以下命令创建hexo new "文件名" #创建的文章会在_pots目录下文章的后缀名是以md命名的文件格式,遵循markdown语法,所以编写文章可以使 ...

  7. IP 数据报

    IP 数据报 1.IP 数据报的格式 一个 IP 数据报由首部和数据两部分组成.(数据报也可以说是数据包) 首部的前一部分是固定长度,共 20 字节,是所有 IP 数据报必须具有的. 在首部的固定部分 ...

  8. Spring源码阅读笔记04:默认xml标签解析

    上文我们主要学习了Spring是如何获取xml配置文件并且将其转换成Document,我们知道xml文件是由各种标签组成,Spring需要将其解析成对应的配置信息.之前提到过Spring中的标签包括默 ...

  9. python入门到放弃-基本数据类型之tuple元组

    #概述 元组俗称不可变的列表,又称只读列表,是python的基本数据类型之一, 用()小括号表示,里面使用,逗号隔开 元组里面可以放任何的数据类型的数据,查询可以,循环可以,但是就是不能修改 #先来看 ...

  10. Nuxt简单使用Google/Baidu Analyze

    博客地址: https://www.seyana.life/post/17 具体账号注册方法和绑定方法可以去到官网下,都有相应的指南, 一般设置也比较简单,只需要把对应js代码添加到head中即可, ...