ElasticSearch 2 (15) - 深刻搜索系列之多字段搜索

ElasticSearch 2 (15) - 深刻搜索系列之多字段搜索

摘要

查询不多是简单的一句话匹配(one-clause match)查询。不少时候,咱们须要用相同或不一样的字符串查询1个或多个字段,也就是说,咱们须要对多个查询语句以及他们相关分数(relevance scores)进行有意义的合并。html

有时候或许咱们正查找一本名为战争与和平(War and Peace)而做者叫Leo Tolstoy的书,或许咱们正用“最少匹配”(“minimum should match”)的方式在文档中进行查找(多是页面标题,也多是页面的内容),或许咱们正搜索全部名字为 John Smith 的用户。算法

本篇文章中,咱们会介绍构造多语句搜索的工具以及不一样场景下不一样的适合解决方案。app

版本

elasticsearch版本: elasticsearch-2.xdom

内容

多字符串查询(Multiple Query Strings)

最简单的多字段(multifield)查询是能够将搜索术语与具体字段映射的。若是咱们知道 War and Peace 是标题,Leo Tolstoy 是做者,咱们只须要将两个条件写成 match 语句,而后将他们用 bool 查询组合起来便可:elasticsearch

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "title":  "War and Peace" }},
        { "match": { "author": "Leo Tolstoy"   }}
      ]
    }
  }
}

bool 查询的采用越多匹配越好 (more-matches-is-better)的方式,因此每一个 match 语句的分数结果会加和在一块儿,为每一个文档获得一个最终的分数,能与两个语句同时匹配的文档比只与一个语句匹配的文档得分要高。ide

固然,咱们并非只能使用 match 语句:能够用 bool 查询组合任意其余类型的查询,甚至其余的 bool 查询。咱们能够为上面的例子添加特定译者版本的偏好:工具

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

为何将译者条件语句放入另外一个独立的 bool 查询中呢?全部的4个 match 查询都是 should 语句,咱们为何不将translator语句与其余语句(如 titleauthor)放在同一层呢?post

答案在于分数的计算方式。bool 查询执行每一个 match 查询,而后把他们加在一块儿,而后将结果与全部匹配的语句数量相乘,再除以全部的语句数量。处于同一层的每条语句具备相同的权重。在上面这个例子中,包含translator语句的 bool 查询,只占总分数的三分之一,若是咱们将translator语句与 titleauthor 两个语句放入同一层,那么 titleauthor 语句只贡献四分之一。性能

句子的优先级排序

有可能上面这个例子中每一个语句贡献三分之一的分数并非咱们想要的,咱们极可能对 titleauthor 两个句子更感兴趣,这样咱们就须要调整查询,使 titleauthor 语句更重要。测试

在咱们军械库中,最容易使用的武器就是 boost 参数。为了提升 titleauthor 字段的权重,咱们为他们分配高于1的 boost 值。

GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { #1
            "title":  {
              "query": "War and Peace",
              "boost": 2
        }}},
        { "match": { #2
            "author":  {
              "query": "Leo Tolstoy",
              "boost": 2
        }}},
        { "bool":  { #3
            "should": [
              { "match": { "translator": "Constance Garnett" }},
              { "match": { "translator": "Louise Maude"      }}
            ]
        }}
      ]
    }
  }
}
  • #1 #2 titleauthor 语句的 boost 值为2。
  • 嵌套的 bool 语句的 boost 值为1。

获取 boost 参数最佳值的一个比较简单的方式就是须要不断试错:设定 boost 值,运行测试查询,如此反复。boost 值比较合理的一个区间是1到10,固然也有多是15。若是为 boost 分配比这更高的值将不会对最终的结果产生更大影响,由于分数最后会被规范化(normalized)。

单字符串查询(Single Query String)

bool 查询是多语句查询的主干。它的适用场景不少,特别是当咱们须要将不一样查询字符串与不一样字段创建映射的时候。

有些用户指望将全部的搜索术语堆积到一个字段中,而后指望ElasticSearch能理解这些搜索,并为他们提供正确的结果。有意思的是多字段搜索的表单一般被称为 高级查询 (Advanced Search)- 由于它对用户而言是高级的,可是,多字段搜索在实现上却很是简单。

