Elasticsearch:使用布尔查询提高搜索的相关性

当你在Elasticsearch中执行搜索时,将对结果进行排序,以便与你的查询相关的文档排名很高。 但是,可以认为与一个应用程序相关的结果肯针对另一应用程序就没有那么相关。 由于 Elasticsearch 超级灵活,因此可以对其进行微调以提供针对你特定用例的最相关的搜索结果。 调整结果的一种相对直接的方法是在发送给 Elasticsearch 的查询中提供其额外的条件查询。

在本博客中,我将向你简要介绍一些示例,以向您展示如何轻松地使用布尔查询 (bool query) 功能以及匹配查询(match queries)和匹配短语查询(match phrases queries)来提高搜索相关性。在开始之前,你可以参阅我之前的文章“Elastic:菜鸟上手指南”来启动自己的 Elasticsearch 集群。
 

在  Elasticsearch 中创建示例文档

为了演示此博客中的概念,我们首先将几个文档编入Elasticsearch。 在整个博客中将查询这些文档以演示各种概念。 我们的演示文档可以按以下方式写入Elasticsearch:

POST _bulk
{ "index" : { "_index" : "demo_idx", "_id": 1} }
{"content":"Distributed nature, simple REST APIs, speed, and scalability"}
{ "index" : { "_index" : "demo_idx", "_id": 2} }
{"content":"Distributed nature, simple APIs, speed, and scalability"}
{ "index" : { "_index" : "demo_idx", "_id": 3} }
{"content":"Known for its simple REST APIs, distributed nature, speed, and scalability, Elasticsearch is the central component of the Elastic Stack, a set of open source tools for data ingestion, enrichment, storage, analysis, and visualization."}

我们在 Kibana 的 Dev tools 中输入如上的命令。这样我们就生成我们所需要的文档。我们尽量使用少量的文档,这样我们可以更容易看清搜索的本质。现在我们有一些数据可以使用。 完成本教程后,你将能够将这些相同的技术应用于更大的数据集,但现在,我们将使其保持简单。

在Elasticsearch中如何对文档进行排名

为了了解本博客的其余部分,对Elasticsearch如何计算用于对查询返回的文档进行排序的分数有一个基本的了解会很有帮助。

在为文档评分之前,Elasticsearch首先通过应用布尔测试来减少候选文档的集合,该布尔测试仅包括与查询匹配的文档。然后为该集中的每个文档计算一个分数,该分数确定如何对文档进行排名。分数表示给定文档与特定查询的相关性。 Elasticsearch使用的默认评分算法是 BM25。决定文档得分的三个主要因素:

  • 字词频率(TF)-搜索字词在我们正在文档中搜索的字段中出现的次数越多,则该文档越相关。
  • 反向文档频率(IDF)-在我们要搜索的字段中包含搜索词的文档越多,该词的重要性就越低。
  • 字段长度-如果文档在非常短的字段(即,只有几个单词)中包含搜索词,则比文档在较长的字段(即,包含很多单词)中包含搜索词更相关。

如果你想了解更多关于文档排名的知识,请参阅我之前的文章 “Elasticsearch:分布式计分”。

 

一个最基本的 match query

基本的匹配查询通常用于执行全文搜索。 默认情况下,具有多个术语的匹配查询将使用OR运算符,该运算符将返回与查询中的任何术语匹配的文档。 即使某些匹配的文档可能只是略微相关,这也可能导致许多文档被匹配。 对我们刚刚建立索引的文档中的content 字段进行搜索将类似于以下内容:

GET demo_idx/_search
{
  "query": {
    "match": {
      "content": {
        "query": "simple rest apis distributed nature"
      }
    }
  }
}

