- 1. 跨字段实体搜索
- 2. 字段中心式查询
- 3. 自定义 _all 字段
- 4. cross-fields 跨字段查询
- 5. Exact-Value 精确值字段
1.1 跨字段实体搜索
跨字段实体搜索(cross-fields entity search),在如 person 、 product 或 address 这样的实体中,需要使用多个字段来唯一标识它的信息。
比如一个人的标识是姓名,姓名可以散落在多个field中,比如first_name和last_name中。
{
"firstname": "Peter",
"lastname": "Smith"
}
一个建筑的标识是地址,地址可以散落在street,city,country,postcode中。
{
"street": "5 Poland Street",
"city": "London",
"country": "United Kingdom",
"postcode": "W1V 3DG"
}
跨多个field搜索一个标识,比如搜索一个人名,或者一个地址,就是cross-fields搜索。
使用单个字符串在多个字段中进行搜索。我们的用户可能想搜索 “Peter Smith” 这个人,或 “Poland Street W1V” 这个地址,这些词出现在不同的字段中,所以如果使用 dis_max 或 best_fields 查询去查找单个最佳匹配字段显然是个错误的方式。
1.2 使用most_fields策略实现跨字段实体搜索存在的弊端
依次查询每个字段并将每个字段的匹配评分结果相加,听起来真像是 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 查询,将 type 设置成 most_fields 然后告诉 Elasticsearch 合并所有匹配字段的评分:
{
"query": {
"multi_match": {
"query": "Poland Street W1V",
"type": "most_fields",
"fields": [ "street", "city", "country", "postcode" ]
}
}
}
用 most_fields 这种方式搜索存在某些问题,这些问题并不会马上显现:
2. 字段中心式查询
- 它是为多数字段匹配任意词设计的(搜索词出现在多数字段中),而不是在所有字段中找到最匹配的。
- 它不能使用 operator 或 minimum_should_match 参数来降低次相关结果造成的长尾效应。
- 词频对于每个字段是不一样的,而且它们之间的相互影响会导致不好的排序结果。
以上三个源于 most_fields 的问题都因为它是字段中心式(field-centric)而不是词中心式(term-centric)的:当真正感兴趣的是匹配词的时候,它为我们查找的是最匹配的字段。
best_fields 类型也是字段中心式的,它也存在类似的问题。首先查看这些问题存在的原因,再想如何解决它们。
问题 1 :在多个字段中匹配相同的词
回想一下 most_fields 查询是如何执行的:Elasticsearch 为每个字段生成独立的 match 查询,再用 bool 查询将他们包起来。
GET /_validate/query?explain
{
"query": {
"multi_match": {
"query": "Poland Street W1V",
"type": "most_fields",
"fields": [ "street", "city", "country", "postcode" ]
}
}
}
两个字段都与 poland 匹配的文档要比一个字段同时匹配 poland 与 street 文档的评分高。
问题 2 :剪掉长尾
我们讨论过使用 and 操作符或设置 minimum_should_match 参数来消除结果中几乎不相关的长尾,或许可以尝试以下方式:
{
"query": {
"multi_match": {
"query": "Poland Street W1V",
"type": "most_fields",
"operator": "and",
"fields": [ "street", "city", "country", "postcode" ]
}
}
}
对于 best_fields 或 most_fields 这些参数会在 match 查询生成时被传入,使用 and 操作符要求所有词都必须存在于相同字段 ,这显然是不对的!可能就不存在能与这个查询匹配的文档。
问题 3 :词频
每个词默认使用 TF/IDF 相似度算法计算相关度评分:
词频:一个词在单个文档的某个字段中出现的频率越高,这个文档的相关度就越高。
逆向文档频率:一个词在所有文档某个字段索引中出现的频率越高,这个词的相关度就越低。
当搜索多个字段时,TF/IDF 会带来某些令人意外的结果。想想用字段 first_name 和 last_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"
}
当查询 full_name 字段时:
- 具有更多匹配词的文档会比只有一个重复匹配词的文档更重要。
- minimum_should_match 和 operator 参数会像期望那样工作。
- 姓和名的逆向文档频率被合并,所以 Smith 到底是作为姓还是作为名出现,都会变得无关紧要。
这么做当然是可行的,但我们并不太喜欢存储冗余数据。取而代之的是 Elasticsearch 可以提供两个解决方案:一个在索引时(自定义_all字段),而另一个是在搜索时(cross_fields)。
3. 自定义 _all 字段3.1 回顾之前学习的_all 字段
_all 字段的索引方式是将所有其他字段的值作为一个大字符串索引的。例如,这个简单搜索返回包含 mary 的所有文档:
GET /_search?q=mary
这个查询的结果在三个地方提到了 mary :
- 有一个用户叫做 Mary
- 6条微博发自 Mary
- 一条微博直接 @mary
Elasticsearch 是如何在三个不同的字段中查找到结果的呢?当索引一个文档的时候,Elasticsearch 取出所有字段的值拼接成一个大的字符串,作为 _all 字段进行索引。例如,当索引这个文档时:
{
"tweet": "However did I manage before Elasticsearch?",
"date": "2014-09-14",
"name": "Mary Jones",
"user_id": 1
}
这就好似增加了一个名叫 _all 的额外字段:
"However did I manage before Elasticsearch? 2014-09-14 Mary Jones 1"
在刚开始开发一个应用时,_all 字段是一个很实用的特性。之后,你会发现如果搜索时用指定字段来代替 _all 字段,将会更好控制搜索结果。
3.2 自定义_all字段
_all 字段的索引方式是将所有其他字段的值作为一个大字符串索引的。这么做并不十分灵活,为了灵活我们可以给人名添加一个自定义 _all 字段,再为地址添加另一个 _all 字段。
Elasticsearch 在字段映射中为我们提供 copy_to 参数来实现这个功能:将first_name 和 last_name 字段中的值会被复制到 full_name 字段。
PUT /person
{
"mappings": {
"properties": {
"first_name": {
"type": "text",
"copy_to": "full_name"
},
"last_name": {
"type": "text",
"copy_to": "full_name"
},
"full_name": {
"type": "text"
}
}
}
}
有了这个映射,我们可以用 first_name 来查询名,用 last_name 来查询姓,或者直接使用 full_name 查询整个姓名。first_name 和 last_name 的映射并不影响 full_name 如何被索引, full_name 将两个字段的内容复制到本地,然后根据 full_name 的映射自行索引。
4. cross-fields 跨字段查询4.1 cross-fields 跨字段查询
自定义 _all 的方式是一个好的解决方案,只需在索引文档前为其设置好映射。不过, Elasticsearch 还在搜索时提供了相应的解决方案:使用 cross_fields 类型进行 multi_match 查询。 cross_fields 使用词中心式的查询方式,这与 best_fields 和 most_fields 使用字段中心式的查询方式非常不同,它将所有字段当成一个大字段,并在每个字段中查找每个词。
为了说明字段中心式与词中心式这两种查询方式的不同,先看看以下字段中心式的 most_fields 查询的 explanation 解释:
GET /_validate/query?explain
{
"query": {
"multi_match": {
"query": "peter smith",
"type": "most_fields",
"operator": "and",
"fields": [ "first_name", "last_name" ]
}
}
}
对于匹配的文档, peter 和 smith 都必须同时出现在相同字段中,要么是 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)
换句话说,词 peter 和 smith 都必须出现,但是可以出现在任意字段中。
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])
换句话说,它会同时在 first_name 和 last_name 两个字段中查找 smith 的 IDF ,然后用两者的最小值作为两个字段的 IDF 。结果实际上就是 smith 会被认为既是个平常的姓,也是平常的名。
4.2 按字段提高权重
采用 cross_fields 查询与自定义 _all 字段相比,其中一个优势就是它可以在搜索时为单个字段提升权重。
这对像 first_name 和 last_name 具有相同值的字段并不是必须的,但如果要用 title 和 description 字段搜索图书,可能希望为 title 分配更多的权重,这同样可以使用前面介绍过的 ^ 符号语法来实现:
GET /books/_search
{
"query": {
"multi_match": {
"query": "peter smith",
"type": "cross_fields",
"fields": [ "title^2", "description" ] // title 字段的权重为 2
}
}
}
自定义单字段查询是否能够优于多字段查询,取决于在多字段查询与单字段自定义 _all 之间代价的权衡,即哪种解决方案会带来更大的性能优化就选择哪一种。
5. Exact-Value 精确值字段在结束多字段查询这个话题之前,我们最后要讨论的是精确值 not_analyzed 未分析字段。将 not_analyzed 字段与 multi_match 中 analyzed 字段混在一起没有多大用处。
原因可以通过查看查询的 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 字段。



