Skip to content

Latest commit

 

History

History
506 lines (375 loc) · 17.3 KB

14、多字段搜索.md

File metadata and controls

506 lines (375 loc) · 17.3 KB

Table of Contents generated with DocToc

多字符串查询

bool 查询采取 more-matches-is-better 匹配越多越好的方式,所以每条 match 语句的评分结果会被加在一起,从而为每个文档提供最终的分数 _score

当然,并不是只能使用 match 语句:可以用 bool 查询来包裹组合任意其他类型的查询, 甚至包括其他的 bool 查询。

语句的优先级

boost 参数:

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { 
            "title":  {
              "query": "War and Peace",
              "boost": 2
        }}},
        { "match": { 
            "author":  {
              "query": "Leo Tolstoy",
              "boost": 2
        }}},
        { "bool":  { 
            "should": [
              { "match": { "translator": "Constance Garnett" }},
              { "match": { "translator": "Louise Maude"      }}
            ]
        }}
      ]
    }
  }
}

单字符查询

当用户输入了单个字符串查询的时候,通常会遇到以下三种情形:

  • 最佳字段

    当搜索词语具体概念的时候,比如 “brown fox” ,词组比各自独立的单词更有意义。像 titlebody 这样的字段,尽管它们之间是相关的,但同时又彼此相互竞争。文档在 相同字段 中包含的词越多越好,评分也来自于 最匹配字段

  • 多数字段

    为了对相关度进行微调,常用的一个技术就是将相同的数据索引到不同的字段,它们各自具有独立的分析链。

    主字段可能包括它们的词源、同义词以及 变音词 或口音词,被用来匹配尽可能多的文档。

    相同的文本被索引到其他字段,以提供更精确的匹配。一个字段可以包括未经词干提取过的原词,另一个字段包括其他词源、口音,还有一个字段可以提供 词语相似性 信息的瓦片词(shingles)。

    其他字段是作为匹配每个文档时提高相关度评分的 信号匹配字段越多 则越好。

  • 混合字段

    对于某些实体,我们需要在多个字段中确定其信息,单个字段都只能作为整体的一部分:

    • Person: first_namelast_name (人:名和姓)

    • Book: titleauthordescription (书:标题、作者、描述)

    • Address: streetcitycountrypostcode (地址:街道、市、国家和邮政编码)

    在这种情况下,我们希望在 任何 这些列出的字段中找到尽可能多的词,这有如在一个大字段中进行搜索,这个大字段包括了所有列出的字段。

最佳字段

使用 dis_max 即分离 最大化查询(Disjunction Max Query) 。分离(Disjunction)的意思是 或(or) ,这与可以把结合(conjunction)理解成 与(and) 相对应。分离最大化查询(Disjunction Max Query)指的是: 将任何与任一查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回

{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

最佳字段调优

通过指定 tie_breaker 这个参数将其他匹配语句的评分也考虑其中:

{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ],
            "tie_breaker": 0.3
        }
    }
}

tie_breaker 参数提供了一种 dis_maxbool 之间的折中选择,它的评分方式如下:

  1. 获得最佳匹配语句的评分 _score
  2. 将其他匹配语句的评分结果与 tie_breaker 相乘。
  3. 对以上评分求和并规范化。

multi_match 查询

multi_match 查询为能在多个字段上反复执行相同查询提供了一种便捷方式。

multi_match 多匹配查询的类型有多种:

  • best_fields (最佳字段)
  • most_fields (多数字段)
  • cross_fields (跨字段)

默认情况下,查询的类型是 best_fields , 这表示它会为每个字段生成一个 match 查询,然后将它们组合到 dis_max 查询的内部。

{
    "multi_match": {
        "query":                "Quick brown fox",
        "type":                 "best_fields", 
        "fields":               [ "title", "body" ],
        "tie_breaker":          0.3,
        "minimum_should_match": "30%" 
    }
}

查询字段名称的模糊匹配

字段名称可以用模糊匹配的方式给出:任何与模糊模式正则匹配的字段都会被包括在搜索条件中, 例如可以使用以下方式同时匹配 book_titlechapter_titlesection_title (书名、章名、节名)这三个字段:

{
    "multi_match": {
        "query":  "Quick brown fox",
        "fields": "*_title"
    }
}

提升单个字段的权重

可以使用 ^ 字符语法为单个字段提升权重,在字段名称的末尾添加 ^boost , 其中 boost 是一个浮点数:

{
    "multi_match": {
        "query":  "Quick brown fox",
        "fields": [ "*_title", "chapter_title^2" ] 
    }
}