上面的查询将被解释为:simple OR rest OR apis OR distributed OR nature。当我们执行上述查询时,将返回以下结果:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.2689934,
    "hits" : [
      {
        "_index" : "demo_idx",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.2689934,
        "_source" : {
          "content" : "Distributed nature, simple REST APIs, speed, and scalability"
        }
      },
      {
        "_index" : "demo_idx",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.6970792,
        "_source" : {
          "content" : "Distributed nature, simple APIs, speed, and scalability"
        }
      },
      {
        "_index" : "demo_idx",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.69611007,
        "_source" : {
          "content" : "Known for its simple REST APIs, distributed nature, speed, and scalability, Elasticsearch is the central component of the Elastic Stack, a set of open source tools for data ingestion, enrichment, storage, analysis, and visualization."
        }
      }
    ]
  }
}

从上面的搜索结果我们可以看出来,所有的三个文档都被搜索到了。

在许多情况下,上述排序可能正是所需要的。在其他情况下,可能需要进行其他调整。不同等级的可接受性将取决于给定应用程序的特定要求。

  • 第一个命中非常好-它包含了我们搜索过的所有单词,尽管不是按照我们输入的顺序。
  • 第二个命中是不错的选择,但请注意,它缺少 “rest”一词,并且其顺序与我们搜索的顺序不同。
  • 最后,对于某些用例,第三次匹配可以被认为是很好的匹配,因为它包含了我们按照​​键入它们的确切顺序搜索的所有单词。

第三个匹配项的排名不高于前两个匹配项的原因是由于以下原因:

  1. 使用OR运算符的匹配查询不考虑单词的位置。因此,即使第三个匹配(_id:3)包含搜索文本,并且它包含了搜索所有词的顺序,但这不会影响得分。
  2. 第三匹配包含比其他匹配更长的内容字段。因此,计分算法的字段长度部分(有利于较短的字段)导致得分较低。在此示例中,由于其较长的内容字段导致的第三次匹配(_id:3)的得分下降大于第二次匹配(_id:2)的得分 (因其缺少 “rest” 一词引起的)。

让我们看看如果在匹配查询中使用AND运算符会发生什么。

 

在 match query 中使用 AND 算子

通过在匹配查询中使用AND运算符可以使搜索更加具体。 这只会返回包含所有搜索词的文档。 对于给定的查询,与使用OR运算符的匹配查询相比,AND运算符返回的文档更少。 这意味着结果集可能会丢失一些用户可能认为相关的文档。 针对我们索引中的content 字段的AND搜索如下所示: 

GET demo_idx/_search
{
  "query": {
    "match": {
      "content": {
        "query": "simple rest apis distributed nature",
        "operator": "and"
      }
    }
  }
}

上面的查询将被解释为:simple AND rest AND apis AND distributed AND nature。当我们执行上述查询时,将返回以下结果:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.2689934,
    "hits" : [
      {
        "_index" : "demo_idx",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.2689934,
        "_source" : {
          "content" : "Distributed nature, simple REST APIs, speed, and scalability"
        }
      },
      {
        "_index" : "demo_idx",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.69611007,
        "_source" : {
          "content" : "Known for its simple REST APIs, distributed nature, speed, and scalability, Elasticsearch is the central component of the Elastic Stack, a set of open source tools for data ingestion, enrichment, storage, analysis, and visualization."
        }
      }
    ]
  }
}

该查询仅返回了两次匹配,并排除了我们提取的第二个文档(_id:2)。 这是因为第二个文档在其内容字段中不包含单词“ rest”,这是满足AND条件所必需的。 现在,我们得到了更准确的结果,但是我们删除了可能相关的匹配。

可以认为第二个匹配项(_id:3)比第一个匹配项(_id:1)更相关,因为它按输入的确切顺序包含搜索词。 但是,就像OR运算符一样,AND运算符不考虑项的位置。 此外,由于第二个匹配的文本字段比第一个匹配的文本字段长,因此评分算法的字段长度部分(有利于较短的字段)导致得分较低。

让我们看看如果使用匹配短语查询会发生什么。

 

match_phrase query

