将数组的至少 "N" 个元素与条件列表匹配

Match at least "N" elements of an array to a list of conditions

我有以下场景: 我的 mongo collection 之一有以下格式的文档:

user: "test",
tracks: [{artist: "A", ...}, {artist: "B", ...}, ..., { artist: "N", ...}]

我想提取所有曲目,其艺术家在给定数组中 arr。为此,我使用以下查询(效果很好)。

collection.find({ tracks: { $elemMatch: { artist: { $in: arr }}}})

但是,现在我想修改查询,以便 returns 只有 collection 中至少有 3 位来自 [=13] 的不同艺术家表演曲目的文档=]数组。我怎样才能做到这一点(除了在从数据库返回结果后过滤结果,这不是一个选项)?

你的问题对我来说有两种可能性,但也许有一些解释可以帮助你入门。

首先我需要向你解释一下,你误解了$elemMatch的意图,在这种情况下它被滥用了。

$elemMatch的想法是创建一个"query document",它实际上应用于数组的元素。目的是在数组中的文档上设置 "multiple conditions" 以便在成员文档中离散地匹配它,而不是在外部文档的整个数组中。即:

{
   "data": [
       { "a": 1, "b": 3 },
       { "a": 2, "b": 2 }
   ]
}

下面的查询将有效,即使该数组中没有实际的单个元素匹配,但整个文档匹配:

db.collection.find({ "data.a": 1, "data.b": 2 })

但是要检查实际元素是否符合这两个条件,您可以在此处使用 $elemMatch:

db.collection.find({ "data": { "a": 1, "b": 2 } })

因此该示例中没有匹配项,它只会匹配具有这两个元素的特定数组元素。


现在我们已经 $elemMatch 解释了,这是您的简化查询:

db.collection.find({ "tracks.artist": { "$in": arr } })

更简单,它的工作原理是通过单个字段查看所有数组成员,并 returning 文档中的任何元素至少包含这些可能结果中的一个。

但不是你问的,等等你的问题。如果您通读最后一条语句,您应该会意识到 $in is actually an $or 条件。它只是针对文档中同一元素询问 "or" 的缩写形式。

考虑到这一点,您所要求的核心是包含所有 "three" 值的 "and" 操作。假设您在测试中只发送 "three" 项,那么您可以使用 $and which is in the shortened form of $all:

的形式
db.collection.find({ "tracks.artist": { "$all": arr } })

那只会 return 您的文档中包含该数组成员中的元素与测试条件中指定的 "all" 元素匹配的文档。这很可能是你想要的,但在某些情况下,你当然想要指定一个列表,比如 "four or more" 艺术家来测试并且只想要 "three" 或其中的一些更少的数字,在在这种情况下 $all 运算符太简洁了。

但是有一种合乎逻辑的方法可以解决这个问题,它只需要对基本查询不可用但可用于 aggregation framework:

的运算符进行更多处理
var arr = ["A","B","C","D"];     // List for testing

db.collection.aggregate([
    // Match conditions for documents to narrow down
    { "$match": {
        "tracks.artist": { "$in": arr },
        "tracks.2": { "$exists": true }      // you would construct in code
    }},

    // Test the array conditions
    { "$project": {
        "user": 1,
        "tracks": 1,                         // any fields you want to keep
        "matched": {
            "$gte": [
                 { "$size": {
                     "$setIntersection": [
                         { "$map": {
                             "input": "$tracks",
                             "as": "t",
                             "in": { "$$t.artist" }
                         }},
                         arr
                     ]
                 }},
                 3
             ]
        }
    }},

    // Filter out anything that did not match
    { "$match": { "matched": true } }
])

第一阶段像以前一样执行标准查询 $match condition in order to filter the documents to only those that are "likely" to match the conditions. The logical case here is to use $in,它会找到那些文档,其中至少有一个元素存在于 "test" 数组中,并且至少存在于一个成员中文档中的字段自己的数组。

下一个子句是您理想情况下应该在代码中构建的内容,因为它与数组的 "length" 相关。这里的想法是你想要至少 "three" 匹配然后你在文档中测试的数组必须至少有 "three" 元素才能满足这个要求,所以没有必要用 [=102 检索文档=] 或更少的数组元素,因为它们永远无法匹配 "three".

由于所有 MongoDB 查询本质上只是一种数据结构的表示,因此构建起来非常容易。即,对于 JavaScript:

var matchCount = 3;    // how many matches we want

var match1 = { "$match": { "tracks.artist": { "$in": arr } } };

match1["$match"]["tracks."+ (matchCount-1)] = { "$exits": true };

那里的逻辑是 "dot notation" 形式 $exists 测试指定索引 ( n-1 ) 处是否存在元素,它需要存在于数组中至少是那个长度。

其余的缩小范围理想情况下使用 $setIntersection method in order to return the matched elements between the actual array and the tested array. Since the array in the document does not match the structure for the "test array" it needs to be transformed via the $map 操作,该操作仅设置为每个数组元素的 return "artist" 字段。

随着这两个数组的 "intersection" 的生成,最终对应用测试的公共元素结果列表的 $size 进行测试,以查看 "at least three"这些元素被发现是共同的。

最后你只是 "filter out" 使用 $match 条件的任何不正确的东西。


理想情况下,您使用 MongoDB 2.6 或更高版本才能使用这些运算符。对于2.2.x和2.4.x的早期版本,还是可以的,只是多了一点工作和处理开销:

db.collection.aggregate([
    // Match conditions for documents to narrow down
    { "$match": {
        "tracks.artist": { "$in": arr },
        "tracks.2": { "$exists": true }      // you would construct in code
    }},

    // Unwind the document array
    { "$unwind": "$tracks" },

    // Filter the content
    { "$match": { "tracks.artist": { "$in": arr } }},

    // Group for distinct values
    { "$group": {
        "_id": { 
           "_id": "$_id",
           "artist": "$tracks.artist"
        }
    }},

    // Make arrays with length
    { "$group": {
        "_id": "$_id._id",
        "artist": { "$push": "$_id.artist" },
        "length": { "$sum": 1 }
    }},

    // Filter out the sizes
    { "$match": { "length": { "$gte": 3 } }}
])