通过连接表查找类似用户的算法

时间:2010-05-20 22:50:37

标签: sql ruby algorithm

我有一个应用程序,用户可以从300种可能的兴趣中选择各种兴趣。每个选定的兴趣都存储在包含user_id和interest_id列的连接表中。

典型用户从300中选择约50个。

我想构建一个系统,用户可以在其中找到与他们最感兴趣的前20位用户。

现在我可以使用以下查询完成此操作:

SELECT i2.user_id, count(i2.interest_id) AS count 
  FROM interests_users as i1, interests_users as i2
    WHERE i1.interest_id = i2.interest_id AND i1.user_id = 35
  GROUP BY i2.user_id
  ORDER BY count DESC LIMIT 20;

但是,此查询在连接表中使用10,000个用户和500,000行执行大约需要500毫秒。所有索引和数据库配置设置都已尽力调整。

我还尝试使用以下查询完全避免使用连接:

select user_id,count(interest_id) count
  from interests_users
    where interest_id in (13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,508)
  group by user_id 
  order by count desc 
  limit 20;

但这个更慢(约800毫秒)。

我怎样才能最好地将收集此类数据的时间降低到100毫秒以下?

我考虑将这些数据放入像Neo4j这样的图形数据库中,但我不确定这是否是最简单的解决方案,或者它是否比我目前所做的更快。

4 个答案:

答案 0 :(得分:1)

您所谈论的内容称为群集。

群集是一个难题,在运行中计算它需要的资源比我们想要的更多,我担心,因为完整的计算是O(N 2 )。

我认为在这条路上寻找想法不太可能产生任何结果(我可能是错的),因为问题固有的复杂性。

但是,我们不必每次都从头开始计算。我无法弄清楚一个不断发展的图片(合理)以及如何更新它。

但我可以弄清楚如何缓存结果!

UserId*  |  LinkedUserId*  |  Count
35       |  135            |  47
35       |  192            |  26

(UserId的一个索引和LinkedUserId的另一个索引,unicity的约束是永远不应该有2行具有相同的UserId / LinkedUserId对)

每当您需要为此用户获取组时,请先查阅缓存表。

现在,我们还需要不时地使一些缓存条目无效:每次用户添加或删除兴趣时,它都可能影响链接到她的所有用户。

当用户添加条目时,使用此兴趣使用户的所有缓存行无效。

当用户删除条目时,使与她链接的用户的所有缓存行无效。

老实说,我不确定它会表现得更好。

答案 1 :(得分:1)

SELECT DISTINCT TOP 20 b.user_id, SUM(1) OVER (PARTITION BY b.user_id) AS match
  FROM interests_users a
  LEFT OUTER JOIN interests_users b ON a.interest_id = b.interest_id AND b.user_id <> 35
 WHERE a.user_id = 35 AND b.user_id IS NOT NULL
 ORDER BY 2 DESC

如果你建立了良好的索引,你应该没事。

答案 2 :(得分:1)

在纯Ruby中,我实际上能够基本上得到我想要的东西。

首先,我创建一个二维数组,其中每列是用户,每行都是感兴趣的。数组中的每个值都是0或1,具体取决于当前用户是否具有该兴趣。该数组存储在内存中,其中包含添加或修改行和列的函数。

然后,当我想要计算与当前用户具有相似兴趣的用户时,我将为当前用户将列设置为“1”的每一行添加所有列。这意味着我需要遍历10,000列并且每列平均运行50次添加操作,然后在最后执行排序操作。

您可能会猜测这需要很长时间,但实际上我的机器(Core 2 Duo,3ghz.Ruby 1.9.1)大约需要50-70毫秒,而我们的生产服务器大约需要110毫秒。好消息是我甚至不需要限制结果集。

这是我用来测试算法的ruby代码。

USERS_COUNT = 10_000
INTERESTS_COUNT = 500

users = []
0.upto(USERS_COUNT) { |u| users[u] = rand(100000)+100000 }

a = []
0.upto(INTERESTS_COUNT) do |r|
  a[r] = []
  0.upto(USERS_COUNT) do |c|
    if rand(10) == 0 # 10% chance of picking an interest
      a[r][c] = 1
    else
      a[r][c] = 0
    end
  end  
end

s = Time.now

countable_rows = []

a.each_index { |i| countable_rows << i unless a[i][0].zero? }

b = {}
0.upto(USERS_COUNT) do |c|
  t = 0
  countable_rows.each { |r| t+= a[r][c] }
  b[t] = users[c]
end
b = b.sort {|x,y| y[0] <=> x[0] }

puts Time.now.to_f - s.to_f

前几行用于创建模拟二维数组。程序的其余部分运行我上面描述的算法。

上面的算法在一段时间内相当好地扩展。显然它不适合50,000多个用户,但由于我们的产品将社区细分为较小的组,因此这种方法运行良好(并且比SQL快得多)。

欢迎任何关于如何调整以获得更好性能的建议。

答案 3 :(得分:1)

您发布的代码答案不正确。通过将计数存储在哈希中,您将忘记许多用户,因为每个用户只保留一个用户。例如,如果两个用户具有相同的兴趣(或者至少与当前用户具有相同数量的匹配兴趣),那么您的t变量将是相同的,并且第一个看到的将被覆盖。第二

以下是您发布的答案代码的正确版本。它更短,更惯用,也应该更快。请注意,我使用的是truefalse,而不是10

USERS_COUNT = 10_000
INTERESTS_COUNT = 500

users = Array.new(USERS_COUNT) { rand(100000)+100000 }

table = Array.new(INTERESTS_COUNT) do
  Array.new(USERS_COUNT) { rand(10) == 0 }
end

s = Time.now
cur_user = 0
cur_interests = table.each_index.select{|i| table[i][cur_user]}

scores = Array.new(USERS_COUNT) do |user|
  nb_match = cur_interests.count{|i| table[i][user] }
  [nb_match, users[user]]
end

scores.sort!

puts Time.now.to_f - s.to_f
顺便说一句,您可以通过转置table来提高性能,这样可以避免一半的查找。