通过使用匹配短语查询可以获得更准确的结果,该查询仅返回与用户搜索的短语完全匹配的文档。 这比使用AND运算符的匹配查询更加严格,因此返回的文档数少于以上两个查询中的任何一个。 针对文档内容字段的匹配词组查询将类似于以下内容:

GET demo_idx/_search
{
  "query": {
    "match_phrase": {
      "content": {
        "query": "simple rest apis distributed nature"
      }
    }
  }
}

上面的查询将匹配包含短语的文档:"simple rest apis distributed nature"。换句话说,上述查询将只返回包含与搜索顺序相同的所有单词的文档。 执行上述查询将返回以下结果。

{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.6961101,
    "hits" : [
      {
        "_index" : "demo_idx",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.6961101,
        "_source" : {
          "content" : "Known for its simple REST APIs, distributed nature, speed, and scalability, Elasticsearch is the central component of the Elastic Stack, a set of open source tools for data ingestion, enrichment, storage, analysis, and visualization."
        }
      }
    ]
  }
}

请注意,此查询仅返回一个匹配。 现在,我们获得了一个非常具体的结果,该结果与用户搜索的内容完全匹配,但这是以不返回可能相关的其他文档为代价的。

上面的解决方案都可能无法为我们提供所需的结果。 本博客的其余部分重点介绍如何通过将上述所有查询组合为一个查询来获得更多相关的搜索结果。

 

结合 OR,AND 及 match_phrase query

我们可能希望精确匹配在搜索结果中排名较高,但也可能希望查看结果中相关性较低的文档。 下面我们展示如何在布尔查询(boolean query)中使用should子句来组合OR,AND和match短语查询,以帮助我们满足需求。 布尔查询中的should子句采用更好匹配的方法,因此每个子句的得分将为每个文档的最终_score做出贡献。

先前的搜索可以组合为单个should子句,如下所示:

GET demo_idx/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "content": {
              "query": "simple rest apis distributed nature"
            }
          }
        },
        {
          "match": {
            "content": {
              "query": "simple rest apis distributed nature",
              "operator": "and"
            }
          }
        },
        {
          "match_phrase": {
            "content": {
              "query": "simple rest apis distributed nature"
            }
          }
        }
      ]
    }
  }
}

上面的查询将评估每个应当子句,并增加每个匹配子句的得分。 match query 查询匹配的任何文档(根据定义)也将匹配AND和OR匹配查询。 同样,任何与AND匹配的文档(根据定义)也将与OR查询匹配。 因此,我们可以预期,与我们搜索的 phrase_match 的文档现在将比与短语匹配的文档更高。 但是,上面的查询将返回以下结果,这可能与我们预期的不完全相同:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 2.5379868,
    "hits" : [
      {
        "_index" : "demo_idx",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 2.5379868,
        "_source" : {
          "content" : "Distributed nature, simple REST APIs, speed, and scalability"
        }
      },
      {
        "_index" : "demo_idx",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 2.0883303,
        "_source" : {
          "content" : "Known for its simple REST APIs, distributed nature, speed, and scalability, Elasticsearch is the central component of the Elastic Stack, a set of open source tools for data ingestion, enrichment, storage, analysis, and visualization."
        }
      },
      {
        "_index" : "demo_idx",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.6970792,
        "_source" : {
          "content" : "Distributed nature, simple APIs, speed, and scalability"
        }
      }
    ]
  }
}

这是相当不错的,但是我们可能并不认为它是完美的。我们获得了所有相关文档的命中,但是命中的排序不完全符合我们的预期。我们可能期望第二个匹配(_id:3)排名第一。毕竟,第二个匹配项与我们搜索的词组完全匹配(因此匹配了所有应该子句),而第一个匹配项(_id:1)仅与AND和OR子句匹配。为什么第二个匹配(_id:3)没有排名第一?

这些文档按此顺序排序,因为第二个匹配(_id:3)的内容字段比其他匹配更长,因此,每个should子句(OR,AND和match短语)赋予该文档的分数具有由于计分算法的字段长度分量的影响而成比例减少。在这种情况下,由于成功的匹配短语从句而增加的分数不足以抵消分数中字段长度的减少。

