具有非空字段的MongoDB / Mongoose权重记录

时间:2014-07-25 00:14:40

标签: node.js mongodb mongoose aggregation-framework

我有一个MongoDB文档集合。我已经为特定字段分配了权重,但我需要将记录与任何非空名称重叠到顶部。我不想按名称排序,我只是喜欢在没有名字的情况下出现名字的记录。

示例架构:

new Schema({
  slug: {
    type: String,
    index: {unique: true, dropDups: true}
  },
  name: String,
  body: {
    type: String,
    required: true
  }
});

示例索引:

MySchema.index({
    name:'text',
    body:'text'
}, {
    name: 'best_match_index',
    weights: {
      name: 10,
      body: 1
    }
});

查询查询:

MyModel.find( criteria, { score : { $meta: 'textScore' } })
  .sort({ score : { $meta : 'textScore' } })
  .skip(offset)
  .limit(per_page)

1 个答案:

答案 0 :(得分:2)

如果我理解你的意思,你所说的就是这样的文件:

{ "name" : "term", "body" : "unrelated" }
{ "name" : "unrelated", "body" : "unrelated" }
{ "body" : "term" }
{ "body" : "term term" }
{ "name" : "unrelated", "body" : "term" }

正常搜索“term”会产生如下结果:

{ "name" : "term", "body" : "unrelated", "score" : 11 }
{ "body" : "term term", "score" : 1.5 }
{ "body" : "term", "score" : 1.1 }
{ "name" : "unrelated", "body" : "term", "score" : 1.1 }

但你想要的是它将最后一个条目作为第二个条目。

为此,您需要将另一个字段“动态”投影到“权重”,您可以在其中使用聚合框架:

MyModel.aggregate([
    { "$match": {
        "$text": { "$search": "term" } 
    }},
    { "$project": {
        "slug": 1,
        "name": 1,
        "body": 1,
        "textScore": { "$meta": "textScore" },
        "nameScore": { 
            "$cond": [
                { "$ne": [{ "$ifNull": [ "$name", "" ] }, ""] },
                1,
                0
            ]
        }
    }},
    { "$sort": { "nameScore": -1, "textScore": -1 } },
    { "$skip": offset },
    { "$limit": per_page }
],function(err,results) {
    if (err) throw err;

    console.log( results );
})

将具有“名称”字段的项目置于不具有以下内容的项目之外:

{ "name" : "term", "body" : "unrelelated", "textScore" : 11, "nameScore" : 1 }
{ "name" : "unrelated", "body" : "term", "textScore" : 1.1, "nameScore" : 1 }
{ "body" : "term term", "textScore" : 1.5, "nameScore" : 0 }
{ "body" : "term", "textScore" : 1.1, "nameScore" : 0 }

基本上$ifNull三元组$cond运算符中存在“name”字段,然后返回1表示存在或0表示不存在。

这会传递到$sort管道,您的排序首先在“nameScore”上,将这些项目浮动到顶部,然后再将“textScore”浮动。

聚合管道有自己的$skip$limit实现,用于分页。

这与.find()实现中的操作基本相同,具有“匹配”,“项目”,“排序”,“跳过”和“限制”。所以处理方式确实没有区别,只是对结果有了更多的控制。

“skip”和“limit”的使用并不是最高效的解决方案,但有时你会坚持使用它,例如在需要提供“页码编号”的情况下。 但是,如果您可以侥幸逃脱并且只需要向前移动,那么您可以尝试将最后看到的“textScore”和“seen_ids”列表跟踪到一定的粒度级别,具体取决于“textScore”值的分布情况是。这些可以作为“跳过”结果的替代方法传递:

MyModel.aggregate([
    { "$match": {
        "$text": { "$search": "term" }
    }},
    { "$project": {
        "slug": 1,
        "name": 1,
        "body": 1,
        "textScore": { "$meta": "textScore" },
        "nameScore": { 
            "$cond": [
                { "$ne": [{ "$ifNull": [ "$name", "" ] }, ""] },
                1,
                0
            ]
        }
    }},
    { "$match": {
        "_id": { "$nin": seen_ids }
        "textScore": { "$gte": last_score },
    }},        
    { "$sort": { "nameScore": -1, "textScore": -1 } },
    { "$limit": page_size }
])

这里唯一有点不幸的是,textScore的$meta还不能暴露给最初的$match操作,这有助于缩小结果范围,而无需通过{{3首先。

所以你真的不能做同样的完整优化,这可以通过专门的$project运算符来完成,但是它的文本版本或允许前一个语句会很好。


您可能会注意到,从.aggregate()选项返回的对象只是原始JavaScript对象,而不是从.find()等操作返回的Mongoose“文档”对象。这是“按设计”,这里的主要原因是,由于聚合框架允许您“操纵”生成的文档,因此无法保证这些文档实际上与您最初查询的模式中的文档相同。

由于你并没有真正“改变”或“重新塑造”你预期目的的文件,现在只需要回到你的代码来做猫鼬在幕后自动做的事情并将每个原始结果“转换”成标准的“类型”。

此列表通常应显示您需要执行的操作:

var async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

mongoose.connect("mongodb://localhost/test");

var testSchema = new Schema({
  name: String,
  body: { type: String, required: true },
  textScore: Number,
  nameScore: Number
},{
  toObject: { virtuals: true },
  toJSON: { virtuals: true }
});

testSchema.virtual('favourite').get(function() {
  return "Fred";
});

var Test = mongoose.model( "Test", testSchema, "textscore" );

Test.aggregate([
  { "$match": {
    "$text": { "$search": "term" }
  }},
  { "$project": {
    "name": 1,
    "body": 1,
    "textScore": { "$meta": "textScore" },
    "nameScore": {
      "$cond": [
        { "$ne": [{ "$ifNull": [ "$name", "" ] }, "" ] },
        1,
        0
      ]
    }
  }},
  { "$sort": { "nameScore": -1, "textScore": -1 } },
],function(err,result) {
  if (err) throw err;

  result = result.map(function(doc) {
    return new Test( doc );
  });
  console.log( JSON.stringify( result, undefined, 4 ));
  process.exit();

});

其中包括输出中的“虚拟”字段:

[
    {
        "_id": "53d1a9b501e1b6c73aed2b52",
        "name": "term",
        "body": "unrelelated",
        "favourite": "Fred",
        "id": "53d1a9b501e1b6c73aed2b52"
    },
    {
        "_id": "53d1ae1a01e1b6c73aed2b56",
        "name": "unrelated",
        "body": "term",
        "favourite": "Fred",
        "id": "53d1ae1a01e1b6c73aed2b56"
    },
    {
        "_id": "53d1ada301e1b6c73aed2b55",
        "body": "term term",
        "favourite": "Fred",
        "id": "53d1ada301e1b6c73aed2b55"
    },
    {
        "_id": "53d1ad9e01e1b6c73aed2b54",
        "body": "term",
        "favourite": "Fred",
        "id": "53d1ad9e01e1b6c73aed2b54"
    }
]