对于多词(multiword)、多字段(multifield)查询没有简单的一刀切方案。为了获得最佳结果,咱们除了须要了解如何使用合适的工具外,还须要了解咱们的数据。

了解咱们的数据(Know Your Data)

当咱们的用户输入了一个单字符串查询的时候,咱们一般会遇到一下情形:

  • 最佳字段

    当咱们搜索具备具体概念的词的时候,好比“brown fox”,词组比它们各自更有意义。像 titlebody 这样的字段,尽管他们是相关的,可是他们也彼此相互竞争。当文档在相同字段中具备更多词的时候,最终的分数来自于最匹配字段(best-matching field)。

  • 多数字段

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

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

    相同的文本被索引到其余字段,以提供更精确的匹配。一个字段能够包括原词,其余词源、口音,以及能够提供词语类似性的 瓦片词 (shingles)。

    其余字段是做为匹配每一个文档时提升相关度分数的信号词,越多字段能匹配则越好。

  • 混合字段

    对于某些实体,咱们须要在多个字段中肯定其信息,单个字段都只能做为总体的一部分:

    • Person: first_namelast_name
    • Book: titleauthordescription
    • Address: streetcitycountrypostcode

    在这种状况下,咱们但愿在全部这些列出的字段中找到尽量多的词,这有如在一个大的字段中进行搜索,这个大的字段包括全部列出字段。

上述全部的全部都是多词、多字段查询(mutiword,multifield queries),可是每一个具体查询都须要使用不一样的策略。在后面章节中,咱们会依次介绍这些策略。

最佳字段(Best Fields)

若是咱们有个网站并为用户提供博客内容搜索的功能,如下面两个博客内容文档为例:

PUT /my_index/my_type/1
{
    "title": "Quick brown rabbits",
    "body":  "Brown rabbits are commonly seen."
}

PUT /my_index/my_type/2
{
    "title": "Keeping pets healthy",
    "body":  "My quick brown fox eats rabbits on a regular basis."
}

用户输入词组“Brown fox”而后点击搜索按钮。事先,咱们并不知道用户的搜索术语是会在 title 仍是 body 中被找到,可是,用户颇有多是想对“Brown fox”这个相关词组进行搜索。以肉眼判断,文档2的匹配度更高,由于它同时具备两个词:

咱们用bool查询试试:

