问题描述
tl;dr:我在一个集合中有 400 万个文档,我使用 $match
过滤到 6000,然后使用 $project
执行操作。 $project
需要 60 秒!如果我在仅包含 6000 个文档的模拟数据库上运行相同的 $project
操作,则需要 0.5 秒。为什么会出现这种不匹配以及如何解决?
UPDATE:花费 60 秒的不是 $project
步骤,而是我在它之后放置的任何步骤。例如,另一个 $match
过滤 $project
的输出以获得高于 X 的点积。
我正在为 Python 使用 pymongo 库,并且我在 Windows 上安装了 MongoDb 版本 v4.4.3。 我在一个集合中有 400 万个文档。每个文件都是这样的:
{
_id: ObjectId("...")
document_id: 0,vector: Array
hash_value: 123
}
其中 vector
是一个包含 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
,$project
,$match
管道(这里 $match
将所有 6000 个文档传递到下一步,因为它们都匹配输入hash_value
),只需 0.5 秒!
似乎我可以通过过滤到可接受的大小来计算快速点积,但是我无法以任何方式使用结果,因为它们加载速度太慢。 >
我的问题是:
- 为什么会这样? (如果可能的话,我想解释一下幕后发生的事情)
- 有什么办法可以防止这种情况发生吗?
- 拆分不同集合中的文档会有帮助吗?
- 如果您认为使用 MongoDB 无法实现良好的性能,那么您知道我可以快速对数百万文档执行此类操作的其他数据库吗?
- 我的数据结构有误吗?为了计算查询和文档之间的点积,是否有更好的方法来构造我的数据?
以下是管道:
{
"$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}}
}
注意事项:
- 我使用
$in
是因为实际上可能有多个输入hash_value
- 我已验证第一个
$match
步骤实际上已正确过滤掉 - 我已经看到了 this 个问题。我遵循了答案中的一些提示(使用 allowdiskUse = True,索引我用于
$match
步骤的字段),但其他评论似乎都与我的案例无关。 - 如果我只保留第一个
$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"),...
}
预先感谢您的慷慨帮助。
解决方法
我找到了解决方案:
第一个 $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}}
}