Elasticsearch开发:如何提高跨索引搜索相关性,同时返回更多相关文档
在Elasticsearch搜索中,我们经常会遇到跨多个索引创建数据视图或索引模式的情况,以便可以进行统一搜索。我们遇到了一种情况,完全匹配的文档的分数低于部分匹配的文档的分数。这是为什么?
示例展示
示例一
我们先看下面的例子:
POST /_bulk
{"index": {"_index": "my_index"}}
{"name": "Vincent van Gogh"}
{"index": {"_index": "my_index"}}
{"name": "Rembrandt van Rijn"}
{"index": {"_index": "my_index"}}
{"name": "Frans Hals"}
{"index": {"_index": "my_index"}}
{"name": "Johann Adam Ackermann"}
{"index": {"_index": "my_index"}}
{"name": "Piet Mondriaan"}
{"index": {"_index": "my_index"}}
{"name": "Claude Monet"}
{"index": {"_index": "my_index"}}
{"name": "Jackson Pollock"}
{"index": {"_index": "my_index"}}
{"name": "Andy Warhol"}
{"index": {"_index": "my_index"}}
{"name": "Frida Kahlo"}
{"index": {"_index": "my_index"}}
{"name": "Johannes Vermeer"}
{"index": {"_index": "my_index"}}
{"name": "Leonardo da Vinci"}
{"index": {"_index": "my_index"}}
{"name": "Pieter Breugel"}
{"index": {"_index": "my_index"}}
{"name": "Johann Sebastian Bach"}
{"index": {"_index": "my_index"}}
{"name": "Johann Christoph Bach"}
{"index": {"_index": "my_index"}}
{"name": "Johann Ambrosius Bach"}
{"index": {"_index": "my_index"}}
{"name": "Clara Schumann"}
在上面的索引my_index中,我把所有的文档都放到了这个索引中。我们进行以下搜索:
GET my_index/_search?filter_path=**.hits
{
"query": {
"match": {
"name": "johann sebastian bach"
}
}
}
此处搜索的“johann sebastian bach”是其中一份文档中的名称。上面搜索返回的结果是:
{
"hits": {
"hits": [
{
"_index": "my_index",
"_id": "gRoPQ4YB2XodIZsbxfzo",
"_score": 4.8769255,
"_source": {
"name": "Johann Sebastian Bach"
}
},
{
"_index": "my_index",
"_id": "ghoPQ4YB2XodIZsbxfzo",
"_score": 2.6585994,
"_source": {
"name": "Johann Christoph Bach"
}
},
{
"_index": "my_index",
"_id": "gxoPQ4YB2XodIZsbxfzo",
"_score": 2.6585994,
"_source": {
"name": "Johann Ambrosius Bach"
}
},
{
"_index": "my_index",
"_id": "eBoPQ4YB2XodIZsbxfzo",
"_score": 1.214482,
"_source": {
"name": "Johann Adam Ackermann"
}
}
]
}
}
显然,这就是我们想要的结果。带有“johann sebastian bach”的文档位于第一位。这完全符合我们的搜索习惯,因为这个搜索结果最符合我们的搜索内容。
示例2
我们现在用不同的方法来演示。这次我们不把上面的文档写在同一个索引中,而是分别写在两个索引中:
POST /_bulk
{"index": {"_index": "painters"}}
{"name": "Vincent van Gogh"}
{"index": {"_index": "painters"}}
{"name": "Rembrandt van Rijn"}
{"index": {"_index": "painters"}}
{"name": "Frans Hals"}
{"index": {"_index": "painters"}}
{"name": "Johann Adam Ackermann"}
{"index": {"_index": "painters"}}
{"name": "Piet Mondriaan"}
{"index": {"_index": "painters"}}
{"name": "Claude Monet"}
{"index": {"_index": "painters"}}
{"name": "Jackson Pollock"}
{"index": {"_index": "painters"}}
{"name": "Andy Warhol"}
{"index": {"_index": "painters"}}
{"name": "Frida Kahlo"}
{"index": {"_index": "painters"}}
{"name": "Johannes Vermeer"}
{"index": {"_index": "painters"}}
{"name": "Leonardo da Vinci"}
{"index": {"_index": "painters"}}
{"name": "Pieter Breugel"}
{"index": {"_index": "composers"}}
{"name": "Johann Sebastian Bach"}
{"index": {"_index": "composers"}}
{"name": "Johann Christoph Bach"}
{"index": {"_index": "composers"}}
{"name": "Johann Ambrosius Bach"}
{"index": {"_index": "composers"}}
{"name": "Clara Schumann"}
如上图,我们将第一部分文档写在painter中,第二部分后面的文档写在作曲家索引。
现在为了说明问题,我们来搜索一个名叫“Johann Sebastian Bach”的人:
GET /painters,composers/_search?filter_path=**.hits
{
"query": {
"match": {
"name": "johann sebastian bach"
}
}
}
上面的搜索结果是:
{
"hits": {
"hits": [
{
"_index": "painters",
"_id": "uBoWQ4YB2XodIZsbJfyz",
"_score": 1.9334917,
"_source": {
"name": "Johann Adam Ackermann"
}
},
{
"_index": "composers",
"_id": "wRoWQ4YB2XodIZsbJfyz",
"_score": 1.8485742,
"_source": {
"name": "Johann Sebastian Bach"
}
},
{
"_index": "composers",
"_id": "whoWQ4YB2XodIZsbJfyz",
"_score": 0.6877716,
"_source": {
"name": "Johann Christoph Bach"
}
},
{
"_index": "composers",
"_id": "wxoWQ4YB2XodIZsbJfyz",
"_score": 0.6877716,
"_source": {
"name": "Johann Ambrosius Bach"
}
}
]
}
}
从上面的搜索结果中我们可以看到第 1 名是“Johann Adam Ackermann”,第二名是我们真正想要的结果是“Johann Sebastian Bach”。这完全强调了我们对任务的理解。哇,这是怎么回事?与我们的预期相反,巴赫并不是最热门的搜索结果。尽管我们的作曲家索引中存在“Johann Sebastian Bach”(分数约为 1.8485742)的字面匹配,但画家“Johann Adam Ackermann”仅匹配我们搜索查询的一小部分,并且分数更高(〜1.9334917)!
请解释一下。
一如既往,我们可以向 Elasticsearch 寻求解释:
GET /painters,composers/_search
{
"explain": true,
"query": {
"match": {
"name": "johann sebastian bach"
}
}
}
这为我们提供了有关正在发生的事情的线索:与我们人类不同,Elasticsearch 不知道“Johann Sebastian Bach”这个名字。一个连贯的单元,因此它会单独查找每个术语。
- 首先,分词器将查询拆分为三个单词:johann OR sebastian OR bach。
- Elasticsearch 然后单独搜索每个术语。
- 最后,它通过合并每个术语的分数来计算总分。
因此 Elasticsearch 默认运行 OR 查询。也就是说,至少一个搜索词必须匹配,但不一定是全部。这解释了为什么 Johann Adam Ackermann 包含在结果中,尽管只有一个单词(“Johann”)与我们的查询匹配。
逆文档频率在工作中
如果您不清楚逆文档频率,请阅读我的上一篇文章“Elasticsearch:分布式评分”。然而,这并没有回答为什么阿塞曼排名高于巴赫的问题。是什么让阿克曼更具相关性?这与 Elasticsearch 计算相关性的方式有关:它取决于 TF/IDF 算法。 IDF(逆文档频率)部分让我们很头疼:对于给定的搜索词,它出现的文档越多,它被认为的相关性就越低。
因此,许多文档中出现的术语权重较小。一般来说,这是有道理的:如果您搜索“the well-tempered Keyboard”,您不会对所有包含“the”等通用术语的文档感兴趣,而只会对少数提到键盘的文档感兴趣,最好是脾气暴躁的键盘。
如果您查看上面的数据,您会发现两个索引加起来包含四个约翰尼斯和三个巴赫。所以这仍然使巴赫成为一个更加独特和相关的术语,不是吗?不幸的是不是,因为:
每个字段都有自己的倒排索引,因此对于 TF/IDF 来说,字段的值就是文档的值。
这意味着我们必须在字段级别进行区分,而不仅仅是在索引级别。 (即使两个索引中的字段都有命名,但它们属于两个不同的索引就意味着它们是两个字段。)在这种情况下,我们的painters.name字段中只有一个Johann(Ackermann),有作曲家的名字中只有三个,这确实与作曲家(约翰·塞巴斯蒂安·巴赫)的痛苦相关。
解决方案 1:仅匹配完整结果
正如我们在上面看到的,Elasticsearch 默认使用 OR 来组合术语。嗯,一个明显的解决方案是告诉 Elasticsearch 匹配所有搜索词。您可以通过将运算符更改为 AND 来完成此操作:
GET /painters,composers/_search
{
"query": {
"match": {
"name": {
"query": "johann sebastian bach",
"operator": "and"
}
}
}
}
就是这样,我们只得到一个结果,即 johann sebastian bach。完全的?
那么,如果用户将巴赫与另一位著名作曲家混淆并搜索约翰·范·巴赫(johann van bach)怎么办?该查询现在返回零结果(因为在巴赫的名字中找不到 van),这对我们的用户来说有点太难了。
解决方案2:支持完整结果
我们可以通过将自定义运算符替换为minimum_should_match来解决这个问题:
GET /painters,composers/_search
{
"query": {
"match": {
"name": {
"query": "johann sebastian bach",
"minimum_should_match": "2<75%"
}
}
}
}
2
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。