多字段搜索
只有一个简单的match
子句的查询是很少的。我们经常需要在一个或者多个字段中查询相同的或者不同的查询字符串,意味着我们需要能够组合多个查询子句以及使他们的相关性得分有意义。
或许我们在寻找列夫·托尔斯泰写的一本叫《战争与和平》的书。或许我们在Elasticsearch的文档中查找minimum should match
,它可能在标题中,或者在一页的正文中。或许我们查找名为John,姓为Smith的人。
在这一章节,我们会介绍用于构建多个查询子句搜索的可能的工具,以及怎么样选择解决方案来应用到你特殊的场景。
多重查询字符串
在明确的字段中的词查询是最容易处理的多字段查询。如果我们知道War and Peace是标题,Leo Tolstoy是作者,可以很容易的用match查询表达每个条件,并且用布尔查询组合起来:
GET /_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "War and Peace" }},
{ "match": { "author": "Leo Tolstoy" }}
]
}
}
}
布尔查询采用”匹配越多越好(More-matches-is-better)”的方法,所以每个match子句的得分会被加起来变成最后的每个文档的得分。匹配两个子句的文档的得分会比只匹配了一个文档的得分高。
当然,没有限制你只能使用match子句:布尔查询可以包装任何其他的查询类型,包含其他的布尔查询,我们可以添加一个子句来指定我们更喜欢看被哪个特殊的翻译者翻译的那版书:
GET /_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "War and Peace" }},
{ "match": { "author": "Leo Tolstoy" }},
{ "bool": {
"should": [
{ "match": { "translator": "Constance Garnett" }},
{ "match": { "translator": "Louise Maude" }}
]
}}
]
}
}
}
为什么我们把翻译者的子句放在一个独立的布尔查询中?所有的匹配查询都是should子句,所以为什么不把翻译者的子句放在和title以及作者的同一级?
答案就在如何计算得分中。布尔查询执行每个匹配查询,把他们的得分加在一起,然后乘以匹配子句的数量,并且除以子句的总数。每个同级的子句权重是相同的。在前面的查询中,包含翻译者的布尔查询占用总得分的三分之一。如果我们把翻译者的子句放在和标题与作者同级的目录中,我们会把标题与作者的作用减少的四分之一。
设置子句优先级
在先前的查询中我们可能不需要使每个子句都占用三分之一的权重。我们可能对标题以及作者比翻译者更感兴趣。我们需要调整查询来使得标题和作者的子句相关性更重要。
最简单的方法是使用boost参数。为了提高标题和作者字段的权重,我们给boost参数提供一个比1高的值:
GET /_search
{
"query": {
"bool": {
"should": [
{ "match": { <1>
"title": {
"query": "War and Peace",
"boost": 2
}}},
{ "match": { <1>
"author": {
"query": "Leo Tolstoy",
"boost": 2
}}},
{ "bool": { <2>
"should": [
{ "match": { "translator": "Constance Garnett" }},
{ "match": { "translator": "Louise Maude" }}
]
}}
]
}
}
}
<1> 标题和作者的boost值为2。
<2> 嵌套的布尔查询的boost值为默认的1。
通过试错(Trial and Error)的方式可以确定”最佳”的boost值:设置一个boost值,执行测试查询,重复这个过程。一个合理boost值的范围在1和10之间,也可能是15。比它更高的值的影响不会起到很大的作用,因为分值会被规范化(Normalized)。
[source,js]
GET /_search { "query": { "bool": { "should": [ { "match": { "title": "War and Peace" }}, { "match": { "author": "Leo Tolstoy" }} ] } }}
// SENSE: 110_Multi_Field_Search/05_Multiple_query_strings.json The `bool` query takes a _more-matches-is-better_ approach, so the score from each `match` clause will be added together to provide the final `_score` for each document. Documents that match both clauses will score higher than documents that match just one clause. Of course, you're not restricted to using just `match` clauses: the `bool` query can wrap any other query type, ((("bool query", "nested bool query in")))including other `bool` queries. We could add a clause to specify that we prefer to see versions of the book that have been translated by specific translators:[source,js]
GET /_search { "query": { "bool": { "should": [ { "match": { "title": "War and Peace" }}, { "match": { "author": "Leo Tolstoy" }}, { "bool": { "should": [ { "match": { "translator": "Constance Garnett" }}, { "match": { "translator": "Louise Maude" }} ] }} ] } }}
// SENSE: 110_Multi_Field_Search/05_Multiple_query_strings.json Why did we put the translator clauses inside a separate `bool` query? All four `match` queries are `should` clauses, so why didn't we just put the translator clauses at the same level as the title and author clauses? The answer lies in how the score is calculated.((("relevance scores", "calculation in bool queries"))) The `bool` query runs each `match` query, adds their scores together, then multiplies by the number of matching clauses, and divides by the total number of clauses. Each clause at the same level has the same weight. In the preceding query, the `bool` query containing the translator clauses counts for one-third of the total score. If we had put the translator clauses at the same level as title and author, they would have reduced the contribution of the title and author clauses to one-quarter each. [[prioritising-clauses]] ==== Prioritizing Clauses It is likely that an even one-third split between clauses is not what we need for the preceding query. ((("multifield search", "multiple query strings", "prioritizing query clauses")))((("bool query", "prioritizing clauses"))) Probably we're more interested in the title and author clauses then we are in the translator clauses. We need to tune the query to make the title and author clauses relatively more important. The simplest weapon in our tuning arsenal is the `boost` parameter. To increase the weight of the `title` and `author` fields, give ((("boost parameter", "using to prioritize query clauses")))((("weight", "using boost parameter to prioritize query clauses")))them a `boost` value higher than `1`:[source,js]
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" }} ] }} ] } }}
// SENSE: 110_Multi_Field_Search/05_Multiple_query_strings.json The `title` and `author` clauses have a `boost` value of `2`. The nested `bool` clause has the default `boost` of `1`. The ``best'' value for the `boost` parameter is most easily determined by trial and error: set a `boost` value, run test queries, repeat. A reasonable range for `boost` lies between `1` and `10`, maybe `15`. Boosts higher than that have little more impact because scores are单一查询字符串(Single Query String)
bool查询是多字段查询的中流砥柱。在很多场合下它都能很好地工作,特别是当你能够将不同的查询字符串映射到不同的字段时。
问题在于,现在的用户期望能够在一个地方输入所有的搜索词条,然后应用能够知道如何为他们得到正确的结果。所以当我们把含有多个字段的搜索表单称为高级搜索(Advanced Search)时,是有一些讽刺意味的。高级搜索虽然对用户而言会显得更”高级”,但是实际上它的实现方式更简单。
对于多词,多字段查询并没有一种万能(one-size-fits-all)的方法。要得到最佳的结果,你需要了解你的数据以及如何使用恰当的工具。
了解你的数据
当用户的唯一输入就是一个查询字符串时,你会经常碰到以下三种情况:
1.最佳字段(Best fields)::
当搜索代表某些概念的单词时,例如”brown fox”,几个单词合在一起表达出来的意思比单独的单词更多。类似title和body的字段,尽管它们是相关联的,但是也是互相竞争着的。文档在相同的字段中应该有尽可能多的单词(译注:搜索的目标单词),文档的分数应该来自拥有最佳匹配的字段。
2.多数字段(Most fields)::
一个用来调优相关度的常用技术是将相同的数据索引到多个字段中,每个字段拥有自己的分析链(Analysis Chain)。
主要字段会含有单词的词干部分,同义词和消除了变音符号的单词。它用来尽可能多地匹配文档。
相同的文本可以被索引到其它的字段中来提供更加精确的匹配。一个字段或许会包含未被提取成词干的单词,另一个字段是包含了变音符号的单词,第三个字段则使用shingle来提供关于单词邻近度(Word Proximity)的信息。
以上这些额外的字段扮演者signal的角色,用来增加每个匹配的文档的相关度分值。越多的字段被匹配则意味着文档的相关度越高。
3.跨字段(Cross fields)::
对于一些实体,标识信息会在多个字段中出现,每个字段中只含有一部分信息:
- Person:
first_name
和last_name
- Book:
title
,author
, 和description
- Address:
street
,city
,country
, 和postcode
此时,我们希望在任意字段中找到尽可能多的单词。我们需要在多个字段中进行查询,就好像这些字段是一个字段那样。
以上这些都是多词,多字段查询,但是每种都需要使用不同的策略。我们会在本章剩下的部分解释每种策略。
+
A common technique for fine-tuning relevance is to index the same data into multiple fields, each with its own analysis chain. The main field may contain words in their stemmed form, synonyms, and words stripped of their _diacritics_, or accents. It is used to match as many documents as possible. The same text could then be indexed in other fields to provide more-precise matching. One field may contain the unstemmed version, another the original word with accents, and a third might use _shingles_ to provide information aboutmatching document. The more fields that match, the better.
Cross fields::+
For some entities, the identifying information is spread across multiple fields, each of which contains just a part of the whole:- Person:
first_name
andlast_name
- Book:
title
,author
, anddescription
- Address:
street
,city
,country
, andpostcode
field.
All of these are multiword, multifield queries, but each requires a different strategy. We will examine each strategy in turn in the rest of this chapter. -->最佳字段
假设我们有一个让用户搜索博客文章的网站(允许多字段搜索,最佳字段查询),就像这两份文档一样:
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."
}
// SENSE: 110_Multi_Field_Search/15_Best_fields.json
用户输入了”Brown fox”,然后按下了搜索键。我们无法预先知道用户搜索的词条会出现在博文的title或者body字段中,但是用户是在搜索和他输入的单词相关的内容。右眼观察,以上的两份文档中,文档2似乎匹配的更好一些,因为它包含了用户寻找的两个单词。
让我们运行下面的bool查询:
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
// SENSE: 110_Multi_Field_Search/15_Best_fields.json
然后我们发现文档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.除以总的查询子句的数量
文档1在两个字段中都包含了brown,因此两个match查询都匹配成功并拥有了一个分值。文档2在body字段中包含了brown以及fox,但是在title字段中没有出现任何搜索的单词。因此对body字段查询得到的高分加上对title字段查询得到的零分,然后在乘以匹配的查询子句数量1,最后除以总的查询子句数量2,导致整体分值比文档1的低。
在这个例子中,title和body字段是互相竞争的。我们想要找到一个最佳匹配(Best-matching)的字段。
如果我们不是合并来自每个字段的分值,而是使用最佳匹配字段的分值作为整个查询的整体分值呢?这就会让包含有我们寻找的两个单词的字段有更高的权重,而不是在不同的字段中重复出现的相同单词。
dis_max查询
相比使用bool查询,我们可以使用dis_max查询(Disjuction Max Query)。Disjuction的意思”OR”(而Conjunction的意思是”AND”),因此Disjuction Max Query的意思就是返回匹配了任何查询的文档,并且分值是产生了最佳匹配的查询所对应的分值:
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Brown fox" }},
{ "match": { "