对阵列交叉点进行计数和排序

时间:2018-04-17 16:15:57

标签: javascript node.js mongodb mongoose aggregation-framework

我有这个架构

module.exports = function(conn, mongoose) {
// var autoIncrement = require('mongoose-auto-increment');


var UsersSchema = new mongoose.Schema({

        first_name: String,
        last_name:String,
        sex: String,
        fk_hobbies: []


    }
    , {
        timestamps: true
    }, {collection: 'wt_users'});




  return conn.model('wt_users', UsersSchema);
};

例如,我在数据库中有这些用户

{
     "_id" : ObjectId("5aca2ac25c1d8adeb4a2dab0"),
     first_name:"Pierro",
     last_name:"pierre",
     sex:"H",
     fk_hobbies: [ 
            {
                "_id" : ObjectId("5ac9f84d5c1f8adeb4a2da97"),
                "name" : "Art"
            }, 
            {
                "_id" : ObjectId("5ac9f84d5c8d8adeb4a2da97"),
                "name" : "Sport"
            }, 
            {
                "_id" : ObjectId("5ac9f84d9c1d8adeb4a2da97"),
                "name" : "Fete"
            }, 
            {
                "_id" : ObjectId("5acaf84d5c1d8adeb4a2da97"),
                "name" : "Série"
            }, 
            {
                "_id" : ObjectId("6ac9f84d5c1d8adeb4a2da97"),
                "name" : "Jeux vidéo"
            }
      ]
},
{
    "_id" : ObjectId("5ac9fa075c1d8adeb4a2da99"),
    first_name:"jean",
    last_name:"mark",
    sex:"H",
    fk_hobbies: [ 
            {
                "_id" : ObjectId("5ac7f84d5c1d8adeb4a2da97"),
                "name" : "Musique"
            }, 
            {
                "_id" : ObjectId("5ac9f24d5c1d8adeb4a2da97"),
                "name" : "Chiller"
            }, 
            {
                "_id" : ObjectId("5ac9f84c5c1d8adeb4a2da97"),
                "name" : "Papoter"
            }, 
            {
                "_id" : ObjectId("5ac9f84d2c1d8adeb4a2da97"),
                "name" : "Manger"
            }, 
            {
                "_id" : ObjectId("5ac9f84d5c1d8adeb4a2da97"),
                "name" : "Film"
            }
       ]
   },
   {
        "_id" : ObjectId("5aca0a635c1d8adeb4a2da9d"),
        first_name:"michael",
        last_name:"ferrari",
        sex:"H",
        fk_hobbies: [ 
            {
                "_id" : ObjectId("5ac9f84d5c1d8adeb4a2ea97"),
                "name" : "fashion"
            }, 
            {
                "_id" : ObjectId("5ac9f84d5c1e8adeb4a2da97"),
                "name" : "Voyage"
            }, 
            {
                "_id" : ObjectId("5ac9f84c5c1d8adeb4a2da97"),
                "name" : "Papoter"
            }, 
            {
                "_id" : ObjectId("5ac9f84d2c1d8adeb4a2da97"),
                "name" : "Manger"
            }, 
            {
                "_id" : ObjectId("5ac9f84d5c1d8adeb4a2da97"),
                "name" : "Film"
            }
      ]
},
{
   "_id" : ObjectId("5ac9fa074c1d8adeb4a2da99"),
    first_name:"Philip",
     last_name:"roi",
     sex:"H",
     fk_hobbies: 
    [ 
            {
                "_id" : ObjectId("5ac7f84d5c1d8adeb4a2da97"),
                "name" : "Musique"
            }, 
            {
                "_id" : ObjectId("5ac9f24d5c1d8adeb4a2da97"),
                "name" : "Chiller"
            }, 
            {
                "_id" : ObjectId("5ac9f84c5c1d8adeb4a2da97"),
                "name" : "Papoter"
            }, 
            {
                "_id" : ObjectId("5ac9f84d2c1d8adeb4a2da97"),
                "name" : "Manger"
            }, 
            {
                "_id" : ObjectId("5ac9f84d5c1d8adeb4a2da97"),
                "name" : "Film"
            }
        ]
}

我想根据以下内容创建一个mongoose查询,该查询匹配用户get id的用户,以及数据库中的其他用户: 查询将首先返回具有相同爱好的最大数量的用户,即5,然后是具有相同4个爱好的用户......

