mongodb 中带有标签的文档:获取标签计数

documents with tags in mongodb: getting tag counts

我有 collection1 个带有 MongoDB 标签的文档。标签是嵌入的字符串数组:

{
    name: 'someObj',
    tags: ['tag1', 'tag2', ...]
}

我想知道集合中每个标签的数量。因此我有另一个 collection2 标签数:

{
    { 
        tag: 'tag1',
        score: 2
    }
    { 
        tag: 'tag2',
        score: 10
    }
}

现在我必须让两者保持同步。在 collection1 中插入或从中删除时,这是相当微不足道的。但是,当我更新 collection1 时,我会执行以下操作:

1.) 获取旧文档

var oldObj = collection1.find({ _id: id });

2.) 计算新旧标签数组的差异

var removedTags = $(oldObj.tags).not(obj.tags).get();
var insertedTags = $(obj.tags).not(oldObj.tags).get();

3.) 更新旧文档

collection1.update(
    { _id: id },
    { $set: obj }
);

4.) 更新插入和删除标签的分数

// increment score of each inserted tag
insertedTags.forEach(function(val, idx) {
    // $inc will set score = 1 on insert
    collection2.update(
        { tag: val },
        { $inc: { score: 1 } },
        { upsert: true }
    )
});
// decrement score of each removed tag
removedTags.forEach(function(val, idx) {
    // $inc will set score = -1 on insert
    collection2.update(
        { tag: val },
        { $inc: { score: -1 } },
        { upsert: true }
    )
});

我的问题:

A) 这种分别记录分数的方法是否有效?或者是否有更有效的一次性查询从 collection1 中获取分数?

B) 即使分开记账是更好的选择:可以用更少的步骤完成吗?让 mongoDB 计算哪些标签是新的/删除的?

回答您的问题:

  • B): 你不必自己计算 dif use $addToSet
  • A):您可以通过聚合框架结合 $unwind 和 $count
  • 来获取计数

正如 nikmilion 正确指出的那样,解决方案是聚合。虽然我会用 nack 来做:我们将把它的结果保存在 collection 中。要做的是用实时结果换取极速提升。

我会怎么做

往往高估了对实时结果的需求。因此,我会使用预先计算好的标签统计数据,并每 5 分钟左右更新一次。这应该足够好了,因为大多数此类调用都是由客户端异步请求的,因此在必须针对特定请求进行计算的情况下,一些延迟可以忽略不计。

db.tags.aggregate(
  {$unwind:"$tags"},
  {$group: { _id:"$tags", score:{"$sum":1} } },
  {$out:"tagStats"}
)
db.tagStats.update(
  {'lastRun':{$exists:true}},
  {'lastRun':new Date()},
  {upsert:true}
)

db.tagStats.ensureIndex({lastRun:1}, {sparse:true})

好的,这是交易。首先,我们展开标签数组,将其按单个标签分组,并为每次出现的相应标签增加分数。接下来,我们 upsert lastRun in the tagStats collection, which we can do since MongoDB is schemaless. Next, we create a sparse index,它只保存索引字段存在的文档的值。如果索引已经存在,ensureIndex 是一个非常便宜的查询;然而,由于我们将在我们的代码中使用该查询,我们不需要手动创建索引。使用此过程,以下查询

db.tagStats.find(
 {lastRun:{ $lte: new Date( ISODate().getTime() - 300000 ) } },
 {_id:0, lastRun:1}
)

变成 covered query:从索引回答的查询,它往往驻留在 RAM 中,使该查询快如闪电(在我的测试中略低于 0.5 毫秒的中位数)。那么这个查询是做什么的呢?当聚合的最后一个 运行 是 运行 超过 5 分钟(5*60*1000 = 300000 毫秒)之前,它将 return 一条记录。当然,您可以根据自己的需要进行调整。

现在,我们可以结束了:

var hasToRun = db.tagStats.find(
  {lastRun:{ $lte: new Date( ISODate().getTime() - 300000 ) } },
  {_id:0, lastRun:1}
);

if(hasToRun){

  db.tags.aggregate(
    {$unwind:"$tags"},
    {$group: {_id:"$tags", score:{"$sum":1} } },
    {$out:"tagStats"}
  )

  db.tagStats.update(
    {'lastRun':{$exists:true}},
    {'lastRun':new Date()},
    {upsert:true}
  );

  db.tagStats.ensureIndex({lastRun:1},{sparse:true});

}
// For all stats
var tagsStats = db.tagStats.find({score:{$exists:true}});
// score for a specific tag
var scoreForTag = db.tagStats.find({score:{$exists:true},_id:"tag1"});

替代方法

如果实时结果 真的 很重要并且您需要所有标签的统计信息,只需使用聚合而不将其保存到另一个collection:

db.tags.aggregate(
  {$unwind:"$tags"},
  {$group: { _id:"$tags", score:{"$sum":1} } },
)    

如果您一次只需要一个特定标签的结果,实时方法可能是使用特殊索引,创建覆盖查询并简单地计算结果:

db.tags.ensureIndex({tags:1})
var numberOfOccurences = db.tags.find({tags:"tag1"},{_id:0,tags:1}).count();