如何从CouchDB加载随机文档(高效公平)?

时间:2010-09-23 14:48:34

标签: random couchdb

我想从存储在CouchDB数据库中的一组文档中加载随机文档。拾取和加载文档的方法应符合以下要求:

  • 效率:文档的查找应该是高效的,最重要的是加载文档的时间不得与文档总数呈线性增长。这意味着无法使用 skip 查询参数。

  • 统一分布:选择应该是真正随机的(尽可能使用标准随机数生成器),每个文档应该有相同的选择机会。

在CouchDB中实现此功能的最佳方法是什么?

5 个答案:

答案 0 :(得分:24)

在更多地考虑之后,我想出了一个解决方案。为了完整起见,我将首先展示两种简单的方法,并解释它们为何存在缺陷。第三个解决方案是我要去的那个。

方法1:跳过

这是一个简单的解决方案:你有一个简单的视图(我们称之为random),它有一个map函数,它发出你想要选择的所有文档和内置的_count reduce函数。要选择随机文档,请按照下列步骤操作:

  • 通过调用以下方式查看视图中的文档总数N
    http://localhost:5984/db/_design/d/_view/random
  • 选择随机数0 <= i < N
  • 加载i'文档:
    http://localhost:5984/db/_design/d/_view/random?reduce=false&skip=i&limit=1

这种方法很糟糕,因为它无法很好地扩展到大量文档。根据{{​​3}},skip参数只能用于一位数值。

上述解决方案必须在返回所选文档之前循环遍历i文档。在SQL术语中,它相当于全表扫描,而不是索引查找。

方法2:文档中的随机数

通过这种方法,在创建时为每个文档生成随机数并存储在文档中。示例文档:

{
  _id: "4f12782c39474fd0a498126c0400708c",
  rand: 0.4591819887660398,
  // actual data...
}

random视图具有以下地图功能:

function(doc) {
  if (doc.rand) {
    emit(doc.rand, doc);
  }
}      

以下是选择随机文档的步骤:

  • 选择一个随机数0 <= r < 1
  • 加载文件:
    http://localhost:5984/db/_design/d/_view/random?startkey=r&limit=1
  • 如果没有返回文档(因为r大于数据库中存储的最大随机数),请环绕并加载第一个文档。

速度非常快,一见钟情。但是,存在一个严重的缺陷:并非所有文件都有被挑选的机会。

在最简单的示例中,数据库中有两个文档。当我选择一个随机文档非常多次时,我希望每个文档都有一半的时间出现。假设文档在创建时被分配了随机数0.2和0.9。因此,在(r <= 0.2) or (r > 0.9)时选择文档A,在0.2 < r <= 0.9时选择文档B.被选中的几率不是每份文件的50%,而是A的30%和B的70%。

当数据库中有更多文档时,您可能会认为情况有所改善,但事实并非如此。文档之间的间隔变得更小,但是区间大小的变化变得更糟:想象三个文档A,B和C,随机数为0.30001057,0.30002057和0.30002058(中间没有其他文档)。选择B的几率是选择C的1000倍。在最坏的情况下,两个文档被分配相同的随机数。然后只能找到其中一个(文档ID较低的那个),另一个基本上是不可见的。

方法3:1和2的组合

我提出的解决方案结合了方法2的速度和方法1的公平性。这是:

与方法2一样,每个文档在创建时分配一个随机数,相同的映射函数用于视图。与方法1一样,我也有_count reduce函数。

以下是加载随机文档的步骤:

  • 通过调用以下方式查看视图中的文档总数N
    http://localhost:5984/db/_design/d/_view/random
  • 选择随机数0 <= r < 1
  • 计算随机索引:i = floor(r*N)
    我的目标是加载i'文档(如方法1中所示)。假设随机数的分布或多或少是均匀的,我猜测i'文档的随机值约为r
  • 查找随机值低于L的文档数rhttp://localhost:5984/db/_design/d/_view/random?endkey=r
  • 看看我们的猜测有多远:s = i - L
  • if (s>=0)
    http://localhost:5984/db/_design/d/_view/random?startkey=r&skip=s&limit=1&reduce=false
  • if (s<0)
    http://localhost:5984/db/_design/d/_view/random?startkey=r&skip=-(s+1)&limit=1&descending=true&reduce=false

因此,诀窍是猜测分配给i'文档的随机数,查看它,看看我们离开了多远,然后跳过我们错过的文档数。

即使对于大型数据库,跳过的文档数量也应保持很小,因为猜测的准确性会随着文档的数量而增加。我的猜测是s在数据库增长时保持不变,但我没有尝试过,我觉得没有资格在理论上证明它。

如果你有更好的解决方案,我会非常感兴趣!

答案 1 :(得分:2)

如果插入性能不是问题,您可以尝试使数字非随机,例如在创建时将其设为doc_count + 1。然后你可以用随机数0&lt; = r&lt; doc_count。但是,要么需要同步文档的创建,要么具有在couchdb外部的序列,例如,一个SQL数据库。

祝你好运

菲利克斯

答案 2 :(得分:1)

“滥用”视图的reduce函数怎么样?

function (keys, values, reduce) {
    if (reduce)
      return values[Math.floor(Math.random()*values.length)];
    else
      return values;
}

答案 3 :(得分:0)

我同意@meliodas:

这是选项2(n = 1000)的分布:

{ 0.2: 233,
  0.9: 767 }

并且有一半时间交换startkey / endkey:

{ 0.2: 572,
  0.9: 428 }

当你查看更多数据时,不确定发行版会发生什么,但它最初似乎更有希望。这根本没有使用选项1,我认为这是不必要的。

答案 4 :(得分:0)

方法2b:文档中的序号

此方法类似于this answer中提到的方法2。方法2使用随机数两次(一次在文档本身中,一次在挑选文档的过程中)。方法2b将仅在拣配过程中使用随机数,并在文档中使用顺序整数。请注意,如果删除了文档,此功能将无效(请参阅下文)。运作方式如下:

在创建时向您的文档中添加连续整数:

{
    _id: "4f12782c39474fd0a498126c0400708c",
    int_id : 0,
    // actual data...
}

另一个文档

{
    _id: "a498126c0400708c4f12782c39474fd0",
    int_id : 1,
    // actual data...
}

,每个文档只加一个。

视图random具有相同的地图功能(尽管您可能希望将其名称更改为“随机”以外的名称):

 function(doc) {
   if (doc.int_id) {
     emit(doc.int_id, doc);
   }
 }  

以下是加载随机文档的步骤:

  • 调用以下方法在视图中查找文档总数N
    http://localhost:5984/db/_design/d/_view/random
  • 选择随机数0 <= r < 1
  • 计算随机索引:i = floor(r*N)
  • 加载文档:
    http://localhost:5984/db/_design/d/_view/random?startkey=i&limit=1

通过这种方式,我们根据设计选择了int_id0N-1的均匀分布。然后,我们选择一个随机索引(介于0和N-1之间)并将其用于该均匀分布。

注意

当删除中间或开头的文档时,此方法不再起作用。 int_id必须从0开始,然后上升到N-1