我完全用Javascipt / node js创建一个解决方案,mongo有任何查询吗? 这是我的解决方案

//var user : the current user that search other similar users : jean mark : 5ac9fa075c1d8adeb4a2da99
//var users : all other users
 var tab = []


                async.each(users, function(item, next1){
                    var j = 0;
                    var hobbies = item["fk_hobbies"]


                    for(var i = 0; i < 5; i++)
                    {
                        var index = hobbies.findIndex(x => x["_id"] == user[0]["fk_hobbies"][i]["_id"].toString());




                        if(index != -1)
                            j++
                    }

                    if(j != 0)
                        tab.push({nbHob:j, user:item})

                    next1()
                }, function ()
                {
                   var tab2 = tab.sort(compare)
                res.json({success:true, data:tab2})
            })


function compare(a,b) {
    if (a.nbHob > b.nbHob)
        return -1;
    if (a.nbHob < b.nbHob)
        return 1;
    return 0;
}

显示的结果是这样的 nbHob:代表相似爱好的数量

{"success":true,"data":[{"nbHob":5,"user":{"_id":"5ac9fa074c1d8adeb4a2da99","u_first_name":"Akram","u_last_name":"Cherif","u_email":"","u_login":"","u_password":"","u_user_type":0,"u_date_of_birth":"","u_civility":0,"u_sex":"H","u_phone_number":"","u_facebook_id":"","u_google_id":"","u_twitter_id":"","u_profile_image":"","u_about":"","u_profession":"","u_fk_additional_infos":[null],"u_budget":0,"u_address":{"country":"France","state":"Paris","city":"TM","zip":76001},"u_fk_hobbies":[{"name":"Musique","_id":"5ac7f84d5c1d8adeb4a2da97"},{"name":"Chiller","_id":"5ac9f24d5c1d8adeb4a2da97"},{"name":"Papoter","_id":"5ac9f84c5c1d8adeb4a2da97"},{"name":"Manger","_id":"5ac9f84d2c1d8adeb4a2da97"},{"name":"Film","_id":"5ac9f84d5c1d8adeb4a2da97"}]}},{"nbHob":3,"user":{"_id":"5aca0a635c1d8adeb4a2da9d","u_first_name":"Chawki","u_last_name":"Gasmi","u_email":"","u_login":"","u_password":"","u_user_type":0,"u_date_of_birth":"","u_civility":0,"u_sex":"H","u_phone_number":"","u_facebook_id":"","u_google_id":"","u_twitter_id":"","u_profile_image":"","u_about":"","u_profession":"","u_fk_additional_infos":[null],"u_budget":{"min":500,"max":850},"u_address":{"country":"","state":"","city":"","zip":0},"u_fk_hobbies":[{"name":"fashion","_id":"5ac9f84d5c1d8adeb4a2ea97"},{"name":"Voyage","_id":"5ac9f84d5c1e8adeb4a2da97"},{"name":"Papoter","_id":"5ac9f84c5c1d8adeb4a2da97"},{"name":"Manger","_id":"5ac9f84d2c1d8adeb4a2da97"},{"name":"Film","_id":"5ac9f84d5c1d8adeb4a2da97"}]}}]}

1 个答案:

答案 0 :(得分:0)

由于每个爱好具有相同的ObjectId价值,所以你的问题数据似乎有些混乱,因为可能很自由的复制/粘贴。但我可以通过一个完整的自包含示例来纠正这个问题:

const { Schema } = mongoose = require('mongoose');

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

mongoose.Promise = global.Promise;
mongoose.set('debug', true);

const hobbySchema = new Schema({
  name: String
});

const userSchema = new Schema({
  first_name: String,
  last_name: String,
  sex: String,
  fk_hobbies: [hobbySchema]
});

const Hobby = mongoose.model('Hobby', hobbySchema)
const User = mongoose.model('User', userSchema);