{
    "query": {
        "bool": {
            "should": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

可是咱们发现查询的结果是文档1具备更高分数:

{
  "hits": [
     {
        "_id":      "1",
        "_score":   0.14809652,
        "_source": {
           "title": "Quick brown rabbits",
           "body":  "Brown rabbits are commonly seen."
        }
     },
     {
        "_id":      "2",
        "_score":   0.09256032,
        "_source": {
           "title": "Keeping pets healthy",
           "body":  "My quick brown fox eats rabbits on a regular basis."
        }
     }
  ]
}

为了理解这个现象,咱们须要回想一下 bool 是如何计算分数的:

  1. 它会执行 should 语句中的两个查询
  2. 将两个查询的分数相加
  3. 与总匹配语句的数目相乘
  4. 并除以总语句的数目(这里为:2)

文档1中,两个字段都包含 brown 这个词,因此两个 match 语句都成功匹配且有一个分数。文档2中,body 字段同时包含 brownfox 这两个词,可是 title 字段没有包含任何词。这样,body 查询结果中的高分,加上 title 查询中的0分,并乘以二分之一,就获得了一个比文档1更低的总体分数。

注:

以公式表示文档1的分数为:

(score_of_doc_1_title_match + score_of_doc_1_body_match) * total_number_of_match_clause / total_number_of_clause

其中:
score_of_doc_1_body_match = 0
total_number_of_match_clause = 1
total_number_of_clause = 2

在这个例子中,titlebody 两个字段处于竞争地位,因此咱们就须要找到单个最佳匹配(best-matching)字段。

若是咱们不是简单将每一个字段的分数结果加在一块儿,而是将最佳匹配(best-matching)字段的分数做为总体查询的分数,会有怎样的结果?这样返回的结果多是:同时包含两个词的单个字段 比 相同词语反复出现的多个不一样字段 相关度更高。

dis_max 查询

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

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

这个查询的结果为:

{
  "hits": [
     {
        "_id":      "2",
        "_score":   0.21509302,
        "_source": {
           "title": "Keeping pets healthy",
           "body":  "My quick brown fox eats rabbits on a regular basis."
        }
     },
     {
        "_id":      "1",
        "_score":   0.12713557,
        "_source": {
           "title": "Quick brown rabbits",
           "body":  "Brown rabbits are commonly seen."
        }
     }
  ]
}

最佳字段查询调优(Tuning Best Fields Queries)

当用户搜索“quick pets”时会发生什么呢?使用以前的例子,两个文档都包含词 quick,可是只有文档2包含词 pets,两个文档中都不具备同时包含两个词的字段。

以下,一个简单的 dis_max 查询会采用单个最佳匹配(best matching)字段,而后忽略其余的匹配:

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

结果是:

{
  "hits": [
     {
        "_id": "1",
        "_score": 0.12713557, #1
        "_source": {
           "title": "Quick brown rabbits",
           "body": "Brown rabbits are commonly seen."
        }
     },
     {
        "_id": "2",
        "_score": 0.12713557, #2
        "_source": {
           "title": "Keeping pets healthy",
           "body": "My quick brown fox eats rabbits on a regular basis."
        }
     }
   ]
}
  • #1 #2 注意这两个分数是相同的。

咱们可能指望在这个例子中,可以同时匹配 titlebody 字段的文档比只与一个字段匹配的文档的相关度更高,但事实并不是如此,由于 dis_max 查询只会使用单个最佳匹配语句的分数(*_score*)做为总体分数。

打破平衡(tie_breaker)

咱们可使用 tie_breaker 这个参数将其余匹配语句的分数也考虑其中:

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

这个查询的结果以下:

{
  "hits": [
     {
        "_id": "2",
        "_score": 0.14757764, #1
        "_source": {
           "title": "Keeping pets healthy",
           "body": "My quick brown fox eats rabbits on a regular basis."
        }
     },
     {
        "_id": "1",
        "_score": 0.124275915, #2
        "_source": {
           "title": "Quick brown rabbits",
           "body": "Brown rabbits are commonly seen."
        }
     }
   ]
}
  • #1 #2 能够看到,文档2比文档1在相关度上有微弱优点。

tie_breaker 参数的出现其实是提供了一种处于 dis_maxbool 中间状态的查询,它分数计算的方式以下:

  1. 得到最佳匹配(best-matching)语句的分数 *_score*
  2. 将其余匹配语句的得分与 tie_breaker 相乘
  3. 将以上分数求和并规范化(normalize)

因为tie_breaker的做用,全部匹配语句都会被考虑其中,可是最佳匹配语句仍然占最终结果的大头。

注意:
tie_breaker 能够是 0 到 1 之间的浮点数,其中,若是数值为0,即表明使用dis_max最佳匹配语句的普通逻辑,若是数值为1,即表示全部匹配语句同等重要。最佳的准确值须要根据数据与查询进行调试得出,可是合理的值一般与零接近(处于 0.1 - 0.4 之间),这样的合理值不会改变 dis_max 使用最佳匹配的本质。

多配查询(multi_match查询)

multi_match 查询为反复执行在多个字段上的查询提供了一种简便的方式。

注意:
multi_match 查询的类型有多种,其中的三种恰巧与 了解咱们的数据(Know YOur Data) 中介绍的三个场景对应,即:best_fields,most_fields,cross_fields。

默认状况下,下面这个查询的类型是 best_fields,这表示它会为每一个字段生成一个查询,而后将他们组合到dis_max 查询的内部:

{
  "dis_max": {
    "queries":  [
      {
        "match": {
          "title": {
            "query": "Quick brown fox",
            "minimum_should_match": "30%"
          }
        }
      },
      {
        "match": {
          "body": {
            "query": "Quick brown fox",
            "minimum_should_match": "30%"
          }
        }
      },
    ],
    "tie_breaker": 0.3
  }
}

上面这个查询以 multi_match 重写更为简洁:

{
    "multi_match": {
        "query":                "Quick brown fox",
        "type":                 "best_fields", #1
        "fields":               [ "title", "body" ],
        "tie_breaker":          0.3,
        "minimum_should_match": "30%" #2
    }
}
  • #1 这个 best_fields 类型是默认值,能够不指定。
  • #2 如 minimum_should_matchoperator 这样的参数会被传递到生成的 match 查询中。

查询字段名称的模糊匹配(Using Wildcards in Field Names)

字段名称能够用模糊匹配的方式给出:任何与模糊匹配(wildcard)正则匹配的字段都会被包括在搜索中,好比,咱们可使用一下方式同时匹配 book_titlechapter_titlesection_title 这三个字段:

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

增长单个字段的权重(Boosting Individual Fields)

可使用脱字号(caret ^ )的语法为单个字段增长权重:只须要在字段末尾添加 ^boost,其中 boost 是一个浮点数:

{
    "multi_match": {
        "query":  "Quick brown fox",
        "fields": [ "*_title", "chapter_title^2" ] #1
    }
}
  • #1 chapter_title 这个字段的boost值为2,而其余两个字段 book_titlesection_title 字段具备默认的boost值为1。

多数字段(Most Fields)

全文搜索被称做是召回(Recall)与准确(Precision)的战场:召回(Recall)指的是返回结果中的全部文档都是相关的;准确(Precision)指的是返回结果中没有不相关的文档。目的是,在结果的第一页中,为用户呈现最相关的文档。

为了提升召回(Recall)的效果,咱们在全网中进行搜索,不只返回与用户搜索术语精确匹配的文档,还会返回咱们认为与查询相关的全部文档。若是一个用户搜索“quick brown box”,一个包含词语fast foxes的文档出如今结果集中会被认为是很是合理的。

固然,若是包含词语fast foxes的文档是咱们找到的惟一相关文档,那么它会出如今结果集的顶端,可是,若是有100个文档都出现了词语“quick brown fox”,那么这个包含词语fast foxes的文档会被认为是次相关的,它可能处于返回结果列表的下面某个地方。当包含了不少潜在匹配以后,咱们须要将最匹配的几个放在结果集的顶端。

对全文相关度提升精度的一个经常使用方式是为同一文本创建不一样方式的索引,每种方式都提供了一个不一样的相关度信号(signal)。主字段(main field)会包括最宽匹配(broadest-matching)形式的术语去尽量的匹配更多的文档。举个例子,咱们能够进行一下操做:

  • 使用词jump做为根(root)来索引 jumps、jumping和jumped样的词。这样,不管用户使用 jumped 仍是 jumping 进行搜索,都能找到匹配的文档。
  • 将同义词包括其中,如jump、leap和hop。
  • 移除变音或口音词:如ésta、 está和esta都会以无变音形式创建索引。

尽管如此,若是咱们有两个文档,其中一个包含词jumped,另外一个包含词jumping,若是咱们使用jumped进行搜索的时候,当前指望前者能有更高的排名。

为了解决这个问题,咱们能够将相同的文本索引到其余字段中去以提供更精确的匹配。一个字段多是为非词根的版本,另外一个字段多是变音过的原始词,还有一个字段可能使用瓦片词(shingles)以提供词语类似性的信息。这些其余的字段做为提升每一个文档的相关度分数的信号(signals),匹配的字段越多越好。

一个文档若是与宽匹配的主字段匹配,那么它会出如今结果列表中,若是它同时与信号(signal)字段匹配,它会获得加分,系统会上提它在结果列表中的位置。

咱们会稍后讨论同义词、词类似性、半匹配以及其余潜在的信号,这里咱们只使用词干(stemmed)和非词干(unstemmed)字段做为简单例子来讲明这种技术。

多字段映射(Multifield Mapping)

第一件要作的事情就是要对咱们的字段索引两次:一次词干模式和一次非词干模式。咱们使用 multifields 来实现(multifields 在String Sorting and Multifields中介绍过)。

DELETE /my_index

PUT /my_index
{
    "settings": { "number_of_shards": 1 }, #1
    "mappings": {
        "my_type": {
            "properties": {
                "title": { #2
                    "type":     "string",
                    "analyzer": "english",
                    "fields": {
                        "std":   { #3
                            "type":     "string",
                            "analyzer": "standard"
                        }
                    }
                }
            }
        }
    }
}
  • #1 参考被破坏的相关度
  • #2 title 字段使用 english 分析器进行词干分析。
  • #3 title.std 字段使用 standard 标准分析器进行非词干分析。

接着咱们索引一些文档:

PUT /my_index/my_type/1
{ "title": "My rabbit jumps" }

PUT /my_index/my_type/2
{ "title": "Jumping jack rabbits" }

这里用一个简单 match 查询 title 字段是否包含 jumping rabbits

GET /my_index/_search
{
   "query": {
        "match": {
            "title": "jumping rabbits"
        }
    }
}

因为使用 english 分析器,这个查询是在查找以 jumprabbit 这两个词干做为术语的文档。两个文档的 title 字段都同时包括两个术语,因此两个文档获得的分数相同:

{
  "hits": [
     {
        "_id": "1",
        "_score": 0.42039964,
        "_source": {
           "title": "My rabbit jumps"
        }
     },
     {
        "_id": "2",
        "_score": 0.42039964,
        "_source": {
           "title": "Jumping jack rabbits"
        }
     }
  ]
}

若是咱们只是对 title.std 字段进行查询,那么只有文档2是匹配的。尽管如此,若是咱们对两个字段同时查询,而后使用 bool 查询将分数结果合并,则两个文档都是匹配的(title 字段的做用),并且文档2的相关度分数更高(title.std 字段的做用):

GET /my_index/_search
{
   "query": {
        "multi_match": {
            "query":  "jumping rabbits",
            "type":   "most_fields", #1
            "fields": [ "title", "title.std" ]
        }
    }
}
  • #1 咱们但愿将全部匹配字段的分数合并起来,因此咱们使用 most_fields 类型。这使 multi_match 查询用 bool 查询将两个字段语句包在里面,而非 dis_max 查询。

    {
    "hits": [
    {
    "_id": "2",
    "_score": 0.8226396, #1
    "_source": {
    "title": "Jumping jack rabbits"
    }
    },
    {
    "_id": "1",
    "_score": 0.10741998, #2
    "_source": {
    "title": "My rabbit jumps"
    }
    }
    ]
    }

  • #1 #2 文档2如今的分数要比文档1高。

咱们用宽匹配字段 title 包括尽量多的文档——以增长召回(Recall)——同时又使用字段 title.std 做为信号(signal)将相关度更高的文档置入结果集的顶部。

每一个字段对于最终分数的贡献能够经过自定义值 boost 来控制。好比,咱们使 title 字段更为重要,这样同时也下降了其余信号字段的做用:

GET /my_index/_search
{
   "query": {
        "multi_match": {
            "query":       "jumping rabbits",
            "type":        "most_fields",
            "fields":      [ "title^10", "title.std" ] #1
        }
    }
}
  • #1 title 字段的 boost 的值为10使它比 title.std 更重要。

如今咱们讨论一种广泛的搜索模式:跨字段实体搜索(cross-fields entity search)。在如人 (person)产品(product)地址(address) 这样的实体中,咱们须要使用多个字段来惟一辨认它的信息。一个 人(person) 实体多是这样索引的:

{
    "firstname":  "Peter",
    "lastname":   "Smith"
}

而一个 地址(address) 多是这样

{
    "street":   "5 Poland Street",
    "city":     "London",
    "country":  "United Kingdom",
    "postcode": "W1V 3DG"
}

这与咱们以前说的多字符串查询很像,可是这里有一个巨大的区别。在多字符串查询中,咱们为每一个字段使用不一样的字符串,在这个例子中,咱们想使用单个字符串在多个字段中进行搜索。

咱们的用户可能想要查找 “Peter Smith”这我的 或 “Poland Street W1V”这个地址,这些词都出如今不一样的字段中,因此若是使用 dis_max / best_fields 查询去查找单个最佳匹配字段显然是错误的。

一种弱弱的方式(A Naive Approach)

咱们依次查询每一个字段,而后每一个字段的匹配结果相加,这看起来像个 bool 查询:

{
  "query": {
    "bool": {
      "should": [
        { "match": { "street":    "Poland Street W1V" }},
        { "match": { "city":      "Poland Street W1V" }},
        { "match": { "country":   "Poland Street W1V" }},
        { "match": { "postcode":  "Poland Street W1V" }}
      ]
    }
  }
}

为每一个字段重复查询字符串会使查询变得冗长,咱们可使用 multi_match 查询,将类型设置成 most_fields 而后告诉ELasticSearch合并全部匹配字段的分数:

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

most_fields 方式的问题

most_fields 这种方式搜索会有一些问题,这些问题不会立刻显现出来:

  • 它是为多数字段匹配任意词而设计的,而不是在全部字段中找到最匹配的。
  • 它不能使用参数 operatorminimum_should_match 来减小此相关结果中的长尾。
  • 词频对于每一个字段是不同的,并且它们之间相互影响可能会生成一个很差的排序结果。

以字段为中心的查询(Field-Centric Queries)

上面这三个来自于 most_fields 的问题都是由于它是 字段为中心的(field-centric)而不是 术语为中心的(term-centric)。当咱们对术语(terms)匹配真正感兴趣时,它为咱们查找的是最匹配的字段(fields)。

注意:

best_fields 类型也是字段为中心的(field-centric)有着相似的问题。

首先咱们来看看这些问题存在的缘由,而后再来解决它们。

问题 1:多个字段从匹配相同词(Matching the Same Word in Multiple Fields)

回想一下 most_fields 查询是如何执行的:ElasticSearch为每一个字段生成一个查询,而后用 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:剪掉长尾(Trimming the Long Tail)

精度控制(Controlling Precision)中,咱们讨论了使用 and 操做符 或者 设置minimum_should_match 参数来消除结果中不相关的长尾:

{
    "query": {
        "multi_match": {
            "query":       "Poland Street W1V",
            "type":        "most_fields",
            "operator":    "and", #1
            "fields":      [ "street", "city", "country", "postcode" ]
        }
    }
}
  • #1 全部词必须呈现。

可是对于 best_fieldsmost_fields 这样的参数会在 match 查询生成时被传入,这个查询的 explaination 以下:

(+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:词频(Term Frequencies)

什么是相关(What is Relevance)中,咱们解释了每一个术语使用默认类似度算法是 TF/IDF:

  • 词频(Term frequency)

    一个词在单个文档的某个字段中出现的频率越高,这个文档的相关度越高。

  • 逆向文件频率(Inverse document frequency)

    一个词在全部文档索引中出现的频率越高,这个词的相关度越低。

当咱们在多个字段中进行搜索时,TF/IDF能够为咱们带来某些意外的结果。

考虑咱们用字段 first_namelast_name 字段查询 “Peter Smith”的例子,Peter是一个普通的名(first name)同时Smith也是个一个很是普通的姓(last name),他们都具备较低的IDF值。可是当咱们索引中有另一我的的名字是 “Smith Williams”时,Smith做为姓(first name)就变得很是不普通以至它有一个较高的IDF值。

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

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

这里的问题是 Smith 在名字段中有着高IDF,它会削弱 “Peter”做为名和“Smith”做为姓时较低的IDF的做用。

解决方案(Solution)

这些问题存在的缘由在于它处理着多个字段,若是咱们将全部这些字段组合成单个字段,这个问题就会不复存在。咱们能够为Person文档添加一个 full_name 字段:

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

当对 full_name 进行查询时:

  • 具备更多匹配词的文档会比只有一个重复匹配词的文档更重要。
  • 参数 minimum_should_matchoperator 会如咱们指望那样工做。
  • 姓和名的逆向文件频率被合并,因此 Smith 究竟是做为姓出现,仍是做为名出现?这个问题会变得可有可无。

这么作固然是可行的,可是咱们不太喜欢存储冗余的数据。ElasticSearch为咱们提供了两个解决方案——一个是索引时的,另外一个是搜索时的——咱们会在稍后讨论这两个方案。

自定义_all字段(Custom *_all* Fields)

在元数据_all 字段中(**Metadata:_all Field**),咱们解释过:*_all* 字段的索引方式是将全部其余字段的值做为一个巨大的字符串进行索引的。尽管这么作并非十分灵活,可是咱们能够为人的姓名添加一个自定义 *_all* 字段,而后再为地址添加另外一个 *_all* 字段。

ElasticSearch在字段映射中,为咱们提供了一个 copy_to 参数来实现这个功能。

PUT /my_index
{
    "mappings": {
        "person": {
            "properties": {
                "first_name": {
                    "type":     "string",
                    "copy_to":  "full_name" #1
                },
                "last_name": {
                    "type":     "string",
                    "copy_to":  "full_name" #2
                },
                "full_name": {
                    "type":     "string"
                }
            }
        }
    }
}
  • #1 #2 first_namelast_name 字段中的值会被复制到 full_name 字段中。

有了这个映射,咱们可使用 first_name 查询名,使用 last_name 查询名,或者直接使用 full_name 查询姓名。

注意:

映射中 first_name 和 last_name 并不知道 full_name是如何被索引的,full_name将两个字段的内容复制到本地,而后自行索引。

跨字段查询(cross_fields Queries)

自定义 _all 的方式是一个好的解决方案,咱们只须要在索引文件以前为其设置好映射便可。不过,ElasticSearch还在搜索时(search-time)提供了相应的解决方案:使用类型 cross_fields 进行multi_match 查询。cross_fields 使用以术语为中心(term-centric)的查询方式,这与 best_fieldsmost_fields 使用的字段为中心(field-centric)的查询方式很是不一样。它将全部字段当作一个大的字段,而后在里面查找每一个术语(each term)。

为了说明这两个查询方式(field-centric和term-centric)的不一样,咱们先看看下面这个以字段为中心的 most_fields 查询的 explanation

GET /_validate/query?explain
{
    "query": {
        "multi_match": {
            "query":       "peter smith",
            "type":        "most_fields",
            "operator":    "and", #1
            "fields":      [ "first_name", "last_name" ]
        }
    }
}
  • #1 全部术语都是必须的。

对于一个匹配的文档,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 类型首先分析查询字符串并生成一个术语列表,而后它在全部字段从依次搜索每一个术语。这种不一样的搜索方式很天然的解决了字段中心式查询(Field-Centric Queries)三个问题中的二个。留给咱们的问题只是:逆向文件频率不一样。

幸运的是,cross_fields 一样能够解决这个问题,经过 validate-query 查看:

GET /_validate/query?explain
{
    "query": {
        "multi_match": {
            "query":       "peter smith",
            "type":        "cross_fields", #1
            "operator":    "and",
            "fields":      [ "first_name", "last_name" ]
        }
    }
}
  • #1 用cross_fields 术语中心式查询。

它经过将不一样字段的逆向索引文件频率(inverse document frequency)混合的方式解决词频(term-frequency)的问题:

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

换句话说,它会同时在 first_namelast_name 两个字段中查找 smith 的IDF,而后用二者的最小值做为两个字段的IDF。结果实际上就是:smith 会被认为既是一个普通的姓,同时也是一个普通的名。

注意:

为了让 cross_fields 查询以最优方式工做,全部的字段都须要使用相同的分析器,具备相同分析器的字段会被分组在一块儿做为混合字段使用。

若是包括了不一样分析链的字段,它们会以 best_fields 的相同方式加到查询结果中。例如:咱们将 title 字段加到以前的查询中(假设他们使用的是不一样的分析器),explaination 的结果以下:

(+title:peter +title:smith)
  (

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

提升字段权重(Per-Field Boosting)

cross_fields 查询与 自定义_all 相比的一个优点就是它能够在搜索时,为单个字段提高权重。

咱们不须要为像 first_namelast_name这样具备相同值的字段这么作,可是若是要用 titledescription 字段搜索图书,咱们可能但愿为 title 分配更多的权重,这一样可使用前面介绍过的 脱字号(caret ^)语法来实现:

GET /books/_search
{
    "query": {
        "multi_match": {
            "query":       "peter smith",
            "type":        "cross_fields",
            "fields":      [ "title^2", "description" ] #1
        }
    }
}
  • \1 title 字段的 boost 为 2,description 字段的boost 为默认值 1。

可以为单个字段指定boost值所带来的好处须要权衡多字段查询与单字段自定义_all之间的代价,即那种方案会给咱们带来更大的(性能)压力。

准确值字段(Exact-Value Fields)

在结束多字段查询这个话题以前,咱们最后须要讨论的是准确值 not_analyzed 字段。将 not_analyzed 字段与 multi_matchanalyzed 字段混在一块儿没有多大用处。

缘由能够经过查看查询的explaination获得,假设咱们将 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 字段。

参考

elastic.co: Multifield Search

相关文章
相关标签/搜索