我想从存储在CouchDB数据库中的一组文档中加载随机文档。拾取和加载文档的方法应符合以下要求:
效率:文档的查找应该是高效的,最重要的是加载文档的时间不得与文档总数呈线性增长。这意味着无法使用 skip 查询参数。
统一分布:选择应该是真正随机的(尽可能使用标准随机数生成器),每个文档应该有相同的选择机会。
在CouchDB中实现此功能的最佳方法是什么?
答案 0 :(得分:24)
在更多地考虑之后,我想出了一个解决方案。为了完整起见,我将首先展示两种简单的方法,并解释它们为何存在缺陷。第三个解决方案是我要去的那个。
这是一个简单的解决方案:你有一个简单的视图(我们称之为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术语中,它相当于全表扫描,而不是索引查找。
通过这种方法,在创建时为每个文档生成随机数并存储在文档中。示例文档:
{
_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较低的那个),另一个基本上是不可见的。
我提出的解决方案结合了方法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
的文档数r
:
http://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)
此方法类似于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_id
从0
到N-1
的均匀分布。然后,我们选择一个随机索引(介于0和N-1之间)并将其用于该均匀分布。
当删除中间或开头的文档时,此方法不再起作用。 int_id
必须从0
开始,然后上升到N-1
。