const userData = [
  {
    "first_name" : "Pierro",
    "last_name" : "pierre",
    "sex" : "H",
    "fk_hobbies" : [ 
       "Art",  "Sport",  "Fete", "Série", "Jeux vidéo"
    ]
  },
  {
   "first_name": "jean",
   "last_name" : "mark",
    "sex" : "H",
    "fk_hobbies" : [
      "Musique", "Chiller", "Papoter", "Manger", "Film"
    ]
  },
  {
    "first_name" : "michael",
    "last_name" : "ferrari",
    "sex" : "H",
    "fk_hobbies" : [ 
      "fashion", "Voyage", "Papoter", "Manger", "Film"
    ]
  },
  {
    "first_name" : "Philip",
    "last_name" : "roi",
    "sex" : "H",
    "fk_hobbies" : [ 
      "Musique", "Chiller", "Papoter", "Manger", "Film"
    ]
  }
];

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri);

    await Promise.all(
      Object.entries(conn.models).map(([k,m]) => m.remove())
    );

    const hobbies = await Hobby.insertMany(
      [
        ...userData
          .reduce((o, u) => [ ...o, ...u.fk_hobbies ], [])
          .reduce((o, u) => o.set(u,1) , new Map())
      ]
      .map(([name,v]) => ({ name }))
    );

    const users = await User.insertMany(userData.map(u => 
      ({ 
        ...u, 
        fk_hobbies: u.fk_hobbies.map(f => hobbies.find(h => f === h.name))
      })
    ));

    let user = await User.findOne({
      "first_name" : "Philip",
      "last_name" : "roi"      
    });
    let user_hobbies = user.fk_hobbies.map(h => h._id );

    let result = await User.aggregate([
      { "$match": {
        "_id": { "$ne": user._id },
        "fk_hobbies._id": { "$in": user_hobbies }
      }},
      { "$addFields": {
        "numHobbies": {
          "$size": {
            "$setIntersection": [
              "$fk_hobbies._id",
              user_hobbies
            ]
          }
        },
        "fk_hobbies": {
          "$map": {
            "input": "$fk_hobbies",
            "in": {
              "$mergeObjects": [
                "$$this",
                {
                  "shared": {
                    "$cond": {
                      "if": { "$in": [ "$$this._id", user_hobbies ] },
                      "then": true,
                      "else": "$$REMOVE"
                    }
                  }
                }
              ]
            }
          }
        }
      }},
      { "$sort": { "numHobbies": -1 } }
    ]);

    log(result);

    mongoose.disconnect();

  } catch(e) {

  } finally {
    process.exit();
  }
})()

大部分只是&#34;设置&#34;重新创建数据集,但只是简单地说我们只是添加用户和他们的爱好并保持一个独特的&#34;每个&#34;独特爱好的标识符&#34;按名字。这可能就是你在问题中的实际含义,它应该是你应该遵循的那种模式。

有趣的部分都在.aggregate()语句中,这就是我们如何查询&#34;然后&#34;计算&#34;匹配的爱好并启用&#34;服务器&#34;在返回客户端之前对结果进行排序。

鉴于当前用户(以及您所包含的列表中的最后一个用户具有最有趣的匹配项),我们将重点关注代码的这一部分:

    // Simulates getting the current user to compare against
    let user = await User.findOne({
      "first_name" : "Philip",
      "last_name" : "roi"      
    });

    // Just get the list of _id values from the current user for reference
    let user_hobbies = user.fk_hobbies.map(h => h._id );

    let result = await User.aggregate([
      // Find all users not the current user with at least one of the hobbies
      { "$match": {
        "_id": { "$ne": user._id },
        "fk_hobbies._id": { "$in": user_hobbies }
      }},
      // Add the count of matches, "optionally" we are marking the matched
      // hobbies in the array as well.
      { "$addFields": {
        "numHobbies": {
          "$size": {
            "$setIntersection": [
              "$fk_hobbies._id",
              user_hobbies
            ]
          }
        },
        "fk_hobbies": {
          "$map": {
            "input": "$fk_hobbies",
            "in": {
              "$mergeObjects": [
                "$$this",
                {
                  "shared": {
                    "$cond": {
                      "if": { "$in": [ "$$this._id", user_hobbies ] },
                      "then": true,
                      "else": "$$REMOVE"
                    }
                  }
                }
              ]
            }
          }
        }
      }},
      // Sort the results by the "most" hobbies, which is "descending" order
      { "$sort": { "numHobbies": -1 } }
    ]);

我已经为您评论过这些步骤,但让我们对此进行扩展。

