聚合期间发生Mongo错误16996 - 生成的文档太大

时间:2015-12-29 11:28:41

标签: mongodb database-design aggregation-framework

我正在解析Wikipedia转储,以便使用面向链接的元数据。其中一个集合名为文章,它采用以下形式:

{
    _id : "Tree",
    id: "18955875",
    linksFrom: " [
        {
        name: "Forest",
        count: 6
        },
        [...]
    ],
    categories: [
        "Trees",
        "Forest_ecology"
        [...]
    ]
}

linksFrom 字段存储了本文指向的所有文章,以及发生的次数。接下来,我想创建另一个字段 linksTo 以及指向本文的所有文章。一开始,我浏览了整个集合并更新了每篇文章,但由于其中很多文章需要花费太多时间。为了性能的目的,我转而使用聚合,并在较小的设置上进行了尝试 - 就像魅力一样,与旧方法相比,速度非常快。聚合管道如下:

db.runCommand(
    {
        aggregate: "articles",
        pipeline : [
            {
                $unwind: "$linksFrom"
            },
            {
                $sort: { "linksFrom.count": -1 }
            },
            {
                $project:
                {
                    name: "$_id",
                    linksFrom: "$linksFrom"
                }
            },
            {
                $group:
                {
                    _id: "$linksFrom.name",
                    linksTo: { $push: { name: "$name", count: { $sum : "$linksFrom.count" } } },
                }
            },
            {
                $out: "TEMPORARY"
            }
        ] ,
        allowDiskUse: true
    }
)

然而,在英文维基百科的大型数据集中,我在几分钟后收到以下错误:

{
    "ok" : 0,
    "errmsg" : "insert for $out failed: { connectionId: 24, err: \"BSONObj size: 24535193 (0x1766099) is invalid. Size must be between 0 and 16793600(16MB) First element: _id: \"United_States\"\", code: 10334, n: 0, ok: 1.0 }",
    "code" : 16996
}

据我所知,有太多文章链接到United_States文章,相应文档的大小超过16MB,目前差不多是24MB。不幸的是,我甚至无法检查是否属于这种情况(错误信息有时会出现问题)...因此,我试图更改模型,以便文章之间的关系存储在ID而不是比起长名,但我担心这可能还不够 - 特别是因为我的计划是为以后的每篇文章合并两个馆藏 ......

问题是:有没有人有更好的主意?我不想尝试增加限制,我宁愿考虑将这些数据存储在数据库中的不同方法。

Markus评论后

更新

Markus是正确的,我正在使用SAX解析器,事实上,我已经以类似的方式存储了所有链接。除了文章之外,我还有三个系列 - 一个包含链接,另外两个包含标签词干标签。第一个按以下方式存储转储中出现的所有链接

{
    _id : "tree",
    stemmedName: "tree",
    targetArticle: "Christmas_tree"
}

_id 存储用于表示给定链接的文本, stemmedName 表示词干_id, targetArticle 标记此文本指向的文章。我正在将 sourceArticle 添加到这个中间,因为它显然是一个好主意。

第二个集合标签包含以下文档:

{
    _id : "tree",
    targetArticles: [
        {
            name: "Christmas_tree",
            count: 1
        },
        {
            name: "Tree",
            count: 166
        }
        [...]
    ]
}

第三个词干标签类似于标签,其_id是根标签的词干版本。

到目前为止,第一个集合链接充当其他两个集合的基线。我按照名称将标签组合在一起,这样我只对每个短语进行一次查找,然后我可以通过一个查询立即获取所有目标文章。然后我使用文章标签集合,以便:

  1. 查找具有给定名称的标签。
  2. 获取所有可能的文章 指向。
  3. 比较这些链接的传入和传出链接 制品。
  4. 这是主要问题所在。如果我将一个给定短语的所有可能文章存储在一个文档中而不是将它们分散在链接集合中,我认为这样会更好。只有现在才发生这种情况 - 只要查找被编入索引 - 一个大文档或许多小文档的整体性能可能相同!这是正确的假设吗?

1 个答案:

答案 0 :(得分:2)

我认为您的数据模型是错误的。个别文章(让我们坚持使用维基百科的例子)链接的频率可能高于你可以存储在文档中的频率(尽管有点理论上)。 Embedding only works with One-To(-Very)-Few™ relationships.

所以基本上,我认为你应该改变你的模型。我会告诉你我将如何做到这一点。

我将在此示例中使用mongo shell和JavaScript,因为它是通用语言。您可能需要相应地进行翻译。

问题

