Mongo中的动态粘性排序是一个简单的值或列表

时间:2014-05-20 00:32:30

标签: mongodb aggregation-framework

我正在尝试动态粘贴排序记录集合,其中每个查询的粘性值都不同。让我举个例子。以下是一些示例文档:

{first_name: 'Joe', last_name: 'Blow', offices: ['GA', 'FL']}
{first_name: 'Joe', last_name: 'Johnson', offices: ['FL']}
{first_name: 'Daniel', last_name: 'Aiken', offices: ['TN', 'SC']}
{first_name: 'Daniel', last_name: 'Madison', offices: ['SC', 'GA']}
... a bunch more names ...

现在假设我想按姓氏的字母顺序显示姓名,但我想在顶部以第一个名字“Joe”挂起所有记录。

在SQL中,这是非常直接的:

SELECT * FROM people ORDER first_name == 'Joe' DESC, last_name

将表达式放在排序标准中的能力使这一点变得微不足道。使用聚合框架我可以这样做:

[
  {$project: {
    first_name: 1,
    last_name: 1
    offices: 1,
    sticky: {$cond: [{$eq: ['$first_name', 'Joe']}, 1, 0]}
  }},
  {$sort: [
    'sticky': -1,
    'last_name': 1
  ]}
]

基本上我使用聚合框架创建一个动态字段,如果名称为Joe则为1,如果名称不是Joe则为0,然后按相反顺序排序。当然,在构建我的聚合管道时,我可以很容易地将'Joe'改为'Daniel',现在'Daniel'将被固定在顶部。这部分是我所说的动态粘性排序。我粘贴排序的值将更改逐个查询

现在这对于像字符串这样的基本值很有用。当我为一个包含数组的值尝试相同的东西时,问题就来了。假设我想要挂钩“FL”办公室的所有用户。有了Mongo对数组的原生理解,我认为我可以做同样的事情。所以:

[
  {$project: {
    first_name: 1,
    last_name: 1
    offices: 1,
    sticky: {$cond: [{$eq: ['$offices', 'FL']}, 1, 0]}
  }},
  {$sort: [
    'sticky': -1,
    'last_name': 1
  ]}
]

但这根本不起作用。我确实知道,如果我将其更改为以下内容,那么Joe Johnson(仅在FL办公室)就会出现在顶部:

[
  {$project: {
    first_name: 1,
    last_name: 1
    offices: 1,
    sticky: {$cond: [{$eq: ['$offices', ['FL']]}, 1, 0]}
  }},
  {$sort: [
    'sticky': -1,
    'last_name': 1
  ]}
]

但它没有把Joe Blow放在首位(谁是FL和GA)。我相信它正在进行简单的匹配。所以我的第一次尝试根本不起作用,因为$ eq返回false,因为我们正在将数组与字符串进行比较。第二次尝试适用于Joe Johnson,因为我们正在比较完全相同的数组。但是Joe Blow自['GA','FL']以来没有工作!= ['FL']。此外,如果我想在顶部固定FL和SC,我不能给它值['FL','SC']来比较。

接下来,我尝试使用$ setUnion和$ size的组合。

[
  {$project: {
    first_name: 1,
    last_name: 1
    offices: 1,
    sticky: {$size: {$setUnion: ['$offices', ['FL', 'SC']]}}
  }},
  {$sort: [
    'sticky': -1,
    'last_name': 1
  ]}
]

我尝试过使用$ let和$ literal的各种组合,但它总是抱怨我试图将一个文字数组传递给$ setUnion的参数。具体来说它说:

disallowed field type Array in object expression

有没有办法做到这一点?

2 个答案:

答案 0 :(得分:1)

无法重现您的错误,但您有一些"拼写错误"在你的问题中,所以我无法确定你的实际情况。

但假设您实际上正在使用MongoDB 2.6或更高版本,那么您可能需要$setIntersection$setIsSubset运算符而不是$setUnion。那些运营商暗示"匹配"与它们进行比较的数组的内容,其中$setUnion只是将提供的数组与现有数组相结合:

db.people.aggregate([
    { "$project": {
        "first_name": 1,
        "last_name": 1,
        "sticky": { 
            "$size": { 
                "$setIntersection": [ "$offices", [ "FL", "SC" ]] 
            }
        },
        "offices": 1
    }},
    { "$sort": {
        "sticky": -1,
        "last_name": 1
    }}
])

在您没有set operators的先前版本中,您只是使用$unwind来处理数组,并且$cond操作与$group之前的操作相同将它们全部重新组合在一起:

db.people.aggregate([
    { "$unwind": "$offices" },
    { "$group": {
        "_id": "$_id",
        "first_name": { "$first": "$first_name" },
        "last_name": { "$first": "$last_name",
        "sticky": { "$sum": { "$cond": [
            { "$or": [
                { "$eq": [ "$offices": "FL" ] },
                { "$eq": [ "$offices": "SC" ] },
            ]},
            1,
            0
        ]}},
        "offices": { "$push": "$offices" }
    }},
    { "$sort": {
        "sticky": -1,
        "last_name": 1
    }}
])

但你肯定是在正确的轨道上。只需选择正确的设置操作或其他方法即可满足您的需求。


或者既然你已经发布了获得你想要的东西的方式,那么更好的方式来编写那种"有序匹配"是这样的:

db.people.aggregate([
    { "$project": {
        "first_name": 1,
        "last_name": 1,
        "sticky": { "$cond": [
            { "$anyElementTrue": {
                "$map": {
                    "input": "$offices",
                    "as": "o",
                    "in": { "$eq": [ "$$o", "FL" ] }
                }
            }},
            2,
            { "$cond": [
                { "$anyElementTrue": {
                    "$map": {
                        "input": "$offices",
                        "as": "o",
                        "in": { "$eq": [ "$$o", "SC" ] }
                    }
                }},
                1,
                0
            ]}
        ]},
        "offices": 1
    }},
    { "$sort": {
        "sticky": -1,
        "last_name": 1
    }}
])

这将优先考虑" office"包含" FL"过" SC"因此,然后超过所有其他人,并在一个领域内进行操作。对于人们来说,这应该也很容易看到如何在没有set运算符的早期版本中使用$unwind将其抽象为表单。你只需提供更高的重量"通过嵌套$cond语句,将值放在顶部所需的项目上。

答案 1 :(得分:0)

我想我找到了最佳方法。

[
  {$project: {
    first_name: 1,
    last_name: 1
    offices: 1,
    sticky_0: {
      $cond: [{
        $anyElementTrue: {
          $map: {
            input: "$offices",
            as:    "j",
            in:    {$eq: ['$$j', 'FL']}
          }
        }
      }, 0, 1]
    },
    sticky_1": {
      $cond: [{
        $anyElementTrue: {
          $map: {
            input: '$offices',
            as:    'j',
            in:    {$eq: ['$$j', 'SC']}
          }
        }
      }, 0, 1]
    }
  }},
  {$sort: [
    'sticky_0': 1,
    'sticky_1': 1,
    'last_name': 1
  ]}
]

基本上在构建我的管道时,我会迭代我想要粘贴的每个项目,并从该项目创建它自己的虚拟字段,只检查一个值。要检查一个值,我使用$ cond,$ anyElementTrue和$ map的组合。这有点令人费解,但它确实有效。如果有更简单的事情,我很乐意听到。