首先,我们假设您已经通过您已经完成的任何方式从数据库返回了当前用户。出于其余操作的目的,该用户真正需要的是&#34;用户&#34;的_id。本身以及每个用户所选择的爱好中的_id值。我们可以执行此处显示的快速.map()操作,但我们保留一份副本以便于参考,而不是通过其余代码重复该操作。

然后我们到达实际的聚合语句。第一个条件是$match,它的工作方式类似于具有所有相同运算符的标准查询表达式。我们想从这些查询条件中得到两件事:

  • 让所有用户除了当前用户以供考虑;
  • 并且这些用户在同一爱好中至少包含一个匹配,其值为_id

所以&#34;其他人的条件&#34;本质上是提供$ne&#34;不等于&#34;运算符在_id值的参数中,当然与当前用户_id进行比较。仅获得具有相同爱好的人的第二个条件是使用$in运算符对_id数组的fk_hobbies字段。在MongoDB查询说法中,我们将其表示为"$fk_hobbies._id",以便与&#34;内部&#34;匹配。 _id属性值。

$in运算符本身需要一个&#34;列表&#34;作为它的参数并比较提供给属性的列表中的每个值,并指定条件。 MongoDB本身并不关心fk_hobbies是数组还是单个值,而只是查找提供列表中任何内容的匹配项。将$in视为撰写$or的简短方法,除非您不需要在每种条件下明确包含相同的属性名称。

现在您已选择了正确的文档,并且已经放弃了任何不共享任何相同爱好的用户,我们可以将其移至下一阶段。另请注意,整个$match认为您只需要那些&#34;匹配&#34;用户。如果你真的想看到&#34;所有用户&#34;包括&#34;没有匹配&#34;的那些,那么你可以简单地省略整个$match管道阶段。你的代码丢弃了任何未计算的代码,因此这段代码根本不会计算任何必须&#34;必须&#34;有一个0计数。

$addFields阶段管道阶段是一种快速的方式来添加新字段&#34;到结果中返回的文件。除了其他用户详细信息之外,您需要的主要输出是"numHobbies",因此这个管道阶段操作符是执行此操作的最佳方式,但如果您的MongoDB服务器稍微长一点,那么您可以只需指定&#34; all&#34;除了使用$project代替的任何新字段外,您还要包含这些字段。

为了&#34;计数&#34;我们基本上使用两个聚合运算符,即$setIntersection$size。这些都应该在您真正应该在生产中使用的MongoDB版本中提供。

按照相应的顺序,$setIntersection运算符&#34;比较集&#34;在这种情况下,_id中的fk_hobbies值列表,包括我们之前存储的当前所选用户以及表达式中考虑的当前文档。此运算符的结果是值列表,它们是&#34;相同的&#34;两个名单之间。

当然,$size运算符会从$setIntersection查看返回的列表(或集合),并返回该列表中的条目数。这当然是&#34;匹配计数&#34;。

下一部分涉及投射&#34;重写&#34; fk_hobbies数组的形式。这完全是可选的,我自己设计用于演示目的。 &#34;如果&#34; 你也想做我在这里做的事情,那么这段代码的作用是为fk_hobbies数组的对象添加一个额外的属性表明该特定爱好与列表匹配的爱好之一。

我说这是&#34;可选&#34;因为我实际上展示了MongoDB 3.6的两个功能。这些涉及在内部数组元素上使用$mergeObjects以及使用Conditionally Exlcuding Fields

单步执行,因为fk_hobbies是一个数组,我们需要使用$map运算符才能重塑&#34;重塑&#34;里面的物体。此运算符允许我们处理每个数组成员并根据我们包含的转换作为其参数返回一个新值。对于JavaScript或任何其他实现类似操作的语言,它的用法与.map()大致相同。

因此,对于数组中的每个对象($$this),我们应用$mergeObjects运算符,它将&#34; merge&#34;它的论据的结果。它们作为当前对象的$$this提供,而表达式中的第二个参数是新的和有趣的。

我们在这里使用$cond运算符,它是一个&#34;三元&#34;运算符(或if..then..else表达式),它考虑条件if,然后返回表达式为真的then参数,或者返回错误的else表达式。这里的表达式是另一种用作聚合表达式的$in形式。在这种形式中,第一个参数是一个奇异值$$this._id,它将与第二个参数中的列表表达式进行比较。第二个参数当然是我们之前保留的当前用户爱好ID的列表,并且再次用于比较。

