首页 > 解决方案 > 即使在使用 $match 过滤后,mongodb (pymongo) 聚合也很慢

问题描述

tl; dr:我在一个集合中有 400 万个文档,我过滤到 6000 个,$match然后使用$project. 需要$project60秒!如果我在$project仅包含 6000 个文档的模拟数据库上运行相同的操作,则需要 0.5 秒。为什么会出现这种不匹配以及如何解决?

更新:这不是$project需要 60 秒的步骤,而是我在它之后放置的任何步骤。例如另一个$match过滤输出$project以获得高于 X 的点积。

我正在为 Python 使用 pymongo 库,并且在 Windows 上安装了 MongoDb 版本 v4.4.3。我的集合中有 400 万份文档。每个文件是这样的:

{
_id: ObjectId("...")
document_id: 0,
vector: Array
hash_value: 123
}

wherevector是一个包含 300 个从 -1 到 1 的双精度数的数组。我在hash_value.

给定一个输入数组,在 (-1,1) 范围内也有 300 个元素,我想计算输入数组和集合中每个数组之间的点积hash_value
我使用了一个聚合管道(请参阅问题末尾的完整管道),它可以:

$match步骤从 400 万个文档过滤到大约 6000 个文档,该$project步骤非常快,但是我之后包含的任何步骤(例如第二个$match`` **takes 1 minute** to run! Even if I just return the results from $project``` 并在 Python 中迭代它们都需要 1 分钟。

为了测试,我还创建了一个只有 6000 个文档和一个hash_value. 如果我在这个 6000 个文档的模拟数据库上运行相同$match的精确管道(这里将所有 6000 个文档传递到下一步,因为它们都与输入匹配),只需要 0.5 秒$project$match$matchhash_value

似乎我可以通过过滤到可接受的大小来计算快速点积,但是我不能以任何方式使用结果,因为它们加载速度太慢。

我的问题是:

  1. 为什么会这样?(如果可能的话,我想解释一下幕后发生的事情)
  2. 有什么办法可以防止这种情况吗?
  3. 将文档拆分到不同的集合中会有帮助吗?
  4. 如果您认为使用 MongoDB 无法实现良好的性能,您是否知道其他数据库可以让我对数百万个文档快速执行这种操作?
  5. 我的数据结构错了吗?为了计算查询和文档之间的点积,是否有更好的方法来构建我的数据?

下面是管道:

{
    "$match": {"$expr": {"$in": ["$hash_value", [123]]}}
},
{"$project": {
    "_id":0,
    "document_id": 1,
    "dotProduct": {
        "$let": {"vars": {"queryVector": [0.2,-0.12,-0.9,....,0.14,0.56]}, "in":{
        "$reduce": {
            "input": { "$range": [ 0, { "$size": "$vector" }] },
            "initialValue": 0,
            "in": { "$add": [ "$$value", { "$multiply": [ { "$arrayElemAt": [ "$vector", "$$this" ] }, { "$arrayElemAt": [ "$$queryVector", "$$this" ] } ] } ] }
                    }
                }
            }
        }            
    }
},
{   
    "$match":{"dotProduct":{"$gt":0.5}}
}

笔记:

  1. 我使用$in是因为实际上可能有多个输入hash_value
  2. 我已经验证了$match第一步实际上是正确过滤掉的
  3. 我已经看到了这个问题。我遵循了答案中的一些提示(使用allowDiskUse = True,索引我用于该$match步骤的字段),但其他评论似乎与我的情况无关。
  4. 如果我只保留第一个$match,获取结果并在 Python 中迭代它们也需要大约 1 分钟。

这是探查器日志:


                "allowDiskUse" : true,
                "cursor" : {

                },
                "$readPreference" : {
                        "mode" : "secondaryPreferred"
                },
                "$db" : "similarity"
        },
        "keysExamined" : 0,
        "docsExamined" : 4000000,
        "cursorExhausted" : true,
        "numYield" : 4789,
        "nreturned" : 1,
        "queryHash" : "9A0FCEC0",
        "planCacheKey" : "9A0FCEC0",
        "locks" : {
                "ReplicationStateTransition" : {
                        "acquireCount" : {
                                "w" : NumberLong(4792)
                        }
                },
                "Global" : {
                        "acquireCount" : {
                                "r" : NumberLong(4792)
                        }
                },
                "Database" : {
                        "acquireCount" : {
                                "r" : NumberLong(4791)
                        }
                },
                "Collection" : {
                        "acquireCount" : {
                                "r" : NumberLong(4791)
                        }
                },
                "Mutex" : {
                        "acquireCount" : {
                                "r" : NumberLong(2)
                        }
                }
        },
        "flowControl" : {

        },
        "storage" : {
                "data" : {
                        "bytesRead" : NumberLong("15295638272"),
                        "timeReadingMicros" : NumberLong(57621295)
                }
        },
        "responseLength" : 176,
        "protocol" : "op_query",
        "millis" : 63990,
        "planSummary" : "COLLSCAN",
        "ts" : ISODate("2021-03-26T10:06:56.185Z"),
        ...
}

预先感谢您的慷慨帮助。

标签: mongodbsearchaggregation-frameworkpymongodot-product

解决方案


我找到了解决方案:$match使用的第一步$in$eq. 使用$eq将响应时间减少到 1.5 - 2.5 秒。我无法弄清楚的原因是延迟执行导致它“看起来”$match很快。换句话说,$match>$project似乎运行得很快,直到我尝试从光标中获取结果。在您从游标中获取结果之前,MongoDB 实际上不会运行管道,这就是为什么很难确定管道中花费时间最长的步骤的原因。

新的管道变成

{
    "$match": { "hash_value" : 123 }
},
{"$project": {
    "_id":0,
    "document_id": 1,
    "dotProduct": {
        "$let": {"vars": {"queryVector": [0.2,-0.12,-0.9,....,0.14,0.56]}, "in":{
        "$reduce": {
            "input": { "$range": [ 0, { "$size": "$vector" }] },
            "initialValue": 0,
            "in": { "$add": [ "$$value", { "$multiply": [ { "$arrayElemAt": [ "$vector", "$$this" ] }, { "$arrayElemAt": [ "$$queryVector", "$$this" ] } ] } ] }
                    }
                }
            }
        }            
    }
},
{   
    "$match":{"dotProduct":{"$gt":0.5}}
}


推荐阅读