如果我们真的想确保在其他匹配项之前显示完全匹配项,则可以按照下一部分中的说明增强单个子句。

 

增强个别 clause

可以在单个子句中添加 boost 功能以使其更加重要。 在我们的案例中,我们希望增强匹配(match_phrase)短语子句,以确保首先返回与我们要搜索的短语完全匹配的文档。 这可以通过以下查询完成:

GET demo_idx/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "content": {
              "query": "simple rest apis distributed nature"
            }
          }
        },
        {
          "match": {
            "content": {
              "query": "simple rest apis distributed nature",
              "operator": "and"
            }
          }
        },
        {
          "match_phrase": {
            "content": {
              "query": "simple rest apis distributed nature",
              "boost": 2
            }
          }
        }
      ]
    }
  }
}

执行上述查询后,我们得到的结果如下所示:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 2.7844405,
    "hits" : [
      {
        "_index" : "demo_idx",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 2.7844405,
        "_source" : {
          "content" : "Known for its simple REST APIs, distributed nature, speed, and scalability, Elasticsearch is the central component of the Elastic Stack, a set of open source tools for data ingestion, enrichment, storage, analysis, and visualization."
        }
      },
      {
        "_index" : "demo_idx",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 2.5379868,
        "_source" : {
          "content" : "Distributed nature, simple REST APIs, speed, and scalability"
        }
      },
      {
        "_index" : "demo_idx",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.6970792,
        "_source" : {
          "content" : "Distributed nature, simple APIs, speed, and scalability"
        }
      }
    ]
  }
}

现在,我们已经按照所需的顺序收到了结果。 包含我们搜索的确切短语的文档是第一匹配。 此外,我们还收到了其他相关性较低的文档,其结果在下拉列表中有所显示。

 

使用 search 模板

上面的查询越来越大。 通过使用搜索模板,可以简化大型或复杂查询的管理。 上述查询的搜索模板如下所示:

POST _scripts/demo_search_template
{
  "script": {
    "lang": "mustache",
    "source": {
      "query": {
        "bool": {
          "should": [
            {
              "match": {
                "content": {
                  "query": "{{query_string}}"
                }
              }
            },
            {
              "match": {
                "content": {
                  "query": "{{query_string}}",
                  "operator": "and"
                }
              }
            },
            {
              "match_phrase": {
                "content": {
                  "query": "{{query_string}}",
                  "boost": 2
                }
              }
            }
          ]
        }
      }
    }
  }
}

可以通过以下调用执行以上搜索模板:

GET _search/template
{
    "id": "demo_search_template", 
    "params": {
        "query_string": "simple rest apis distributed nature"
    }
}

它将返回与我们之前收到的结果完全相同的结果。

 

查看分数计算的详细信息

Elasticsearch提供了一个解释性API和一个解释性查询参数,以了解如何计算分数。 例如,可以使用我们的基本匹配(OR)查询执行说明,如下所示:

GET demo_idx/_search
{
  "explain": true,
  "query": {
    "match": {
      "content": {
        "query": "simple rest apis distributed nature"
      }
    }
  }
}

这将返回一个大而详细的响应,显示针对每个匹配文档计算出的分数的各个组成部分。 但是,对响应的分析超出了本博客文章的范围。

 

其他相关调整资源

为了更严格地评估搜索结果的质量,排名评估API可能会有所帮助。 此外,如Elasticsearch 7.0博客中的“更简单的相关性调整”中所述,可以实现更多的自定义相关性评分。

 

例子项目

可在ES Local Indexer项目中找到此博客中提出的概念的演示。 这是一个简单的基于Python的桌面搜索应用程序,它将html文档索引到Elasticsearch中,并提供了一个基于浏览器的界面来搜索和分页提取的文档。 与项目特别相关的是搜索主体,它演示了此博客中讨论的许多概念,还演示了跨多个字段进行搜索的复杂布尔查询。