【问题标题】:Count and Sort On Array Intersection对数组交点进行计数和排序
【发布时间】:2018-09-27 16:57:48
【问题描述】:

我有这个架构

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 查询,该查询与通过 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"}]}}]}

【问题讨论】:

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


    【解决方案1】:

    由于每个爱好都具有相同的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();
      }
    })()
    

    其中大部分只是重新创建数据集的“设置”,但简单地说,我们只是添加用户及其爱好,并按名称为每个“独特爱好”保留一个“唯一”标识符。这可能是您在问题中的实际意思,也是您应该遵循的那种模型。

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

    给定一个当前用户(并且您包含的列表中的最后一个具有最有趣的匹配项),然后我们专注于这部分代码:

        // 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 } }
        ]);
    

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

    首先,我们假设您已经通过您已经完成的任何方式从数据库中返回了当前用户。出于其余操作的目的,该用户真正需要的只是“用户”本身的_id,当然还有来自该用户选择的每个爱好的_id 值。我们可以按照此处所示进行快速的.map() 操作,但我们保留一份副本以方便参考,并且不会在其余代码中重复该操作。

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

    • 获取所有用户除了当前用户考虑;
    • 如果这些用户包含至少一个相同爱好的匹配项,则为 _id 值。

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

    $in 运算符本身将“列表”作为其参数,并比较列表中提供给条件分配的属性的每个值。 MongoDB 本身并不关心fk_hobbies 是数组还是单个值,它只会在提供的列表中寻找匹配项。将$in 视为编写$or 的简写方式,除非您不需要在每个条件上显式包含相同的属性名称。

    现在您已经选择了正确的文档,并且已经放弃了任何不具有相同爱好的用户,我们可以进入下一阶段。另请注意,整个$match 认为您只想要那些“匹配”用户是合乎逻辑的。如果您真的想查看“所有用户”,包括那些“不匹配”的用户,那么您可以简单地省略整个 $match 管道阶段。您的代码正在丢弃任何未计数的内容,因此此代码根本不会费心计算“必须”具有0 计数的任何内容。

    $addFields 阶段流水线阶段是一种向结果中返回的文档“添加新字段”的快速方法。您想要的主要输出是"numHobbies" 以及其他用户详细信息,因此此管道阶段操作符是执行此操作的最佳方式,但如果您的 MongoDB 服务器有点旧,那么您可以简单地指定“所有" 除了使用 $project 的任何新字段之外,您还希望包含的字段。

    为了“统计”共同爱好的数量,我们基本上使用了两个聚合运算符,它们是$setIntersection$size。这两个都应该在您真正应该在生产中使用的 MongoDB 版本中可用。

    $setIntersection 运算符按相应顺序“比较集合”,在本例中是 fk_hobbies 中的 _id 值列表,既来自我们之前存储的当前选定用户,也来自当前文档中正在考虑的表达。此运算符的结果是两个列表之间“相同”的值列表。

    $size 运算符自然会查看来自$setIntersection 的返回列表(或集合)并返回该列表中的条目数。这当然是“匹配计数”。

    下一部分涉及投影fk_hobbies 数组的“重写”形式。这完全是可选的,并且是我自己设计的,用于演示目的。 “如果” 你也想做我正在做的事情,那么这段代码所做的就是为 fk_hobbies 数组的对象添加一个附加属性,以指示该特定爱好的位置与列表匹配的其中之一。

    我说这是“可选的”,因为我实际上是在演示可用于 MongoDB 3.6 的两个功能。这些涉及到在内部数组元素上使用$mergeObjects 以及使用Conditionally Exlcuding Fields

    逐步完成,因为fk_hobbies 是一个数组,我们需要使用$map 运算符来“重塑”其中的对象。该运算符允许我们处理每个数组成员并根据我们作为参数包含的转换返回一个新值。它的用法与 .map() 用于 JavaScript 或任何其他实现类似操作的语言非常相似。

    因此,对于数组中的每个对象 ($$this),我们应用 $mergeObjects 运算符,它将“合并”其参数的结果。这些是作为当前对象的$$this 提供的,而表达式中的第二个参数正在做一些新的和有趣的事情。

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

    单独使用$in 将返回匹配的truefalse。但是这里额外展示的操作是在$cond 表达式中,falseelse 条件返回新的和特殊的$$REMOVE 值。这意味着使用我们的"shared" 属性,我们将添加到数组中的每个对象,而不是在没有匹配项的情况下为其分配false 的值,我们实际上并没有在输出文档中包含该属性全部。

    那个“可选”部分实际上只是作为一种“很好的接触”来指示在条件中匹配哪些“爱好”,而不是简单地返回计数。如果您喜欢它,请使用它,如果您没有具有这些功能的 MongoDB 3.6,您可以简单地对来自聚合输出的返回文档进行相同的更改:

        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 } : {} )
              })
            )
          })
        )
    

    无论哪种方式,您希望从任何 $addFields$project 语句中获得的主要内容是指示计数的实际 "numHobbies" 值。我们在服务器上这样做的主要原因是我们也可以在服务器上$sort,这反过来又允许您将$limit$skip之类的内容添加到更大的结果集中,以便在它所在的位置进行分页从集合中获取所有结果根本不切实际,即使它们在初始匹配或常规查询中被过滤。

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

    [
      {
        "_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
      }
    ]
    

    所以返回了两个用户,我们将匹配的爱好分别计算为53,并返回匹配最多的第一个。您还可以看到在每个匹配的爱好上添加了"shared" 属性,以指示每个返回的用户列表中的哪些爱好也与他们进行比较的原始用户共享。


    注意:您可能只是在“尝试”,但您在问题中使用async.each() 并不是真正必要的,因为内部代码实际上都不是“异步”本身。即使在此处的列表中,在让当前用户进行比较之后,您真正需要“等待”作为异步调用的唯一事情就是 .aggregate() 响应本身。

    因此,如果您在其中的任何部分假设您将“在循环中等待请求”,那么您就错了。只需向数据库询问结果并等待其返回即可。

    对数据库的一次请求就足够了。

    注意现在也是 2018 年,所以你真的应该开始了解 Promise 和 async/await 的用法。这样代码就干净多了,当然任何新开发的应用程序都应该在有这种支持的环境中运行。因此,像“node async”这样的“callback helper”库有点“老套”,在现代环境中已经过时了。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-12-27
      • 2013-01-20
      • 2019-11-09
      • 1970-01-01
      • 2013-09-01
      • 1970-01-01
      • 2017-09-04
      • 1970-01-01
      相关资源
      最近更新 更多