单独使用$in会返回truefalse匹配的位置。但是,此处额外展示的操作是在$cond表达式内,else false条件会返回新的$$REMOVE"shared"。这意味着我们将false属性添加到数组中的每个对象,而不是在没有匹配的情况下为其分配值 let result = await User.aggregate([ { "$match": { "_id": { "$ne": user._id }, "fk_hobbies._id": { "$in": user_hobbies } }}, { "$addFields": { "numHobbies": { "$size": { "$setIntersection": [ "$fk_hobbies._id", user_hobbies ] } } }}, { "$sort": { "numHobbies": -1 } } ]); // map each result after return result = result.map(r => ({ ...r, fk_hobbies: r.fk_hobbies.map(h => ({ ...h, ...(( user_hobbies.map(i => i.toString() ).indexOf( h._id.toString() ) != -1 ) ? { "shared": true } : {} ) }) ) }) ) ,我们实际上并不包含输出文档中的属性。

那&#34;可选&#34;部分真的只是作为一个&#34;漂亮的触摸&#34;表明哪些&#34;爱好&#34;在条件匹配,而不是简单地返回计数。如果您喜欢它,那么使用它,如果您没有具有这些功能的MongoDB 3.6,您可以简单地从聚合输出中对返回的文档进行相同的更改:

"numHobbies"

无论哪种方式,您想要的任何$addFields$project语句的主要内容是指示计数的实际[ { "_id": "5ad6bbe63365bc3428feed8a", "first_name": "jean", "last_name": "mark", "sex": "H", "fk_hobbies": [ { "_id": "5ad6bbe63365bc3428feed7d", "name": "Musique", "__v": 0, "shared": true }, { "_id": "5ad6bbe63365bc3428feed7e", "name": "Chiller", "__v": 0, "shared": true }, { "_id": "5ad6bbe63365bc3428feed7f", "name": "Papoter", "__v": 0, "shared": true }, { "_id": "5ad6bbe63365bc3428feed80", "name": "Manger", "__v": 0, "shared": true }, { "_id": "5ad6bbe63365bc3428feed81", "name": "Film", "__v": 0, "shared": true } ], "__v": 0, "numHobbies": 5 }, { "_id": "5ad6bbe63365bc3428feed90", "first_name": "michael", "last_name": "ferrari", "sex": "H", "fk_hobbies": [ { "_id": "5ad6bbe63365bc3428feed82", "name": "fashion", "__v": 0 }, { "_id": "5ad6bbe63365bc3428feed83", "name": "Voyage", "__v": 0 }, { "_id": "5ad6bbe63365bc3428feed7f", "name": "Papoter", "__v": 0, "shared": true }, { "_id": "5ad6bbe63365bc3428feed80", "name": "Manger", "__v": 0, "shared": true }, { "_id": "5ad6bbe63365bc3428feed81", "name": "Film", "__v": 0, "shared": true } ], "__v": 0, "numHobbies": 3 } ] 值。我们在服务器上执行此操作的主要原因是我们也可以在服务器上$sort,这样就可以将$limit$skip之类的内容添加到更大的结果集中出于分页的目的,从集合中获取所有结果是不切实际的,即使它们是在初始匹配或常规查询中过滤的。

无论如何,从样本列表中生成的小问题文档样本中,我们得到如下结果:

5

因此返回了两个用户,我们将匹配的爱好分别计为3"shared",并返回匹配最多的用户。您还可以在每个匹配的爱好中看到添加async.each()属性,以指示每个返回用户列表中的哪些爱好也与他们与之比较的原始用户共享。

  

注意:你可能只是&#34;尝试事情&#34;但是你在问题中使用.aggregate()并不是必需的,因为内部代码实际上都不是&#34; async&#34;本身。即使在这里列出,你真正需要的唯一事情是等待&#34;作为当前用户进行比较后的异步调用是async/await响应本身。

     

所以,如果在任何一部分你假设你会在一个循环中等待请求,那么你就错了。只需向数据库询问结果并等待他们返回。

     

只需要一个数据库请求。

     

N.B 它也是2018年,所以你真的应该开始理解{{1}}的承诺和用法。代码更加清晰,当然任何新开发的应用程序都应该在具有此支持的环境中运行。那么&#34;回调助手&#34;像&#34;节点异步&#34;这样的图书馆是一个小小的旧帽子&#34;并且在现代背景下过时了。