让我们从您想要回答的问题开始:

  1. 对于给定文章,哪些文章链接到该文章?
  2. 对于给定文章,该文章链接到哪些文章?
  3. 对于给定的文章,有多少文章链接到它?
  4. 可选:对于给定的文章,它链接到多少篇文章?
  5. 抓取

    我要做的基本上是在文章上实现SAX解析器,为您遇到的每篇文章链接创建一个新文档。文件本身应该相当简单:

    {
      "_id": new ObjectId(),
      // optional, for recrawling or pointing out a given state
      "date": new ISODate(),
      "article": wikiUrl,
      "linksTo": otherWikiUrl
    }
    

    请注意,您不应该插入,而应该插入。这样做的原因是我们不想记录链接的数量,而是链接到的文章。如果我们执行了插入操作,articlelinksTo的相同组合可能会多次出现。

    所以我们在遇到链接时的陈述就像这样:

    db.links.update(
      { "article":"HMS_Warrior_(1860)", "linksTo":"Royal_Navy" },
      { "date": new ISODate(), "article":"HMS_Warrior_(1860)", "linksTo":"Royal_Navy" },   
      { upsert:true }
    )
    

    回答问题

    正如您可能已经猜到的那样,回答问题现在变得相当简单。我使用以下语句创建了一些文档:

    db.links.update(
      { "article":"HMS_Warrior_(1860)", "linksTo":"Royal_Navy" },
      { "date": new ISODate(), "article":"HMS_Warrior_(1860)", "linksTo":"Royal_Navy" },
      { upsert:true }
    )
    db.links.update(
      { "article":"Royal_Navy", "linksTo":"Mutiny_on_the_Bounty" },
      { "date":new ISODate(), "article":"Royal_Navy", "linksTo":"Mutiny_on_the_Bounty" },
      { upsert:true }
    )
    db.links.update(
      { "article":"Mutiny_on_the_Bounty", "linksTo":"Royal_Navy"},
      { "date":new ISODate(), "article":"Mutiny_on_the_Bounty", "linksTo":"Royal_Navy" },
      { upsert:true }
    )
    

    对于给定的文章,哪些文章链接到该文章?

    我们发现我们不应该使用聚合,因为这可能会超出大小限制。但我们不必这样做。我们只需使用游标并收集结果:

    var toLinks =[]
    
    var cursor = db.links.find({"linksTo":"Royal_Navy"},{"_id":0,"article":1})
    cursor.forEach(
      function(doc){
        toLinks.push(doc.article);
      }
    )
    printjson(toLinks)
    // Output: [ "HMS_Warrior_(1860)", "Mutiny_on_the_Bounty" ]
    

    对于给定的文章,该文章链接到哪个文章?

    这与第一个问题非常相似 - 我们基本上只更改查询:

    var fromLinks = []
    var cursor = db.links.find({"article":"Royal_Navy"},{"_id":0,"linksTo":1})
    cursor.forEach(
      function(doc){
        fromLinks.push(doc.linksTo)
      }
    )
    printjson(fromLinks)
    // Output: [ "Mutiny_on_the_Bounty" ]
    

    对于给定的文章,有多少文章链接到它?

    很明显,如果你已经回答了问题1,你可以查看toLinks.length。但是,让我们假设你没有。还有另外两种方法可以做到这一点

    使用.count()

    您可以在副本集上使用此方法。在分片群集上,这不起作用。但这很容易:

    db.links.find({ "linksTo":"Royal_Navy" }).count()
    // Output: 2
    

    使用聚合

    这适用于任何环境,并不复杂得多:

    db.links.aggregate([
      { "$match":{ "linksTo":"Royal_Navy" }},
      { "$group":{ "_id":"$linksTo", "isLinkedFrom":{ "$sum":1 }}}
    ])
    // Output: { "_id" : "Royal_Navy", "isLinkedFrom" : 2 }
    

    可选:对于给定的文章,它链接到多少篇文章?

    同样,您可以通过使用.count()方法从问题2中读取数组的长度来回答这个问题。再次聚合很简单

    db.links.aggregate([
      { "$match":{ "article":"Royal_Navy" }},
      { "$group":{ "_id":"$article", "linksTo":{ "$sum":1 }}}
    ])
    // Output: { "_id" : "Royal_Navy", "linksTo" : 1 }
    

    指数

    至于指数,我还没有真正检查过它们,但是这些领域的个别指数可能就是你想要的:

    db.links.createIndex({"article":1})
    db.links.createIndex({"linksTo":1})
    

    复合指数无济于事,因为订单很重要,我们并不总是要求第一个字段。所以这可能是最优化的。

    结论

    我们正在使用一个非常简单,可扩展的模型和相当简单的查询和聚合来获得您对数据所回答的问题。