目录
1.简介
1.1.多重查询字符串
设置子句优先级
1.2.单一查询字符串(Single Query String)
1.3.最佳字段(Best fields)
dis_max查询(Disjuction Max Query)
最佳字段查询的调优
tie_breaker
1.4.多重匹配(multi_match)
在字段名中使用通配符
加权个别字段
1.5.多数字段(Most Fields)
多字段映射(Multifield Mapping)
1.6.跨字段实体搜索(Cross-fields Entity Search)
使用most_fields存在的问题
1.7.以字段为中心的查询(Field-centric Queries)
1.8.全字段查询
1.9.跨域查询(Cross-fields Queries)
逐字段加权(Per-field Boosting)
1.10.精确值字段(Exact-value Fields)
1.简介
1.1.多重查询字符串
【举例】
GET /_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "War and Peace" }},
{ "match": { "author": "Leo Tolstoy" }},
{ "bool": {
"should": [
{ "match": { "translator": "Constance Garnett" }},
{ "match": { "translator": "Louise Maude" }}
]
}}
]
}
}
}
【说明】
布尔查询采用"匹配越多越好(More-matches-is-better)"的方法,所以每个match子句的得分会被加起来变成最后的每个文档的得分。匹配两个子句的文档的得分会比只匹配了一个文档的得分高。
为什么把翻译者的子句放在一个独立的布尔查询中?
所有的匹配查询都是should子句,所以为什么不把翻译者的子句放在和title以及作者的同一级?
答案就在如何计算得分中。
布尔查询执行每个匹配查询,把他们的得分加在一起,然后乘以匹配子句的数量,并且除以子句的总数。
每个同级的子句权重是相同的。
在前面的查询中,包含翻译者的布尔查询占用总得分的三分之一。
如果把翻译者的子句放在和标题与作者同级的目录中,会把标题与作者的作用减少的四分之一。
设置子句优先级
权重
【举例】
GET /_search
{
"query": {
"bool": {
"should": [
{ "match": { // 标题的boost值为2
"title": {
"query": "War and Peace",
"boost": 2
}}},
{ "match": { // 作者的boost值为2
"author": {
"query": "Leo Tolstoy",
"boost": 2
}}},
{ "bool": { // 嵌套的布尔查询的boost值为默认的1
"should": [
{ "match": { "translator": "Constance Garnett" }},
{ "match": { "translator": "Louise Maude" }}
]
}}
]
}
}
}
通过试错(Trial and Error)的方式可以确定"最佳"的boost值:设置一个boost值,执行测试查询,重复这个过程。一个合理boost值的范围在1和10之间,也可能是15。比它更高的值的影响不会起到很大的作用,因为分值会被规范化(Normalized)。
1.2.单一查询字符串(Single Query String)
现在的用户期望能够在一个地方输入所有的搜索词条,然后应用能够知道如何为他们得到正确的结果。
【数据】
① 最佳字段(Best fields)::
当搜索代表某些概念的单词时,例如"brown fox",几个单词合在一起表达出来的意思比单独的单词更多。类似title和body的字段,尽管它们是相关联的,但是也是互相竞争着的。文档在相同的字段中应该有尽可能多的单词,文档的分数应该来自拥有最佳匹配的字段。
② 多数字段(Most fields)::
一个用来调优相关度的常用技术是将相同的数据索引到多个字段中,每个字段拥有自己的分析链(Analysis Chain)。
主要字段会含有单词的词干部分,同义词和消除了变音符号的单词。它用来尽可能多地匹配文档。
相同的文本可以被索引到其它的字段中来提供更加精确的匹配。一个字段或许会包含未被提取成词干的单词,另一个字段是包含了变音符号的单词,第三个字段则使用shingle来提供关于单词邻近度(Word Proximity)的信息。
以上这些额外的字段扮演者signal的角色,用来增加每个匹配的文档的相关度分值。越多的字段被匹配则意味着文档的相关度越高。
③ 跨字段(Cross fields)::
对于一些实体,标识信息会在多个字段中出现,每个字段中只含有一部分信息:
- Person: first_name 和 last_name
1.3.最佳字段(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."
}
[查询]
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
[结果]
{
"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."
}
}
]
}
[计算分值]
- 运行should子句中的两个查询
- 相加查询返回的分值
- 将相加得到的分值乘以匹配的查询子句的数量
- 除以总的查询子句的数量
在这个例子中,title和body字段是互相竞争的。
想要找到一个最佳匹配(Best-matching)的字段。使用最佳匹配字段的分值作为整个查询的整体分值如下
dis_max查询(Disjuction Max Query)
OR
返回匹配了任何查询的文档,并且分值是产生了最佳匹配的查询所对应的分值
【举例】
{
"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."
}
}
]
}
最佳字段查询的调优
【举例】
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
]
}
}
}
【结果】
{
"hits": [
{
"_id": "1",
"_score": 0.12713557,
"_source": {
"title": "Quick brown rabbits",
"body": "Brown rabbits are commonly seen."
}
},
{
"_id": "2",
"_score": 0.12713557,
"_source": {
"title": "Keeping pets healthy",
"body": "My quick brown fox eats rabbits on a regular basis."
}
}
]
}
【分析】
期望的是同时匹配了title字段和body字段的文档能够拥有更高的排名,但是结果并非如此。
需要记住:dis_max查询只是简单的使用最佳匹配查询子句得到的_score。
tie_breaker
【举例】
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
],
"tie_breaker": 0.3
}
}
}
【结果】
{
"hits": [
{
"_id": "2",
"_score": 0.14757764,
"_source": {
"title": "Keeping pets healthy",
"body": "My quick brown fox eats rabbits on a regular basis."
}
},
{
"_id": "1",
"_score": 0.124275915,
"_source": {
"title": "Quick brown rabbits",
"body": "Brown rabbits are commonly seen."
}
}
]
}
【计算过程】
- 取得最佳匹配查询子句的_score
- 将其它每个匹配的子句的分值乘以tie_breaker
- 将以上得到的分值进行累加并规范化
通过tie_breaker参数,所有匹配的子句都会起作用,只不过最佳匹配子句的作用更大。
提示:
tie_breaker的取值范围是0到1之间的浮点数,取0时即为仅使用最佳匹配子句,取1则会将所有匹配的子句一视
同仁。
它的确切值需要根据你的数据和查询进行调整,但是一个合理的值会靠近0,(比如,0.1 -0.4),来确保不会压倒dis_max查询具有的最佳匹配性质。
1.4.多重匹配(multi_match)
对多个字段执行相同的查询
默认情况下,该查询以best_fields类型执行,它会为每个字段生成一个match查询,然后将这些查询包含在一个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",
"fields": [ "title", "body" ],
"tie_breaker": 0.3,
"minimum_should_match": "30%"
}
}
在字段名中使用通配符
【举例】
{
"multi_match": {
"query": "Quick brown fox",
"fields": "*_title"
}
}
加权个别字段
通过caret语法(^)进行加权:仅需要在字段名后添加^boost,其中的boost是一个浮点数
【举例】
{
"multi_match": {
"query": "Quick brown fox",
"fields": [ "*_title", "chapter_title^2" ] <1>
}
}
1.5.多数字段(Most Fields)
全文搜索是一场召回率(Recall) - 返回所有相关的文档,以及准确率(Precision) - 不返回无关文档,之间的战斗。目标是在结果的第一页给用户呈现最相关的文档。
相关:词根,同义词,移除变音符号或者声调符号
多字段映射(Multifield Mapping)
将字段索引两次:一次是提取了词干的形式,一次是未提取词干的形式。
【举例】
DELETE /my_index
PUT /my_index
{
"settings": { "number_of_shards": 1 },
"mappings": {
"my_type": {
"properties": {
"title": { // title字段使用了english解析器进行词干提取。
"type": "string",
"analyzer": "english",
"fields": {
"std": { // title.std字段则使用的是standard解析器,因此它没有进行词干提取。
"type": "string",
"analyzer": "standard"
}
}
}
}
}
}
}
PUT /my_index/my_type/1
{ "title": "My rabbit jumps" }
PUT /my_index/my_type/2
{ "title": "Jumping jack rabbits" }
GET /my_index/_search
{
"query": {
"match": {
"title": "jumping rabbits"
}
}
}
[结果]
{
"hits": [
{
"_id": "1",
"_score": 0.42039964,
"_source": {
"title": "My rabbit jumps"
}
},
{
"_id": "2",
"_score": 0.42039964,
"_source": {
"title": "Jumping jack rabbits"
}
}
]
}
GET /my_index/_search
{
"query": {
"multi_match": {
"query": "jumping rabbits",
"type": "most_fields",
"fields": [ "title", "title.std" ]
}
}
}
[结果]
{
"hits": [
{
"_id": "2",
"_score": 0.8226396,
"_source": {
"title": "Jumping jack rabbits"
}
},
{
"_id": "1",
"_score": 0.10741998, <1>
"_source": {
"title": "My rabbit jumps"
}
}
]
}
GET /my_index/_search
{
"query": {
"multi_match": {
"query": "jumping rabbits",
"type": "most_fields",
"fields": [ "title^10", "title.std" ] // 让title字段的相关性比title.std更重要。
}
}
}
1.6.跨字段实体搜索(Cross-fields Entity Search)
person
{
"firstname": "Peter",
"lastname": "Smith"
}
address
{
"street": "5 Poland Street",
"city": "London",
"country": "United Kingdom",
"postcode": "W1V 3DG"
}
【举例】
{
"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查询进行替代】
{
"query": {
"multi_match": {
"query": "Poland Street W1V",
"type": "most_fields",
"fields": [ "street", "city", "country", "postcode" ]
}
}
}
使用most_fields存在的问题
- 它被设计用来找到匹配任意单词的多数字段,而不是找到跨越所有字段的最匹配的单词。
- 它不能使用operator或者minimum_should_match参数来减少低相关度结果带来的长尾效应。
- 每个字段的词条频度是不同的,会互相干扰最终得到较差的排序结果。
1.7.以字段为中心的查询(Field-centric Queries)
most_fields是以字段为中心(Field-centric),而不是以词条为中心(Term-centric)
① 问题1:在多个字段中匹配相同的单词
考虑一下most_fields查询是如何执行的:ES会为每个字段生成一个match查询,然后将它们包含在一个bool查询中。
GET /_validate/query?explain
{
"query": {
"multi_match": {
"query": "Poland Street W1V",
"type": "most_fields",
"fields": [ "street", "city", "country", "postcode" ]
}
}
}
【产生下面的解释(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)
② 问题2:减少长尾
{
"query": {
"multi_match": {
"query": "Poland Street W1V",
"type": "most_fields",
"operator": "and", // 所有的term必须存在
"fields": [ "street", "city", "country", "postcode" ]
}
}
}
但是,使用best_fields或者most_fields,这些参数会被传递到生成的match查询中。该查询的解释如下
(+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:词条频度
默认用来计算每个词条的相关度分值的相似度算法TF/IDF:
1.词条频度(Term Frequency)::
在一份文档中,一个词条在一个字段中出现的越频繁,文档的相关度就越高。
2.倒排文档频度(Inverse Document Frequency)::
一个词条在索引的所有文档的字段中出现的越频繁,词条的相关度就越低。 当通过多字段进行搜索时,TF/IDF会产生一些令人惊讶的结果。
【举例】
{
"query": {
"multi_match": {
"query": "Peter Smith",
"type": "most_fields",
"fields": [ "*_name" ]
}
}
}
smith在first_name字段中的高IDF值会压倒peter在first_name字段和smith在last_name字段中的两个低IDF值。实际上不是想要的结果。
【解决方案】
这个问题仅在处理多字段时存在。如果将所有这些字段合并到一个字段中,该问题就不复存在了。
{
"first_name": "Peter",
"last_name": "Smith",
"full_name": "Peter Smith" // 新增
}
[解释]
当只查询full_name字段时:
- 拥有更多匹配单词的文档会胜过那些重复出现一个单词的文档。
- minimum_should_match和operator参数能够正常工作。
- first_name和last_name的倒排文档频度会被合并,因此smith无论是first_name还是last_name都不再重要。 但是数据冗余。
1.8.全字段查询
在元数据:特殊的_all字段会将其它所有字段中的值作为一个大字符串进行索引。尽管将所有字段的值作为一个字段进行索引并不是非常灵活。如果有一个自定义的_all字段用来索引人名,另外一个自定义的_all字段用来索引地址就更好了。
PUT /my_index
{
"mappings": {
"person": {
"properties": {
"first_name": {
"type": "string",
"copy_to": "full_name" // first_name字段中的值会被拷贝到full_name字段中。
},
"last_name": {
"type": "string",
"copy_to": "full_name" // last_name字段中的值会被拷贝到full_name字段中。
},
"full_name": {
"type": "string"
}
}
}
}
}
这个映射,可以通过first_name字段查询名字,last_name字段查询姓氏,或者full_name字段查询姓氏和名字。
提示:
first_name和last_name字段的映射和full_name字段的索引方式的无关。
full_name字段会从其它两个字段中拷贝字符串的值,然后仅根据full_name字段自身的映射进行索引。
1.9.跨域查询(Cross-fields Queries)
如果你在索引文档前就能够自定义_all字段的话,那么使用_all字段就是一个不错的方法。
但是,ES同时也提供了一个搜索期间的解决方案:使用类型为cross_fields的multi_match查询。
cross_fields类型采用了一种以词条为中心(Term-centric)的方法,这种方法和best_fields及most_fields采用的以字段为中心(Field-centric)的方法有很大的区别。它将所有的字段视为一个大的字段,然后在任一字段中搜索每个词条。
【举例】
GET /_validate/query?explain
{
"query": {
"multi_match": {
"query": "peter smith",
"type": "most_fields",
"operator": "and", // 表示所有的词条都需要出现
"fields": [ "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)
cross_fields类型首先会解析查询字符串来得到一个词条列表,然后在任一字段中搜索每个词条。仅这个区别就能够解决在以字段为中心的查询中提到的3个问题中的2个,只剩下倒排文档频度的不同这一问题。
【举例】
GET /_validate/query?explain
{
"query": {
"multi_match": {
"query": "peter smith",
"type": "cross_fields", // 以词条为中心(Term-centric)进行匹配
"operator": "and",
"fields": [ "first_name", "last_name" ]
}
}
}
通过混合(Blending)字段的倒排文档频度来解决词条频度的问题:
+blended("peter", fields: [first_name, last_name])
+blended("smith", fields: [first_name, last_name])
【解释】
它会查找词条smith在first_name和last_name字段中的IDF值,然后使用两者中较小的作为两个字段最终的IDF值。因为smith是一个常见的姓氏,意味着它也会被当做一个常见的名字。
提示:
为了让cross_fields查询类型能以最佳的方式工作,所有的字段都需要使用相同的解析器。
使用了相同的解析器的字段会被组合在一起形成混合字段(Blended Fields)。
如果你包含了使用不同解析链(Analysis Chain)的字段,它们会以和best_fields相同的方式被添加到查询中。
(+title:peter +title:smith)
(
+blended("peter", fields: [first_name, last_name])
+blended("smith", fields: [first_name, last_name])
)
逐字段加权(Per-field Boosting)
使用cross_fields查询相比使用自定义_all字段的一个优点是你能够在查询期间对个别字段进行加权。
【举例】
GET /books/_search
{
"query": {
"multi_match": {
"query": "peter smith",
"type": "cross_fields",
"fields": [ "title^2", "description" ]
}
}
}
【解释】
能够对个别字段进行加权带来的优势应该和对多个字段执行查询伴随的代价进行权衡,因为如果使用自定义的_all字段,那么只需要要对一个字段进行查询。选择能够给你带来最大收益的方案。
1.10.精确值字段(Exact-value Fields)
作为not_analyzed类型的精确值字段。
在multi_match查询中将not_analyzed字段混合到analyzed字段中是没有益处的。
【举例】
GET /_validate/query?explain
{
"query": {
"multi_match": {
"query": "peter smith",
"type": "cross_fields",
"fields": [ "title", "first_name", "last_name" ]
}
}
}
【结果】
title:peter smith
(
blended("peter", fields: [first_name, last_name])
blended("smith", fields: [first_name, last_name])
)
该词条在title字段的倒排索引中并不存在,因此永远不可能被找到。在multi_match查询中避免使用not_analyzed字段。