欢迎来到词语自助餐!下好您的订单,我们会为您提供 20 篇最优秀的文档,这些文档最符合您的查询!我们说最优秀,但我们不会确切地告诉您它们与您的查询的匹配程度,我们只承诺它们是我们拥有的最好的。你说不可接受?你说你是这个地区最著名的餐厅评论家,你需要能够区分完美匹配和文字沙拉?哦,那听起来确实像麻烦……
Meilisearch 是一个 开源 搜索引擎,旨在提供快速、高度相关的搜索,并能轻松集成。它允许将磁盘上的文档存储在称为 索引 的组中,并搜索这些索引以获取与每个查询相关的文档。顺便说一下,如果您想让生活更轻松,并专注于提供的搜索体验,我们提供了一个 云解决方案,它始终受益于我们最新的版本 😉
在最近的 1.3 版本发布 之前,Meilisearch 无法知道文档与特定搜索查询的匹配程度。本文深入了解了导致我们添加此功能的旅程。
动机
为文档提供相关性评分满足了许多高度要求的用例
- 根据文档的评分调整搜索结果的呈现方式。例如,开发人员 bakerfugu 有一个 日历应用程序,并希望使用颜色区分来突出显示会议和事件的相关性。但是,搜索结果顺序并不足够,因为即使是最顶部的结果也可能在相关性方面有所不同。如果没有评分,我们只知道它是 Meilisearch 能找到的最好的。
- 实现聚合搜索。聚合搜索是一种显示来自 多个索引搜索 的结果的方式,就好像搜索是在单个统一索引上执行的一样。用户在 各种 GitHub 讨论 中报告了实际用例。
- 实现分片。分片类似于联合搜索,因为它结合了多个搜索查询的结果。与联合搜索不同,分片涉及在多个 Meilisearch 实例上查询一个分区索引,而不是在同一个实例上查询多个索引。预计数据将更加同质,但重新排序必须在查询完成之后、“离线”进行。
- 了解相关性。Meilisearch 使用一组预定义的规则对文档进行排名。通过为每个 排名规则 生成详细的评分,可以更深入地了解单个规则如何应用于特定查询的文档。这有助于发现最佳设置以在每个用例中最大限度地提高相关性。
从这四个用例中可以看出,实现评分功能的设计空间很大。这些解决方案应该具备哪些特性才能最好地解决所有这些用例?让我们来看看它们。
目标特性
从不同的用例中,我们可以看到多个不同的用户和使用群体。我们确定了两个轴线,可以沿其提供多种解决方案
- 聚合评分与详细评分。对于日历用例,每个文档的单个“聚合”评分就足够了,而对于了解相关性,则需要每个排名规则的详细评分。理想的解决方案应该提供聚合评分和详细评分。
- 机器可读与人类可读。大多数用例都需要信息,这些信息可以由集成或前端自动消化。但是,如果我们想要让相关性更容易理解,我们需要提供人类可读的信息。因此,该解决方案应该在这两个特性之间取得平衡。
此外,要启用聚合搜索和分片,评分必须独立于所搜索索引中包含的文档。事实上,如果向索引添加文档会更改其他文档的评分,那么将该评分与包含不同文档的另一个索引进行比较将毫无意义。
最后,我们希望评分系统直观:Meilisearch 应该按相关性评分降序返回文档,遵循 最小惊奇原则。
特别是最后一个特性,使我们倾向于一个基于 Meilisearch 已执行的排名的解决方案,作为保证排名和评分之间一致性的一种方法。
总之,解决广泛的用例需要一个评分功能,该功能不仅提供聚合评分和详细评分,而且还满足机器和人类可读性。它还应该保持评分独立性,无论搜索索引中的文档如何,并且与 Meilisearch 的现有排名系统保持一致。为了了解如何根据排名构建此评分系统,我们首先需要了解排名本身。
递归桶排序
本节介绍了 Meilisearch 如何根据搜索查询对文档进行排名。如果您已经了解 Meilisearch 使用的递归桶排序算法,可以跳过本节。此外,由于本节专注于搜索算法,因此不会涵盖引擎的其他部分,如索引。如果您有兴趣了解更多关于这些内容的信息,我们之前在专门的文章中介绍过它们 专门文章。
Meilisearch 的核心使用一种称为“桶排序”的算法。它根据一组排名规则将文档排序到不同的桶中。第一个排名规则适用于所有文档,而每个后续规则只用作一个 tiebreaker 来对在桶中被认为相等的文档进行排序。当所有“最内层”桶包含单个文档时,或者在应用最后一个排名规则后,排序将结束。
例如,words
排名规则 根据文档中找到的查询词数量对文档进行排序。如果多个文档最终位于同一个桶中,则使用另一个排名规则,如 typo 来区分它们。
对于查询“Badman dark knight returns”,words
排名规则将返回的文档排序到 4 个桶中,从包含所有词语(可能包含错别字)的文档到只包含“Badman”的文档。typo
排名规则帮助我们进一步区分最后一个桶中的文档。
👉 注意,此规则对其他三个桶没有影响,因为它们只包含这样的文档,即查询包含错别字“Badman -> Batman”。
现在我们已经很好地了解了 Meilisearch 对文档进行排名的方式,让我们回顾一下评分功能的所需属性
- 它应该提供聚合评分和详细评分
- 它应该满足机器和人类可读性
- 它应该保持评分独立性,无论搜索索引中的文档如何
- 它应该与 Meilisearch 的现有排名系统保持一致。
我们如何扩展 Meilisearch 的排名行为以生成满足这些标准的评分?我们将在下一步中探讨这一点。
从排序到评分
根据我们刚刚了解到的内容,每个排名规则将整个数据集分成几个桶,然后按顺序返回它们。然后,我们可以使用每个桶的排名和每个规则的总桶数来计算评分,从而产生递归桶评分算法。
让我们重新使用我们领先的“badman”示例来实践这一点。我们计算 words
桶的数量为 4,并且对于每个桶,typo
内桶的数量。我们得到以下图表所示的结果。
通过将递归桶排序应用于我们对 示例 movies.json 数据集 的查询,我们得到以下排名。为简单起见,我们已将数据集配置为只有标题是可搜索属性,这使得结果更容易理解。有了它,我们能够为每个文档分配一个由两个部分组成的详细评分,从而得出以下结果
词语和错别字评分 | 电影标题 |
---|---|
words 4/4,typo 1/1 |
- Batman: The Dark Knight Returns, Part 1 - Batman: The Dark Knight Returns, Part 2 |
words 3/4, typo 1/1 |
- Batman Unmasked: The Psychology of the Dark Knight - Legends of the Dark Knight: The History of Batman |
words 1/4, typo 2/2 |
- Angel and the Badman |
words 1/4, typo 1/2 |
- Batman: Year One - Batman: Under the Red Hood |
这为我们的每个排名规则提供了第一个形状的详细评分,尽管我们可能希望用特定于规则的语义信息来增强这些评分,例如匹配词的数量和错别字的数量。
我们还没有探索如何从这些复杂评分(适合高级用例)生成每个文档的单个评分,如何确保它独立于数据集,以及如何处理排序规则的独特情况。
那么,我们如何从这些复杂的评分(适合高级用例)转变为每个文档的单个聚合评分,以满足那些不需要如此高细节级别的用例?我们将在下一节中讨论这个问题。
聚合评分
为了使评分系统直观,聚合评分必须与 Meilisearch 给出的排名一致。让我们记住,后续的排名规则主要用于解决先前规则的平局。同样,后来的排名规则只应该细化从先前规则推导出的评分。
考虑到这一点,让我们修改我们之前的图表。与其用“4/4”标记 words
桶(表示匹配所有单词的文档),不如说这个桶中的文档落在 3/4 到 4/4 的范围内。我们将让 typo
排名规则决定它们确切地落在哪里。只取最后一个 words
桶,因为它是有两个 typo
内桶的那个
通过这种方式,我们可以计算每个文档的聚合评分
words
4/4,typo
1/1: 1.0words
3/4,typo
1/1: 0.75words
1/4,typo
2/2: 0.25words
1/4,typo
1/2: 0.125
上面提供了一些分数的直观理解,但需要注意的是,实现的具体细节应谨慎处理。特别是,我们只为前三个最佳 `words` 存储桶设置了一个 `typo` 存储桶,因为 **在我们的索引中**,没有文档同时落入这些 words 存储桶,却没有在“Batman”上出现拼写错误。
现在,如果我们添加一部名为“The badman returns to the dark knight”的新电影会怎样?现在第一个 `words` 存储桶中有两个 `typo` 存储桶,而“Batman: the dark knight returns, part 1”不再是完美的匹配:它的分数变为 **0.875 而不是 1.0**。我们需要避免这个问题。
实现数据独立性
我们的分数应该完全独立于索引中包含的文档。每个规则都应该能够仅根据查询来计算理论上的最大存储桶数量,而不用依赖索引中的文档。
对于 `typo` 规则,这意味着将索引的 拼写容错设置 应用于查询,并计算可能的最大拼写错误数量。默认设置通常允许在一个至少五个字符的词中出现一个拼写错误,而在至少九个字符的词中最多允许出现两个拼写错误。
根据这些设置,查询“Badman dark knight returns”最多允许出现 3 个拼写错误(“badman”上 1 个,“knight”上 1 个,“returns”上 1 个),总共从 0 到 3 个拼写错误有 4 个可能的存储桶。这意味着“Batman: the dark knight returns, part 1”实际上应该得到 **0.9375** 的分数,无论“The badman returns to the dark knight”是否是一部存在于索引或任何地方的电影(将上述层级列表中的分数更正留作练习)。
幸运的是,对于大多数排序规则来说,计算理论上的最大存储桶数量可以用一种自然的方式来完成(详细说明每种排序规则的方式超出了本文的范围,但如果您有兴趣,我们会回答您的问题 😊)。不幸的是,the sort
和 geosort
排序规则 是明显的例外。
排序规则不影响分数
排序规则家族 允许根据文档的某个字段的值对文档进行排序。
这就带来了一个问题。如果我们需要一个规则可以返回的最大存储桶数量,那么当根据产品的价格进行排序时,这个数字应该是什么?请记住,这个值必须独立于索引中的文档。
我们在这里考虑了多种选择,例如根据值的分布自动推断出各种存储桶,同时仍然允许开发人员在评分时指定存储桶划分。最终,我们选择了最简单的选择,它没有添加任何额外的 API 表面:分数不受排序规则的影响。它们被视为返回单个存储桶。
此决定产生了一些不匹配的阻抗,因为在实践中,Meilisearch 使用排序规则时,会根据相同值的存储桶对文档进行排名。这意味着,如果您的排序规则先按价格升序排序,然后再按拼写错误区分,您将得到以下顺序
- 价格为 100 美元的文档,没有拼写错误
- 价格为 100 美元的文档,有拼写错误
- 价格为 200 美元的文档,再次没有拼写错误
- 价格为 200 美元的文档,有拼写错误
由于排序规则不影响分数,(2) 中返回的文档的相关性分数将低于 (3) 中返回的文档。毕竟,前者有拼写错误,而后者没有。这可能会让用户感到意外,我们希望避免这种情况,但没有找到实用的方法。
从另一个角度来看这个问题,它可能看起来更自然。虽然大多数排序规则都按相关性对文档进行排序,但按字段排序的排序规则按该字段的值对文档进行排序,最终,排序只有一种。
总而言之,在使用聚合分数对文档进行排名时,任何按字段排序的排序规则的效果在排名中会被抵消。在使用详细分数时,这不是问题,因为详细分数提供了用于对按字段排序的排序规则进行排名的值,因此开发人员可以在重新排序时将其考虑在内。
使用评分 API
Meilisearch v1.3 允许在搜索请求中指定一个 `showRankingScore` 查询参数,并将该参数设置为 `true` 将导致一个 `_rankingScore` 浮点数被注入到搜索返回的文档中。
然后,您可以获取每个文档的分数,例如,根据值选择不同的表情符号或 CSS 类。
分数 | 表情符号 |
---|---|
0.99 | 👑 |
0.95 | 💎 |
0.90 | 🏆 |
... | ... |
0.25 | 🐓 |
我相信开发人员会想出许多使用这个分数的方法,比这个可怜的后端工程师的尝试更具想象力 😳
Meilisearch v1.3 也公开了 `_rankingScoreDetails`。但是,由于它们添加了大量的 API 表面,目前它们被一个 运行时实验标志 限制。我们感谢您的 反馈!
结论
实现相关性分数是 Meilisearch 为具有大型设计空间的复杂功能执行的设计流程的一个例子。
它也为社区互动提供了一个很好的机会,因为我们发布了该功能的几个原型,并获得了用户的反馈。我们特别感谢用户 @LukasKalbertodt,他关于原型的 宝贵的反馈 帮助我们改进了解决方案 ❤️
我们使用评分 API 获得的早期结果非常有希望
- **调试相关性。** Meilisearch v1.3 已经包含了相关性改进,这些改进是通过详细分数揭示的 相关性 问题 而变得明显的
- **解锁聚合搜索。** 使用聚合分数对来自异构搜索查询和索引的文档进行重新排序,解锁了聚合搜索的第一个版本。如果使用 实验功能开关 启用详细分数,则可以使用它们以更细致的方式对文档进行重新排序
- **解锁混合搜索。** 相关性分数可以作为实现混合搜索的一种手段,与我们在 语义搜索 方面所做的工作相结合
我们希望您喜欢深入了解评分功能,并希望它对您有所帮助。请随时在我们的 Discord 社区中向我们提供反馈。
如需了解更多关于 Meilisearch 的信息,您还可以订阅我们的 新闻稿,查看我们的 路线图,参与我们的 产品讨论,从 源代码 构建或在 云 上创建一个项目。期待您的光临!