dot Redis 8 已发布 - 并且是开源的

了解更多

使用 Redis 计算 Jaccard 相似度

世界上很多事物都有听起来很花哨的名字,但实际上是非常简单的想法。Jaccard 相似度就是其中之一。这是一个简单的计算,由植物学家 Paul Jaccard 于 1901 年创建,您可以使用集合来确定它们的相似程度。

找到相似的东西很有用——您可能想找到与您有相似品味的人,作为配对(浪漫或其他)过程的一部分,或者找到相似的文档来检测抄袭。

Redis 及其Set 数据结构拥有计算 Jaccard 相似度所需的大部分内容。但在我们开始讨论 Redis 之前,让我们看看如何计算它。

基数、并集和交集,天哪!

如果您对集合论有哪怕是最轻微的背景,这将是一个回顾。我们将从集合开始。集合是唯一对象的集合。在数学符号中,它们用花括号和集合成员的列表表示。这是一个我喜欢的电影的简单集合:

您可以计算集合的成员。这是集合的基数。上面的集合的基数为 6。

当然,您可以拥有多个集合。这是一个我妻子喜欢的电影的简单集合

您可以看到她和我有一些共同的电影偏好,但她也有一些我不那么喜欢的电影。反之亦然。您还可以看到她的集合的基数为 7。

现在我们有了两个集合,我们可以开始做一些有趣的事情。首先,我们可以合并这些集合。这被称为并集。并集本身就是一个集合,并且像所有集合一样,具有基数(在这种情况下为 9)。我们可以使用并集符号将这两个集合的并集写成一个等式,如下所示:

我们还可以确定集合的共同点。这被称为交集。交集也是一个集合,并且也具有基数——4——就像并集一样。我们可以使用交集符号将两个集合的交集写成一个等式,如下所示:

集合论还有很多内容,但这就是我们了解和计算 Jaccard 相似度所需要知道的全部内容。

一个简单的计算

Jaccard 相似度是交集基数与并集基数之比。在我们的例子中,我和我的妻子有 4 部共同的电影,我们之间有 9 部电影。4 除以 9 约为 0.444 或 44.4%。因此,我们对电影的品味相似度为 44.4%。

就是这样。这就是 Jaccard 相似度的全部内容。

让我们使用 Redis Set 命令来计算它。

使用 Redis 和 X 档案

这次我不使用我和妻子喜欢的电影,而是使用 Scully 和 Mulder 看到不明飞行物的州。(这些数据完全是虚构的。我很确定他们在每个州都看到了不明飞行物。)

首先,我们需要为 Scully 和 Mulder 创建几个集合

> SADD ufos:scully "California" "Nevada" "Oregon"
> SADD ufos:scully "Wyoming" "New Mexico" "Ohio"

内华达州新墨西哥州俄亥俄州。听起来很合理:

> SADD ufos:mulder "Florida" "Kansas" "South Carolina"
> SADD ufos:mulder "West Virginia" "New Mexico" "Ohio"

新墨西哥州和俄亥俄州再次出现。

现在我们需要创建一些新集合,它们是这两个集合的交集和并集。我们需要存储它们,以便我们可以获得它们的基数

> SINTERSTORE ufos:intersection ufos:scully ufos:mulder
(integer) 2

> SUNIONSTORE ufos:union ufos:scully ufos:mulder
(integer) 10

现在我们有了交集和并集,我们可以获得它们各自的基数

> SCARD ufos:intersection
(integer) 2

> SCARD ufos:union
(integer) 10

或者,我们可以跳过该调用,直接使用SINTERSTORESUNIONSTORE命令的结果。无论哪种方式,我们只需将这两个数字相除即可得到结果。结果为 20%。

SINTERSTORE 和 SUNIONSTORE

您可能不太熟悉SINTERSTORESUNIONSTORE命令及其可能造成的后果,因此我认为进行一些教育可能是有序的。

SINTERSTORESUNIONSTORE 的基本功能与 SINTERSUNION 相同,只不过它们将结果存储在一个键中(因此末尾带有 STORE)。使用它们很简单。只需传入要创建的集合的所需键,以及一个或多个包含您想要操作的集合的键

> SUNIONSTORE destination_key key1 key2 key2 …
(integer) 51

虽然 SINTERSUNION 返回它们收到的集合的交集和并集,但 SINTERSTORESUNIONSTORE 创建新的集合(并返回所述集合的大小)。这些集合可能非常大,并且将被写入 AOF 并被复制(如果您正在执行这些操作。)

在我们的示例中,它们旨在成为中间值。因此,如果它们很大并且需要写入 AOF 并复制,则可能会对性能产生重大影响。

SINTERSUNION 没有这个问题。它们只是返回它们创建的集合,而不存储它。我们可以使用它们构建我们的解决方案,只需计算返回的成员即可获得基数。当然,这些集合也可能非常大,并且将它们返回给客户端可能是一个相当昂贵的操作。

一旦您深入研究问题,通常会有更多隐藏的复杂性。我们可以使用 SINTERSUNION 解决我们的 Jaccard 相似度问题,或者我们可以选择 SINTERSTORESUNIONSTORE。大小、写入 AOF 和复制都是这里的因素。那么哪种方法是正确的?像往常一样,答案是“视情况而定”。

一些注意事项

首先也是最重要的,请记住我们正在使用集合。更重要的是,我们正在使用交集和并集命令。上面的键名称仅适用于没有集群的单个实例。如果您使用集群,则需要确保对键进行分槽,以便它们都转到同一个分片。有关更多信息,请查看 Kyle Davis 关于 Redis 集群键的最佳实践的精彩博客文章。

其次:必须在 Redis 之外进行最终计算不是很令人满意。如果我们要使用 Redis 来计算 Jaccard 相似度,那么我们想要使用 Redis,而不是 Redis 和客户端代码。我们可以使用 Lua 来完成此操作。

如果您只想将 Lua 用于此计算的最后一英里,下面的代码可以正常工作

> EVAL "local inter_card = redis.pcall('scard', KEYS[1]); local union_card = redis.pcall('scard', KEYS[2]); local similarity = inter_card / union_card; return tostring(similarity)" 2 ufos:intersection ufos:union

但是,如果您想避免使用 SUNIONSTORESINTERSTORE 创建中间集合,还有其他方法可以做到这一点。但存在权衡。我有一个GitHub 存储库,其中包含几种解决此问题的方法,如果您想深入了解,可以进行探索。(它使用 JavaScript。我希望你会原谅我。)

去做一些事情

就像这样,您现在可以计算 Jaccard 相似度并将其用于各种很酷的事情。正在寻找一些有趣的数据来尝试它吗?查看这个大型 UFO 目击事件数据集