在MongoDB中合并文档
我有一个很大的MongoDB集合,大约有50万条记录。
它的结构是这样的:
{'_id': '.....',
'passid':'ag325gdtew',
'text': '.......',
'count': '.......',
'title': '......',
'body': '.......'
}
在很多文档中,passid
这个字段是相同的,我想用不同的方法把它们合并在一起。
我想要:
- 保持相同的
passid
- 把每个文档中的文本和标题合并在一起(文本+标题),然后把最终的字符串放在新文档的一个字段里(文本1+文本2+文本3)
- 创建一个字段列表,里面是每个字段的计数 [计数1, 计数2, 计数3]
- 删除body字段
所以新文档会是这样的:
{'_id': '.....',
'passid':'ag325gdtew',
'text': '.......', (string)
'count': ['..','...','..'] (list)
}
目前,我是用Python来做这个,但文件太大了,脚本运行了好几个小时。
我已经做了以下工作:
- 用聚合和分组来获取一个唯一的
passid
列表 - 遍历这个列表中的每个
passid
- 使用find(
passid
)来获取所有具有相同passid
的文档的游标 - 用Python来合并字符串和列表
- 删除旧的文档
- 保存新的文档
正如我所说,这真的很耗时间。你知道有没有更快的方法吗?
这是代码:
passids= db.collection.aggregate({ "$group": {"_id": '$passid'}})
for i in passids['result']:
doc = {}
doc['passid'] = i['_id']
documents = db.collection.find({"passid": i['_id']})
doc['count'] = []
doc['text'] = ""
for d in documents:
doc['text'] = doc['text'] + " " + d['text']
doc['text'] = doc['text'] + " " + d['title']
doc['count'].append(d['count'])
db.collection.remove(d)
db.collection.save(doc)
2 个回答
根据我的经验,使用Mongo数据库时,操作慢的主要原因是与数据库之间的往返次数太多。所以,尽量减少对数据库的调用次数。如果你的文档足够小(就像你举的例子那样),整个集合都能放进内存里,那么你可以通过一次性插入多个文档和删除多个文档来节省大量时间:
passids= db.collection.aggregate({ "$group": {"_id": '$passid'}})
new_docs = []
for i in passids['result']:
doc = {}
doc['passid'] = i['_id']
documents = db.collection.find({"passid": i['_id']})
doc['count'] = []
doc['text'] = ""
for d in documents:
doc['text'] = doc['text'] + " " + d['text']
doc['text'] = doc['text'] + " " + d['title']
doc['count'].append(d['count'])
new_docs.append(doc)
# Instead of removing all the documents one by one,
# dropping the collection is much faster
db.collection.drop()
db.collection.insert(new_docs)
为了安全起见,我建议先把文档保存到一个新的集合里,等确认一切正常后再删除旧的集合。
如果你想减少你的应用程序和数据库之间的网络流量,最好的办法通常是尽量让代码在离数据库尽可能近的地方运行,这样速度会更快。
如果这样做不可能,而且只适合偶尔使用的操作,你可以考虑在服务器上运行代码,使用 db.eval()
。
警告 在考虑使用
db.eval()
之前,你必须仔细阅读手册页面。虽然这是最快的解决方案,但有几个重要的缺点需要注意:
- 在执行期间,这会对数据库获取写锁。
- 由于JavaScript的单线程特性,除了获取写锁之外,其他使用JavaScript解释器的任务,比如“mapReduce”作业,将无法运行。
- 在分片集群上无法运行,如果你的托管服务使用了身份验证,用户账户需要比基本的读写权限更高的特殊权限才能执行这个任务。
考虑完以上所有内容后,你可以抬起头,承认这个方法的存在,然后继续前进。
只要你能处理输出到不同的集合,你可以开始使用 mapReduce
,这会简化逻辑。
你可以定义一个映射器:
var mapper = function() {
var passid = this.passid;
delete this["_id"];
delete this["body"];
emit( passid, this );
};
然后定义一个归约器:
var reducer = function(key,values) {
var reducedObject = {
"text": "",
"count": []
};
values.forEach(function(value) {
reducedObject.text = reducedObject.text + " " + value.text;
reducedObject.text = reducedObject.text + " " + value.title;
reducedObject.push( value.count );
});
return reducedObject;
};
接着你可以运行mapReduce操作:
db.collection.mapReduce(
mapper,
reducer,
{
"out": { "replace": "newcollection" }
}
)
由于mapReduce的输出格式,你可能不想要这个作为最终输出,所以你可以这样修改:
db.eval(function() {
db.newcollection.find().forEach(function(doc) {
var newDoc = {};
for ( var k in doc.values ) {
newDoc[k] = doc.values[k];
}
db.newcollection.update({ _id: doc._id }, newDoc );
});
})
这样会把结果放入一个重新整理过的集合中,你甚至可以考虑在数据库之间移动这个结果,以避免锁定问题。这可能仍然需要你将其与原始集合交换,但有办法做到这一点。
作为另一种选择,你可以直接运行 db.eval()
操作。这基本上将过程转换为相应的JavaScript:
db.eval(function() {
var lastid = "";
var counter = 0;
var text = "";
var count = [];
db.collection.find().forEach(function(doc) {
if ( (doc.passid != lastid) && (counter != 0) ) {
db.collection.update(
{ "_id": doc._id },
{
"passid": lastid,
"text": text,
"count": count
}
);
text = "";
count = [];
}
text = text + " " + doc.text;
text = text + " " doc.title;
count.push( doc.count );
counter++;
lastid = passid;
});
})
大批量修改文档通常不是件好事,但有一些方法可以处理这个问题,并保持所有操作在服务器上进行。