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

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

本文关键字:元素 条件 列表 数组      更新时间:2023-09-26

>我有以下情况:我的一个 mongo 收藏有以下格式的文档:

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

我想提取所有曲目,其艺术家位于给定的数组arr中。为此,我使用以下查询(工作正常)。

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

但是,现在我想修改查询,以便它仅返回集合中那些至少由 arr 数组中的 3 位不同艺术家执行的曲目的文档。我如何实现这一点(除了在从数据库返回结果后过滤结果,这不是一个选项)?

你的问题对我来说有两种可能性,但也许有一些解释可以让你开始。

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

$elemMatch的想法是创建一个"查询文档",该文档实际上应用于数组的元素。目的是在数组中的文档上具有"多个条件",以便在成员文档中离散地匹配它,而不是在外部文档的整个数组中。即:

{
   "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 } })

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

但不是你在问什么,所以你的问题。如果你通读了最后一句话,你应该意识到$in实际上是一种$or条件。它只是在文档中的同一元素上询问"or"的缩写形式。

考虑到这一点,您要求的核心是包含所有"三个"值的"and"操作。假设您在测试中只发送"三个"项目,那么您可以使用$and形式,其缩写形式为 $all

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

这只会返回在该数组成员中具有与测试条件中指定的"所有"元素匹配的元素的文档。这很可能是你想要的,但在某些情况下,你当然想指定一个列表,比如说,"四个或更多"艺术家进行测试,并且只想要"三个"或更少的数字,在这种情况下,$all运算符太简洁了。

但是有一个合乎逻辑的方法来解决这个问题,它只需要对基本查询不可用但可用于聚合框架的运算符进行更多的处理:

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条件,以便将文档筛选为仅那些"可能"与条件匹配的文档。这里的逻辑情况是像以前一样使用$in它将找到那些文档,其中"test"数组中至少存在一个元素存在于文档自己的数组中的至少一个成员字段中。

理想情况下,下一个子句是您应该在代码中构建的东西,因为它与数组的"长度"有关。这里的想法是你想要至少"三个"匹配,那么你在文档中测试的数组必须至少有"三个"元素才能满足这一点,所以检索具有"两个"或更少数组元素的文档是没有意义的,因为它们永远无法匹配"三个"。

由于所有MongoDB查询本质上只是数据结构的表示,因此它使得构建起来非常容易。

var matchCount = 3;    // how many matches we want
var match1 = { "$match": { "tracks.artist": { "$in": arr } } };
match1["$match"]["tracks."+ (matchCount-1)] = { "$exits": true };

那里的逻辑是"点符号"形式,$exists测试指定索引(n-1)处是否存在元素,并且数组至少需要具有该长度。

理想情况下,缩小范围的其余部分使用 $setIntersection 方法,以便返回实际数组和测试数组之间的匹配元素。由于文档中的数组与"测试数组"的结构不匹配,因此需要通过$map操作进行转换,该操作设置为仅从每个数组元素返回"artist"字段。

当这两个数组的"交集"被制作时,最后测试所得到的公共元素列表的$size,其中应用测试以查看发现这些元素中的"至少三个"是共同的。

最后,您只需使用$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 } }}
])