多数字段

全文搜索被称作是 召回率(Recall)精确率(Precision) 的战场: 召回率 ——返回结果中的所有文档都是相关的; 精确率 ——返回结果中没有不相关的文档。目的是在结果的第一页中为用户呈现最为相关的文档。

为了提高召回率的效果,我们扩大搜索范围 ——不仅返回与用户搜索词精确匹配的文档,还会返回我们认为与查询相关的所有文档。

提高全文相关性精度的常用方式是为同一文本建立多种方式的索引, 每种方式都提供了一个不同的相关度信号 signal 。主字段会包括最广匹配(broadest-matching)形式的词去尽可能的匹配更多的文档。举个例子,我们可以进行以下操作:

  • 使用词干提取来索引 jumpsjumpingjumped 样的词,将 jump 作为它们的词根形式。这样即使用户搜索 jumped ,也还是能找到包含 jumping 的匹配的文档。
  • 将同义词包括其中,如 jumpleaphop
  • 移除变音或口音词:如 éstaestáesta 都会以无变音形式 esta 来索引。

多字段映射

首先要做的事情就是对我们的字段索引两次: 一次使用词干模式以及一次非词干模式。为了做到这点,采用 multifields 来实现:

PUT /my_index
{
    "settings": { "number_of_shards": 1 }, 
    "mappings": {
        "my_type": {
            "properties": {
                "title": { 
                    "type":     "string",
                    "analyzer": "english",
                    "fields": {
                        "std":   { 
                            "type":     "string",
                            "analyzer": "standard"
                        }
                    }
                }
            }
        }
    }
}

title 字段使用 english 英语分析器来提取词干。title.std 字段使用 standard 标准分析器,所以没有词干提取。

GET /my_index/_search
{
   "query": {
        "multi_match": {
            "query":  "jumping rabbits",
            "type":   "most_fields", 
            "fields": [ "title", "title.std" ]
        }
    }
}

跨字段实体搜索

为每个字段重复查询字符串会使查询瞬间变得冗长,可以采用 multi_match 查询, 将 type 设置成 most_fields 然后告诉 Elasticsearch 合并所有匹配字段的评分:

{
  "query": {
    "multi_match": {
      "query":       "Poland Street W1V",
      "type":        "most_fields",
      "fields":      [ "street", "city", "country", "postcode" ]
    }
  }
}

most_fields 这种方式搜索也存在某些问题,这些问题并不会马上显现:

  • 它是为多数字段匹配 任意 词设计的,而不是在 所有字段 中找到最匹配的。
  • 它不能使用 operatorminimum_should_match 参数来降低次相关结果造成的长尾效应。
  • 词频对于每个字段是不一样的,而且它们之间的相互影响会导致不好的排序结果。

字段中心式查询

以上三个源于 most_fields 的问题都因为它是 字段中心式(field-centric) 而不是 词中心式(term-centric) 的:当真正感兴趣的是匹配词的时候,它为我们查找的是最匹配的 字段

问题 1 :在多个字段中匹配相同的词

回想一下 most_fields 查询是如何执行的:Elasticsearch 为每个字段生成独立的 match 查询,再用 bool 查询将他们包起来。

可以通过 validate-query API 查看:

GET /_validate/query?explain
{
  "query": {
    "multi_match": {
      "query":   "Poland Street W1V",
      "type":    "most_fields",
      "fields":  [ "street", "city", "country", "postcode" ]
    }
  }
}

生成 explanation 解释:

(street:poland   street:street   street:w1v)
(city:poland     city:street     city:w1v)
(country:poland  country:street  country:w1v)
(postcode:poland postcode:street postcode:w1v)

两个 字段都与 poland 匹配的文档要比一个字段同时匹配 polandstreet 文档的评分高。

问题 2 :剪掉长尾

之前讨论过使用 and 操作符或设置 minimum_should_match 参数来消除结果中几乎不相关的长尾,或许可以尝试以下方式:

{
    "query": {
        "multi_match": {
            "query":       "Poland Street W1V",
            "type":        "most_fields",
            "operator":    "and", 
            "fields":      [ "street", "city", "country", "postcode" ]
        }
    }
}

所有词必须呈现。

但是对于 best_fieldsmost_fields 这些参数会在 match 查询生成时被传入,这个查询的 explanation 解释如下:

(+street:poland   +street:street   +street:w1v)
(+city:poland     +city:street     +city:w1v)
(+country:poland  +country:street  +country:w1v)
(+postcode:poland +postcode:street +postcode:w1v)

