MongoDB嵌套结构的查询与筛选

作为文档(Document)数据库,当我们在与MongoDB数据模型打交道时,不可避免会处理嵌套的数据结构。可以将嵌套数据结构的根视为一个Aggregate,针对一些业务场景,这种建模是合理的。然而在面对查询的一些查询条件时,实现起来就有些捉急了。

让我们先来看看一个实例数据模型,它是一条精简了的舆情数据,每条舆情数据包含了多条观点数据:

{
    "_id" : ObjectId("586237369d88f67788841014"),
    "cid" : "4036689846489797",
    "sv_id" : "WEIBOR201612170000088787",
    "publish_time" : "2016-10-24",
    "publish_authorname" : "洋芋金豆",
    "data_source_id" : "WEIBOR",
    "source_url" : "http://weibo.com/1006550592/Eej3KmOSj?refer_flag=1001030103_?type=comment",
    "create_date" : "2016-12-27 17:41:10",
    "create_by" : "SYSTEM",
    "fsource":"互联网数据",
    "ssource":"电商",
    "tsource":"京东",
    "spec_source":"否",
    "voice_vol":"1020",
    "viewpoints" : [ 
        {
            "viewpoint_id" : "VPAD0008E002",
            "context" : "感觉挺好",
            "product_name" : "x5",
            "viewpoint_emotion":"正向",
            "viewpoint_context":"感觉挺好的"        
    }, 
    {
            "viewpoint_id" : "VPAD0008E003",
            "context" : "不错",
            "product_name" : "x7",
            "viewpoint_emotion":"正向",
            "viewpoint_context":"还不错"    
    },
    {
            "viewpoint_id" : "VPAD0008E004",
            "context" : "太差",
            "product_name" : "x6",
            "viewpoint_emotion":"负向",
            "viewpoint_context":"太差劲"    
    }   
  ]
}

我们的查询要求其实很简单,就是能够根据嵌套结果中ViewPoint的viewpoint_id查询符合条件的记录。使用find结合点表示法就可以完成查询:

db.consensus.find({"viewpoints.viewpoint_emotion": "正向"})

然而结果却不如人意。因为MongoDB会将内嵌文档中的数据视为一条完整的记录。如果按照这种方式查询,则会将满足viewpoint_emotion为正向的所有舆情信息包含观点信息都返回。换言之,如果某条舆情包含了多条观点,且至少有一条观点数据的viewpoint_emotion值为正向,那么不管其他观点的值,都会作为查询结果被返回。

这自然不是我期望的结果。

直接使用find进行查询还有另外一个问题,那就是针对内嵌的数组数据进行多条件查询时,并不会针对内嵌的单条记录进行组合查询。例如:

db.consensus.find({
    "viewpoints.viewpoint_emotion": "正向",
    "viewpoints.product_name": "x6"    
})

检查前面的示例数据,在舆情中的观点信息,其实没有任何一条同时满足find中的条件。然而MongoDB中的find机制是,如果有一条观点信息满足了viewpoint_emotion条件,另一条观点信息满足了product_name条件,也认为该舆情信息符合查询条件。

解决的办法是使用$elemMatch

db.consensus.find({
    "viewpoints": {
        $elemMatch: {
            "viewpoint_emotion": "正向",
            "product_name": "x6" 
        }   
    }
})

MongoDB官方文档对$elemMatch的解释是:

The $elemMatch operator matches documents that contain an array field with at least one element that matches all the specified query criteria.

仔细分析我们的需求,当我们对舆情以及观点进行查询时,其实隐含了一个需求,就是对不符合条件的观点进行filter的工作。$elemMatch满足了针对数组元素进行组合查询,但返回的结果并没有针对查询条件对数组元素进行过滤。

aggregate中的$filter可以实现过滤功能:

db.consensus.aggregate([
    {$match: {"viewpoints.viewpoint_emotion": "正向", "viewpoints.product_name":"x6"}},
    {
      $project: {
        "sv_id": 1,
        "viewpoints": {
          "$filter": {
            input: "$viewpoints",
            as: "v",
            cond: { $and: [
               {$eq: ["$$v.viewpoint_emotion", "正向"]},
               {$eq: ["$$v.product_name", "x6"]}
           ]}
          }
        }
      }
    }
])

通过在filter中设置cond,就可以将不满足条件的观点信息过滤掉。至于查询的条件,则是由aggregate的$match来完成的。

然而问题来了,虽然$filter实现了过滤功能,但$match却无法做到针对数组的$elemMatch的效果。如果按照上面的代码对舆情数据进行查询,那么只要观点数据中任何一条满足观点情况为“正向”,而另外一条数据满足产品名称为“x6”,都可以视为是满足匹配条件。这样就会有许多不符合我们需求的舆情记录被保留,即使这个时候对观点数据进行了正确的filter,但匹配的数据记录不对,最终我们得到的结果仍然是不对的。

排除直接针对查询结果进行编码处理的方法,还有一个解决办法是将文档中的内嵌数组数据执行aggregate中的unwind。$unwind操作符能够将指定的数组元素拍平。以舆情数据为例,假设舆情数据A包含了3条观点数据,舆情数据B包含了2条观点数据,执行$unwind之后,返回的就是5条既包含了舆情信息又包含了观点信息的扁平记录。

变成扁平记录之后,就可以直接对这个结果进行match操作了:

db.consensus.aggregate([
    {$unwind: "$viewpoints"},
    {$match: {"viewpoints.viewpoint_emotion": "正向", "viewpoints.product_name":"x6"}},

注意,虽然数组数据被拍平,但内嵌的文档结构仍然存在,因而还是需要用点表示法进行match。如果你想要的查询结果本身就是这样一个扁平的结果,使用$unwind就能完全满足需求了。

2017-01-19 11:01345MongoDB