对数组的每个元素发出查询

时间:2017-07-28 22:05:09

标签: javascript node.js mongodb

我目前正在查询我的mondo数据库中的一个集合中的url数组,它返回一个数组。然后我想使用该数组来浏览另一个集合,并找到上一个查询返回数组中每个元素的匹配元素。在阵列上使用forEach并进行单独查询是否合适? 我的代码看起来像这样,第一个函数getUrls工作得很好。我得到的当前错误是:

  

(node:10754)UnhandledPromiseRejectionWarning:未处理的promise promise(拒绝id:1):TypeError:无法读取undefined的属性'limit'   (节点:10754)[DEP0018]弃用警告:不推荐使用未处理的拒绝承诺。将来,未处理的承诺拒绝将使用非零退出代码终止Node.js进程。

async function useUrls () {
    let domains = await getUrls()
    let db = await mongo.connect("mongodb://35.185.206.31:80/lc_data")
    let results = []
    domains.forEach( domain =>{ 
        let query = {"$match": 
                        {"email_domain": domain}
                    }
        let cursor = db.collection('circleback')
            .aggregate([query], (err, data) =>{
                if(err)
                    throw err;
                console.log("cb", data)
            }).limit(1100)
    })

1 个答案:

答案 0 :(得分:0)

如上所述,问题中的代码存在一些问题,其中大部分问题可以通过查看此响应结尾处提供的完整示例列表来解决。你在这里要求的是对#B;前N个结果的变化"问题,有几种方法可以实现"处理这个。

有点排名来自"最差"最好的":

聚合$ slice

所以而不是"循环"根据您的功能结果,您可以使用$in将结果全部提供给查询。这减轻了对循环输入的需求,但是这里需要的另一件事是"每个输出的前N个"。

确实没有"稳定" MongoDB中的机制尚未实现,但是"如果"它对于给定集合的大小是合理的,那么事实上你可以简单$group在你的" distinct"密钥与提供的$in参数匹配,然后$push将所有文档放入数组并$slice结果:

let results = await db.collection('circleback').aggregate([
  { "$match": { "email_domain": { "$in": domains } } },
  { "$group": {
    "_id": "$email_domain",
    "docs": { "$push": "$$ROOT" }
  }},
  { "$sort": { "_id": 1 } },
  { "$addFields": { "docs": { "$slice": [ "$docs", 0, 1100 ] } } }
]).toArray();

"更广泛"这里的问题是MongoDB无法限制"初始$push上的数组内容。事实上这正在等待一个长期悬而未决的问题。 SERVER-9377

因此,尽管我们可以在理论上进行这种操作,但由于16MB BSON限制通常会限制"数组大小,即使$slice结果确实会保持在该上限之下。

串行循环执行异步/等待

您的代码显示您在此环境下运行,因此我建议您实际使用它。只需await来自源的每次循环迭代:

let results = [];
for ( let domain of domains ) {
  results = results.concat(
    await db.collection('circleback').find({ "email_domain": domain })
      .limit(1100).toArray()
  );
}

简单的函数允许您执行此操作,例如通过.find().toArray()的标准游标结果作为数组返回,然后使用.concat()加入以前的结果数组。< / p>

它简单有效,但我们可以做得更好

异步方法的并发执行

所以不要使用&#34;循环&#34;并且在每个被称为异步函数的await上,您可以改为同时执行它们(或者至少#34;最多&#34;)。事实上,这是你目前在问题中提出的问题的一部分,因为实际上并没有等待#34;等等。用于循环迭代。

我们可以使用Promise.all()来有效地执行此操作,但是如果它实际上是非常大的&#34;将同时运行的承诺数量,这将遇到与超出调用堆栈的经验相同的问题。

为了避免这种情况,我们仍然可以使用Bluebird与Promise.map()的承诺。这有一个&#34;并发限制器&#34;选项,只允许指定数量的操作同时执行:

let results = [].concat.apply([],
  await Promise.map(domains, domain => 
    db.collection('circleback').find({ "email_domain": domain })
      .limit(1100).toArray()
  ,{ concurrency: 10 })
);

实际上你甚至可以使用像Bluebird这样的库来承诺&#34;插件&#34; .map()功能与其他任何返回Promise的功能,例如您的&#34;来源&#34;函数返回"domains"列表。然后你可以&#34;链&#34;正如后面的例子中所示。

未来的MongoDB

MongoDB的未来版本(来自MongoDB 3.6)实际上有一个新的&#34;非相关&#34; $lookup的形式,允许这里的特例。所以回到最初的聚合示例,我们可以得到&#34; distinct&#34;每个匹配键的值,然后$lookup带有"pipeline"参数,然后允许$limit应用于结果。

let results = await db.collection('circleback').aggregate([
  { "$match": { "email_domain": { "$in": domains } } },
  { "$group": { "_id": "$email_domain"  }},
  { "$sort": { "_id": 1 } },
  { "$lookup": {
     "from": "circleback",
     "let": {
       "domain": "$_id"
     },
     "pipeline": [
       { "$redact": {
         "$cond": {
           "if": { "$eq": [ "$email_domain", "$$domain" ] },
           "then": "$$KEEP",
           "else": "$$PRUNE"
         }
       }},
       { "$limit": 1100 }
     ],
     "as": "docs"
  }}
]).toArray();

这将始终保持在16MB BSON限制之下,当然假设$in的参数允许这种情况。

示例清单

作为完整的示例列表,您可以运行,并且通常一起使用,因为默认数据集创建是故意非常大的。它演示了上述所有技术以及一些常用的使用模式。

const mongodb = require('mongodb'),
      Promise = require('bluebird'),
      MongoClient = mongodb.MongoClient,
      Logger = mongodb.Logger;

const uri = 'mongodb://localhost/bigpara';

function log(data) {
  console.log(JSON.stringify(data,undefined,2))
}

(async function() {

  let db;

  try {
    db = await MongoClient.connect(uri,{ promiseLibrary: Promise });

    Logger.setLevel('info');
    let source = db.collection('source');
    let data = db.collection('data');

    // Clean collections
    await Promise.all(
      [source,data].map( coll => coll.remove({}) )
    );

    // Create some data to work with

    await source.insertMany(
      Array.apply([],Array(500)).map((e,i) => ({ item: i+1 }))
    );

    let ops = [];
    for (let i=1; i <= 10000; i++) {
      ops.push({
        item: Math.floor(Math.random() * 500) + 1,
        index: i,
        amount: Math.floor(Math.random() * (200 - 100 + 1)) + 100
      });
      if ( i % 1000 === 0 ) {
        await data.insertMany(ops,{ ordered: false });
        ops = [];
      }
    }

    /* Fetch 5 and 5 example
     *
     * Note that the async method to supply to $in is a simulation
     * of any real source that is returning an array
     *
     * Not the best since it means ALL documents go into the array
     * for the selection. Then you $slice off only what you need.
     */
    console.log('\nAggregate $in Example');
    await (async function(source,data) {
      let results = await data.aggregate([
        { "$match": {
          "item": {
            "$in": (await source.find().limit(5).toArray()).map(d => d.item)
          }
        }},
        { "$group": {
          "_id": "$item",
          "docs": { "$push": "$$ROOT" }
        }},
        { "$addFields": {
          "docs": { "$slice": [ "$docs", 0, 5 ] }
        }},
        { "$sort": { "_id": 1 } }
      ]).toArray();
      log(results);
    })(source,data);


    /*
     * Fetch 10 by 2 example
     *
     * Much better usage of concurrent processes and only get's
     * what is needed. But it is actually 1 request per item
     */
    console.log('\nPromise.map concurrency example');
    await (async function(source,data) {
      let results = [].concat.apply([],
        await source.find().limit(10).toArray().map(d =>
          data.find({ item: d.item }).limit(2).toArray()
        ,{ concurrency: 5 })
      );
      log(results);
    })(source,data);

    /*
     * Plain loop async/await serial example
     *
     * Still one request per item, requests are serial
     * and therefore take longer to complete than concurrent
     */
    console.log('\nasync/await serial loop');
    await (async function(source,data) {
      let items = (await source.find().limit(10).toArray());
      let results = [];
      for ( item of items ) {
        results = results.concat(
          await data.find({ item: item.item }).limit(2).toArray()
        );
      }
      log(results);
    })(source,data);


    /*
     * Non-Correlated $lookup example
     *
     * Uses aggregate to get the "distinct" matching results and then does
     * a $lookup operation to retrive the matching documents to the
     * specified $limit
     *
     * Typically not as efficient as the concurrent example, but does
     * actually run completely on the server, and does not require
     * additional connections.
     *
     */

    let version = (await db.db('admin').command({'buildinfo': 1})).version;
    if ( version >= "3.5" ) {
      console.log('\nNon-Correlated $lookup example $limit')
      await (async function(source,data) {
        let items = (await source.find().limit(5).toArray()).map(d => d.item);

        let results = await data.aggregate([
          { "$match": { "item": { "$in": items } } },
          { "$group": { "_id": "$item" } },
          { "$sort": { "_id": 1 } },
          { "$lookup": {
            "from": "data",
            "let": {
              "itemId": "$_id",
            },
            "pipeline": [
              { "$redact": {
                "$cond": {
                  "if": { "$eq": [ "$item", "$$itemId" ] },
                  "then": "$$KEEP",
                  "else": "$$PRUNE"
                }
              }},
              { "$limit": 5 }
            ],
            "as": "docs"
          }}
        ]).toArray();
        log(results);

      })(source,data);
    } else {
      console.log('\nSkipped Non-Correlated $lookup demo');
    }

  } catch(e) {
    console.error(e);
  } finally {
    db.close();
  }

})();