使用MongoDB聚合框架进行不同的排序和分组

时间:2013-08-18 05:55:21

标签: python mongodb distinct aggregation-framework

我最近一直在玩MongoDB的聚合框架并且认为这对我一直试图解决的问题是一个很好的解决方案。

所以,我说我正在编写讨论区软件,我的帖子有以下文档结构:

{
  '_id': ObjectId,
  'created_at': datetime,
  'poster_id': ObjectId,
  'discussion_id': ObjectId,
  'body': string
}

我在posts集合中存储了以下(简化的)示例文档:

{
  '_id': 1,
  'created_at': '2013-08-18 12:00:00',
  'poster_id':  1,
  'discussion_id':  1,
  'body': 'imma potato'
}

{
  '_id': 2,
  'created_at': '2013-08-18 13:00:00',
  'poster_id':  1,
  'discussion_id':  1,
  'body': 'im still a potato'
}

{
  '_id': 3,
  'created_at': '2013-08-18 14:00:00',
  'poster_id':  2,
  'discussion_id':  1,
  'body': 'you are definitely a potato'
}

{
  '_id': 4,
  'created_at': '2013-08-18 15:00:00',
  'poster_id':  3,
  'discussion_id':  1,
  'body': 'Wait... he is potato?'
}

{
  '_id': 5,
  'created_at': '2013-08-18 16:00:00',
  'poster_id':  2,
  'discussion_id':  1,
  'body': 'Yes! He is potato.'
}

{
  '_id': 6,
  'created_at': '2013-08-18 16:01:00',
  'poster_id':  3,
  'discussion_id':  1,
  'body': 'IF HE IS POTATO... THEN WHO WAS PHONE!?'
}

我要做的是返回poster_id s的独特地图,其最新帖子_id按最新帖子降序排序。因此,最后,给定上面的示例代码,映射看起来非常类似于:

{
  3:6,
  2:5,
  1:2
}

以下是我使用pymongo实现的MongoDB聚合框架在Python中编写的方法示例:

def get_posters_with_latest_post_by_discussion_ids(self, discussion_ids, start=None, end=None, skip=None, limit=None, order=-1):
    '''Returns a mapping of poster ids to their latest post associated with
    the given list of discussion_ids. A date range, ordering and paging properties
    can be applied.
    '''
    pipelines = []

    if order:
        pipelines.append({ '$sort': { 'created_at': order } })

    if skip:
        pipelines.append({ '$skip': skip })

    if limit:
        pipelines.append({ '$limit': limit })

    match = {
        'discussion_id': {
            '$in': discussion_ids
        }
    }

    if start and end:
        match['created_at'] = {
            '$gte': start,
            '$lt': end
        }

    pipelines.append({ '$match': match })
    pipelines.append({ '$project': { 'poster_id': '$poster_id' } })
    pipelines.append({ '$group': { '_id': '$poster_id', 'post_id': { '$first': '$_id' } } })

    results = self.db.posts.aggregate(pipelines)

    poster_to_post_map = {}
    for result in results['result']:
        poster_to_post_map[result['_id']] = result['post_id']

    return poster_to_post_map

现在我已经有了映射,我可以单独查询postersposts个集合以获取完整的文档,然后将它们混合在一起显示。

现在,问题不是它不起作用,它确实......那种。假设我有更多的帖子,我想通过他们最新帖子的海报列表进行翻页。如果我的页面限制是“每页10个海报”,并且在生成的10个文档中,存在一个包含2个或更多帖子的单张海报,我实际上在地图中找回的项目少于10个。

例如,我有10个帖子,1个海报在初始结果中有3个帖子。然后聚合框架将丢弃其他2个帖子并将最新的帖子与该用户相关联,从而产生包含8个条目而不是10个条目的地图。

这非常令人沮丧,因为我无法可靠地对结果进行分页。我也无法准确判断我是否在结果的最后一页,因为一组结果可能会或可能不会返回0或更多匹配。

如果有的话,我在这里做错了什么?

我想要完成的事情很简单,聚合框架似乎非常适合我的问题。

如果它是传统关系数据库中的存储过程,这很简单,但这就是我们转移到无模式文档存储时所牺牲的东西;关系在数据库的上下文之外进行管理。

无论如何,代码应该很容易理解,我会回答你可能遇到的任何问题。

无论哪种方式,感谢花时间阅读。 :)

修改:已解决

以下是未来观众解决方案的要点:https://gist.github.com/wilhelm-murdoch/6260469

1 个答案:

答案 0 :(得分:2)

如果您考虑如何描述聚合框架,它实际上是一个非常简单的修复。

取自docs

  

从概念上讲,集合中的文档会通过聚合   管道,它们在通过时转换这些对象。对于   那些熟悉类UNIX的shell(例如bash)的概念是   类似于用于将文本过滤器串起来的管道(即|)。

您之前可能已经阅读过,但再次解释的原因是您可以按几乎任何顺序将操作传递到该管道中 - 并且不止一次。例如,在MYSQL中,LIMIT始终列在查询的末尾,并在所有其他分组函数之后应用于结果集。

在MongoDB中,操作按照您将它们添加到管道的顺序运行。所以操作顺序很重要。

看看上面的代码,看起来你实际上正在取出所有东西 - 并且取决于你的IF语句,首先对它进行排序,应用你的偏移和限制,然后在投影和分组之前匹配该结果集。

所以 - 长话短说 - 看起来你需要重新排序:

pipelines = []

match = {
    'discussion_id': {
        '$in': discussion_ids
    }
}

if start and end:
    match['created_at'] = {
        '$gte': start,
        '$lt': end
    }

pipelines.append({ '$match': match })

if order:
    pipelines.append({ '$sort': { 'created_at': order } })

pipelines.append({ '$project': { 'poster_id': '$poster_id' } })
pipelines.append({ '$group': { '_id': '$poster_id', 'post_id': { '$first': '$_id' } } })

if skip:
    pipelines.append({ '$skip': skip })

if limit:
    pipelines.append({ '$limit': limit })

results = self.db.posts.aggregate(pipelines)