Neo4j实时推荐性能

Neo4j real-time recommendation performance

我正在尝试了解 neo4j 在实时推荐系统中的性能。

以下是一个密码查询(取自他们的沙箱),它计算与查询用户“Cynthia Freeman”最相似的前 100 个用户(以余弦距离表示):

MATCH 
    (p1:User {name: "Cynthia Freeman"})-[x:RATED]->(m:Movie)<-[y:RATED]-(p2:User)
WITH 
    COUNT(m) AS numberMovies, 
    SUM(x.rating * y.rating) AS xyDotProduct,
    SQRT(REDUCE(xDot = 0.0, a IN COLLECT(x.rating) | xDot + a^2)) AS xLength,
    SQRT(REDUCE(yDot = 0.0, b IN COLLECT(y.rating) | yDot + b^2)) AS yLength,
    p1, p2 
WHERE
    numberMovies > 10
RETURN 
    p1.name, p2.name, xyDotProduct / (xLength * yLength) AS sim
ORDER BY sim DESC
LIMIT 100;

如果我的理解是正确的,LIMIT 子句后面没有 magic,因为仍然需要对所有其他用户进行距离计算,所以解决这个实时查询似乎有点牵强,除非 neo4j 在幕后做一些事情。

在另一个例子中,他们预先计算了用户节点之间的这种[:SIMILARITY]关系并将其存储在图中,因此查询前N个最相似的用户成为节点的排序。这将直观地使图变得密集,因此与简单地使用相似性矩阵相比没有存储优势。

我是否遗漏了有关图形数据库(尤其是 neo4j)工作方式的一些基本知识?这如何扩展到实时应用程序,其中可以有数以万计的用户,甚至可以与他们交互的产品更多?

如果您想在数万个或更多节点上使用某种余弦距离度量来做 real-time 推荐,最好将预先计算的值存储为关系。

至于使图变得密集,您可以将 SIMILAR 关系限制为前 K 个相似节点,并定义相似性截止阈值,这可以使您的图尽可能稀疏。您只能存储相关结果。因此,例如,在一个包含 10,000 个节点的图中,如果每个项目都与前 10 个其他节点有连接,那么这不是一个真正密集的图。如果您还删除从一个节点指向另一个节点并返回的重复关系,则可以删除更多。因此,如果有 10k*10k(如果您将关系视为无向关系,则除以二)可能的关系,您将不会有十亿种可能的关系,但最多只有 100k。

Graph Data Science library支持两种计算余弦距离的算法:

first naive version 计算所有对之间的距离,可以使用 topKsimilarityCutoff 参数进行调整。

就在最近,optimized implementation of the kNN algorithm was added in the GDS 1.4 pre-release. It uses the implementation described in this article: https://dl.acm.org/doi/abs/10.1145/1963405.1963487

但是,对于 real-time 计算 10k+ 个节点之间的相似度,可能仍需要超过 100 毫秒才能使 real-time 响应最大化,因此使用 pre-computed 相似度关系使得感觉。

除了@TomažBratanič 的出色建议外,您现有的查询还可以提高效率。它正在为每个 p1/p2 对执行数学计算,即使是后来因共享电影数量不超过 10 而被过滤掉的对。相反,您应该尝试过滤掉不需要的 p1/p2你做计算之前。

例如:

MATCH
    (p1:User {name: "Cynthia Freeman"})-[x:RATED]->(m:Movie)<-[y:RATED]-(p2:User)
WITH
    COLLECT({xr: x.rating, yr: y.rating}) AS data
    p1, p2
WHERE
    SIZE(data) > 10
WITH
    REDUCE(s = 0, d IN data | s + d.xr * d.yr) AS xyDotProduct,
    SQRT(REDUCE(xDot = 0.0, a IN data | xDot + a.xr^2)) AS xLength,
    SQRT(REDUCE(yDot = 0.0, b IN data | yDot + b.yr^2)) AS yLength,
    p1, p2
RETURN 
    p1.name, p2.name, xyDotProduct / (xLength * yLength) AS sim
ORDER BY sim DESC
LIMIT 100;