换句话说,使用 and 操作符要求所有词都必须存在于 相同字段 ,这显然是不对的!可能就不存在能与这个查询匹配的文档。

问题3:词频

想想用字段 first_namelast_name 查询 “Peter Smith” 的例子, Peter 是个平常的名 Smith 也是平常的姓,这两者都具有较低的 IDF 值。但当索引中有另外一个人的名字是 “Smith Williams” 时,姓 Smith 就会非常的不平常,以致它有一个较高的 IDF 值!

下面这个简单的查询可能会在结果中将 “Smith Williams” 置于 “Peter Smith” 之上,尽管事实上是第二个人比第一个人更为匹配。

{
    "query": {
        "multi_match": {
            "query":       "Peter Smith",
            "type":        "most_fields",
            "fields":      [ "*_name" ]
        }
    }
}

这里的问题是 smith 在名字段中具有高 IDF ,它会削弱 “Peter” 作为名和 “Smith” 作为姓时低 IDF 的所起作用。

解决方案

存在这些问题仅仅是因为我们在处理着多个字段,如果将所有这些字段组合成单个字段,问题就会消失。可以为 person 文档添加 full_name 字段来解决这个问题:

{
    "first_name":  "Peter",
    "last_name":   "Smith",
    "full_name":   "Peter Smith"
}

自定义 _all 字段

为了灵活我们可以给人名添加一个自定义 _all 字段,再为地址添加另一个 _all 字段。

Elasticsearch 在字段映射中为我们提供 copy_to 参数来实现这个功能:

PUT /my_index
{
    "mappings": {
        "person": {
            "properties": {
                "first_name": {
                    "type":     "string",
                    "copy_to":  "full_name" 
                },
                "last_name": {
                    "type":     "string",
                    "copy_to":  "full_name" 
                },
                "full_name": {
                    "type":     "string"
                }
            }
        }
    }
}

cross-fields 跨字段查询

自定义 _all 的方式是一个好的解决方案,只需在索引文档前为其设置好映射。 不过, Elasticsearch 还在搜索时提供了相应的解决方案:使用 cross_fields 类型进行 multi_match 查询。 cross_fields 使用词中心式(term-centric)的查询方式,这与 best_fieldsmost_fields 使用字段中心式(field-centric)的查询方式非常不同,它将所有字段当成一个大字段,并在 每个字段 中查找 每个词

字段中心式:对于匹配的文档, petersmith 都必须同时出现在相同字段中,要么是 first_name 字段,要么 last_name 字段:

(+first_name:peter +first_name:smith)
(+last_name:peter  +last_name:smith)

词中心式 会使用以下逻辑:

+(first_name:peter last_name:peter)
+(first_name:smith last_name:smith)

换句话说,词 petersmith 都必须出现,但是可以出现在任意字段中。

cross_fields 类型首先分析查询字符串并生成一个词列表,然后它从所有字段中依次搜索每个词。

cross_fields 类型也能解决词频的问题,通过 validate-query 可以看到:

GET /_validate/query?explain
{
    "query": {
        "multi_match": {
            "query":       "peter smith",
            "type":        "cross_fields", 
            "operator":    "and",
            "fields":      [ "first_name", "last_name" ]
        }
    }
}

它通过 混合 不同字段逆向索引文档频率的方式解决了词频的问题:

+blended("peter", fields: [first_name, last_name])
+blended("smith", fields: [first_name, last_name])

按字段提高权重

如果要用 titledescription 字段搜索图书,可能希望为 title 分配更多的权重,这同样可以使用前面介绍过的 ^ 符号语法来实现:

GET /books/_search
{
    "query": {
        "multi_match": {
            "query":       "peter smith",
            "type":        "cross_fields",
            "fields":      [ "title^2", "description" ] 
        }
    }
}

Exact-Value 精确值字段

查看查询的 explanation 解释得到,设想将 title 字段设置成 not_analyzed

GET /_validate/query?explain
{
    "query": {
        "multi_match": {
            "query":       "peter smith",
            "type":        "cross_fields",
            "fields":      [ "title", "first_name", "last_name" ]
        }
    }
}

因为 title 字段是未分析过的,Elasticsearch 会将 “peter smith” 这个完整的字符串作为查询条件来搜索!

title:peter smith
(
    blended("peter", fields: [first_name, last_name])
    blended("smith", fields: [first_name, last_name])
)

显然这个项不在 title 的倒排索引中,所以需要在 multi_match 查询中避免使用 not_analyzed 字段。

导航

目录

上一章:13、全文搜索

下一章:15、近似匹配