【问题标题】:Match next and previous to the queried item匹配查询项目的下一个和上一个
【发布时间】:2015-08-27 04:29:55
【问题描述】:

问题:

我有一个包含不同类型事件的集合。一个事件的日期可能会改变,我们想知道它是什么时候改变的以及它从什么地方改变的,所以当前日期存储在一个包含所有以前事件的数组中。

[
  { 
     _id: ..., 
     eventDates : [
       { created: ISODate(...), eventDate: ISODate(...) },
       { created: ISODate(...), eventDate: ISODate(...) }
     ],
     eventType: ...,
     otherData: ....
  }
]

所以基本上,每个对象都有一个类型和一个日期列表,其中最后创建的 eventDate 才是最重要的。

对于每个 eventType,我想列出相对于给定日期的上一个和下一个事件。

一个例子:

所以给出以下数据:

[ { 
    _id: 1,
    eventType: "sports", 
    eventDates: [ 
      //first scheduled to 1st of February... 
      { created: ISODate(2015-01-01), eventDate: ISODate(2015-02-01) },
      //...but later rescheduled to 10th of February.
      { created: ISODate(2015-01-02), eventDate: ISODate(2015-02-10) }
    ],
    otherData: ...
  },
  { 
    _id: 2,
    eventType: "sports", 
    eventDates: [ 
      //Scheduled to 5st of February... 
      { created: ISODate(2015-01-10), eventDate: ISODate(2015-02-05) }
    ],
    otherData: ...
  },
  { 
    _id: 3,
    eventType: "sports", 
    eventDates: [ 
      //Scheduled to 1st of March... 
      { created: ISODate(2015-01-20), eventDate: ISODate(2015-03-01) }
      //...but later rescheduled to 20th of February.
      { created: ISODate(2015-01-30), eventDate: ISODate(2015-02-20) }
    ],
    otherData: ...
  }
]

我预计 2015 年 2 月 15 日的输出如下:

[ {
     eventType: "sports",
     previousEvent: { 
          _id: 1,
          eventType: "sports", 
          eventDates: [ 
              { created: ISODate(2015-01-01), eventDate: ISODate(2015-02-01) },
              { created: ISODate(2015-01-02), eventDate: ISODate(2015-02-10) }
          ],
          otherData: ...
     },
     nextEvent: {
          _id: 3,
          eventType: "sports", 
          eventDates: [ 
             { created: ISODate(2015-01-20), eventDate: ISODate(2015-03-01) }
             { created: ISODate(2015-01-30), eventDate: ISODate(2015-02-20) }
          ],
          otherData: ...
     }
  }

对于 2015 年 3 月 1 日的日期,我会像以前一样拥有第三个事件,而下一个事件将为空(不再安排更多事件)。 类似地,对于日期 2015-02-01,我会将第二个事件作为下一个事件,而前一个事件将为空(没有过去的事件)。

我的尝试

第一步是找到活动的当前日期。为此,我将首先$unwind 所有日期,以便稍后选择最新的:

db.getCollection("Events").aggregate([
  {$unewind: "$eventDates" }
])

=> { result: [
       { _id: ...., eventDates: { created: ISODate(...), eventDate: ISODate(...) }, otherData: ...},
       { _id: ...., eventDates: { created: ISODate(...), eventDate: ISODate(...) }, otherData: ...},
       { _id: ...., eventDates: { created: ISODate(...), eventDate: ISODate(...) }, otherData: ...}
     ], ok: 1 } 

接下来,我会尝试把$group$max的所有旧的eventDates都扔掉

db.getCollection("Events").aggregate([
  {$unwind: "$eventDates" },
  {$group: { _id: "$_id", eventDate: { $max: "$eventDates.created" }}
])

=> { result: [
     { _id: ...., eventDate: ISODate(...) },
     { _id: ...., eventDate: ISODate(...) }
  ], ok: 1 }

但是现在,我只有每个事件的 id 以及最后一次设置事件日期的时间。我没有事件日期本身,也没有其他事件数据。

如何告诉$group-step 从具有最高@eventDates.created-value 的文档中返回所有内容?

第二步$project eventDate 放入一个新的“过去是”字段。我猜这很简单。

db.getCollection("Events").aggregate([
  {$unwind: "$eventDates" },
  {$project: { event: "$$ROOT", inThePast: {$lt: ["$eventDate.eventDate", new Date()]}}},
  {$group: { _id: "$_id", eventDate: { $max: "$eventDates.created" }}
])

第三步是新的挑战。对于每个 eventType,我想要日期最高的过去事件,以及最低日期的未来事件....

我想最终得到的是一个类似于这个的结构:

 { result: [
    {  eventType: ....,
       previous: { _id: ...., eventDate: ISODate(...), otherData: ...},
       next: { _id: ...., eventDate: ISODate(...), otherDate: ....}
    }
   ], ok: 1 }

我在这方面完全正确吗?

【问题讨论】:

  • 所以“简而言之”,如果我说得对的话。您想使用输入日期(或当前日期)进行测试并找到在该日期之前和之后“立即”发生的事件?或者只是组中最小和最大的?真正好的问题会给出样本数据和预期的结果。它既清晰又给我们一些可玩的东西:)
  • 不得不说这实际上可能是一个“规范问题”,即如何从给定的匹配点找到“上一个”和“下一个”项目,如果只是措辞比它更好,并且带有清晰的样品和结果。自上次结果以来,我一直在研究这个一个多小时。但就目前而言,除非可以编辑该问题,否则这个问题(以及我认为我现在接近的答案)将逐渐消失。
  • 今晚晚些时候我会花一些时间来解决这个问题,以使事情更清楚。就像制定问题并向样本添加实际时间戳一样。够了吗?
  • 好吧,看看您是否可以将它与现在给出的答案相匹配,因为这应该可以解决问题。我至少冒昧地选择了一个更合适的标题。而且我猜你至少应该为一个真正难以解决的问题投赞成票。
  • 我试图更好地表述它并添加了一些示例数据以更清楚地说明我要解决的问题。

标签: mongodb mongodb-query aggregation-framework


【解决方案1】:

现在我们已经明确了目标,即从给定日期获取事件的“下一个”发生日期以及事件的“上一个”发生,因此清理先前的响应。因此,这些是分组中那些在找到的“前一个”和“后一个”数据中与查询日期“最近”的事件。

这是一个最小的数据示例,显示了具有相同架构模式的两条记录:

[
    {
        "eventDates": [
            { "created": new Date("2015-08-01"), "eventDate": new Date("2015-08-01") },
            { "created": new Date("2015-08-02"), "eventDate": new Date("2015-08-02") },
            { "created": new Date("2015-08-03"), "eventDate": new Date("2015-08-03") },
        ],
        "eventType": "sport",
        "otherData": "something"            
    },
    {
        "eventDates": [
            { "created": new Date("2015-08-04"), "eventDate": new Date("2015-08-04") },
            { "created": new Date("2015-08-05"), "eventDate": new Date("2015-08-05") },
            { "created": new Date("2015-08-06"), "eventDate": new Date("2015-08-06") },
        ],
        "eventType": "sport",
        "otherData": "something"            
    }
]

目标是在事先不知道这些实际上只是样本中的某一天的情况下找到“上一个”和“下一个”事件的日期。我们将使用的日期是new Date("2015-08-03")

这是清单(大量评论以进行解释):

var currDate = new Date("2015-08-03");

db.getCollection("Events").aggregate([
    // Unwind the array
    { "$unwind": "$eventDates" },
    // Group and identify "next" and "prev", while pushing array of documents
    { "$group": {
        "_id": "$eventType",
        // All of the events in group with the difference from the date
        "all": {
            "$push": {
                "_id": "$_id",
                "eventDates": "$eventDates",
                "otherData": "$otherData",
                "diff": { "$subtract": [ currDate, "$eventDates.eventDate" ] }
            }
        },
        // The largest negative (smallest) differnce from the date
        "next": {
            "$max": {
                "$cond": [
                    { "$lt": [ currDate, "$eventDates.eventDate" ] },
                    { "$subtract": [ currDate, "$eventDates.eventDate" ] },
                    null       
                ]
            }
        },
        // The smallest positive (smallest) difference from the date
        "prev": {
            "$min": {
                "$cond": [
                    { "$gt": [ currDate, "$eventDates.eventDate" ] },
                    { "$subtract": [ currDate, "$eventDates.eventDate" ] },
                    null       
                ]
            }
        }
    }},
    // Filter array for "next" and "prev" only
    { "$project": {
        "all": {
            // filtering false from the result array
            "$setDifference": [
                // process each array member with conditions
                { "$map": {
                    "input": "$all",
                    "as": "el",
                    "in": {
                        "$cond": [
                            // Am I equal to one of the identified differences?
                            { "$or": [
                                { "$eq": [ "$$el.diff", "$prev" ] },
                                { "$eq": [ "$$el.diff", "$next" ] }
                            ]},
                            // If so then return me
                            {
                                "_id": "$$el._id",
                                "eventDates": "$$el.eventDates",
                                "otherData": "$$el.otherData",
                                "diff": "$$el.diff",
                                // and set my type my which one I matched
                                "type": {
                                    "$cond": [
                                        { "$eq": [ "$$el.diff", "$prev" ] },
                                        "prev",
                                        "next"
                                    ]
                                }
                            },
                            // if not then return false
                            false
                        ]
                    }
                }},
                // removing all false elements from array
                [false]
            ]
        }
    }},
    // Unwind the array
    { "$unwind": "$all" },
    // Group back pulling fields for "next" and "prev"
    { "$group": {
        "_id": "$_id",
        // matching prev element to the field
        "prev": {
            "$min": {
                "$cond": [
                    { "$eq": [ "$all.type", "prev" ] },
                    "$all",
                    null
                ]
            }
        },
        // matching next element to the field
        "next": {
            "$min": {
                "$cond": [
                    { "$eq": [ "$all.type", "next" ] },
                    "$all",
                    null
                ]
            }
        }
    }}
])

故障

  1. 第一步是$unwind,当然这里的想法是在不同文档的文档的数组内容中查找项目。理想情况下,您希望匹配一个日期范围以最小化首先处理的文档。

  2. 下一步是$group,除了要使用的一般分组键之外,还有一些目标:

    1. 我们想要分组中的所有可能的文档,因为您稍后想要这些数据,所以这里我们$push。我们还想计算从查询日期到项目上的“eventDate”的日期值的“差异”。这是一个$subtract,其中从另一个日期减去一个日期的结果是它们之间的毫秒数。这也将在以后使用。

    2. 您需要与当前日期输入的最大负差(最小的负差)。这是通过过滤那些日期为$lt 的日期来确定的,然后返回最大的日期$max 值。这是“下一个”项目差异。

    3. 您希望与当前日期输入的正差最小。这与$gt 查询的日期具有相同的过滤,然后返回$min 的最小值。这是“上一个”项目的区别。

  3. 在组中所有文档的数组就位后,您现在需要“过滤”这些文档,以便根据上一阶段的差异值返回“下一个”和“上一个”匹配项。

    我们这样做的方法是使用$map 运算符来检查数组值并确定匹配项,并使用$setDifference 运算符过滤掉我们返回false 而不是匹配中的文档的任何元素.

    处理将通过$cond 完成,它将返回一个文档或false$or 条件是测试元素是否匹配上一阶段的“prev”或“next”:

    { "$or": [
        { "$eq": [ "$$el.diff", "$prev" ] },
        { "$eq": [ "$$el.diff", "$next" ] }
    ]},
    

    文档返回中还有另一个$cond,它的工作是根据相同的基本匹配条件为“prev”或“next”“标记”一个“类型”。这将用于稍后阶段的分配。

  4. 现在数组中唯一剩下的应该是我们查询日期的“上一个”和“下一个”文档。为了将它们变成单数形式,您首先要再次$unwind。成本不高,因为每组只有两个文档。

  5. 最终分配在另一个$group 下,它有另一个$cond 将匹配的文档返回到之前设置的“类型”或不匹配的null 值。在每种情况下,“文档”值在词法上都被认为比null“小”,所以这里使用的累加器是$min。这将为响应中的每个“prev”和“next”字段返回一个奇异值。

结果

结果当然会将Date("2015-08-02")Date("2015-08-04") 的日期显示为从查询日期开始的上一个和下一个项目:

{
    "_id" : "sport",
    "prev" : {
        "_id" : ObjectId("55de01a4b64dc3c80673a58d"),
        "eventDates" : {
            "created" : ISODate("2015-08-02T00:00:00Z"),
            "eventDate" : ISODate("2015-08-02T00:00:00Z")
        },
        "otherData" : "something",
        "diff" : NumberLong(86400000),
        "type" : "prev"
    },
    "next" : {
        "_id" : ObjectId("55de01a4b64dc3c80673a58e"),
        "eventDates" : {
            "created" : ISODate("2015-08-04T00:00:00Z"),
            "eventDate" : ISODate("2015-08-04T00:00:00Z")
        },
        "otherData" : "something",
        "diff" : NumberLong(-86400000),
        "type" : "next"
    }
}

总结

对此的另一种看法是运行两个单独的聚合操作。无需将所有文档推入数组即可获得那些“下一个”和“上一个”差异值。然后当然要在第二个聚合操作中使用这些值来过滤掉仅与每个分组边界上的那些条件匹配的文档,并将它们作为“下一个”和“上一个”文档返回。

实际上可能是多个查询并行运行。但这就是为什么这里一般解释逻辑的原因,因此它提供了开发流程以适应您的需求的指南。

【讨论】:

  • 哇!真的很期待今晚晚些时候对其进行测试,但它看起来很有希望:-) 谢谢。
  • @Vegar 我只关心你的真正意思。您的问题并不清楚“上一个”和“下一个”到底应该是什么。
  • 对此我很抱歉。目前,我想展示“刚刚发生的事情”和“接下来会发生什么”。我做了一个快速测试,看起来你成功了。但是,我还没有完成。问题及其样本数据已大大简化,因此我想我需要将其与其他几个组和过滤器结合起来。但我想我从你的回答中学到了很多,所以希望我能做到。或不。 ;-)
  • @Vegar 好的,如果你确定的话。但是$first$last 并不意味着“紧随其后”,而只是您应该期望的“第一个”和“最后一个”。 “之前”和“之后”是一个不同的问题,我实际上是在要求澄清。这也不是一件简单的事情。
  • 只要对数据进行排序 - 为什么“last”不是条件为真的最后一个事件?是否可以将“最大值”与相同的条件结合起来?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-14
  • 1970-01-01
  • 2011-07-24
  • 2016-03-05
  • 2013-06-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多